Operations — identity service
Operations — identity service
Section titled “Operations — identity service”Environment variables
Section titled “Environment variables”The server binary reads configuration from environment variables via sdk/config.MustBind.
Missing required fields cause the process to exit at startup.
| Variable | Required | Default | Notes |
|---|---|---|---|
DATABASE_URL | yes | — | PgBouncer URL (port 6432, transaction pool). Server only. |
MIGRATION_DB_URL | yes* | — | Direct Postgres URL (port 5432, session pool). Migrator only. |
IDENTITY_AUTHKEY_KEK | yes | — | 64 hex chars (32-byte AES-256 key). Encrypts RS256 privkeys. |
SERVER_PORT | no | 8081 | HTTP listen port. |
IDENTITY_GRPC_PORT | no | 50051 | gRPC listen port. |
ENV | no | dev | dev | staging | prod. Stamped into JWT env claim. |
JWT_TTL | no | 3600s | Access token lifetime. Parsed as Go duration string. |
REQUEST_TIMEOUT | no | 30s | Per-request deadline. |
READ_HEADER_TIMEOUT | no | 5s | HTTP header read deadline (Slowloris mitigation). |
SHUTDOWN_TIMEOUT | no | 30s | Graceful shutdown window. |
BODY_LIMIT_BYTES | no | 1048576 | Maximum request body size (1 MiB). |
DB_MAX_CONNS | no | 20 | PgBouncer pool max connections. |
DB_MIN_CONNS | no | 2 | PgBouncer pool min connections. |
DB_MAX_CONN_LIFETIME | no | 30m | PgBouncer connection max lifetime. |
LOG_LEVEL | no | info | debug | info | warn | error. |
OTEL_EXPORTER_OTLP_ENDPOINT | no | "" | OTLP gRPC endpoint. Empty disables tracing export. |
* — MIGRATION_DB_URL is required for cmd/migrator; not read by cmd/server.
Source:
// from paper-board/identity/internal/config/config.go:23-39type Config struct { DatabaseURL string `env:"DATABASE_URL" validate:"omitempty,url"` MigrationDatabaseURL string `env:"MIGRATION_DB_URL" validate:"omitempty,url"` ServerPort int `env:"SERVER_PORT" default:"8081"` // ... AuthKeyKEKHex string `env:"IDENTITY_AUTHKEY_KEK"`}Helm values
Section titled “Helm values”Chart: infra/helm/agent-manager/charts/identity (or standalone identity/helm/identity).
Key values:
# from paper-board/identity/helm/identity/values.yamlserver: replicas: 1 service: port: 8081 grpcPort: 50051 config: jwtTTL: "3600s" requestTimeout: "30s" bodyLimitBytes: 1048576 dbMaxConns: 20 logLevel: "info" env: "dev"
migrator: enabled: true image: repository: ghcr.io/paper-board/identity-migrator tag: v0.2.1
bootstrap: enabled: false # set true on first install; idempotent on upgrade owner: email: "" name: "Owner" orgName: "" orgSlug: "" env: "live" withInitialKey: false passwordSecret: name: "" # K8s Secret name carrying the owner password key: "password"Secrets are injected as K8s Secret objects. Do not commit secret values in values.yaml. Use
secrets.create: true with authkeyKEK, dbURL, and migrationDbURL set via --set or an
external secrets operator.
Migrator
Section titled “Migrator”The migrator runs as a Helm pre-install / pre-upgrade Job hook (ADR-0004). It uses a direct
Postgres connection (MIGRATION_DB_URL, port 5432) so that the advisory lock (lock_id=1) works
in session mode. Never point the migrator at PgBouncer (port 6432) — the transaction-pool mode
releases the session-level advisory lock between statements.
Commands
Section titled “Commands”# Apply all pending migrationsidentity-migrator up
# Dry-run — print SQL without executingidentity-migrator up --dry-run
# Roll back N stepsidentity-migrator down 1
# Force schema_migrations version (recovery only)identity-migrator force 3
# Print current applied versionidentity-migrator version
# Drop all tables (dev only — requires MIGRATOR_ENV=dev)identity-migrator dropMigration files
Section titled “Migration files”Location: migrations/schema/. Naming: NNNNNN_<name>.up.sql / .down.sql (6-digit zero-padded).
Current migrations:
| Version | Name | Description |
|---|---|---|
| 000001 | init | users, organizations, org_members, api_keys, auth_keys |
| 000003 | refresh_tokens | refresh_tokens table for RFC 6749 rotation |
| 000004 | idempotency_keys | idempotency_keys table for sdk/idempotency |
Security hardening (Phase 2)
Section titled “Security hardening (Phase 2)”C2 — NetworkPolicy egress
Section titled “C2 — NetworkPolicy egress”The server pod has no direct Postgres access. Egress is restricted to PgBouncer (:6432) and kube-dns (:53). The migrator Job has a separate policy allowing direct Postgres (:5432) only during the pre-install/pre-upgrade hook window.
# from paper-board/identity/helm/identity/values.yamlnetworkPolicy: enabled: true ingress: - from: - podSelector: matchLabels: app: agents ports: - port: 50051C4-A — Refresh-token rotation and reuse-detection
Section titled “C4-A — Refresh-token rotation and reuse-detection”Refresh tokens follow a token-family model. Each POST /v1/auth/login creates a new family.
POST /v1/auth/refresh rotates the token within the family (one-time use). Presenting a
previously-consumed token triggers reuse-detection: the entire family is revoked immediately and
the response body includes "action": "re_authenticate".
C4-B — Idempotency-key middleware
Section titled “C4-B — Idempotency-key middleware”All mutating endpoints except the three auth flows (/v1/auth/login, /v1/auth/refresh,
/v1/auth/logout) require an Idempotency-Key header. Duplicate requests within the
deduplication window return the stored response without re-executing the handler. Keys are stored
in identity.idempotency_keys.
C5 — Bootstrap Helm Job
Section titled “C5 — Bootstrap Helm Job”A post-install / post-upgrade Helm Job runs identity-cli bootstrap to create the initial
owner user and organization. The job is idempotent: re-running on upgrade is a no-op once the
owner row exists. Enable with bootstrap.enabled=true; supply owner credentials via a K8s Secret
(never in plain values.yaml).
M6 — JWT signing-key auto-rotation
Section titled “M6 — JWT signing-key auto-rotation”At server startup the process checks for an active auth key in identity.auth_keys. If none
exists it generates a new RS256 key pair, encrypts the private key with the AES-256 Key
Encryption Key (KEK) loaded from IDENTITY_AUTHKEY_KEK, and inserts it as active. This ensures
the service is self-bootstrapping: a fresh deployment never fails because no signing key exists.
The auth_keys table enforces a partial unique index (auth_keys_one_active) that allows at
most one active key at a time.
OCI — Image signing and GHCR
Section titled “OCI — Image signing and GHCR”Images are pushed to ghcr.io/paper-board/identity-{server,migrator,cli}:<tag> via the release
workflow. The Helm chart references these images by digest-pinned tag. Internal visibility on GHCR
requires the consuming repo to be added to the package’s “Manage Actions access” list with Read
permission (see memory note paper_board_ghcr_internal_helm_access.md).
Deployment diagram
Section titled “Deployment diagram”flowchart TB subgraph k8s ["Kubernetes namespace: paper-board"] subgraph identity_ns ["identity"] server["identity-server\nDeployment\n:8081 HTTP / :50051 gRPC"] migJob["identity-migrator\npre-install/pre-upgrade Job"] bootJob["identity-cli bootstrap\npost-install Job (optional)"] end pgbouncer["PgBouncer\nService :6432"] end subgraph postgres ["Postgres cluster"] pg[("identity schema")] pgDirect[("Postgres :5432\n(migrator direct)")] end
server --> pgbouncer --> pg migJob --> pgDirect bootJob --> pgbouncer
classDef controlPlane fill:#10b981,stroke:#047857,color:#fff classDef dataPlane fill:#3b82f6,stroke:#1d4ed8,color:#fff classDef sandbox fill:#f97316,stroke:#c2410c,color:#fff classDef external fill:#ef4444,stroke:#b91c1c,color:#fff classDef persistence fill:#6b7280,stroke:#374151,color:#fff
class server controlPlane class migJob,bootJob dataPlane class pgbouncer,pg,pgDirect persistence