Security hardening¶
This doc covers the security posture of the v1.1.1 FCC deployment artifacts — Docker images, Helm chart defaults, and recommended production hardening.
Threat model¶
The FCC stack is designed for trusted-internal deployments: - Team-scale knowledge work (docs, workflows, notebooks) - Research labs running reproducible experiments - Enterprise adopters deploying behind a corporate firewall
It is not designed to be a multi-tenant SaaS — there is no user-level authentication in the framework itself. Authentication must be enforced at the ingress layer (reverse proxy, API gateway, or OAuth2 proxy sidecar).
Container image security¶
All 4 images share these baseline hardening choices:
Non-root user¶
Every image runs as UID 1001 (fcc:fcc), created with --no-create-home
(except jupyter, which sets HOME=/app). This enforces the baseline
Pod Security Standard restricted profile without requiring
SecurityContextConstraints.
Minimal base images¶
| Image | Base | Why |
|---|---|---|
| backend | python:3.12-slim |
No build-essential, no git, no shell beyond /bin/sh |
| frontend | nginx:1.27-alpine |
Multi-stage: node builder discarded, only static + nginx remain |
| streamlit | python:3.12-slim |
Adds curl for healthcheck only |
| jupyter | python:3.12-slim |
Adds curl for healthcheck only |
Pinned dependencies¶
- Base image version pinned via
ARG PYTHON_VERSION=3.12(not:latest) - Node version pinned via
ARG NODE_VERSION=20 - nginx version pinned via
ARG NGINX_VERSION=1.27 - Python deps installed from the fcc wheel with transitive pins in
pyproject.toml
HEALTHCHECK¶
Every image declares a HEALTHCHECK directive that probes the
documented health endpoint every 30 seconds. This lets container
orchestrators (Docker, Kubernetes, Nomad, etc.) detect wedged
containers without external probe config.
Image scanning¶
The docker-publish.yml workflow builds multi-arch images but does not
yet run an explicit vulnerability scanner. For production adopters:
# Trivy (recommended)
trivy image ghcr.io/rollingthunderfourtytwo-afk/fcc-backend:1.1.1
# Grype
grype ghcr.io/rollingthunderfourtytwo-afk/fcc-backend:1.1.1
# Docker Scout
docker scout cves ghcr.io/rollingthunderfourtytwo-afk/fcc-backend:1.1.1
Future versions (>= v1.2.0) will add Trivy to the CI pipeline with SARIF upload to GitHub Security tab.
Kubernetes security defaults¶
The Helm chart enforces these defaults for every pod:
podSecurityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false # fcc writes to .ai_cache/ — override per-service if not needed
capabilities:
drop:
- ALL
runAsNonRoot: true
These defaults map to the Pod Security Admission restricted profile
with one exception: readOnlyRootFilesystem is false because FCC
writes to .ai_cache/ at runtime. If your deployment doesn't use AI
call caching, override via:
ServiceAccount¶
The chart creates a dedicated ServiceAccount with
automountServiceAccountToken: false — FCC doesn't call the Kubernetes
API from any pod, so the token should never be present inside the
container filesystem.
RBAC¶
The chart creates a namespace-scoped Role (not ClusterRole) with the minimum permissions required:
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
resourceNames:
- <release>-fcc-config
- <release>-fcc-secrets
verbs: ["get", "watch"]
Resource names are scoped to FCC's own ConfigMap and Secret — the service account cannot read any other configmaps/secrets in the namespace.
NetworkPolicies (opt-in)¶
The default chart does not ship NetworkPolicies because they depend on a cluster-installed CNI (Calico, Cilium, etc.) that supports them. For production, recommended policies:
# deny-all default
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: fcc-default-deny
spec:
podSelector:
matchLabels:
app.kubernetes.io/part-of: fcc-agent-team
policyTypes: [Ingress, Egress]
# allow ingress from frontend → backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: fcc-frontend-to-backend
spec:
podSelector:
matchLabels:
app.kubernetes.io/component: backend
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/component: frontend
ports:
- port: 8765
protocol: TCP
Secret management¶
The Helm chart ships an optional Secret template that only renders
when at least one credential is supplied via values. For production,
do NOT put secrets in values.yaml — use one of:
Option A: Pre-created Secret (simplest)¶
kubectl create secret generic fcc-secrets \
--namespace fcc \
--from-literal=ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
--from-literal=OPENAI_API_KEY="$OPENAI_API_KEY" \
--from-literal=JUPYTER_TOKEN="$(openssl rand -hex 32)"
Then install without secret-related values — the chart references the
secret by name (<release>-fcc-secrets).
Option B: External Secrets Operator¶
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: fcc-secrets
namespace: fcc
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: fcc-fcc-secrets # matches the chart's include "fcc.secretName"
creationPolicy: Owner
data:
- secretKey: ANTHROPIC_API_KEY
remoteRef:
key: prod/fcc/anthropic-api-key
- secretKey: JUPYTER_TOKEN
remoteRef:
key: prod/fcc/jupyter-token
Option C: Sealed Secrets¶
echo -n "$ANTHROPIC_API_KEY" | kubectl create secret generic fcc-secrets \
--dry-run=client --from-file=ANTHROPIC_API_KEY=/dev/stdin -o yaml | \
kubeseal --format yaml > sealed-fcc-secrets.yaml
kubectl apply -f sealed-fcc-secrets.yaml
TLS and ingress¶
The chart does NOT terminate TLS itself — it relies on the cluster's ingress controller (nginx-ingress, Traefik, cert-manager) to handle certificates.
Production pattern with cert-manager:
frontend:
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/websocket-services: "{{ .Release.Name }}-backend"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
hosts:
- host: fcc.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: fcc-tls
hosts:
- fcc.example.com
The long proxy timeouts are required for WebSocket connections to stay open through the ingress.
JupyterLab security¶
JupyterLab with no token is unsafe. The Dockerfile default is an empty token for local-dev convenience only. In production:
- Set
jupyter.tokento a strong random value - Only expose JupyterLab to authenticated users (behind an OAuth2 proxy or via VPN)
- Never enable
jupyter.token=""on internet-accessible deployments
The production overlay (values-prod.yaml) uses
${JUPYTER_TOKEN:?required} to force the caller to supply a token,
otherwise Helm refuses to install.
Supply chain¶
- Source repo: https://github.com/rollingthunderfourtytwo-afk/l2_fcc_agent_team_ext
- All commits on
mainare signed by GitHub's web UI or the maintainer's gpg key - Images are built via a reproducible GitHub Actions workflow
- PyPI publishing via trusted publisher (OIDC) — no long-lived API tokens