Skip to the content.

Air-gapped deployment kit + supply-chain posture

Audience: an operator or a FedRAMP/IL-style reviewer standing fak up inside a network boundary that nothing leaves. Tracking issue: #3279 (epic #3256, workstream C).

The pitch this page has to make honest: fak is local-first and governed. The January 2026 scan that found 175,108 publicly-reachable, auth-less local-model servers is the failure mode this kit exists to prevent — running locally is the easy half, staying governed while doing it is the half that needs a checklist.

Related routes, which this page does not restate:

What ships, and what does not

Element Status
One static CGO_ENABLED=0 binary, no request-path egress Shipped
Required-bearer-token door (--require-key-env) Shipped
Loopback-by-default listener (--addr 127.0.0.1:8080) Shipped
Offline capability floor (fak preflight) + tamper-evident journal (fak audit verify) Shipped
Captured zero-network governed session, mock-planner seam Shipped — witness below
Generated SBOM (sbom/fak.spdx.json) Shipped
SBOM/go.mod drift gate (go test ./internal/architest -run TestSBOM) Shipped — see Regenerate and verify
Shift-left zero-egress bootstrap gate (make test-airgap) Shipped (#11387) — witness below
Captured zero-network governed session, --gguf model-backed seam Not yet witnessed — see Not yet witnessed
A guard that refuses an auth-less bind on a routable interface Shipped (#5373) — startup refusal UNAUTHENTICATED_OFF_HOST_BIND. See Bind safety

Stage the artifacts (what crosses the boundary, once)

Everything below crosses the boundary on removable media or an internal mirror, and nothing crosses again at runtime:

  1. The binary. Build it outside the boundary from a tagged commit through the one canonical recipe, then carry the artifact in:

    git checkout v0.41.0
    GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
      OUT=/tmp/fak VERSION="$(cat VERSION)" sh scripts/build.sh
    sha256sum /tmp/fak     # record; compare against the published SHA256SUMS entry
    

    Or carry the source clone and build inside the boundary — the module vendors nothing but needs its two golang.org/x modules in the local module cache, so pre-populate GOMODCACHE or pass -mod=mod against an internal proxy. See Supply-chain posture for exactly which two.

  2. The policy manifest — your capability floor, e.g. a copy of examples/customer-support-readonly-policy.json.
  3. The model weights (optional, only for the model-backed tier) — a GGUF file for --gguf. The kernel proof below needs no model at all.
  4. This page and sbom/fak.spdx.json — the reviewer’s evidence.

Hardened bring-up

# 1. The token door. Required on EVERY request; the gateway compares digests in
#    constant time and 401s without it.
export FAK_API_KEY='<a token your secret store issued>'

# 2. Serve. Explicit loopback bind + required key + the local model path.
#    Drop --gguf to run the kernel/mock tier with no weights at all.
./fak serve \
  --addr 127.0.0.1:8080 \
  --require-key-env FAK_API_KEY \
  --gguf /srv/models/your-model.gguf

# 3. Prove the door is shut, from the same host.
curl -f http://127.0.0.1:8080/healthz                       # live
curl -s -o /dev/null -w '%{http_code}\n' \
  http://127.0.0.1:8080/v1/chat/completions -d '{}'         # expect 401 (no bearer)

Flags used, all verified live in cmd/fak/serve.go:

Flag Default What it does
--addr 127.0.0.1:8080 HTTP listen address. The default is loopback, so an unconfigured fak serve is not reachable off-host.
--require-key-env "" (no auth) Names an env var holding a bearer token to require on every request. Empty means no auth — set it.
--gguf "" Load local GGUF weights into the in-kernel engine at boot. With --gguf and no --base-url, /v1/chat/completions and /v1/messages are served in-kernel, so there is no upstream to call.

Bind safety (read this one)

The default --addr is loopback, and since #5373 the 175,108-server shape is a kernel refusal, not a convention: fak serve will not come up on an interface reachable from off this host while no inbound token door is named. Captured on this host from the staged binary — it exits before any socket is bound:

$ ./fak serve --addr 0.0.0.0:8080
fak serve: UNAUTHENTICATED_OFF_HOST_BIND — refusing to bind 0.0.0.0:8080, which is
reachable from off this host, with no inbound token door: every request would be served
unauthenticated. Fix it one of three ways: bind loopback (--addr 127.0.0.1:8080, the
default), require a bearer (--require-key-env FAK_API_KEY, with that env var set), or bind
per-tenant keys (--key-principal acme=ACME_KEY). If this host really is meant to serve an
unauthenticated interface, pass --unsafe-allow-unauthenticated-bind to proceed anyway.
  next:   fak recover UNAUTHENTICATED_OFF_HOST_BIND
$ echo $?
2

What a reviewer needs to know about the rule’s edges, all of it in cmd/fak/serve_bind_safety.go:

In a regulated boundary the host controls are still yours to own, because the refusal governs this process and not the network around it:

The zero-network governed-session witness

This is acceptance bullet 1’s captured artifact, at the mock-planner seam: a governed session that runs end to end with no network, no model, no key, and no GPU. Reproduce it inside the boundary with the staged binary and policy.

### 1. binary build info (2 deps, CGO_ENABLED=0)
	mod	github.com/anthony-chaudhary/fak	v0.41.1-0.20260724051306-1a9fcd5adef1+dirty
	dep	golang.org/x/sys	v0.46.0	h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
	dep	golang.org/x/term	v0.44.0	h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
	build	CGO_ENABLED=0

### 2. capability floor: DENY
verdict=DENY reason=POLICY_BLOCK by=monitor
### 3. capability floor: ALLOW
verdict=ALLOW reason=NONE by=monitor

### 4. governed session, no network
seam        : OFFLINE (deterministic mock planner)
injection in context                YES           no
destructive op executed             YES           no
task completed (booked)             YES          YES
  destructive op prevented  : YES

The commands that produced it, run from a scratch directory with the staged binary:

go version -m ./fak | grep -E '^\s+(mod|dep|build\s+CGO)'
./fak preflight --policy ./customer-support-readonly-policy.json --tool refund_payment --args '{}'   # -> DENY  (POLICY_BLOCK)
./fak preflight --policy ./customer-support-readonly-policy.json --tool search_kb     --args '{}'   # -> ALLOW (NONE)
./fak agent --offline

What a reviewer should take from it: the refusal is structural. refund_payment is denied by the capability floor with no model in the loop, so there is no prompt to talk past; and in the governed session the injected instruction reaches the context in the baseline arm (YES) but not the fak arm (no), while the task still completes.

Captured 2026-07-24 at commit 1a9fcd5ad, Go go1.26.5. The +dirty suffix is this shared-trunk working tree, not the release artifact — a release build stamps the tag.

Pair it with the tamper-evident trail for the evidence half:

./fak audit verify <journal.jsonl>     # exit 1 if the hash chain was edited

Shift-left network-isolated harness bootstrap gate with zero-egress enforcement

To guarantee that air-gapped harness bring-up cannot leak traffic, contact remote services, or depend on out-of-boundary resources, make test-airgap (TestAirGapBootstrap_ZeroEgress in internal/airgaptest) enforces a shift-left CI gate with zero-egress enforcement.

Run the gate locally or in CI:

make test-airgap
# Runs: go test -v ./internal/airgaptest -run 'TestAirGapBootstrap_ZeroEgress'

Enforced invariants and failure tokens

  1. Zero-egress enforcement (AIRGAP_EGRESS_VIOLATION):
    • The test environment isolates proxy variables (HTTP_PROXY=http://127.0.0.1:1, HTTPS_PROXY=http://127.0.0.1:1, ALL_PROXY=http://127.0.0.1:1, GOPROXY=off).
    • An active socket tracer/egress trap intercepts all dial attempts. Any connection attempt to a non-loopback address (not 127.0.0.1, ::1, or localhost) fails closed immediately with error token AIRGAP_EGRESS_VIOLATION.
    • The test asserts that exactly zero outbound connections outside loopback occurred across the entire bootstrap and execution lifecycle.
  2. Offline remote dependency refusal (AIRGAP_UNRESOLVED_REMOTE_DEPENDENCY):
    • Bundles and lockfiles are verified strictly offline. If a lockfile or bundle references a remote MCP server URL (such as http://external.service/mcp or https://...) or unbundled remote assets, fakpack.Verify and bootstrap preflight fail closed with error token AIRGAP_UNRESOLVED_REMOTE_DEPENDENCY.
  3. Hermetic end-to-end execution:
    • A synthetic self-contained .fakpack bundle is built with valid v2 harness.lock.json, local MCP server binary (os.Args[0] TestHelperProcess pattern), local disk-backed memory journal, in-kernel mock model, and security policy.
    • The bundle is verified offline with fakpack.Verify.
    • The all-in-one supervisor boots from the bundle, waits for /healthz to report 200 OK, and dispatches agent turns via /v1/fak/agent/sessions.
    • Full tool dispatch to the MCP child process over stdio, session completion (session.end), and durable disk-backed memory journal records are verified.

Supply-chain posture

The honest, build-verifiable statement: fak is one static Go binary whose entire external dependency set is two golang.org/x extended-standard-library modules, pinned by a 4-line go.sum.

The older “zero external dependencies, no go.sum” phrasing is stale — do not use it. It was true once; it is not true at this commit, and shipping it would be a false claim. The correct number is two modules. The go.mod header comment has since been corrected, and internal/architest/zerodep_claim_test.go reds if the older wording reaches a new reader-facing page — three (AGENTS.md, INDEX.md, docs/product-scorecard/README.md) still carry it as pinned, review-visible debt.

Module Version Direct? License
golang.org/x/term v0.44.0 direct BSD-3-Clause
golang.org/x/sys v0.46.0 indirect (via x/term) BSD-3-Clause

Why this is a defensible posture rather than a slogan:

The generated SBOM is sbom/fak.spdx.json (SPDX 2.3).

Regenerate and verify this SBOM

Every package entry resolves from the build, with no network and no third-party tool:

go list -m all                    # -> exactly: the module + x/sys v0.46.0 + x/term v0.44.0
cat go.sum                        # -> exactly 4 lines; the h1: digests in the SBOM's sourceInfo
go version -m ./fak | grep dep    # -> the module set actually LINKED into the shipped binary

go version -m is the authoritative one: it reports what is in the artifact, not what the source tree merely declares. If those three disagree with sbom/fak.spdx.json, the SBOM is stale — regenerate it against go list -m all and re-cut. A release that changes go.mod must re-cut the SBOM.

You no longer have to remember to run them. TestSBOMMatchesGoMod in internal/architest/sbom_drift_test.go reds the trunk on every go test ./... when this SBOM and go.mod disagree, and names the module and the direction:

It also catches a half-refreshed entry (versionInfo bumped, purl or proxy URL left behind), and fails closed on a replace directive, which this SPDX shape has no field to express. The gate covers the require set, direct and indirect; exclude and the toolchain directives are deliberately out of scope because they put no bytes in the artifact.

Regulated-deployment checklist

Work top to bottom; each line is checkable, not aspirational.

Boundary

Access

Governance

Supply chain

Residual risk to record in your ATO package

Not yet witnessed

Stated plainly so nothing above has to be walked back.

The --gguf model-backed air-gapped session has no captured witness here. The witness above is the mock-planner seam, which needs no weights. The model-backed run needs a host with resident GGUF weights; it was not reachable from the Windows development box (no local model, and native go test is blocked by an OS Application-Control policy — see AGENTS.md). It is not blocked on missing capability, only on execution: run the hardened bring-up with --gguf on a fleet compute node (fleet-compute-nodes.md), capture the /healthz + 401 + governed-turn transcript, and append it beside the witness above.

Generation bookkeeping (gen/next, per generation.md):