Confidential Clients
A confidential client is an OAuth2 client that can securely hold a secret and authenticate itself to the Auth Server without a user being present. This makes them suitable for machine-to-machine (M2M) communication — for example, a background service that needs to call a Telicent API on its own behalf.
Confidential clients use the OAuth2 Client Credentials grant (client_credentials). Unlike public clients, they do not redirect users or use PKCE. Instead, they present a client_id and client_secret directly to the token endpoint to obtain an access token.
To complete the M2M flow, a confidential client is linked to a service account, which provides the identity and permissions encoded in the issued token.
Seed data: registering a confidential client
Confidential clients are registered by including them in the clients.confidential list of the clients seed file. The Auth Server reads this file at startup and creates any clients that do not already exist.
Important: Seed data is processed on every startup but only creates clients — it does not update clients that already exist in the database. If you need to change a client’s configuration after its first deployment, use the Client API or, for secret rotation, the secret management endpoints described below.
Field reference
| Field | Required | Default | Description |
|---|---|---|---|
client_id | Yes | — | Unique identifier for the client. Must be unique across all clients. |
client_secret_env_var_name | Yes | — | Name of the environment variable that contains the plaintext client secret. The Auth Server reads this variable at startup, hashes the value, and stores the result. |
client_name | No | Confidential Client: {client_id} | Human-readable display name. |
client_authentication_method | No | client_secret_basic | Authentication method used when the client presents its credentials. Supported values: client_secret_basic, client_secret_post. |
access_token_ttl_minutes | No | 30 | Lifetime of issued access tokens in minutes. |
scope | No | — | Space-separated list of OAuth2 scopes the client is permitted to request. |
Example seed file
clients:
public: []
confidential:
- client_id: my-service
client_name: My Service
client_secret_env_var_name: MY_SERVICE_CLIENT_SECRET
client_authentication_method: client_secret_post
access_token_ttl_minutes: 60
scope: read
The plaintext secret is never stored. The Auth Server reads MY_SERVICE_CLIENT_SECRET from the environment, BCrypt-hashes it, and persists only the hash. The plaintext value is not recoverable from the database.
Helm chart configuration
When deploying with the Telicent Core Helm chart, confidential clients are configured through the bootstrap section of values.yaml.
Supplying the client secret
Because the client secret must be injected as an environment variable, you should store it in a Kubernetes Secret and reference it via extraEnvVars:
bootstrap:
clients:
confidential:
- client_id: my-service
client_name: My Service
client_secret_env_var_name: MY_SERVICE_CLIENT_SECRET
client_authentication_method: client_secret_post
scope: read
extraEnvVars:
- name: MY_SERVICE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: my-service-client-secret
key: client_secret
The Kubernetes Secret must exist in the same namespace before the Auth Server starts. Create it separately:
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: my-service-client-secret
stringData:
client_secret: "<your-secret-here>"
Using an existing ConfigMap for clients
If you manage your full client list outside of Helm values, you can supply a pre-existing ConfigMap and bypass the generated one entirely:
bootstrap:
clients:
existingConfigMap: "my-clients-configmap"
When existingConfigMap is set, the bootstrap.clients.public and bootstrap.clients.confidential values are ignored. The ConfigMap must contain a clients.yaml key whose value follows the seed file format shown above.
Managed platform clients
The Auth chart automatically manages Kubernetes Secrets for certain platform services that require M2M access to the Auth Server. Each such secret is auto-generated on first install and preserved across upgrades. Refer to the documentation for each individual platform service to find which bootstrap.clients sub-section governs its secret and which environment variable name is pre-injected into the Auth Server pod.
The general pattern is the same in all cases: a Kubernetes Secret is created (or you reference an existing one), the plaintext value is injected into the Auth Server pod as a named environment variable, and you register the client by adding an entry to bootstrap.clients.confidential that references that variable:
bootstrap:
clients:
confidential:
- client_id: my-platform-service
client_name: My Platform Service
client_secret_env_var_name: MY_SERVICE_CLIENT_SECRET
client_authentication_method: client_secret_post
scope: write
Propagating secrets to client services
The Auth Server stores only a BCrypt hash of the client secret, so the same plaintext value that was used to register the client must be made available to the service that will authenticate with it. When a Kubernetes Secret is created by the Auth chart, it must also be mounted or injected into the client service’s pod.
Two patterns are common, depending on how the consuming service is configured.
Pattern 1: Direct Kubernetes Secret reference
Some services read their client secret directly from a named Kubernetes Secret (for example, via a secretKeyRef in their own deployment template). When the service chart and the Auth chart both default to the same secret name, no extra wiring is needed as long as they are deployed in the same namespace. If you override the secret name in the Auth chart, set the corresponding value in the service chart to the same name:
# Auth chart: override the auto-generated secret
bootstrap:
clients:
some-service:
existingSecret: "my-service-secret" # must contain key: client_secret
# Service chart: reference the same secret
some-service:
config:
oidcExistingSecret: "my-service-secret"
Pattern 2: Environment variable injection
Other services expect the client secret as a named environment variable. Use extraEnvVars with a secretKeyRef to inject the secret from the Kubernetes Secret into the client service’s pod:
some-service:
extraEnvVars:
- name: CLIENT_SECRET # must match the env var name the service reads
valueFrom:
secretKeyRef:
name: my-service-secret # Kubernetes Secret containing key: client_secret
key: client_secret
Consult each service’s own documentation for the exact environment variable name it expects.
Internal networking with Istio
All communication between services inside a Telicent CORE deployment and the Auth Server travels over the Kubernetes cluster network using the Auth Server’s internal service DNS name (for example http://auth:8080). The Istio service mesh enforces mutual TLS (mTLS) between pods, but mTLS operates at the transport layer — the application-layer scheme seen by the Auth Server is plain HTTP.
This matters because the Auth Server is built on Spring Authorization Server, which by default expects OAuth2 endpoints to be accessed over HTTPS. When a service calls the token endpoint at http://auth:8080/oauth2/token without signalling that the original request was over HTTPS, Spring Authorization Server rejects the request.
The fix: X-Forwarded-Proto header
The Auth Server runs a ForwardedHeaderFilter (registered by ProxyAwareConfig) at the highest servlet precedence. This filter reads the X-Forwarded-Proto header and rewrites the request so that Spring sees the connection as HTTPS regardless of the transport-layer scheme. You must add this header to every request from a client service to the Auth Server’s token endpoint:
X-Forwarded-Proto: https
How you add this header depends on the HTTP client library used by the calling service. For example, in a Java application using Java’s java.net.http.HttpRequest:
HttpRequest request = HttpRequest.newBuilder()
.header("Content-Type", "application/x-www-form-urlencoded");
For Traefik-proxied browser flows, this header is already injected by the headers-ssl middleware configured in the telicent-core Traefik chart — only direct service-to-service calls that bypass Traefik require it to be set explicitly.
Istio authorization policy
The Auth Server’s Istio AuthorizationPolicy uses an explicit allow-list of ServiceAccount principals permitted to call it. Platform services that require internal access are already included. If you are integrating a new service, ensure its Kubernetes ServiceAccount principal is added to an AuthorizationPolicy that allows access to the Auth Server, otherwise Istio will block the request before it reaches the application.
Linking a service account
A confidential client on its own only authenticates the calling application. To obtain a token with meaningful permissions, the client must be linked to a service account. The service account provides the roles, permissions, and groups that are encoded into the issued token.
You can link a service account in two ways.
Via seed data
Add the service account to the bootstrap.serviceAccounts list in values.yaml, and set clientId to the client_id of the confidential client:
bootstrap:
serviceAccounts:
- id: svc-my-service
name: svc-my-service
description: Service account for My Service
active: true
clientId: my-service
roles: ["USER"]
permissions: []
groups: []
attributes: {}
Unlike client seed data, service account seed data is applied on every startup — existing service accounts are updated with any changes in the seed file.
roles, permissions, and groups may be specified by name. The Auth Server resolves them to internal IDs at startup. If any referenced name cannot be resolved, the application will fail to start.
Via the Admin API
After deployment, you can link or unlink a service account using the Admin API:
PUT /service-accounts/{id}/clients/{clientId}
DELETE /service-accounts/{id}/clients/{clientId}
Both endpoints require the users.write permission.
Managing secrets after deployment
Initial secret provisioning via seed data
When the Auth Server starts for the first time and the confidential client does not yet exist in the database, it is created with the hashed value of the environment variable named in client_secret_env_var_name. This is the only time seed data sets the client secret — subsequent restarts skip clients that already exist.
Rotating or replacing secrets
Once a client has been created, use the Admin API secret management endpoints to issue or change its secret. These endpoints require the client.write permission.
| Action | Endpoint | Description |
|---|---|---|
| Issue initial secret | POST /clients/{clientId}/secret | Generates a new secret. Returns the plaintext value once only. |
| Rotate secret | POST /clients/{clientId}/secret/rotate | Replaces the current secret. The previous secret remains valid for a configurable grace period. |
| Delete a secret slot | DELETE /clients/{clientId}/secret?target={current|previous} | Removes the current or previous secret immediately. |
| Check secret status | GET /clients/{clientId}/secret | Returns metadata only — does not reveal the secret value. |
The rotation grace period is controlled by the APP_CLIENT_SECRET_PREVIOUS_GRACE_WINDOW environment variable (default: 7 days). During this window, both the old and new secrets are valid, giving consuming services time to update their credentials without an outage.
Note: Secret values are one-time returns. If you lose the value returned by generate or rotate, you cannot retrieve it — you must rotate again.
Secret lifecycle configuration
| Environment variable | Default | Description |
|---|---|---|
APP_CLIENT_SECRET_TTL | P365D | Lifetime of a client secret before it is considered expired. |
APP_CLIENT_SECRET_PREVIOUS_GRACE_WINDOW | P7D | How long the previous secret remains valid after rotation. |
APP_CLIENT_SECRET_ROTATION_REQUIRED_BEFORE_EXPIRY | P28D | Lead time before expiry at which the metadata endpoint signals rotationRequired: true. |
Values follow ISO 8601 duration format (for example, P30D for 30 days, PT2H for 2 hours).