diff --git a/build b/build index 9782b7f..54cb164 100755 --- a/build +++ b/build @@ -582,6 +582,44 @@ PIN_MID

Your archive's tools are yours. The server is convenience; deletion of the server doesn't break your archive — every per-version download above is a real, immutable static file. Save what you trust.

+ +
+

Build your .zddc apps: block

+

Pick a channel or pinned version for each tool. The YAML on the right updates as you go — copy it into a .zddc file at the level of your archive where you want the config to apply. Closer-to-leaf wins, so a project subdirectory can override what its parent says.

+
+
+ + + + + +
+
+ +
+ + +
+
+
+

Tip: leave a tool on stable if you don't have a reason to pin it. Pin specific versions when you depend on a behavior in that release. The server's resolution order is: a real file in your archive → .zddc apps: entry (closer-to-leaf wins) → embedded fallback.

+
+

Channels

@@ -695,6 +733,63 @@ PIN_MID // non-linux client, that needs to flip to the detected platform). rewire(picker.value); })(); + + // ── Apps: composer ───────────────────────────────────────────── + // Picks per-app channels/versions from dropdowns, emits an equivalent + // .zddc apps: YAML block in a textarea, with a Copy button. Reuses + // the version-picker's option list as the source of truth for what + // values are valid (avoids duplicating the version data into JS). + (function() { + var picker = document.getElementById('version-picker'); + var textarea = document.getElementById('composer-yaml'); + var copyBtn = document.getElementById('composer-copy'); + var status = document.getElementById('composer-status'); + if (!picker || !textarea) return; + var selects = document.querySelectorAll('.composer-select'); + if (selects.length === 0) return; + + // Clone version-picker's option list (channels + pinned versions) + // into each per-app composer-select. innerHTML round-trips fine + // because every option is a static + // produced by the build script — no inline JS, no event handlers. + var optionsHTML = picker.innerHTML; + selects.forEach(function(sel) { + sel.innerHTML = optionsHTML; + sel.value = 'stable'; // default per-app + }); + + function rebuild() { + var lines = ['apps:']; + selects.forEach(function(sel) { + lines.push(' ' + sel.dataset.app + ': ' + sel.value); + }); + textarea.value = lines.join('\n') + '\n'; + } + + selects.forEach(function(sel) { + sel.addEventListener('change', rebuild); + }); + rebuild(); + + if (copyBtn) { + copyBtn.addEventListener('click', function() { + textarea.select(); + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(textarea.value); + } else { + document.execCommand('copy'); + } + if (status) { + status.textContent = 'Copied to clipboard'; + setTimeout(function() { status.textContent = ''; }, 2000); + } + } catch (e) { + if (status) status.textContent = 'Copy failed — select the text and copy manually'; + } + }); + } + })(); diff --git a/zddc/README.md b/zddc/README.md index 42102f8..acda5ef 100644 --- a/zddc/README.md +++ b/zddc/README.md @@ -564,8 +564,11 @@ implemented today — this list is informational so security reviewers don't have to redo the gap analysis from scratch. - **FIPS 140-3 cryptography** (NIST SC-13) — current build uses Go stdlib - crypto. Required: build with `GOEXPERIMENT=systemcrypto` + RHEL FIPS - userspace, or use the `microsoft/go` (formerly goboring) toolchain. + crypto. *Required:* parallel `zddc-server-fips` build target using the + `microsoft/go` toolchain on a RHEL/UBI base with the validated OpenSSL + FIPS module. See `federal.html` for the deployment-shape narrative and + the §"FIPS-validated cryptography" subsection below for the engineering + detail. - **TLS hardening** (NIST SC-8(1)) — *partially complete.* Server now sets `MinVersion: tls.VersionTLS12`, the NIST SP 800-52 Rev. 2 AEAD-only cipher allowlist (ECDHE+AES-GCM and ECDHE+ChaCha20Poly1305 @@ -576,8 +579,9 @@ have to redo the gap analysis from scratch. audit-grade documentation pack mapping the cipher list to FIPS 140-3 validated implementations. - **Authenticated proxy↔server channel** (NIST IA-3) — current trust is - network-level isolation only. Required: mTLS or signed forwarding token - (e.g. JWT signed by the proxy with a key zddc-server validates). + network-level isolation only. *Required:* mTLS or a signed forwarding + token (JWT). See §"Authenticated proxy↔server channel" subsection + below for the design trade-off and recommended path. - **Multi-factor authentication** (NIST IA-2(1)) — delegated to upstream proxy. Required: documented reference deployment with PIV/CAC via oauth2-proxy or equivalent. @@ -606,9 +610,15 @@ have to redo the gap analysis from scratch. - **Continuous monitoring hooks** (NIST CA-7) — automated alerting on `.zddc` changes, admin endpoint use, repeated 403s from one identity. Required: structured event emission to syslog/SIEM beyond the local file. +- **Configuration baseline export** (NIST CM-3) — change-control review + needs a way to enumerate every `.zddc` file's resolved effective ACL + for diff against a baseline. *Required:* `zddc-server policy export` + subcommand. See §"Policy export for change control" below. - **Supply-chain integrity** (NIST SI-7) — vendored libs (jszip, docx-preview, xlsx) need SBOM, CVE tracking, automated update pipeline. - `apps:` URL fetches need SHA-256 pinning and signature verification. + `apps:` URL fetches need code signing (operator trusts a published + public key once; no per-artifact hash management). See §"Code-signed + apps: URL fetches" below. - **Data-at-rest encryption** (NIST SC-28) — delegated to the deployment platform. Required: documented baseline (cloud KMS, LUKS, dm-crypt) with key-rotation procedures. @@ -619,6 +629,279 @@ have to redo the gap analysis from scratch. A full SSP / control-by-control mapping consumes this list as input; it is not a substitute for one. +The four bullets most likely to need engineering depth — FIPS, the +authenticated proxy channel, policy export, and signed `apps:` URL +fetches — have their own subsections below capturing the design +considerations and effort estimates so a future implementor doesn't +restart from zero. + +#### FIPS-validated cryptography (NIST SC-13) + +**Why the current build doesn't qualify.** Go's stdlib crypto is +correct and well-reviewed but has not been submitted to NIST's CMVP +(Cryptographic Module Validation Program). Federal evaluators want a +specific module identifier ("the cryptography is performed by validated +module #4282" or whatever certificate number applies); Go stdlib has +none. + +**Why "swap in microsoft/go" isn't free.** The microsoft/go fork +redirects every `crypto/*` call into the host's OpenSSL via cgo. The +moment we adopt it as the default toolchain: + +- The "single static binary, CGO-free" property in the Features list + (line 19) goes away. The binary dynamically links `libssl`/`libcrypto` + and needs OpenSSL installed at the target ABI. +- Cross-compile becomes harder: cgo cross-compilation needs the target + platform's OpenSSL during the build. Today `./build` cross-compiles + all four release platforms from one container with no extra tooling. +- macOS/Windows builds either bundle a non-validated OpenSSL (defeats + the FIPS purpose) or fall back to Go stdlib for those platforms + (validation not transitive). The four-platform release matrix + collapses to one (linux-amd64). +- Binary size grows ~5-10 MB; deployment gains a runtime dependency. +- Commercial customers gain literally nothing — Go stdlib crypto is + cryptographically correct; FIPS is a procurement-checkbox property, + not a strength property. + +**Recommended path: parallel `zddc-server-fips` build target.** Don't +swap the default toolchain. Instead, ship a second binary built with +microsoft/go on a RHEL/UBI base with the validated OpenSSL FIPS package +installed. linux-amd64 only. Distributed at +`releases/zddc-server-fips__linux-amd64`. Federal customers +download the fips variant; everyone else continues to download the lean +pure-Go binary. + +The validation belongs to OpenSSL, not to microsoft/go — the cert +number is for the OpenSSL FIPS provider. The microsoft/go toolchain is +the bridge that lets Go-built apps consume that validated module. So +the deployment commitment is "RHEL/UBI image, kernel-side +`update-crypto-policies --set FIPS`, validated OpenSSL on the host." A +helm chart `helm/zddc-server-fips/` would bundle the matching base +image and `securityContext`. + +**Effort estimate:** +- `Containerfile.fips` and `helm/zddc-server-fips/` chart: ~50 lines + each. +- `./build` updates to add the new target: ~30 lines (one + `podman build -f Containerfile.fips` invocation, one + `promote_zddc_server_fips` helper, integration into the existing + release flow). +- Verification: a smoke test that runs the fips binary, hits a TLS + endpoint, and confirms the negotiated cipher came from the FIPS + provider (visible via `SSL_CIPHER` exposed from OpenSSL). +- Documentation: ~50 lines updating `federal.html` and this README to + describe the supported deployment shape concretely. + +Total: ~250 lines net once the customer engages. + +**What stays unchanged.** Application code is identical between tracks. +The TLS hardening already shipped (cipher allowlist, curve preferences, +HSTS) reuses verbatim — every cipher we ship is on the OpenSSL FIPS +allowlist, so the same `tls.Config` works in both tracks. The +difference is purely the toolchain and the host OS configuration. + +#### Authenticated proxy↔server channel (NIST IA-3) + +**The current trust assumption.** zddc-server reads +`X-Auth-Request-Email` from any client that can connect to the bind +address. Network isolation (loopback bind + same-host proxy, or k8s +NetworkPolicy gating ingress) is the only thing preventing email +forgery. NIST IA-3 (Device Identification and Authentication) requires +**cryptographic** binding between system components for higher assurance +levels. + +**Two design options.** + +**Option A — mTLS.** + +The proxy presents a client certificate during the TLS handshake to +zddc-server. zddc-server verifies the cert against a trusted CA and +extracts the email from the cert's subject alternative names (or +matches the cert against an allowlist). + +- *Pros:* Works at the connection layer; transparent to handler code + (the email comes from `r.TLS.PeerCertificates[0]` instead of a + header). Reuses existing TLS infrastructure. Zero per-request + overhead beyond the standard TLS handshake. +- *Cons:* Cert distribution. Every proxy needs its own client cert; + every cert eventually expires. Operator runs a small private CA + (cert-manager + k8s, smallstep, Vault PKI) or rotates manually. + Operationally heavier than the alternative. + +**Option B — JWT (signed forwarding token).** + +The proxy signs a JWT for each forwarded request using its private key. +zddc-server verifies the signature with the proxy's public key. The JWT +carries the email, an issuer claim, an audience claim, and a short +expiry (replay protection). Sent in `Authorization: Bearer ` or a +custom `X-Auth-Forwarding-Token` header. + +- *Pros:* Lightweight. No PKI — just one public key to distribute to + zddc-server. Easy to debug (JWT is base64 JSON; `jwt.io` exists). + Easy to rotate (run two trusted public keys during rotation). + Integrates with existing oauth2-proxy / nginx pipelines that have + "sign and forward" modes built in. +- *Cons:* Adds a JWT library dep (~50KB). Requires a freshness model + (short expiry, e.g. 30s — so an exfiltrated token is useless after + half a minute). Requires key distribution + rotation, but at much + smaller scale than mTLS. + +**Recommended path: JWT first; mTLS as a sidecar option.** + +JWT is the better starting point. Lighter operationally, doesn't +compete with the existing TLS termination story (an stunnel-FIPS or +nginx-FIPS sidecar can still do mTLS at the network layer if needed), +and integrates with future role-based access (item AC-3(7) above) — +`roles` claims in the JWT can flow into the ABAC roles work. + +**Implementation sketch.** + +- New env var `ZDDC_JWT_PUBKEY=/etc/zddc/proxy-pub.pem` or + `ZDDC_JWT_PUBKEY_INLINE=...PEM bytes...`. +- New middleware (peer to `ACLMiddleware` in + `zddc/internal/handler/middleware.go`) that, when configured, reads + `Authorization: Bearer ` from the request, verifies the + signature, validates `aud == zddc-server`, `exp` in the future, + `nbf` in the past, and extracts `email` (and `roles`) from the + verified claims. +- Refusal mode: when `ZDDC_JWT_PUBKEY` is set and a request lacks a + valid JWT, return 401. No silent fallback — the operator opted in + to cryptographic binding. +- Library choice: `github.com/golang-jwt/jwt/v5` (pure Go, well + maintained) or `github.com/lestrrat-go/jwx` (more features, larger). + Lean toward the simpler `golang-jwt/jwt`. + +**Effort estimate:** ~150 lines of Go (middleware + config wiring + +key parsing) + the JWT library dependency + ~30 lines of deployment +docs showing oauth2-proxy and nginx sign-and-forward configurations. + +#### Policy export for change control (NIST CM-3) + +**Why this is needed.** Federal change-control review wants a baseline +file that can be diffed against current state. For zddc-server, "the +configuration" is the union of every `.zddc` file in the served tree. +Today an operator wanting "what's the effective access at /Project-A?" +has to walk the cascade by hand. The `/.profile/effective-policy` +endpoint shortcuts a single `(path, email)` query, but it doesn't +enumerate the whole policy. + +**What the export produces.** For every directory containing a `.zddc`, +the command emits: + +- The directory path +- The raw `.zddc` contents +- The resolved effective ACL (the chain back to root, flattened into + this directory's effective allow/deny lists) +- Equivalence-class summary: `(email-class, decision)` pairs covering + groups like `*@mycompany.com → allow`, `*@external.com → deny`, + specific exceptions + +Output formats: + +- **JSON** — machine-readable for `diff` against a baseline file in a + Git repo or a change-control tool +- **Markdown** — human-readable summary suitable for ATO documentation +- **CSV** — pivot-ready for spreadsheet review + +**Use cases:** + +- *Baseline + diff workflow.* Operator commits the export to a Git repo. + Every `.zddc` change auto-runs the export in CI; the diff is the + change-control artifact. Reviewers see "alice was added to + /Project-X/.zddc, which means alice can now access /Project-X/, + /Project-X/sub/, … because the cascade propagates." +- *Drift detection.* Export at deploy time vs export every day; any + unexpected change triggers an alert. +- *ATO documentation.* Plug the export into the SSP's "Configuration + Inventory" section. + +**Implementation sketch.** Mostly mechanical reuse of existing pieces: + +- `zddc.ScanZddcFiles(root)` already walks the tree (used by the + profile page's admin-subtrees listing). +- `zddc.EffectivePolicy(root, dir)` already builds the chain (used by + every ACL check). +- `zddc.MatchesPattern` already covers the email matching. + +What's missing is the CLI subcommand wiring (today the binary takes +flags only; needs subcommand parsing for `policy export`), the +equivalence-class computation (group emails by which patterns they +match — small graph problem), and the format renderers. + +**Effort estimate:** ~250 lines of Go (CLI subcommand + equivalence- +class computation + JSON/Markdown/CSV renderers) + ~100 lines of tests. + +#### Code-signed `apps:` URL fetches (NIST SI-7) + +**The supply-chain risk today.** The `apps:` mechanism fetches a URL +once on first request, caches the bytes forever, and serves them to +every viewer below that level. There's no integrity check. If the +fetched URL is ever compromised — DNS hijack, CDN account takeover, +malicious upstream commit, MITM during the one fetch window — every +customer caches the bad bytes. The blast radius is "every user who +visits an archive page in a subtree where this `.zddc` applies." + +**Why code signing instead of SHA-256 pinning.** SHA-256 pinning would +require operators to track-and-update a hash in `.zddc` every time an +artifact changes. Wrong workflow for this product. Code signing +sidesteps the operator entirely: + +- Release pipeline signs each artifact once at publish time with a + long-lived private key. +- Operator trusts the published public key once and never deals with + hashes. +- zddc-server fetches the URL, downloads the detached signature + (e.g. `.sig`), verifies against the configured public key, + caches if valid. + +**Implementation has three parts** (each is a separate piece of work +that interlocks with the others): + +1. **Signing in the build pipeline.** `./build alpha|beta|release` + gains a signing step: after producing each `_v*.html` and + each `zddc-server_v*` binary, sign with a project key (Ed25519 or + RSA) and publish the `.sig` alongside. Private key stored + securely (HashiCorp Vault, GitHub Actions secret, etc.). + +2. **Public key on the website.** A "Verify your downloads" section + on the releases page with the PEM public key inline, a download + link to `pubkey.pem`, the key fingerprint shown in plain text, + and a `curl` example for verifying a downloaded artifact. + +3. **Verifier in zddc-server.** New code in `apps/fetch.go` reading + a `signed_by:` field from `.zddc apps:` syntax (or just an + embedded default trusting the project key) and validating each + fetched artifact's signature before caching it. Two + implementation flavors: + + - *Simple:* stdlib `crypto/ed25519`. ~200 lines including the + `apps:` schema extension to accept `signed_by:` and the + fetch-and-verify path. Fine for trusting one project key. + - *Sigstore:* `github.com/sigstore/sigstore` for transparency- + log-backed signing (Rekor inclusion proofs). Larger + dependency (~5MB). Federally-acceptable; aligns with how + modern federal stacks (DoD's Iron Bank, etc.) consume signed + artifacts. + +**Recommended sequencing:** start with the simple Ed25519 case for the +project's own artifacts. Add Sigstore as an opt-in (`signed_by: +sigstore:...`) when a federal customer engages. + +**What's currently in place.** Nothing — no signing, no `.sig` files, +no verifier. The current `apps/fetch.go` is fetch-once-cached-forever +with no integrity check. The whole feature is greenfield. + +**Effort estimate (simple Ed25519 case):** +- `./build` signing step + key generation procedure: ~100 lines + ops + documentation. +- `pubkey.pem` distribution on the website + verify section in + `index.html` releases page: ~50 lines of HTML. +- `apps/` parser extension for `signed_by:` field: ~50 lines. +- Verifier logic in `apps/fetch.go`: ~150 lines. +- Tests: ~150 lines. + +Total: ~500 lines net plus a key-management procedure. + ### External policy decider (OPA-compatible) For deployments that need policy decisions made by an external,