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).


[EARLY DRAFT RELEASE] Copyright 2020-2026 Telicent Limited. All rights reserved