docs(server): apps composer on releases page; deeper federal-readiness gap analysis
Two doc/website improvements:
build:341 build_releases_index() — new "Build your apps: block" section
between the pinning narrative and the channels explainer. Per-app
dropdowns (one each for archive/transmittal/classifier/mdedit/landing),
a live-updating YAML textarea, and a Copy button. The dropdowns clone
their options from the existing #version-picker (channels at top,
pinned versions below) so we don't duplicate version data into JS —
the picker is the single source of truth for "what versions exist."
~80 lines of HTML+JS added; no SHA-256 anywhere (per user direction
that code signing is the future supply-chain answer, not hash pinning).
zddc/README.md § Federal-readiness gap analysis — promoted four items
that previously were one-line bullets to per-item subsections so a
future implementor doesn't have to redo the design conversation:
- FIPS-validated cryptography (NIST SC-13): captures cgo + OpenSSL
implications, the platform-matrix reality, and the parallel
zddc-server-fips build target architecture (linux-amd64 only,
RHEL/UBI base, validated OpenSSL on host).
- Authenticated proxy↔server channel (NIST IA-3): mTLS vs JWT
trade-offs spelled out. Recommended: JWT first; mTLS available
for deployments that already operate a private CA.
- Policy export for change control (NIST CM-3): zddc-server policy
export subcommand emitting every directory's resolved ACL in
JSON / Markdown / CSV. Reuses zddc.ScanZddcFiles +
zddc.EffectivePolicy + zddc.MatchesPattern.
- Code-signed apps: URL fetches (NIST SI-7): replaces SHA-256
pinning (operator hash-tracking burden) with code signing
(operator trusts a public key once). Three-part implementation
(build pipeline signs, public key on website, verifier in
apps/fetch.go).
The bullet list at the top of the gap analysis stays as a one-line
index pointing at the subsections.
Items #6 (ABAC roles) and #7 (logs: block in root .zddc) stay as
bullets — commercial-deployment features, not federal-track.
No code changes to the binary. No tests touched. ~280 lines added
across the two files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
570604ca80
commit
8b33683a59
2 changed files with 383 additions and 5 deletions
95
build
95
build
|
|
@ -582,6 +582,44 @@ PIN_MID
|
||||||
<p class="pin-note">Your archive's tools are <strong>yours</strong>. 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.</p>
|
<p class="pin-note">Your archive's tools are <strong>yours</strong>. 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.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── Apps: composer ───────────── -->
|
||||||
|
<section class="card" style="border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-xl);">
|
||||||
|
<h2 style="margin-top:0;">Build your <code>.zddc apps:</code> block</h2>
|
||||||
|
<p>Pick a channel or pinned version for each tool. The YAML on the right updates as you go — copy it into a <code class="inline">.zddc</code> file at the level of your archive where you want the config to apply. <em>Closer-to-leaf wins</em>, so a project subdirectory can override what its parent says.</p>
|
||||||
|
<div class="composer-grid" style="display: grid; grid-template-columns: minmax(260px, 1fr) 2fr; gap: var(--spacing-lg); margin-top: var(--spacing-md);">
|
||||||
|
<div class="composer-rows" style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
|
||||||
|
<label class="composer-row" style="display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm);">
|
||||||
|
<span style="flex: 1;"><code class="inline">archive</code> <span style="color: var(--color-text-muted); font-size: 0.85rem;">— Archive Browser</span></span>
|
||||||
|
<select class="composer-select" data-app="archive" style="min-width: 140px;"></select>
|
||||||
|
</label>
|
||||||
|
<label class="composer-row" style="display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm);">
|
||||||
|
<span style="flex: 1;"><code class="inline">transmittal</code> <span style="color: var(--color-text-muted); font-size: 0.85rem;">— Transmittal Creator</span></span>
|
||||||
|
<select class="composer-select" data-app="transmittal" style="min-width: 140px;"></select>
|
||||||
|
</label>
|
||||||
|
<label class="composer-row" style="display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm);">
|
||||||
|
<span style="flex: 1;"><code class="inline">classifier</code> <span style="color: var(--color-text-muted); font-size: 0.85rem;">— Classifier</span></span>
|
||||||
|
<select class="composer-select" data-app="classifier" style="min-width: 140px;"></select>
|
||||||
|
</label>
|
||||||
|
<label class="composer-row" style="display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm);">
|
||||||
|
<span style="flex: 1;"><code class="inline">mdedit</code> <span style="color: var(--color-text-muted); font-size: 0.85rem;">— Markdown Editor</span></span>
|
||||||
|
<select class="composer-select" data-app="mdedit" style="min-width: 140px;"></select>
|
||||||
|
</label>
|
||||||
|
<label class="composer-row" style="display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm);">
|
||||||
|
<span style="flex: 1;"><code class="inline">landing</code> <span style="color: var(--color-text-muted); font-size: 0.85rem;">— Landing</span></span>
|
||||||
|
<select class="composer-select" data-app="landing" style="min-width: 140px;"></select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="composer-output" style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
|
||||||
|
<textarea id="composer-yaml" readonly rows="10" style="width: 100%; font-family: ui-monospace, SFMono-Regular, monospace; font-size: 0.875rem; padding: var(--spacing-sm); border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-bg); color: var(--color-text); resize: vertical;"></textarea>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span id="composer-status" style="font-size: 0.85rem; color: var(--color-text-muted);"></span>
|
||||||
|
<button id="composer-copy" type="button" class="dl-primary" style="padding: 0.4rem 0.9rem; font-size: 0.875rem;">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: var(--spacing-md); font-size: 0.9rem; color: var(--color-text-muted);">Tip: leave a tool on <strong>stable</strong> 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 → <code class="inline">.zddc apps:</code> entry (closer-to-leaf wins) → embedded fallback.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ───────────── Channels explainer ───────────── -->
|
<!-- ───────────── Channels explainer ───────────── -->
|
||||||
<section class="card" style="border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-xl); margin-bottom: var(--spacing-xl);">
|
<section class="card" style="border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-xl); margin-bottom: var(--spacing-xl);">
|
||||||
<h2 style="margin-top:0;">Channels</h2>
|
<h2 style="margin-top:0;">Channels</h2>
|
||||||
|
|
@ -695,6 +733,63 @@ PIN_MID
|
||||||
// non-linux client, that needs to flip to the detected platform).
|
// non-linux client, that needs to flip to the detected platform).
|
||||||
rewire(picker.value);
|
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 <option value="...">label</option>
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
293
zddc/README.md
293
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.
|
have to redo the gap analysis from scratch.
|
||||||
|
|
||||||
- **FIPS 140-3 cryptography** (NIST SC-13) — current build uses Go stdlib
|
- **FIPS 140-3 cryptography** (NIST SC-13) — current build uses Go stdlib
|
||||||
crypto. Required: build with `GOEXPERIMENT=systemcrypto` + RHEL FIPS
|
crypto. *Required:* parallel `zddc-server-fips` build target using the
|
||||||
userspace, or use the `microsoft/go` (formerly goboring) toolchain.
|
`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
|
- **TLS hardening** (NIST SC-8(1)) — *partially complete.* Server now
|
||||||
sets `MinVersion: tls.VersionTLS12`, the NIST SP 800-52 Rev. 2
|
sets `MinVersion: tls.VersionTLS12`, the NIST SP 800-52 Rev. 2
|
||||||
AEAD-only cipher allowlist (ECDHE+AES-GCM and ECDHE+ChaCha20Poly1305
|
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
|
audit-grade documentation pack mapping the cipher list to FIPS
|
||||||
140-3 validated implementations.
|
140-3 validated implementations.
|
||||||
- **Authenticated proxy↔server channel** (NIST IA-3) — current trust is
|
- **Authenticated proxy↔server channel** (NIST IA-3) — current trust is
|
||||||
network-level isolation only. Required: mTLS or signed forwarding token
|
network-level isolation only. *Required:* mTLS or a signed forwarding
|
||||||
(e.g. JWT signed by the proxy with a key zddc-server validates).
|
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
|
- **Multi-factor authentication** (NIST IA-2(1)) — delegated to upstream
|
||||||
proxy. Required: documented reference deployment with PIV/CAC via
|
proxy. Required: documented reference deployment with PIV/CAC via
|
||||||
oauth2-proxy or equivalent.
|
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
|
- **Continuous monitoring hooks** (NIST CA-7) — automated alerting on
|
||||||
`.zddc` changes, admin endpoint use, repeated 403s from one identity.
|
`.zddc` changes, admin endpoint use, repeated 403s from one identity.
|
||||||
Required: structured event emission to syslog/SIEM beyond the local file.
|
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,
|
- **Supply-chain integrity** (NIST SI-7) — vendored libs (jszip,
|
||||||
docx-preview, xlsx) need SBOM, CVE tracking, automated update pipeline.
|
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
|
- **Data-at-rest encryption** (NIST SC-28) — delegated to the deployment
|
||||||
platform. Required: documented baseline (cloud KMS, LUKS, dm-crypt) with
|
platform. Required: documented baseline (cloud KMS, LUKS, dm-crypt) with
|
||||||
key-rotation procedures.
|
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
|
A full SSP / control-by-control mapping consumes this list as input; it is
|
||||||
not a substitute for one.
|
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_<channel>_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 <jwt>` 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 <jwt>` 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. `<artifact>.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 `<tool>_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)
|
### External policy decider (OPA-compatible)
|
||||||
|
|
||||||
For deployments that need policy decisions made by an external,
|
For deployments that need policy decisions made by an external,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue