Kubernetes (Helm)

TL;DR — Official Helm chart at oci://ghcr.io/authplane/charts/authplane. Ships with an optional Bitnami PostgreSQL subchart, split ingresses (public OAuth on :9000 + internal admin on :9001), Vault Transit signing support, ServiceMonitor for Prometheus, OTEL wiring. Migrations run automatically on pod boot. HA-ready with autoscaling.enabled — Vault Transit recommended when running multi-replica to avoid the shared-PVC problem.

Prerequisites

  • Kubernetes 1.26+
  • Helm 3.8+
  • A PostgreSQL instance — either the Bitnami subchart the chart ships (dev/testing) or your own external DB (production)

Quick Start

helm install authplane oci://ghcr.io/authplane/charts/authplane \
  --version 0.1.0 \
  -f values-production.yaml

From source (development)

git clone https://github.com/authplane/authserver.git
cd authserver

# Update chart dependencies (downloads Bitnami PostgreSQL subchart)
helm dependency update charts/authplane

# Install with built-in PostgreSQL
helm install authplane charts/authplane \
  --set config.server.issuer=https://auth.example.com \
  --set postgresql.enabled=true \
  --set postgresql.auth.password=changeme \
  --set secrets.sessionSecret=$(openssl rand -hex 32) \
  --set secrets.adminApiKey=$(openssl rand -hex 32)

Storage modes

PostgreSQL (production)

Recommended for production. Either the Bitnami subchart or your own external DB works; either way the chart auto-adds an init container that waits for PostgreSQL to be ready before starting AuthPlane.

Option A — Bitnami subchart (dev/testing convenience):

# values-dev.yaml
config:
  server:
    issuer: https://auth.example.com
  storage:
    driver: postgres

postgresql:
  enabled: true
  auth:
    username: authplane
    password: changeme
    database: authplane

secrets:
  sessionSecret: "generate-with-openssl-rand-hex-32"
  adminApiKey: "generate-with-openssl-rand-hex-32"
helm install authplane charts/authplane -f values-dev.yaml

Option B — External PostgreSQL (production):

# values-production.yaml
config:
  server:
    issuer: https://auth.example.com
  storage:
    driver: postgres

externalDatabase:
  host: postgres.database.svc
  port: 5432
  user: authplane
  password: secret
  database: authplane
  sslmode: require
  # Or use a pre-existing Secret with the full DSN:
  # existingSecret: my-db-secret
  # existingSecretKey: dsn

secrets:
  existingSecret: authplane-secrets  # pre-created with session-secret + admin-api-key

SQLite (dev / single-node)

SQLite mode requires persistent storage and is limited to a single replica.

# values-sqlite.yaml
replicaCount: 1

config:
  server:
    issuer: http://localhost:9000
  storage:
    driver: sqlite

persistence:
  enabled: true
  size: 1Gi

secrets:
  sessionSecret: "generate-with-openssl-rand-hex-32"
  adminApiKey: "generate-with-openssl-rand-hex-32"

Warning — SQLite doesn’t support multiple replicas. The chart’s NOTES.txt warns if replicaCount > 1 while driver: sqlite.

Cache propagation window — with driver: sqlite, changes made via the Admin API (new resource servers, scope updates, broker-provider config) take up to 30 seconds to become visible to /.well-known/oauth-authorization-server and the token endpoint. SQLite has no LISTEN/NOTIFY. PostgreSQL propagates in milliseconds via PG NOTIFY. Usually only matters during initial bring-up.

Migrations

Migrations run automatically on pod startup. The AuthPlane binary embeds all migration SQL via go:embed and applies pending migrations before serving traffic. No separate Job needed. The chart’s init container waits for PostgreSQL connectivity; the main process handles the migrations themselves.

If you want to run migrations manually (e.g., during a large upgrade):

kubectl exec -it deploy/authplane -- /authserver migrate

OIDC Federation (Google, Okta, Entra)

Users see a “Continue with [Provider]” button on the login page. Full setup in Guides: Federate to your IdP; the chart-specific bits:

Google Workspace

config:
  oidc:
    enabled: true
    issuer: https://accounts.google.com
    client_id: "YOUR_GOOGLE_CLIENT_ID"
    client_secret: "YOUR_GOOGLE_CLIENT_SECRET"
    display_name: "Google Workspace"
    scopes: [openid, email, profile]
    include_groups_scope: false     # Google doesn't support groups scope

Okta

config:
  oidc:
    enabled: true
    issuer: https://your-org.okta.com
    client_id: "YOUR_OKTA_CLIENT_ID"
    client_secret: "YOUR_OKTA_CLIENT_SECRET"
    display_name: "Okta"
    scopes: [openid, email, profile]
    include_groups_scope: true      # Okta supports groups

Secrets management for OIDC client secrets

Don’t put client secrets in values.yaml. Two options:

Env-var injection — reference a pre-created Kubernetes Secret:

config:
  oidc:
    enabled: true
    client_secret_ref: AUTHPLANE_OIDC_CLIENT_SECRET

extraEnv:
  - name: AUTHPLANE_OIDC_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: oidc-credentials
        key: client-secret

Full config as sealed Secret — for GitOps workflows:

existingConfigSecret: my-sealed-config

Vault Transit signing (HSM-grade)

AuthPlane supports HashiCorp Vault Transit for JWT signing — private keys never leave Vault. Recommended for multi-replica deployments (avoids the shared-PVC problem) and any compliance environment.

vault:
  signing:
    enabled: true
    address: https://vault.vault.svc:8200
    mount: transit
    keyName: authserver-signing
    auth:
      method: approle
      approle:
        roleId: "..."
        secretId: "..."
    # Or a pre-existing Secret with keys:
    #   vault-token                    (token auth)
    #   vault-approle-role-id + vault-approle-secret-id  (approle)
    # existingSecret: vault-signing-creds

Setting vault.signing.enabled=true auto-sets AUTHPLANE_SIGNING_KEY_STORE=vault_transit and injects the connection env vars.

Vault auth patterns in Kubernetes

  • AppRole (recommended) — inject roleId / secretId via existingSecret or External Secrets Operator.
  • Vault Agent Sidecar — Vault Agent annotations via podAnnotations; mount injected secrets via extraVolumes / extraVolumeMounts.
  • Vault CSI Provider — mount secrets as volumes via extraVolumes.

The chart does NOT include a Vault subchart — Vault is infrastructure your organization already runs.

Full walkthrough in Operate: Vault Transit.

Ingress — split public + admin

The chart provides separate ingress resources for OAuth (:9000) and Admin (:9001). These are different security boundaries — the Admin API should have restricted access.

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: auth.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: auth-tls
      hosts:
        - auth.example.com

adminIngress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8
  hosts:
    - host: auth-admin.internal.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: auth-admin-tls
      hosts:
        - auth-admin.internal.example.com

Without an admin ingress, port-forward for the UI:

kubectl port-forward svc/authplane 9001:9001
# Open http://localhost:9001/admin/ui/

Secrets management

The chart handles three categories of secrets.

1. Session secret + Admin API key

# Option A — Inline (dev only; NOT stable across helm upgrade)
secrets:
  sessionSecret: "generate-with-openssl-rand-hex-32"
  adminApiKey: "generate-with-openssl-rand-hex-32"

# Option B — Pre-existing Secret (production)
secrets:
  existingSecret: authplane-secrets
  # Must contain keys: session-secret, admin-api-key

Warning — auto-generated secrets change on every helm upgrade. Always use explicit values or existingSecret for production; regenerated secrets invalidate every active session on upgrade.

2. Full config as Secret

The chart renders the full AuthPlane config as a Kubernetes Secret (not ConfigMap) because it can contain sensitive values (DSN, Vault tokens). For GitOps:

existingConfigSecret: my-sealed-config

3. Database password

# Option A — Inline
externalDatabase:
  password: secret

# Option B — Pre-existing Secret with the full DSN
externalDatabase:
  existingSecret: my-db-secret
  existingSecretKey: dsn

Observability

Prometheus (ServiceMonitor)

config:
  observability:
    metrics:
      provider: prometheus
      path: /metrics

serviceMonitor:
  enabled: true
  interval: 15s

OpenTelemetry (logs + traces + metrics)

config:
  observability:
    logging:
      outputs:
        otel: true
        otel_endpoint: otel-collector.monitoring:4317
        insecure: true
    tracing:
      enabled: true
      endpoint: otel-collector.monitoring:4317
      insecure: true
      sample_rate: 1.0
    metrics:
      provider: both
      otel_endpoint: otel-collector.monitoring:4317
      insecure: true

Full metric catalog and Grafana dashboards in Guides: Monitoring.

Production checklist

SettingRecommendation
config.server.issuerSet to your external HTTPS URL — no trailing slash
config.session.securetrue (requires HTTPS)
config.storage.driverpostgres
secrets.existingSecretPre-created Secret (not inline)
externalDatabase.sslmoderequire or verify-full
ingress.tlsConfigured with cert-manager
adminIngressRestricted via IP allowlist or separate internal ingress
autoscaling.enabledtrue with minReplicas: 2
podDisruptionBudget.enabledtrue
networkPolicy.enabledtrue
resourcesSet both requests and limits
vault.signing.enabledtrue when multi-replica

Scaling

  • PostgreSQL mode — AuthPlane is stateless. Scale horizontally with HPA.
  • Signing keys — use Vault Transit (vault.signing.enabled: true) for multi-replica. With keyfile, all replicas share the PVC (ReadWriteMany required, or co-locate pods on the same node).
  • Session affinity — not required. Sessions are stored in signed cookies, not server-side.
  • Database connections — total = config.storage.postgres.max_conns × replicas. Size your Postgres accordingly.

Local testing with Kind

For iterating on the chart itself, kind is the fast local loop. Key steps:

kind create cluster --name authplane-test
docker build -t authplane:local .
kind load docker-image authplane:local --name authplane-test
helm dependency update charts/authplane
helm install authplane charts/authplane -f values-sqlite.yaml \
  --set image.repository=authplane --set image.tag=local
kubectl port-forward svc/authplane 9000:9000 9001:9001

Uninstallation

helm uninstall authplane

Note — PVCs are not deleted automatically. Remove them manually if no longer needed:

kubectl delete pvc -l app.kubernetes.io/instance=authplane

Chart reference

Every configurable parameter lives in the chart’s values.yaml — pull it from the OCI registry (helm show values oci://ghcr.io/authplane/charts/authplane) to see the shipped defaults. For raw manifests (no Helm), template the chart with helm template … and commit the output.