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: requireat minimum,verify-fullwhen 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.passwordSecretandapi.admin.passwordSecretfrom 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: trueso structured logs land in your aggregator intact.
Checklist
| Item | Status |
|---|---|
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 |