Container Deployment
Run oidc-exchange as a long-lived container in ECS, EKS, Cloud Run, or any container orchestrator. 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.96-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:bookworm-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 handles all of this.
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 KMS: if running in AWS (ECS/EKS), use
adapter = "kms"with IAM roles for service accounts or task 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 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 a KMS key 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 production-grade ECS Fargate with auto-scaling and ALB, see the dedicated 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.