Skip to content

Security & Production Checklist

iam.dev-mode: true exists so a first boot needs zero secrets. In production it must be false (or unset), and IAM’s secret validator refuses to start with missing or default secrets — a deliberate fail-closed stance.

iam:
dev-mode: false # or remove the line entirely
jwt:
algorithm: ES256 # DEFAULT — asymmetric; requires an EC P-256 keypair
private-key: ${IAM_JWT_PRIVATE_KEY} # PEM PKCS#8 EC P-256, from your secret manager
public-key: ${IAM_JWT_PUBLIC_KEY} # PEM X.509/SPKI — also published at /.well-known/jwks.json
access-token-ttl: 15m
refresh-token-ttl: 7d
refresh-token-delivery: COOKIE # COOKIE | BODY | BOTH
default-tenant:
admin-password: ${IAM_ADMIN_PASSWORD} # never the default
oauth2:
client-secret-encryption-key: ${IAM_OAUTH2_KEY} # exactly 32 bytes
callback-base-url: https://api.yourapp.com
frontend-redirect-url: https://app.yourapp.com/auth/callback
security:
bcrypt-strength: 12 # OWASP minimum 10
cors-allowed-origins:
- https://app.yourapp.com # no localhost in prod
rate-limit:
enabled: true # per-endpoint rules on auth routes
  • JWT signing configured for the chosen algorithm — ES256 (the default) needs a keypair, not iam.jwt.secret (see below)
  • Secrets injected from a secret manager — never committed
  • dev-mode off (ephemeral secrets are logged and don’t survive restarts)
  • HTTPS everywhere; refresh-token cookie is Strict same-site by default
  • CORS narrowed to real origins
  • Rate limiting left on (login 10/min, forgot-password 3/5min, sensible defaults)
  • Audit retention fits your compliance window (iam.audit.retention-days, default 90)
  • If multi-instance: consider iam.cache.store: redis for instant cross-node invalidation

The signing algorithm is iam.jwt.algorithm, and its default is ES256 (asymmetric, EC P-256). This is the single most common production boot failure: a config carried over from an older build that sets only iam.jwt.secret will not start — the secret validator runs at bean-init (before the port binds) and refuses to boot without the ES256 keypair. Pick one:

ES256 — the default (recommended). Sibling services verify tokens against the public key via the JWKS endpoint, so no service but IAM ever holds signing material:

iam:
jwt:
algorithm: ES256 # optional — this is the default
private-key: ${IAM_JWT_PRIVATE_KEY} # PEM PKCS#8, EC P-256 — REQUIRED, or boot fails
public-key: ${IAM_JWT_PUBLIC_KEY} # PEM X.509/SPKI
key-id: "" # optional; blank derives the RFC 7638 JWK thumbprint
verification-keys: {} # optional rotation window: kid -> public key PEM

Generate a pair:

Terminal window
openssl ecparam -genkey -name prime256v1 -noout \
| openssl pkcs8 -topk8 -nocrypt -out jwt-private.pem
openssl ec -in jwt-private.pem -pubout -out jwt-public.pem

With an asymmetric algorithm active, IAM publishes GET /.well-known/jwks.json (RFC 7517 JWK Set, permitAll; the endpoint does not exist in HS512 mode). Tokens carry a kid header; verification-keys keeps retired public keys verifiable during a rotation window.

HS512 — symmetric opt-out (single-service / legacy). One shared secret, no JWKS endpoint:

iam:
jwt:
algorithm: HS512
secret: ${IAM_JWT_SECRET} # minimum 64 bytes (512 bits, RFC 7518 §3.2)

The floor is a real 64 bytes / 512 bits — a 32–63-byte secret is rejected, as is a degenerate one (fewer than 8 distinct bytes, e.g. "a".repeat(64)). Generate one with openssl rand -base64 48. iam.dev-mode: true generates an ephemeral EC keypair per boot (or an HS secret only if you pin algorithm: HS512) — never for production. See the 1.3.1 → 2.0.0 upgrade guide §4.1 for the full migration.

  • JWT access + refresh tokens, refresh rotation, token cleanup job
  • Password hashing with BCrypt (cost 12)
  • 2FA (TOTP + backup codes) — iam.two-factor.enabled: true
  • OAuth2/OIDC login (Google, Microsoft, any OIDC issuer) with PKCE state handling and encrypted client secrets at rest
  • Password reset with single-use, expiring tokens and optional session invalidation
  • Audit trail of permission and entity changes (actor, IP, before/after JSON)
  • Tenant write-guard + read filter in multi-tenant row-level mode

IAM’s SecurityFilterChain is @ConditionalOnMissingBean — define your own and IAM backs off, exposing its filters for you to compose. Extra public routes without replacing the chain:

iam:
security:
additional-permit-all:
- /actuator/health
- /public/**
  • OAuth2 authorization state is in-memory — fine for single-instance; front multi-instance deployments with sticky sessions or an external store.
  • IAM is an authorization-first library with OAuth2 login; it is not an IdP. For enterprise SSO federation (SAML, SCIM), put Keycloak/Okta in front for authentication and keep IAM for the fine-grained authorization they can’t do.
  • Run your own security review before internet exposure. The library ships tested controls, not a substitute for your threat model.