feat(apps): also accept apps_pubkey: inline in root .zddc
Adds a second way to configure the apps signing pubkey alongside the existing --apps-pubkey / ZDDC_APPS_PUBKEY (path-to-PEM-file) form: an inline PEM block under apps_pubkey: in the root .zddc file. Resolution order: 1. --apps-pubkey / ZDDC_APPS_PUBKEY (path) ← env/flag wins 2. apps_pubkey: inline PEM in root .zddc ← second 3. nothing ← URL fetches refused Honored only at the root .zddc — same trust-anchor treatment as the existing admins: field. Subtree write authority cannot re-anchor trust because subtree apps_pubkey: entries are ignored. (Same unmarshal pattern as the rest of ZddcFile; the root-only enforcement is in setupApps where we explicitly read filepath.Join(cfg.Root, ".zddc") rather than walking a chain.) Why offer both: env/flag fits k8s + systemd deployment shapes where the operator already manages a config volume and prefers env-based plumbing. Inline-in-.zddc fits the "everything in one config file" mental model and matches how operators already think about admins: and acl:. Either ships a working URL-fetch-verify story; the choice is operator preference. Logged differently per source so operators can grep for which path populated the key: apps signing pubkey loaded source=env/flag path=/path/to/pubkey.pem apps signing pubkey loaded source="root .zddc apps_pubkey" Smoke-tested end-to-end: a root .zddc with inline apps_pubkey: PEM block + apps: archive: <upstream-URL> + ZDDC_APPS_PUBKEY unset — the server logs "loaded source=root .zddc apps_pubkey" at startup, fetches the URL, verifies the .sig against the inline key, caches. Tampering still rejects; missing .sig still rejects; everything that worked yesterday still works. Docs: env-var tables in zddc/README.md and AGENTS.md note the inline alternative; the federal-readiness gap analysis subsection on code signing now lists both paths in its resolution order; the release-page "Verify your downloads" section mentions both for operators. Production binary unchanged at ~13 MB. All 11 Go test packages green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9765fa2f5e
commit
52dde0b014
5 changed files with 54 additions and 22 deletions
|
|
@ -396,7 +396,7 @@ ZDDC_ROOT=/path/to/your/archive ZDDC_TLS_CERT=none ZDDC_ADDR=:8080 \
|
||||||
| `ZDDC_OPA_URL` | `internal` | Policy decider endpoint. `internal` (default) = in-process Go evaluator (same `.zddc` cascade we always had). `http(s)://...` or `unix:///...` = external OPA — every access decision becomes a `POST /v1/data/zddc/access/allow` to the configured endpoint. Federal customers with their own audited Rego use this; commercial deployments leave it `internal`. |
|
| `ZDDC_OPA_URL` | `internal` | Policy decider endpoint. `internal` (default) = in-process Go evaluator (same `.zddc` cascade we always had). `http(s)://...` or `unix:///...` = external OPA — every access decision becomes a `POST /v1/data/zddc/access/allow` to the configured endpoint. Federal customers with their own audited Rego use this; commercial deployments leave it `internal`. |
|
||||||
| `ZDDC_OPA_FAIL_OPEN` | *(empty)* | External OPA only. `1` = allow on transport error; default = fail closed (deny). |
|
| `ZDDC_OPA_FAIL_OPEN` | *(empty)* | External OPA only. `1` = allow on transport error; default = fail closed (deny). |
|
||||||
| `ZDDC_OPA_CACHE_TTL` | `1s` | External OPA only. Per-decision cache TTL — amortizes round-trips on bursty patterns (e.g. `.archive` listings hit the same `(email, dir)` tuple many times). `0` disables. Format is Go `time.ParseDuration`. |
|
| `ZDDC_OPA_CACHE_TTL` | `1s` | External OPA only. Per-decision cache TTL — amortizes round-trips on bursty patterns (e.g. `.archive` listings hit the same `(email, dir)` tuple many times). `0` disables. Format is Go `time.ParseDuration`. |
|
||||||
| `ZDDC_APPS_PUBKEY` | *(empty)* | Path to PEM Ed25519 pubkey for verifying signatures on URL-fetched `apps:` artifacts. Empty = URL apps refused. Download from `zddc.varasys.io/pubkey.pem` (canonical channels) or supply your own. No baked-in default — same posture as TLS certs. |
|
| `ZDDC_APPS_PUBKEY` | *(empty)* | Path to PEM Ed25519 pubkey for verifying signatures on URL-fetched `apps:` artifacts. Empty = URL apps refused. Download from `zddc.varasys.io/pubkey.pem` (canonical channels) or supply your own. No baked-in default — same posture as TLS certs. Alternative inline form: `apps_pubkey:` in root `.zddc` (root-only, env/flag wins). |
|
||||||
| `ZDDC_ACCESS_LOG` | `<ZDDC_ROOT>/.zddc.d/logs/access-<host>.log` | JSON-line audit log (lumberjack-rotated, 100 MB / 10 backups / 90 days, gzipped). Server auto-mkdirs the parent. Set explicitly to empty (`--access-log=`) to disable. Per-host filename + `host` field in every record so multi-replica deployments writing to the same `.zddc.d/` dir disambiguate cleanly. |
|
| `ZDDC_ACCESS_LOG` | `<ZDDC_ROOT>/.zddc.d/logs/access-<host>.log` | JSON-line audit log (lumberjack-rotated, 100 MB / 10 backups / 90 days, gzipped). Server auto-mkdirs the parent. Set explicitly to empty (`--access-log=`) to disable. Per-host filename + `host` field in every record so multi-replica deployments writing to the same `.zddc.d/` dir disambiguate cleanly. |
|
||||||
|
|
||||||
### Release tagging
|
### Release tagging
|
||||||
|
|
|
||||||
2
build
2
build
|
|
@ -717,7 +717,7 @@ openssl pkeyutl -verify -pubin -inkey pubkey.pem \
|
||||||
<p style="font-size: 0.85rem;">Output is <code class="inline">Signature Verified Successfully</code> on a clean download. Any other output (or no output and a non-zero exit) means the bytes do not match the published signature — do not trust them.</p>
|
<p style="font-size: 0.85rem;">Output is <code class="inline">Signature Verified Successfully</code> on a clean download. Any other output (or no output and a non-zero exit) means the bytes do not match the published signature — do not trust them.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style="margin-top: var(--spacing-md); font-size: 0.9rem; color: var(--color-text-muted);">For server deployments, configure the public key once and forget it: download <code class="inline">pubkey.pem</code>, save it on your server, and pass <code class="inline">ZDDC_APPS_PUBKEY=/path/to/pubkey.pem</code> when starting <code class="inline">zddc-server</code>. The resolver fetches the <code class="inline">.sig</code> automatically and rejects any unsigned or invalid-signature artifact, falling back to the embedded copy. Operators wanting to enforce signature verification on locally-saved artifacts (Path A, "drop a copy into your archive") run the <code class="inline">openssl</code> verify command above as part of their save workflow.</p>
|
<p style="margin-top: var(--spacing-md); font-size: 0.9rem; color: var(--color-text-muted);">For server deployments, configure the public key once and forget it. Two equivalent ways: pass <code class="inline">ZDDC_APPS_PUBKEY=/path/to/pubkey.pem</code> when starting <code class="inline">zddc-server</code>, or paste the PEM contents inline as <code class="inline">apps_pubkey:</code> in the root <code class="inline">.zddc</code> file (root-only, like <code class="inline">admins:</code>). The resolver fetches the <code class="inline">.sig</code> automatically and rejects any unsigned or invalid-signature artifact, falling back to the embedded copy. Operators wanting to enforce signature verification on locally-saved artifacts (Path A, "drop a copy into your archive") run the <code class="inline">openssl</code> verify command above as part of their save workflow.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ───────────── Channels explainer ───────────── -->
|
<!-- ───────────── Channels explainer ───────────── -->
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ 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. |
|
| `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) |
|
||||||
|
|
@ -874,11 +874,15 @@ sidesteps the operator entirely:
|
||||||
3. **Verifier in zddc-server (`apps/fetch.go`).** When fetching a
|
3. **Verifier in zddc-server (`apps/fetch.go`).** When fetching a
|
||||||
URL-pinned `apps:` artifact, also fetch `<url>.sig`, then call
|
URL-pinned `apps:` artifact, also fetch `<url>.sig`, then call
|
||||||
`VerifyEd25519` against `Fetcher.VerifyKey`. The key is loaded
|
`VerifyEd25519` against `Fetcher.VerifyKey`. The key is loaded
|
||||||
at startup from `cfg.AppsPubKey` (`--apps-pubkey` /
|
at startup with this resolution order:
|
||||||
`ZDDC_APPS_PUBKEY`). Failure cases — sig 404, transport error,
|
1. `--apps-pubkey` / `ZDDC_APPS_PUBKEY` (path to PEM file)
|
||||||
wrong key, tampered body — all reject; the body is dropped and
|
2. `apps_pubkey:` inline PEM in the root `.zddc` file (root-only,
|
||||||
the apps resolver falls back to the embedded copy. No baked-in
|
same trust-anchor treatment as `admins:`)
|
||||||
default public key; same posture as TLS certificates.
|
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 model.** The operator decides which signing infrastructure to
|
||||||
trust by configuring `--apps-pubkey`. The website publishes the
|
trust by configuring `--apps-pubkey`. The website publishes the
|
||||||
|
|
|
||||||
|
|
@ -314,21 +314,39 @@ func setupApps(cfg config.Config) (*apps.Server, error) {
|
||||||
}
|
}
|
||||||
fetcher := apps.NewFetcher(cache, slog.Default())
|
fetcher := apps.NewFetcher(cache, slog.Default())
|
||||||
|
|
||||||
// Apps signing pubkey. Optional — when unset, URL-fetched apps
|
// Apps signing pubkey. Resolution order, highest priority first:
|
||||||
// refuse-by-default (the resolver falls back to embedded). Operators
|
// 1. --apps-pubkey / ZDDC_APPS_PUBKEY (path to PEM file)
|
||||||
// using zddc.varasys.io's canonical channels download pubkey.pem
|
// 2. apps_pubkey: inline PEM in the root <ZDDC_ROOT>/.zddc file
|
||||||
// from there and configure --apps-pubkey or ZDDC_APPS_PUBKEY. Same
|
// (root-only — same trust-anchor treatment as admins:)
|
||||||
// posture as TLS certificates: zddc-server bakes nothing in.
|
// 3. nothing → URL-fetched apps refuse-by-default; only embedded
|
||||||
if cfg.AppsPubKey != "" {
|
// + local-path apps work
|
||||||
|
//
|
||||||
|
// Same posture as TLS certificates: zddc-server bakes nothing in.
|
||||||
|
// Operators using zddc.varasys.io's canonical channels download
|
||||||
|
// pubkey.pem from there and either configure the path via env/flag
|
||||||
|
// or paste the PEM contents inline into root .zddc.
|
||||||
|
switch {
|
||||||
|
case cfg.AppsPubKey != "":
|
||||||
pub, err := apps.LoadPubKey(cfg.AppsPubKey)
|
pub, err := apps.LoadPubKey(cfg.AppsPubKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("apps-pubkey: %w", err)
|
return nil, fmt.Errorf("apps-pubkey: %w", err)
|
||||||
}
|
}
|
||||||
fetcher.VerifyKey = pub
|
fetcher.VerifyKey = pub
|
||||||
slog.Info("apps signing pubkey loaded", "path", cfg.AppsPubKey)
|
slog.Info("apps signing pubkey loaded", "source", "env/flag", "path", cfg.AppsPubKey)
|
||||||
|
default:
|
||||||
|
// Fall back to apps_pubkey: in root .zddc.
|
||||||
|
rootZddc, err := zddc.ParseFile(filepath.Join(cfg.Root, ".zddc"))
|
||||||
|
if err == nil && rootZddc.AppsPubKey != "" {
|
||||||
|
pub, err := apps.ParsePubKeyPEM([]byte(rootZddc.AppsPubKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("root .zddc apps_pubkey: %w", err)
|
||||||
|
}
|
||||||
|
fetcher.VerifyKey = pub
|
||||||
|
slog.Info("apps signing pubkey loaded", "source", "root .zddc apps_pubkey")
|
||||||
} else {
|
} else {
|
||||||
slog.Warn("apps-pubkey not configured; URL-fetched apps will be refused (only embedded + local-path apps will work). " +
|
slog.Warn("apps-pubkey not configured; URL-fetched apps will be refused (only embedded + local-path apps will work). " +
|
||||||
"Set --apps-pubkey or ZDDC_APPS_PUBKEY to a PEM file containing the Ed25519 signing key you trust.")
|
"Set --apps-pubkey, ZDDC_APPS_PUBKEY, or apps_pubkey: in the root .zddc file to a PEM Ed25519 public key you trust.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps.NewServer(cfg.Root, cache, fetcher, version), nil
|
return apps.NewServer(cfg.Root, cache, fetcher, version), nil
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,21 @@ type ACLRules struct {
|
||||||
// server serves the version baked into the binary at compile time (//go:embed).
|
// server serves the version baked into the binary at compile time (//go:embed).
|
||||||
// Fetched URL sources are cached in <ZDDC_ROOT>/_app/; the cache is fetch-once
|
// Fetched URL sources are cached in <ZDDC_ROOT>/_app/; the cache is fetch-once
|
||||||
// and never re-validates — operators delete the file to force a refetch.
|
// and never re-validates — operators delete the file to force a refetch.
|
||||||
|
//
|
||||||
|
// AppsPubKey is the inline PEM of the Ed25519 public key used to verify
|
||||||
|
// signatures on URL-fetched apps artifacts. Honored only at the root
|
||||||
|
// .zddc file (same root-only treatment as Admins, for the same reason:
|
||||||
|
// it's a trust anchor; subtree write authority must not be able to
|
||||||
|
// re-anchor it). Lower priority than --apps-pubkey / ZDDC_APPS_PUBKEY:
|
||||||
|
// when both are set, the env/flag (file path) wins. Empty in either
|
||||||
|
// place = URL-fetched apps refused (only embedded + local-path apps
|
||||||
|
// work). See zddc-server's setupApps.
|
||||||
type ZddcFile struct {
|
type ZddcFile struct {
|
||||||
ACL ACLRules `yaml:"acl" json:"acl"`
|
ACL ACLRules `yaml:"acl" json:"acl"`
|
||||||
Admins []string `yaml:"admins" json:"admins,omitempty"`
|
Admins []string `yaml:"admins" json:"admins,omitempty"`
|
||||||
Title string `yaml:"title" json:"title,omitempty"`
|
Title string `yaml:"title" json:"title,omitempty"`
|
||||||
Apps map[string]string `yaml:"apps,omitempty" json:"apps,omitempty"`
|
Apps map[string]string `yaml:"apps,omitempty" json:"apps,omitempty"`
|
||||||
|
AppsPubKey string `yaml:"apps_pubkey,omitempty" json:"apps_pubkey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFile reads and parses a .zddc YAML file.
|
// ParseFile reads and parses a .zddc YAML file.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue