Skip to content

Configure service account clients

You’ll configure a Keycloak client using the OAuth 2.0 Client Credentials Grant so that automated processes — CI/CD pipelines, backend services, and scripts — can obtain tokens and access SSO-protected applications without a user session.

  • UDS Core deployed
  • UDS CLI installed
  • A UDS Package CR for the workload that needs machine-to-machine access
  • The clientId of the target SSO-protected application (used as the token audience)

Service account tokens (Client Credentials Grant) are designed for machine-to-machine authentication where there is no interactive user. Key characteristics:

  • Tokens have a service-account- username prefix and include a client_id claim
  • The aud (audience) claim is not set by default — you must add an audience mapper to allow the token to access a specific SSO-protected application
  • serviceAccountsEnabled: true requires standardFlowEnabled: false and is incompatible with publicClient: true
  1. Add a service account client to the Package CR

    Configure an SSO client with serviceAccountsEnabled: true and an audience mapper pointing to the target Authservice client:

    package.yaml
    apiVersion: uds.dev/v1alpha1
    kind: Package
    metadata:
    name: my-automation
    namespace: argo
    spec:
    sso:
    - name: httpbin-api-client
    clientId: httpbin-api-client
    standardFlowEnabled: false
    serviceAccountsEnabled: true
    protocolMappers:
    - name: audience
    protocol: "openid-connect"
    protocolMapper: "oidc-audience-mapper"
    config:
    # Set to the clientId of the Authservice-protected application
    included.client.audience: "uds-core-httpbin"
    access.token.claim: "true"
    introspection.token.claim: "true"
    id.token.claim: "false"
    lightweight.claim: "false"
    userinfo.token.claim: "false"
  2. Apply the Package CR

    Terminal window
    uds zarf tools kubectl apply -f package.yaml

    The UDS Operator creates the Keycloak client and stores the client secret in a Kubernetes secret in the application namespace.

  3. Retrieve the client secret

    The client secret is stored in a Kubernetes secret named sso-client-<client-id>:

    Terminal window
    # Linux
    uds zarf tools kubectl get secret -n <namespace> sso-client-<client-id> -o jsonpath='{.data.secret}' | base64 -d
    # macOS
    uds zarf tools kubectl get secret -n <namespace> sso-client-<client-id> -o jsonpath='{.data.secret}' | base64 -D
  4. (Optional) Configure multiple audiences

    If a service account token needs access to multiple Authservice-protected applications, add separate audience mappers for each target.

    package.yaml
    spec:
    sso:
    - name: multi-target-client
    clientId: multi-target-client
    standardFlowEnabled: false
    serviceAccountsEnabled: true
    defaultClientScopes:
    - openid
    protocolMappers:
    - name: audience-app-1
    protocol: "openid-connect"
    protocolMapper: "oidc-audience-mapper"
    config:
    included.custom.audience: "uds-core-app-1"
    access.token.claim: "true"
    introspection.token.claim: "true"
    id.token.claim: "true"
    lightweight.claim: "true"
    userinfo.token.claim: "true"
    - name: audience-app-2
    protocol: "openid-connect"
    protocolMapper: "oidc-audience-mapper"
    config:
    included.custom.audience: "uds-core-app-2"
    access.token.claim: "true"
    introspection.token.claim: "true"
    id.token.claim: "true"
    lightweight.claim: "true"
    userinfo.token.claim: "true"

Confirm the service account client is configured correctly:

  1. Log in to the Keycloak admin UI (uds realm)
  2. Go to Clients and find your client ID
  3. Verify Service accounts roles is On and Standard flow is Off

Test token retrieval:

Terminal window
# Replace <domain>, <client-id>, and <client-secret> with your values
curl -s -X POST \
"https://sso.<domain>/realms/uds/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=<client-id>" \
-d "client_secret=<client-secret>" \
| jq .

A successful response includes an access_token. Verify the aud claim includes the expected audience:

Terminal window
# Extract and decode the access token payload
# Linux
echo "<access_token>" | cut -d. -f2 | base64 -d 2>/dev/null | jq .aud
# macOS
echo "<access_token>" | cut -d. -f2 | base64 -D 2>/dev/null | jq .aud

Alternatively, paste the token into jwt.io for a visual breakdown.

Problem: 401 when accessing an Authservice-protected application

Section titled “Problem: 401 when accessing an Authservice-protected application”

Symptoms: Token is obtained successfully but the application returns 401.

Solution: Verify the audience mapper is pointing to the correct target. The included.client.audience value must match the clientId of the target application’s Authservice SSO client — not this service account client’s own clientId.

Check the decoded token’s aud claim, or paste it into jwt.io to inspect it visually:

Terminal window
# Decode the access token payload (replace TOKEN with the actual token value)
# Linux
echo "TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .aud
# macOS
echo "TOKEN" | cut -d. -f2 | base64 -D 2>/dev/null | jq .aud

Problem: serviceAccountsEnabled: true rejected by the operator

Section titled “Problem: serviceAccountsEnabled: true rejected by the operator”

Symptoms: Package CR fails to apply with a validation error.

Solution: Ensure standardFlowEnabled is set to false and publicClient is not set to true. Both are incompatible with service accounts:

sso:
- name: my-service-client
clientId: my-service-client
standardFlowEnabled: false # Required
serviceAccountsEnabled: true
# publicClient: true # Do not set — incompatible with service accounts

Problem: Client secret is not found in the namespace

Section titled “Problem: Client secret is not found in the namespace”

Symptoms: The expected Kubernetes secret does not exist after applying the Package CR.

Solution: Check the UDS Operator logs for errors during client creation:

Terminal window
uds zarf tools kubectl logs -n pepr-system -l app=pepr-uds-core-watcher --tail=50 | grep <client-id>

These guides and concepts may be useful to explore next: