Vault Transit signing

TL;DR — Configure signing.key_store: vault_transit and AuthPlane sends every JWT payload to Vault for signing. Private key material never lives on the AuthPlane host. Adds ~2 ms latency per signing op, adds Vault as a hard dependency. Best fit for compliance environments, HSM-backed Vault deployments, zero-trust setups, and multi-replica Kubernetes.

When to choose Vault Transit

AuthPlane supports three signing key stores. Pick this one only when you need it.

Key storeBest forTrade-off
keyfile (default)Single-node deployments, developmentSimple; keys on disk. ReadWriteMany PVC or shared filesystem for multi-node.
postgres_keyMulti-node Postgres deploymentsKeys stored in DB with AES encryption at rest. No shared filesystem needed.
vault_transitCompliance-sensitive, HSM-backed, zero-trustKeys never leave Vault. Adds Vault dependency + ~2 ms per sign.

Why Vault Transit

  • Keys never leave Vault — signing material lives inside Vault’s HSM-backed or software-backed Transit engine
  • Audit trail — Vault logs every signing operation
  • Managed rotation — Vault Transit handles key rotation on its side
  • FIPS compliance — when Vault is backed by an HSM

Prerequisites

  • HashiCorp Vault 1.12+
  • Transit secrets engine enabled
  • A Transit key configured for signing — ECDSA-P256 for ES256, RSA-2048 for RS256

Step 1 — Enable the Transit engine

vault secrets enable transit

Step 2 — Create a signing key

For ES256 (ECDSA P-256), AuthPlane’s default:

vault write transit/keys/authserver-signing type=ecdsa-p256

For RS256:

vault write transit/keys/authserver-signing type=rsa-2048

Step 3 — Create a Vault policy

authserver-signing-policy.hcl:

path "transit/sign/authserver-signing" {
  capabilities = ["update"]
}
path "transit/verify/authserver-signing" {
  capabilities = ["update"]
}
path "transit/keys/authserver-signing" {
  capabilities = ["read"]
}

Apply:

vault policy write authserver-signing authserver-signing-policy.hcl

Step 4 — Choose an authentication method

Option A — Static token

Fine for dev; requires token rotation for production.

vault token create -policy=authserver-signing -period=768h
signing:
  algorithm: ES256
  key_store: vault_transit
  vault_transit:
    address: https://vault:8200
    token: "hvs.your-vault-token"
    mount: transit
    key_name: authserver-signing

Enable AppRole, create a role tied to the signing policy:

vault auth enable approle
vault write auth/approle/role/authserver \
    token_policies="authserver-signing" \
    token_ttl=1h \
    token_max_ttl=4h

Get the role ID and secret ID:

vault read auth/approle/role/authserver/role-id
vault write -f auth/approle/role/authserver/secret-id

Config:

signing:
  algorithm: ES256
  key_store: vault_transit
  vault_transit:
    address: https://vault:8200
    mount: transit
    key_name: authserver-signing
    approle:
      role_id: "your-role-id"
      secret_id: "your-secret-id"
      mount: approle

AuthPlane requests a fresh Vault token via AppRole on boot and re-authenticates before the token expires.

Environment variables

AUTHPLANE_SIGNING_KEY_STORE=vault_transit
AUTHPLANE_SIGNING_ALGORITHM=ES256
AUTHPLANE_VAULT_ADDR=https://vault:8200

# Option A: static token
AUTHPLANE_VAULT_TOKEN=hvs.your-token

# Option B: AppRole
AUTHPLANE_VAULT_APPROLE_ROLE_ID=your-role-id
AUTHPLANE_VAULT_APPROLE_SECRET_ID=your-secret-id
AUTHPLANE_VAULT_APPROLE_MOUNT=approle

AUTHPLANE_VAULT_TRANSIT_MOUNT=transit
AUTHPLANE_VAULT_TRANSIT_KEY_NAME=authserver-signing
AUTHPLANE_VAULT_TIMEOUT=10s

Kubernetes (Helm) setup

Same knobs via values.yaml — see Kubernetes (Helm) → Vault Transit. Recommended Vault-auth patterns in Kubernetes:

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

The Helm chart doesn’t ship a Vault subchart — Vault is infrastructure your organization already runs.

Docker Compose (dev/testing)

Dev-mode Vault for local iteration only. Never for production.

services:
  vault:
    image: hashicorp/vault:1.15
    ports:
      - "8200:8200"
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: dev-root-token
    cap_add:
      - IPC_LOCK

  authserver:
    image: authplane/authserver:latest
    environment:
      AUTHPLANE_SIGNING_KEY_STORE: vault_transit
      AUTHPLANE_VAULT_ADDR: http://vault:8200
      AUTHPLANE_VAULT_TOKEN: dev-root-token
      AUTHPLANE_VAULT_TRANSIT_KEY_NAME: authserver-signing
    depends_on:
      - vault

Then inside the vault container:

vault secrets enable transit
vault write transit/keys/authserver-signing type=ecdsa-p256

Note — dev mode Vault (VAULT_DEV_ROOT_TOKEN_ID) is for testing only. In production, use a properly initialized and sealed Vault.

Validation rules

Boot fails if:

  • signing.vault_transit.address is missing when key_store: vault_transit
  • Both token and approle.role_id are set (they’re mutually exclusive)
  • approle.role_id is set but approle.secret_id is missing

Key rotation with Vault Transit

Two levels:

  1. Vault-side rotationvault write -f transit/keys/authserver-signing/rotate bumps the key version inside Vault. AuthPlane’s next signing operation picks up the new version transparently.
  2. AuthPlane-side rotationauthserver admin key rotate still works and triggers a new key version request against Vault. Prefer this route if you want AuthPlane’s audit trail alongside Vault’s.

The JWKS keeps both old and new keys visible until the old one expires so in-flight tokens continue to verify.

Troubleshooting

vault transit: permission denied

The token or AppRole doesn’t have the required policy. Verify:

vault token lookup
vault policy read authserver-signing

Common cause: policy typo, or policy applied to a different role than the one whose credentials AuthPlane has.

vault transit: key not found

The Transit key doesn’t exist under the configured mount. Create it:

vault write transit/keys/authserver-signing type=ecdsa-p256

Double-check the mount matches vault_transit.mount in your config.

vault transit: connection refused

AuthPlane can’t reach Vault. Check:

  • vault_transit.address is correct
  • In Docker Compose, use the service name (http://vault:8200) not localhost
  • In Kubernetes, use the ClusterIP or the FQDN (vault.vault.svc:8200)
  • Vault is unsealed and initialized (vault status)

AppRole authentication loops or reauthenticates constantly

Token TTL is too short and secret_id_ttl expired. Bump token_ttl and token_max_ttl on the role, or regenerate the secret_id.