Production Guide

What to change before a FerrisKey deployment carries real users.

Database

Turn off the embedded PostgreSQL

The chart ships with postgresql.enabled: true so a first install works with no prerequisites. It stores data in a single StatefulSet with no replication and no backup. Set postgresql.enabled: false and point at a real database.

  • Use a managed service (RDS, Cloud SQL, Azure Database for PostgreSQL) or a PostgreSQL cluster someone actually operates.
  • Require TLS. sslMode: require at minimum, verify-full when you can validate the certificate chain.
  • Give FerrisKey its own database rather than a schema inside a shared one.
  • Set up automated backups with point-in-time recovery, and restore one at least once to prove it works.
  • Put PgBouncer in front of it if the deployment sees heavy traffic.

TLS and exposure

Let the chart render the routing. It puts / on the console and api.server.rootPath on the API, on one hostname, which is what the console expects by default.

publicHost: iam.yourorg.com

ingress:
  enabled: true
  class: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  tls:
    - hosts:
        - iam.yourorg.com
      secretName: ferriskey-tls

api:
  server:
    allowedOrigins:
      - https://iam.yourorg.com

allowedOrigins applies to every route, including the ones that carry no realm: /config, the health probes, and the API documentation. Per-client web origins only cover realm-scoped routes, so a console served from a different origin than the API must have its origin listed here.

If your cluster uses Gateway API, swap the Ingress block for gatewayAPI.httpRoute with the parent references of your gateway. publicHost is required either way.

Admin credentials

The chart generates an admin password into a Secret on first install. In production, create the Secret yourself so the value comes from wherever you keep secrets, and point the chart at it:

api:
  admin:
    username: admin
    email: admin@yourorg.com
    passwordSecret:
      create: false
      name: ferriskey-admin
      key: password

Generate the password randomly, at least 16 characters, and rotate it on a schedule you actually keep.

Resources and scaling

The API pods are stateless, so scaling out is a replica count. Resources are set per workload; there is no global override.

api:
  replicas: 3
  resources:
    requests:
      cpu: 250m
      memory: 256Mi
    limits:
      memory: 512Mi

webapp:
  replicas: 2

Add a PodDisruptionBudget so a node drain does not take every replica at once:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: ferriskey-api
  namespace: ferriskey
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: ferriskey-api

Check the label on your own pods with kubectl get pod -n ferriskey --show-labels before applying it.

Monitoring

The API serves Prometheus metrics on /metrics. With the Prometheus Operator installed, the chart can create the ServiceMonitor for you:

api:
  serviceMonitor:
    enabled: true
    interval: 30s

Alert on error rate, request latency, and database connection failures. For traces, turn on the OTLP exporter:

opentelemetry:
  enabled: true
  otlpEndpoint: http://tempo.observability.svc.cluster.local:4317

That sets ACTIVE_OBSERVABILITY and OTLP_ENDPOINT on the API.

Security hardening

The chart already runs every container as non-root with a read-only root filesystem, all capabilities dropped, and the RuntimeDefault seccomp profile. What is left to you:

  • Network policies. The API needs PostgreSQL, plus outbound access to external identity providers if you use Abyss and to your SMTP relay.
  • External secret management. Feed database.passwordSecret and api.admin.passwordSecret from Vault, an external secrets operator, or your cloud’s secret manager, instead of letting the chart generate them.
  • Image scanning in CI, on the images you actually deploy.
  • Audit forwarding. Enable SeaWatch and ship its events to your SIEM.
  • Log format. Set api.log.json: true so structured logs land in your aggregator intact.

Checklist

ItemStatus
postgresql.enabled: false, external database with TLS
Database backups configured and a restore tested
TLS terminated, HTTP redirected
api.server.allowedOrigins restricted to real origins
Admin password supplied from an existing Secret
Resource requests and limits set per workload
PodDisruptionBudget applied
ServiceMonitor enabled and alerts wired up
Network policies applied
Secrets sourced externally
SeaWatch events forwarded