Container Deployment
Run oidc-exchange as a long-lived container on a service such as Amazon Elastic Container Service (ECS), Amazon Elastic Kubernetes Service (EKS), or Cloud Run. The binary runs as an axum HTTP server when AWS_LAMBDA_RUNTIME_API is not set.
A runnable example is in examples/container/.
Dockerfile
Section titled “Dockerfile”FROM rust:1.99-slim AS builderWORKDIR /appRUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*COPY . .RUN cargo build --release --bin oidc-exchange
FROM debian:trixie-slimRUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/*COPY --from=builder /app/target/release/oidc-exchange /usr/local/bin/COPY config/ /app/config/WORKDIR /appEXPOSE 8080ENV OIDC_EXCHANGE_ENV=productionCMD ["oidc-exchange"]The build needs pkg-config and libssl-dev because the HTTP client links the system OpenSSL. WORKDIR /app matters at runtime: the binary loads config from config/ relative to its working directory, so the process must run in the directory that holds the copied config/ tree. For a prebuilt image, the published ghcr.io/antstanley/oidc-exchange (pinned by digest) already sets WORKDIR /app and the binary, but it ships no configuration: mount or bake in a config/ directory containing default.toml.
Configuration
Section titled “Configuration”Create config/production.toml:
[server]host = "0.0.0.0"port = 8080issuer = "https://auth.example.com"
[key_manager]adapter = "local"
[key_manager.local]private_key_path = "/etc/secrets/signing-key.pem"algorithm = "EdDSA"kid = "prod-1"
[repository]adapter = "dynamodb"
[repository.dynamodb]table_name = "oidc-exchange"region = "us-east-1"
[audit]adapter = "stdout"durability = "enforce"emit_threshold = "info"
[telemetry]enabled = trueexporter = "otlp"
[providers.google]adapter = "oidc"issuer = "https://accounts.google.com"client_id = "${GOOGLE_CLIENT_ID}"client_secret = "${GOOGLE_CLIENT_SECRET}"scopes = ["openid", "email", "profile"]# Origins Google's discovery document may name beyond the issuer's origin:endpoint_origins = ["https://oauth2.googleapis.com", "https://www.googleapis.com"]endpoint_origins pins which origins a provider’s discovery document is allowed to name; each entry must be a bare https://host[:port], and an unpinned origin logs a warning when discovered (see Identity Providers).
Key management
Section titled “Key management”Containers give you flexibility in key management:
- Local keys: mount a signing key via a volume or Kubernetes secret. Use
adapter = "local", which supports Ed25519 (EdDSA) only. ECDSA or RSA signing requires the KMS adapter. - AWS Key Management Service (KMS): On ECS or EKS, use
adapter = "kms". Grant access through Identity and Access Management (IAM) task roles or service account roles.
Docker Compose
Section titled “Docker Compose”services: oidc-exchange: build: . ports: - "8080:8080" environment: OIDC_EXCHANGE_ENV: production GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} AWS_REGION: us-east-1 volumes: - ./keys:/etc/secrets:ro healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 5s retries: 3
dynamodb-local: image: amazon/dynamodb-local ports: - "8000:8000"ECS Fargate
Section titled “ECS Fargate”Build and push the image to Amazon Elastic Container Registry (ECR):
docker build -t oidc-exchange .docker tag oidc-exchange:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/oidc-exchange:latestaws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.comdocker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/oidc-exchange:latestIn your task definition, set the key configuration:
- Use
adapter = "kms"with the key’s Amazon Resource Name (ARN) and attach the appropriate IAM policy to the task role. - Alternatively, inject a local signing key via AWS Secrets Manager into the container environment.
For ECS Fargate with automatic scaling and an Application Load Balancer (ALB), see the ECS Fargate guide.
Kubernetes
Section titled “Kubernetes”apiVersion: apps/v1kind: Deploymentmetadata: name: oidc-exchangespec: replicas: 2 selector: matchLabels: app: oidc-exchange template: metadata: labels: app: oidc-exchange spec: automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 fsGroup: 65532 seccompProfile: type: RuntimeDefault containers: - name: oidc-exchange image: your-registry/oidc-exchange@sha256:<pin-to-a-published-digest> securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ALL] ports: - containerPort: 8080 env: - name: OIDC_EXCHANGE_ENV value: production - name: GOOGLE_CLIENT_ID valueFrom: secretKeyRef: name: oidc-exchange-secrets key: google-client-id - name: GOOGLE_CLIENT_SECRET valueFrom: secretKeyRef: name: oidc-exchange-secrets key: google-client-secret volumeMounts: - name: signing-key mountPath: /etc/secrets readOnly: true - name: tmp mountPath: /tmp livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 30 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 2 periodSeconds: 10 resources: requests: cpu: 100m memory: 64Mi limits: cpu: 500m memory: 128Mi volumes: - name: signing-key secret: secretName: oidc-exchange-signing-key defaultMode: 0440 - name: tmp emptyDir: {}---apiVersion: v1kind: Servicemetadata: name: oidc-exchangespec: selector: app: oidc-exchange ports: - port: 80 targetPort: 8080 type: ClusterIPScaling
Section titled “Scaling”oidc-exchange is stateless (all state is in the configured database). Scale horizontally without coordination. Each instance holds an in-memory JWKS cache for upstream providers; this warms up on first request per provider and refreshes automatically.