Two interlocking pieces shipped together: 1. Strict Ed25519 signature verification on URL-fetched apps artifacts. Every URL the apps cascade resolves must publish a corresponding <url>.sig (raw 64-byte Ed25519 signature). The fetcher rejects on any failure (sig 404, transport error, wrong key, tampered body) and the resolver falls back to the embedded copy. The trusted public key is OPERATOR-CONFIGURED via --apps-pubkey / ZDDC_APPS_PUBKEY (PEM file path). No baked-in default — same posture as TLS certificates. Operators using zddc.varasys.io's canonical channels download pubkey.pem from there and configure the local path. Operators with their own signing infrastructure pass their own public key. Build pipeline (./build) gains sign_release_artifacts: walks dist/release-output/ after promote and produces an Ed25519 .sig alongside every real file. ZDDC_SIGNING_KEY=~/.config/zddc-signing/ key.pem (mode 0600). Symlinks skip — the .sig at the symlink target is what counts. Test coverage: parse-PEM round-trip, malformed/wrong-type PEM rejection, valid-signature accept, tampered-body reject, wrong-key reject, malformed-signature reject, end-to-end fetch+sign+verify, fetch-rejects-tampered, fetch-rejects-missing-sig, fetch-rejects- wrong-key. Existing fetch tests updated to use signed-fixture helpers. 2. Dev Helm chart mounts production data READ-ONLY and layers an OverlayFS writable scratch on top. Prod data is the lowerdir; dev's writes (form submissions, archive index state, .zddc edits) land in upperdir; main container sees the merged read-write view at $ZDDC_ROOT. Setup runs in a privileged init container; main container runs unprivileged. Solves the dev-replica-on-shared- dataset problem at the filesystem layer with no zddc-server code change. Docs: env-var tables in zddc/README.md and AGENTS.md gain a ZDDC_APPS_PUBKEY row. The Federal-readiness gap analysis "Code-signed apps: URL fetches" subsection is rewritten as "what's currently in place" instead of "what would need to be added," with a forward pointer to per-entry signed_by: (multi-key) and Sigstore as the federally-acceptable evolution. The website "Verify your downloads" section + the embedded pubkey gone — but the website needs separate updates landing in zddc-website to publish pubkey.pem and add the verify section. Pending in that repo's commit. Production binary unchanged at 13.1 MB. All 11 Go test packages green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| zddc-server-dev | ||
| zddc-server-prod | ||
| README.md | ||
Helm charts
Two example charts for deploying zddc-server on Kubernetes.
Both compile zddc-server from source via an init container — no
container image needs to be pulled from a registry, and no binary needs
to be built ahead of time. The init container clones the repo at a
configured git ref and runs go build; the main container is plain
alpine + the freshly built static binary.
Charts
| Chart | When to use |
|---|---|
zddc-server-prod/ |
Production. Pin zddc.gitRef to a stable tag (zddc-server-vX.Y.Z). Slower probe cadence; image-pull policy IfNotPresent. |
zddc-server-dev/ |
Development / soak. Tracks main by default; helm upgrade triggers a pod recreate so each rollout pulls the latest commit. Faster probes; debug-level logging (request headers logged — sensitive). |
The chart values are nearly identical between the two; the differences
are encoded as defaults in each chart's values.yaml.example.
Quick start
# Pre-requisite: a PersistentVolumeClaim for ZDDC_ROOT data
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: zddc-root
spec:
accessModes: [ReadWriteMany] # or RWO if single replica is fine
resources: { requests: { storage: 100Gi } }
storageClassName: your-shared-fs # NFS, CephFS, SMB, etc.
EOF
# Production install
cp helm/zddc-server-prod/values.yaml.example my-prod-values.yaml
$EDITOR my-prod-values.yaml # set zddc.gitRef, hostnames, etc.
helm install zddc-server-prod helm/zddc-server-prod/ -f my-prod-values.yaml
# Dev install (tracks main HEAD)
cp helm/zddc-server-dev/values.yaml.example my-dev-values.yaml
$EDITOR my-dev-values.yaml
helm install zddc-server-dev helm/zddc-server-dev/ -f my-dev-values.yaml
# Trigger a rebuild from latest main HEAD (dev chart)
helm upgrade zddc-server-dev helm/zddc-server-dev/ -f my-dev-values.yaml
What the chart does and doesn't do
Does:
- Clones the configured
zddc.gitRepoatzddc.gitRefin an init container, builds the Go binary, copies it to a sharedemptyDir, and starts the main container against that binary. - Wires the
ZDDC_*environment-variable contract (root path, addr, email header, CORS allowlist, log level, index path). - Mounts a caller-supplied PersistentVolumeClaim at
ZDDC_ROOT. - Optionally creates an Ingress (
ingress.enabled: true).
Does not:
- Create the PVC. Operators provision storage themselves; the chart only references it by name.
- Manage TLS for the pod. zddc-server runs in plain HTTP mode behind
whatever ingress / authenticating reverse proxy the cluster already
has.
ZDDC_TLS_CERT=noneandZDDC_INSECURE_DIRECT=1are hardcoded in the templates because the chart is opinionated about the TLS-terminated-upstream deployment shape. - Authenticate users. zddc-server reads the user's email from a header
set by the upstream proxy (
X-Auth-Request-Emailby default). The chart does not deploy oauth2-proxy / nginx-auth-request / Pomerium / etc. — bring your own. - Manage secrets.
values.yaml.examplecontains no secrets and never should. ACL email lists belong in.zddcfiles inside the data volume; image-pull credentials and TLS certs (if you enable ingress TLS) reference Kubernetes secrets you've created separately.
Why build from source instead of using a registry image
Three reasons:
- Reproducibility. The init container's logs show exactly which git ref was built. There's no opaque "what did I deploy" question that a registry tag can introduce.
- One distribution channel. Codeberg release-asset binaries already exist for direct downloads; the chart compiles its own binary from the same source git ref so there's nothing extra to maintain (no separate image registry, no image-promotion pipeline).
- Smaller blast radius. A compromised build image affects only pods that pull during the compromise window. A compromised registry image stays compromised across rollbacks until the digest is rotated.
The cost: every pod start takes 30-60s to clone + go build instead
of pulling a pre-baked image. Acceptable for both chart audiences
(production rollouts are infrequent; dev rollouts trade build time
for tracking-main convenience).
Linting
helm lint helm/zddc-server-prod/
helm lint helm/zddc-server-dev/
# Render to inspect (uses default values from values.yaml.example):
helm template test-prod helm/zddc-server-prod/ \
--values helm/zddc-server-prod/values.yaml.example