Service Accounts
A service account is a non-human identity that authenticates to the Akuity Platform with a token from an OIDC issuer you trust, such as GitHub Actions or a Kubernetes cluster. The workload presents the short-lived token its platform already gives it, and the Akuity Platform exchanges it for a short-lived Akuity credential. Nothing is stored in the pipeline or the cluster, so there is no secret to leak or rotate.
Compare this with API keys, which are long-lived secrets you have to distribute and protect. Prefer a service account wherever the workload can obtain an OIDC token.
The flow has three parts:
- Register the OIDC issuer once, so the platform knows where to get its signing keys.
- Create a service account bound to that issuer, with a claim match that says which tokens may use it and which role it holds.
- The workload exchanges its OIDC token for an Akuity credential and uses it like a login.
Service accounts are rolled out per organization. If the Service Accounts tab is not visible on your organization page, contact Akuity support.
Registering an OIDC issuer
Organization owners, or members with a custom role that grants it, can manage issuers and service accounts.
-
Select the organization from the pull down menu and switch to the Service Accounts tab.
-
In the OIDC Issuers section, click New Issuer.
-
Enter a Name, for example
github-actions. -
Enter the Issuer URL. This must be exactly the
issclaim of the tokens the issuer produces, for examplehttps://token.actions.githubusercontent.com. -
Choose a Key source, which is where the platform reads the issuer's public signing keys from:
Key source Use when Discovery The issuer publishes <issuer URL>/.well-known/openid-configurationon a public address. Works for GitHub, GitLab, Okta, Entra ID and most managed Kubernetes clusters.JWKS URL The issuer's key set is published on a public address that is not derived from the issuer URL. Static JWKS The keys are not reachable from the internet, for example a private Kubernetes cluster. Paste the JWKS document. Keys fetched from a URL are refreshed automatically, so key rotation on the issuer's side needs no action. A static JWKS must be updated by hand when the issuer rotates its keys.
-
Optionally set Audience overrides and the Token TTL, then click Create.
After creation the issuer shows its default audience, https://akuity.cloud/api/v1/organizations/<organization ID>. Tokens must carry this value in their aud claim, or one of the audience overrides if you set any. A token may list more audiences than that, as long as one matches.
The token TTL is the lifetime of the Akuity credentials issued for tokens from this issuer, between 1 minute and 24 hours. The default is 1 hour.
Creating a service account
-
In the Service Accounts section, click Create Service Account.
-
Enter a Description, for example
ci-deployer. -
Select the Issuer the account trusts. Each service account is bound to exactly one issuer.
-
Fill in the Claim match. Each row pairs a claim name from the OIDC token with a pattern. The pattern is a regular expression that must match the whole claim value. Every row must match for the exchange to succeed.
The
subclaim is required, because it is what identifies the workload: the repository and branch on GitHub, the namespace and service account on Kubernetes. Asubpattern that would accept any value is rejected. Nested claims are addressed with a dotted path, such askubernetes.io.namespace.Some examples:
Claim Pattern Matches subrepo:acme/app:ref:refs/heads/mainPushes to mainof theacme/appGitHub repositorysubrepo:acme/app:environment:productionJobs running in the productionGitHub environmentsubrepo:acme/.*:ref:refs/tags/v.*Tag builds in any repository of the acmeorganizationsubsystem:serviceaccount:ci:deployerThe deployerservice account in thecinamespace of a Kubernetes clusteractorrenovate\[bot\]Only workflows started by that GitHub actor -
Assign a Role or one or more custom roles. Organization service accounts hold the Member role and organization custom roles. The Owner role cannot be given to a service account.
-
Optionally restrict the IP Allowlist to the addresses or CIDR ranges the workload runs from, if your plan includes it. Both the exchange and every request made with the credential must come from an allowed address.
-
Click Create and note the service account ID shown in the list. The workload needs it, along with the organization ID.
Workspace service accounts
A service account can also be scoped to a single workspace. Open the workspace settings, select Service Accounts, and create the account there. It binds to one of the organization's issuers in the same way, but holds a workspace role (Admin or Member) or workspace custom roles, and can only reach that workspace, exactly like a workspace API key.
Exchanging a token
The workload trades its OIDC token for an Akuity credential, then uses the credential as a bearer token. No login or API key is involved at any point.
The simplest way is the Akuity CLI, which reads the OIDC token from a flag, a file, or stdin and prints the credential:
akuity service-account exchange-token \
--organization-id <organization ID> \
--service-account-id <service account ID> \
--subject-token-file token.jwt
Export the result as AKUITY_SERVICE_ACCOUNT_TOKEN and every other akuity command authenticates with it. The same variable is honored by the Terraform provider.
You can also call the exchange endpoint directly. It uses the parameter names of RFC 8693 token exchange but takes a JSON body and requires the service account ID, so it is not a drop-in OAuth token endpoint:
curl -sS -X POST "https://akuity.cloud/api/v1/organizations/<organization ID>/oauth/token" \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "<OIDC token>",
"service_account_id": "<service account ID>"
}'
{
"access_token": "<credential>",
"issued_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_type": "Bearer",
"expires_in": 3600
}
Send the credential as Authorization: Bearer <credential> on API requests. It expires after the issuer's token TTL. Exchange a new one for each run rather than storing it.
Examples
- GitHub Actions
- Kubernetes
GitHub Actions gives every job an OIDC token when the workflow requests the id-token: write permission. Register https://token.actions.githubusercontent.com as an issuer with the Discovery key source, and bind a service account with a sub pattern such as repo:acme/app:ref:refs/heads/main.
The token's sub claim depends on how the job runs: repo:<owner>/<repo>:ref:refs/heads/<branch> for a branch, repo:<owner>/<repo>:ref:refs/tags/<tag> for a tag, repo:<owner>/<repo>:pull_request for a pull request, and repo:<owner>/<repo>:environment:<name> for a job that targets a GitHub environment. See GitHub's OIDC documentation for every claim available.
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
env:
AKUITY_ORG_ID: <organization ID>
AKUITY_SERVICE_ACCOUNT_ID: <service account ID>
steps:
- name: Install the Akuity CLI
run: |
curl -sSL -o akuity "https://dl.akuity.io/akuity-cli/$(curl -sL https://dl.akuity.io/akuity-cli/stable.txt)/linux/amd64/akuity"
chmod +x akuity && sudo mv akuity /usr/local/bin/akuity
- name: Authenticate with the Akuity Platform
run: |
audience="https://akuity.cloud/api/v1/organizations/$AKUITY_ORG_ID"
token=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$audience" | jq -r .value)
credential=$(echo "$token" | akuity service-account exchange-token \
--organization-id "$AKUITY_ORG_ID" \
--service-account-id "$AKUITY_SERVICE_ACCOUNT_ID" \
--subject-token-file -)
echo "::add-mask::$credential"
echo "AKUITY_SERVICE_ACCOUNT_TOKEN=$credential" >> "$GITHUB_ENV"
- name: Use the CLI
run: akuity argocd instance list --organization-id "$AKUITY_ORG_ID"
The audience query parameter makes GitHub put the issuer's default audience into the token. Without it GitHub uses the repository URL, and the exchange is refused.
Every Kubernetes cluster is an OIDC issuer for its own service accounts. A pod can request a token for a specific audience through a projected volume, and the cluster signs it with its service account keys.
First register the cluster as an issuer. Read its issuer URL and keys with cluster admin access:
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
kubectl get --raw /openid/v1/jwks > jwks.json
For managed clusters (EKS, GKE, AKS) the issuer URL is public and the Discovery key source works. For clusters whose issuer URL is private, such as https://kubernetes.default.svc.cluster.local, choose the Static JWKS key source and paste the contents of jwks.json. Keep the issuer URL exactly as the cluster reports it.
Then create a service account bound to that issuer with the sub pattern system:serviceaccount:<namespace>:<service account name>.
In the cluster, mount a projected token whose audience is the issuer's default audience. This job exchanges it and runs a CLI command:
apiVersion: v1
kind: ServiceAccount
metadata:
name: deployer
namespace: ci
---
apiVersion: batch/v1
kind: Job
metadata:
name: akuity-sync
namespace: ci
spec:
template:
spec:
serviceAccountName: deployer
restartPolicy: Never
volumes:
- name: akuity-token
projected:
sources:
- serviceAccountToken:
path: token
audience: https://akuity.cloud/api/v1/organizations/<organization ID>
expirationSeconds: 600
containers:
- name: akuity
image: alpine:3.20
volumeMounts:
- name: akuity-token
mountPath: /var/run/akuity
readOnly: true
command: ["/bin/sh", "-c"]
args:
- |
apk add --no-cache curl >/dev/null
curl -sSL -o /usr/local/bin/akuity "https://dl.akuity.io/akuity-cli/$(curl -sL https://dl.akuity.io/akuity-cli/stable.txt)/linux/amd64/akuity"
chmod +x /usr/local/bin/akuity
export AKUITY_SERVICE_ACCOUNT_TOKEN=$(akuity service-account exchange-token \
--organization-id <organization ID> \
--service-account-id <service account ID> \
--subject-token-file /var/run/akuity/token)
akuity argocd instance list --organization-id <organization ID>
The kubelet refreshes the projected token before it expires, so a long-running pod can exchange a fresh token whenever its Akuity credential runs out.
Revoking credentials
Credentials are short-lived, but you can cut them off early:
- Select Revoke tokens on a service account to invalidate every credential issued to it. New exchanges keep working.
- Disable the service account to stop both existing credentials and new exchanges.
- Changing a service account's issuer or claim match, or changing an issuer's URL or keys, also revokes the credentials issued under the old settings. The affected workloads simply exchange a new token on their next run.
Every change to issuers and service accounts is recorded in the organization's audit logs, and actions taken with a credential are attributed to the service account.