docs(server): rewrite README apps section for the local-only override model
Brings zddc/README.md in line with the apps-fetch removal: - "Apps: virtual tool HTMLs" override section now describes the 3-tier local resolution (on-disk file → <root>/.zddc.zip member → embedded); drops the URL/channel/version spec forms, the _app/ cache, signatures, and the apps: example. - Remove the ZDDC_APPS_PUBKEY env-var row. - Security invariants #4 (in two places) reframed: a tool HTML on disk or in the site .zddc.zip is a full UI mount (write access = UI-mounting authority; <root>/.zddc.zip = site-wide), with no fetch and nothing to sign. - Federal gap analysis: SI-7 no longer cites apps URL fetches; delete the whole "Code-signed apps: URL fetches (NIST SI-7)" subsection (feature gone). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4eeb25c0ef
commit
4e86b1533d
1 changed files with 50 additions and 120 deletions
170
zddc/README.md
170
zddc/README.md
|
|
@ -61,7 +61,6 @@ There is no Containerfile / Dockerfile / compose file in this repo. Two ways to
|
||||||
| `ZDDC_OPA_URL` | `internal` | Policy decider endpoint. `internal` = built-in Go evaluator (default). `http(s)://...` or `unix:///...` = external OPA-compatible server (federal deployments using their own audited Rego). See "External policy decider" below. |
|
| `ZDDC_OPA_URL` | `internal` | Policy decider endpoint. `internal` = built-in Go evaluator (default). `http(s)://...` or `unix:///...` = external OPA-compatible server (federal deployments using their own audited Rego). See "External policy decider" below. |
|
||||||
| `ZDDC_OPA_FAIL_OPEN` | *(empty)* | External OPA only. `1` = on transport error, allow the request (availability over correctness). Default = fail closed (deny). Never set to `1` in federal contexts. |
|
| `ZDDC_OPA_FAIL_OPEN` | *(empty)* | External OPA only. `1` = on transport error, allow the request (availability over correctness). Default = fail closed (deny). Never set to `1` in federal contexts. |
|
||||||
| `ZDDC_OPA_CACHE_TTL` | `1s` | External OPA only. Per-decision cache TTL — bursts of identical queries (a single `.archive` listing can hit the same `(email, dir)` tuple many times) collapse to one OPA round-trip. Set `0` to disable. Format is Go's `time.ParseDuration` (`500ms`, `2s`, `1m`). |
|
| `ZDDC_OPA_CACHE_TTL` | `1s` | External OPA only. Per-decision cache TTL — bursts of identical queries (a single `.archive` listing can hit the same `(email, dir)` tuple many times) collapse to one OPA round-trip. Set `0` to disable. Format is Go's `time.ParseDuration` (`500ms`, `2s`, `1m`). |
|
||||||
| `ZDDC_APPS_PUBKEY` | *(empty)* | Path to a PEM-encoded Ed25519 public key used to verify signatures on URL-fetched `apps:` artifacts. Empty (default) = URL-fetched apps refused; only embedded + local-path apps work. Operators using zddc.varasys.io's canonical channels download `pubkey.pem` from there and pass the local path here. Operators with their own signing infrastructure pass their own public key. Same posture as `ZDDC_TLS_CERT` — zddc-server bakes nothing in. **Alternative inline form:** the same key can be pasted as `apps_pubkey:` in the root `<ZDDC_ROOT>/.zddc` (root-only, like `admins:`). The env/flag wins when both are set. |
|
|
||||||
| `ZDDC_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
| `ZDDC_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||||
| `ZDDC_INDEX_PATH` | `.archive` | URL path segment name for the virtual archive index |
|
| `ZDDC_INDEX_PATH` | `.archive` | URL path segment name for the virtual archive index |
|
||||||
| `ZDDC_EMAIL_HEADER` | `X-Auth-Request-Email` | HTTP request header containing the authenticated user's email (the oauth2-proxy / nginx auth-request convention) |
|
| `ZDDC_EMAIL_HEADER` | `X-Auth-Request-Email` | HTTP request header containing the authenticated user's email (the oauth2-proxy / nginx auth-request convention) |
|
||||||
|
|
@ -672,14 +671,15 @@ naive intuition suggests.
|
||||||
at `zddc/internal/zddc/file.go:17-20` (and `IsAdmin`) only reads root. This is
|
at `zddc/internal/zddc/file.go:17-20` (and `IsAdmin`) only reads root. This is
|
||||||
the only upward-escalation gate; subtree write access never grants admin.
|
the only upward-escalation gate; subtree write access never grants admin.
|
||||||
|
|
||||||
4. **An `apps:` URL override is a full UI mount, not just a tool version pin.**
|
4. **Dropping a tool HTML on disk is a full UI mount, not just a file.**
|
||||||
Any `.zddc` writer in a subtree can pin `archive: https://attacker.example/...`
|
A real `<app>.html` at a path — or an `<app>.html` member of the site
|
||||||
and serve arbitrary HTML to every viewer below that level. Subtree write
|
`<ZDDC_ROOT>/.zddc.zip` bundle — is served verbatim to every viewer at or
|
||||||
authority on `.zddc` should be treated as full UI-mounting authority. The
|
below that scope. So write access to a directory is effectively UI-mounting
|
||||||
`_app/` cache is fetch-once-and-keep — operators clear it by deleting
|
authority for it, and write access to `<root>/.zddc.zip` is a **site-wide**
|
||||||
`<ZDDC_ROOT>/_app/<scheme>/<host>[:<port>]/<path>`. (See "Apps: virtual tool HTMLs" below for
|
UI mount (treat it like `admins:` — keep the root writable only by admins).
|
||||||
the resolver order; SHA-256 pinning is on the federal-readiness list, not
|
There is no remote fetch and nothing to sign: the bytes are whatever sits on
|
||||||
currently implemented.)
|
the local filesystem, governed by the same ACL/WORM as any other file. (See
|
||||||
|
"Apps: virtual tool HTMLs" below for the resolver order.)
|
||||||
|
|
||||||
5. **Relying on `/Archive/` being unbrowsable to "hide" sibling vendor folders'
|
5. **Relying on `/Archive/` being unbrowsable to "hide" sibling vendor folders'
|
||||||
existence.** Sibling-vendor names are hidden because directories the caller
|
existence.** Sibling-vendor names are hidden because directories the caller
|
||||||
|
|
@ -742,10 +742,12 @@ guarantee these for the model above to hold:
|
||||||
logging, ship the JSON-line file to an external append-only sink (syslog,
|
logging, ship the JSON-line file to an external append-only sink (syslog,
|
||||||
SIEM) via a sidecar; do not treat the local rotation as the system of
|
SIEM) via a sidecar; do not treat the local rotation as the system of
|
||||||
record.
|
record.
|
||||||
4. **`apps:` URL fetches have no integrity check.** Fetched once on first
|
4. **Tool-HTML overrides are local files, not fetched/signed.** A tool's HTML
|
||||||
miss, cached at `<ZDDC_ROOT>/_app/<scheme>/<host>[:<port>]/<path>` forever — no SHA-256 pin,
|
comes from a real file at the path, an `<app>.html` member of the site
|
||||||
no signature. Use only URLs you control, treat the apps cache as a trust
|
`<ZDDC_ROOT>/.zddc.zip`, or the embedded default — never the network. There
|
||||||
boundary, and audit who has `.zddc` write authority where.
|
is nothing to verify; the trust boundary is filesystem write access. Audit
|
||||||
|
who can write tool HTML at each scope (especially `<root>/.zddc.zip`, a
|
||||||
|
site-wide UI mount).
|
||||||
|
|
||||||
### Debugging permissions
|
### Debugging permissions
|
||||||
|
|
||||||
|
|
@ -949,9 +951,9 @@ have to redo the gap analysis from scratch.
|
||||||
subcommand. See §"Policy export for change control" below.
|
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 code signing (operator trusts a published
|
Tool HTML is no longer fetched at runtime (overrides are local files /
|
||||||
public key once; no per-artifact hash management). See §"Code-signed
|
the site `.zddc.zip` bundle, governed by filesystem ACL), so there is no
|
||||||
apps: URL fetches" below.
|
remote-artifact signing requirement here.
|
||||||
- **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.
|
||||||
|
|
@ -962,11 +964,10 @@ 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
|
The bullets most likely to need engineering depth — FIPS, the
|
||||||
authenticated proxy channel, policy export, and signed `apps:` URL
|
authenticated proxy channel, and policy export — have their own
|
||||||
fetches — have their own subsections below capturing the design
|
subsections below capturing the design considerations and effort
|
||||||
considerations and effort estimates so a future implementor doesn't
|
estimates so a future implementor doesn't restart from zero.
|
||||||
restart from zero.
|
|
||||||
|
|
||||||
#### FIPS-validated cryptography (NIST SC-13)
|
#### FIPS-validated cryptography (NIST SC-13)
|
||||||
|
|
||||||
|
|
@ -1164,77 +1165,6 @@ match — small graph problem), and the format renderers.
|
||||||
**Effort estimate:** ~250 lines of Go (CLI subcommand + equivalence-
|
**Effort estimate:** ~250 lines of Go (CLI subcommand + equivalence-
|
||||||
class computation + JSON/Markdown/CSV renderers) + ~100 lines of tests.
|
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** that interlock:
|
|
||||||
|
|
||||||
1. **Signing in the build pipeline.** `./build release` runs
|
|
||||||
`sign_release_artifacts` (in `./build`) after promote: walks
|
|
||||||
`dist/release-output/` and produces a detached Ed25519 signature
|
|
||||||
(`<artifact>.sig`) alongside every real file. Private key path comes
|
|
||||||
from `ZDDC_SIGNING_KEY`; absent or unreadable → release fails.
|
|
||||||
Symlinks (the canonical `<tool>.html` and `zddc-server_<platform>`
|
|
||||||
URLs) skip — the .sig at the symlink target is what counts; a
|
|
||||||
companion `.sig` symlink chains the canonical URL to that target.
|
|
||||||
|
|
||||||
2. **Public key on the website.** `pubkey.pem` is a real file in
|
|
||||||
`~/src/zddc-website/`, deployed to `zddc.varasys.io/pubkey.pem`.
|
|
||||||
The releases-page index includes a "Verify your downloads"
|
|
||||||
section with a download link, the SHA-256 fingerprint shown in
|
|
||||||
plain text, and a `curl + openssl pkeyutl -verify` example.
|
|
||||||
|
|
||||||
3. **Verifier in zddc-server (`apps/fetch.go`).** When fetching a
|
|
||||||
URL-pinned `apps:` artifact, also fetch `<url>.sig`, then call
|
|
||||||
`VerifyEd25519` against `Fetcher.VerifyKey`. The key is loaded
|
|
||||||
at startup with this resolution order:
|
|
||||||
1. `--apps-pubkey` / `ZDDC_APPS_PUBKEY` (path to PEM file)
|
|
||||||
2. `apps_pubkey:` inline PEM in the root `.zddc` file (root-only,
|
|
||||||
same trust-anchor treatment as `admins:`)
|
|
||||||
3. nothing → URL fetches refused
|
|
||||||
Failure cases at fetch time — sig 404, transport error, wrong key,
|
|
||||||
tampered body — all reject; the body is dropped and the apps
|
|
||||||
resolver falls back to the embedded copy. No baked-in default
|
|
||||||
public key; same posture as TLS certificates.
|
|
||||||
|
|
||||||
**Trust model.** The operator decides which signing infrastructure to
|
|
||||||
trust by configuring `--apps-pubkey`. The website publishes the
|
|
||||||
canonical-channel public key; operators who use `apps: archive: stable`
|
|
||||||
download it once, save the file on their server, and configure the
|
|
||||||
path. Operators running their own signing point at their own pubkey
|
|
||||||
instead. zddc-server has no opinion.
|
|
||||||
|
|
||||||
**Future direction.** A `signed_by:` field per-`apps:` entry would let
|
|
||||||
a single deployment trust multiple signing keys (one for canonical
|
|
||||||
channels, one for an in-house mirror). Sigstore integration
|
|
||||||
(transparency-log-backed signing via `github.com/sigstore/sigstore`)
|
|
||||||
is the federally-acceptable evolution. Both are additive on top of
|
|
||||||
the current single-key-per-server model.
|
|
||||||
|
|
||||||
**What's currently in place.** All three parts. The scaffolding
|
|
||||||
matches the design above one-to-one; future enhancements are
|
|
||||||
extensions, not refactors.
|
|
||||||
|
|
||||||
### 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,
|
||||||
|
|
@ -1521,8 +1451,8 @@ fsnotify watcher's debounce window (~2 s) — no service restart needed.
|
||||||
`zddc-server` virtually serves the tool HTMLs (archive, transmittal,
|
`zddc-server` virtually serves the tool HTMLs (archive, transmittal,
|
||||||
classifier, landing, browse, form, tables) at the appropriate paths.
|
classifier, landing, browse, form, tables) at the appropriate paths.
|
||||||
The current-stable build of each tool is **baked into the binary at
|
The current-stable build of each tool is **baked into the binary at
|
||||||
compile time** via `//go:embed`; that's the default. No fetch happens
|
compile time** via `//go:embed`; that's the default. Overrides are
|
||||||
out of the box.
|
**local only** — there is no network fetch, ever.
|
||||||
|
|
||||||
### Where each tool is served
|
### Where each tool is served
|
||||||
|
|
||||||
|
|
@ -1536,40 +1466,40 @@ out of the box.
|
||||||
|
|
||||||
Outside these locations, the corresponding `<app>.html` URL returns 404.
|
Outside these locations, the corresponding `<app>.html` URL returns 404.
|
||||||
|
|
||||||
### Override and version-pin
|
### Override (local only)
|
||||||
|
|
||||||
For any path, the resolution order is:
|
For any path, the resolution order is:
|
||||||
|
|
||||||
1. **Real file at the path** — operator drops `archive.html` (or any other)
|
1. **Real file at the path** — drop a real `archive.html` (or `browse.html`,
|
||||||
into a directory; the static handler serves it. Beats everything below.
|
or a brand-new `mytool.html`) into a directory; the static handler serves
|
||||||
2. **Closer-to-leaf `.zddc apps:` entry** — walks `.zddc` files leaf→root
|
it. Beats everything below.
|
||||||
for an `apps.<app>` entry. The first match wins. Spec is one of:
|
2. **Site bundle `<ZDDC_ROOT>/.zddc.zip`** — a local zip whose `<app>.html`
|
||||||
- `stable` (canonical upstream "current stable")
|
members override the embedded default site-wide (and let you add new
|
||||||
- `v0.0.4` (canonical upstream exact-version pin)
|
`<name>.html` tools). The server reads members from the filesystem via
|
||||||
- `https://...` (full URL to a custom mirror)
|
`internal/zipfs` — no fetch, no signature. The bundle is re-stat'd on each
|
||||||
- `./local.html` / `/abs/path.html` (local file)
|
request, so dropping in a new one takes effect immediately.
|
||||||
3. **Embedded** — the build-time HTML compiled into the binary.
|
3. **Embedded** — the build-time HTML compiled into the binary.
|
||||||
|
|
||||||
URL sources are fetched once on first request and cached forever in
|
There is no `apps:` `.zddc` key, no channels/versions, no URL fetching, and no
|
||||||
`<ZDDC_ROOT>/_app/<scheme>/<host>[:<port>]/<path>`. There is no background refresh and no
|
signature verification — all removed in favour of this local model. `.zddc.zip`
|
||||||
hash verification — to pull a new build, delete the cache file. Concurrent
|
is config, not content: a direct `GET /.zddc.zip` returns 404 for everyone,
|
||||||
misses for the same URL share one outbound fetch (singleflight). Direct
|
while the server reads its members internally (so resolution works for any
|
||||||
URL access to `/_app/...` is blocked at dispatch; cached HTMLs are served
|
user). The `X-ZDDC-Source` response header reports what was served:
|
||||||
only via the apps resolver.
|
`bundle:<app>.html` or `embedded:<app>@<build>` (an on-disk override is served
|
||||||
|
by the static handler with its own headers).
|
||||||
If a configured URL fetch fails (network down, 5xx), the server falls back
|
|
||||||
to the embedded copy and emits a one-time WARN log per source. The
|
|
||||||
`X-ZDDC-Source` response header always reports what was served:
|
|
||||||
`fetch:URL`, `cache:URL`, `path:/abs`, or `embedded:<app>@<build>`.
|
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```yaml
|
Override `browse` everywhere and add a custom `report` tool via the bundle:
|
||||||
# <ZDDC_ROOT>/Project-A/.zddc
|
|
||||||
apps:
|
```sh
|
||||||
classifier: v0.0.4 # pin classifier to v0.0.4 for this project
|
cd <ZDDC_ROOT> && zip .zddc.zip browse.html report.html
|
||||||
archive: https://my-mirror.internal/zddc/archive_v0.0.4.html # custom mirror, pinned
|
```
|
||||||
browse: ./our-browse.html # local fork
|
|
||||||
|
Or override a single tool in one project by dropping a file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp our-browse.html <ZDDC_ROOT>/Project-A/browse.html
|
||||||
```
|
```
|
||||||
|
|
||||||
### Env vars
|
### Env vars
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue