From one container to a hardened cluster.
DFENS is a single static binary in a distroless container that runs as a non-root user.
The same image runs under docker run, Docker Compose, or Kubernetes. What
changes is how you supply config, state, and network isolation.
The image
There is no public image yet. Approved design partners are issued a registry credential
and an image reference; request
access to be onboarded. The examples below use REGISTRY/ai-firewall:<tag>
as a placeholder. Substitute the reference you're given (and, for a private registry,
configure the corresponding pull credentials).
What the image provides and expects:
| Aspect | Value |
|---|---|
| Base | Distroless, runs as nonroot, no shell. |
| Ports | 8080 (proxy + inspect), 9090 (metrics). |
| Config | /etc/ai-firewall/config.yaml (a minimal default ships in the image). |
| Rules | /etc/ai-firewall/rules.d/ + registry.yaml, baked in. |
| State | A writable dir for the audit sequencer, ruleset cache, and disable overlay. |
| Health | GET /health, always 200. Parse the body. |
Single container
The fastest way to see it work is to mount a config, publish the ports, and give it a writable state volume:
docker run --rm \ -p 8080:8080 -p 9090:9090 \ -v "$PWD/config.yaml:/etc/ai-firewall/config.yaml:ro" \ -v ai-firewall-data:/data \ REGISTRY/ai-firewall:<tag> # In another shell — confirm it's up curl -s localhost:8080/health {"status":"ok","ready":true,"degraded_rules":[],"disabled_rules":[]}
A minimal config.yaml is on the Configuration
page. Set server.state_dir: "/data" so the writable volume is actually used.
The default state path is not writable under the read-only root filesystem.
Docker Compose
Compose is the simplest way to run DFENS alongside its optional admin console (a web UI
for managing rulesets and reviewing decisions across instances). The repository ships a
reference stack under examples/compose-admin/: a firewall instance, the admin
service, and profiles for Postgres-backed state and auto-scaling replicas.
services:
firewall:
image: REGISTRY/ai-firewall:<tag>
ports:
- "127.0.0.1:8080:8080" # bind loopback: keep the listener private
volumes:
- ./config/firewall.yaml:/etc/ai-firewall/config.yaml:ro
- firewall-data:/data
environment:
AI_FIREWALL_ADMIN_TOKEN: ${AI_FIREWALL_ADMIN_TOKEN:?generate one}
# Inbound client token (optional). Callers send it as X-AI-Firewall-Key.
AI_FIREWALL_CLIENT_TOKEN: ${AI_FIREWALL_CLIENT_TOKEN:-}
restart: unless-stopped
volumes:
firewall-data:
The loopback bind (127.0.0.1:) keeps the listener private; set
AI_FIREWALL_CLIENT_TOKEN to also require a client token on every request
(see client authentication). The full
rig, including the admin console and its one-time initialisation step, is documented in the
example's own README.
Kubernetes
The repository ships a minimal, provider-agnostic manifest set under deploy/k8s/:
a ConfigMap, a hardened two-replica Deployment with /health probes, a ClusterIP
Service exposing the proxy and metrics ports, a NetworkPolicy, and a Secret template, wired
together with Kustomize.
# 1. Point the Deployment at the image reference you were issued sed -i 's|REGISTRY/ai-firewall:v0.6.1|your-registry/ai-firewall:<tag>|' deployment.yaml # 2. (Optional) create the Secret: vault-mode provider keys, an admin token, # and/or the inbound client token (X-AI-Firewall-Key). kubectl apply -f secret.example.yaml # after editing in real values # 3. Apply the whole stack kubectl apply -k deploy/k8s # 4. Confirm rollout and readiness kubectl rollout status deploy/ai-firewall kubectl port-forward svc/ai-firewall 8080:8080 & curl -s localhost:8080/health
Two things in these manifests are load-bearing, not cosmetic:
- Network isolation. The Service is
ClusterIPand the NetworkPolicy admits ingress on8080only from pods you labelai-firewall-client: "true", and9090only from the monitoring namespace. Combined with an inbound client token, this is defense-in-depth: network reachability and an application credential. (The NetworkPolicy requires a NetworkPolicy-enforcing CNI such as Calico or Cilium.) - Writable state. The pod runs with a read-only root filesystem, so the
config sets
state_dir: /dataand the Deployment mounts a writableemptyDirthere.emptyDiris correct when the audit sink isstdout(your log pipeline is the durable record); switch to a PVC if you write audit to a local file.
The Deployment ships a hardened pod security context (non-root, all capabilities dropped, no privilege escalation) and probes both liveness and readiness against /health.
These manifests are validated for structure but are a reviewed starting point, not a
turnkey production artifact. They carry no Ingress and no cloud-specific annotations by
design. Dry-run them against your cluster and fold them into your own Kustomize or Helm
setup. The deploy/k8s/README.md documents every file and the customisation
points.
Running your own rules
The image bakes in the default rule set. To run your own, load the rule files into a ConfigMap (or bake them into a derived image) and mount them over the rules directory:
kubectl create configmap ai-firewall-rules --from-file=./rules/
Then add a volume and a volumeMount at /etc/ai-firewall/rules.d in
the Deployment. A ConfigMap caps at 1 MiB total; for large rule sets use a PVC or a derived
image instead.
ML operators (the fat image)
The default image is pure-Go and dependency-free. The ML-backed operators
(@detectPromptInjectionML, @classify, @toxicity) live
in a separate :fat image that loads ONNX models at startup.
- The fat image is substantially larger and needs materially more memory. Raise the container's memory request and limit well above the thin image's defaults before using it.
- Lengthen the readiness probe's initial delay to cover multi-second model loading.
- Size both against your own measurement; the thin image is the right default unless you specifically need the ML operators.
Back to the overview, or revisit Configuration.