Service-to-Service Authentication
This document describes how internal services authenticate with each other using short-lived JWTs issued by Oathkeeper. The same middleware supports both user and service identities.
Overview
- Services use Kubernetes service account (SA) tokens to request a short-lived JWT from Oathkeeper.
- Oathkeeper validates the SA token via the JWT authenticator, mints a service JWT, and forwards it via the token-reflector.
- Services include the service JWT in
Authorization: Bearer <token>when calling other internal services. - The receiving service validates the JWT and attaches a
ServiceIdentityto the request context.
Identity Model
The middleware attaches a domain.Identity to the request context:
UserIdentityfor human users authenticated via Kratos.ServiceIdentityfor internal services authenticated via Kubernetes SA.
The subject (JWT sub, short for "subject") is the primary identifier:
- User: stable unique user ID from the identity provider.
- Service:
system:serviceaccount:<namespace>:<name>.
Token Exchange Flow
- Service reads its projected SA token at
/var/run/secrets/tokens/token. - Service calls the Oathkeeper token exchange endpoint:
GET http://oathkeeper-proxy.default:4455/token-exchange/<target-service>Authorization: Bearer <sa-token>
- Oathkeeper validates the SA token using the JWT authenticator and the Kubernetes JWKS served by token-reflector.
- Oathkeeper mints a service JWT and forwards the request to token-reflector.
- Token-reflector returns a JSON response:
{
"access_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 3600
}
- Caller uses the JWT to call the target service.
Token Claims
Service JWTs include:
sub: full service account nameaud: array with the target service nametype:servicenamespace: inferred fromsub
User JWTs include:
sub: user IDtype:usersession: Kratos session traits
Middleware Responsibilities
Authn:
VerifyJWTverifies the JWT signature (JWKS).IdentitybuildsUserIdentityorServiceIdentityand attaches it to context.
Authz:
RequireServiceAudienceenforces the service audience using the configured service name.- Each service sets a default via envconfig:
service_namedefaults to the service's name (for example,immersion-api).
- Each service sets a default via envconfig:
RejectBannedUsersblocks banned users (except/current-user/role).
Local Development Setup
- Each service runs in its own namespace prefixed with
tdk-. - Each service has its own ServiceAccount.
token-reflectorruns intdk-token-reflector.- Oathkeeper validates SA tokens using a JWKS URL served by token-reflector:
http://token-reflector.tdk-token-reflector/jwks
Example: immersion-api -> profile-api
immersion-apirequests a token forprofile-apivia Oathkeeper.immersion-apicallshttp://profile-api.tdk-profile-api/internal/v1/pingwith the JWT.profile-apivalidates the JWT and checks the service audience.
Quickstart (Calling Another Service)
- Ensure your config includes
oathkeeper_urlandservice_name(defaults are set per service). - Initialize the S2S client.
- Use the generated internal client with a custom HTTP transport.
Using a Generated Internal Client (Preferred)
import (
"net/http"
"github.com/tadoku/tadoku/services/common/client/s2s"
profileclient "github.com/tadoku/tadoku/services/profile-api/http/rest/openapi/internalapi"
)
s2sClient := s2s.NewClient(cfg.OathkeeperURL)
httpClient := &http.Client{
Transport: s2s.NewAuthTransport(s2sClient, "profile-api", nil),
}
client, err := profileclient.NewClient(
"http://profile-api.tdk-profile-api",
profileclient.WithHTTPClient(httpClient),
)
if err != nil {
return err
}
resp, err := client.InternalPing(ctx)
if err != nil {
return err
}
_ = resp
Failure Modes
- Invalid audience:
403 Forbidden - Missing/invalid JWT:
401 Unauthorized - Missing service name configuration: service tokens rejected (
403) - Token-reflector unavailable: token exchange returns
502 Bad Gateway