ZDDC/helm/zddc-server-dev/templates/deployment.yaml
ZDDC 9765fa2f5e feat(apps): code-signed URL fetches; dev chart overlays prod data RO
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>
2026-05-04 21:59:07 -05:00

175 lines
6.8 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "zddc-server.fullname" . }}
labels:
{{- include "zddc-server.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
# Dev: always re-pull the build image and re-clone source, so a kubectl
# rollout restart picks up new commits on the tracked ref.
strategy:
type: Recreate
selector:
matchLabels:
{{- include "zddc-server.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "zddc-server.selectorLabels" . | nindent 8 }}
annotations:
# Forces pod recreation on every helm upgrade, ensuring the init
# container re-clones the tracked ref. Useful in dev where you
# want `helm upgrade` to pick up new main HEAD without changing
# values.
zddc.varasys.io/build-time: {{ now | quote }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: zddc-bin
emptyDir: {}
# Production data volume — mounted READ-ONLY so the dev pod
# cannot corrupt prod even with a bug. Becomes the lowerdir of
# the OverlayFS mount below.
- name: data-readonly
persistentVolumeClaim:
claimName: {{ .Values.data.pvcName }}
readOnly: true
# Writable scratch for OverlayFS upperdir + workdir. emptyDir
# is ephemeral by default — dev tweaks evaporate on pod restart,
# which is usually right for a dev replica. Replace with a
# small PVC if persistence across restarts matters.
- name: overlay-scratch
emptyDir: {}
# The composed read-write view zddc-server reads from. Populated
# by the setup-overlay init container; passed through to the main
# container as ZDDC_ROOT.
- name: data
emptyDir: {}
initContainers:
# OverlayFS sandwich:
# lowerdir = /mnt/data-readonly (prod data, RO)
# upperdir = /mnt/overlay-scratch/upper
# workdir = /mnt/overlay-scratch/work
# merged = /mnt/data (what main container sees)
#
# Why this exists: dev runs against the same on-disk dataset as
# prod, but its writes (anything zddc-server writes — index
# state, form submissions during testing, .zddc edits via the
# admin page, etc.) MUST NOT mutate prod data. OverlayFS solves
# this at the filesystem layer: prod data is RO, dev's writes
# land in upperdir, the dev container sees the merged view. No
# zddc-server code change required.
#
# Requires CAP_SYS_ADMIN (the overlay mount syscall is
# privileged). Stays scoped to this one init container; the main
# container runs without elevated privs.
- name: setup-overlay
image: {{ printf "%s:%s" .Values.runtimeImage.repository .Values.runtimeImage.tag | quote }}
securityContext:
privileged: true
command: ["/bin/sh", "-c"]
args:
- |
set -eu
mkdir -p /mnt/overlay-scratch/upper /mnt/overlay-scratch/work
mount -t overlay overlay \
-o lowerdir=/mnt/data-readonly,upperdir=/mnt/overlay-scratch/upper,workdir=/mnt/overlay-scratch/work \
/mnt/data
echo "OverlayFS mounted: /mnt/data-readonly (RO) + /mnt/overlay-scratch (RW) -> /mnt/data"
ls -la /mnt/data | head -10
volumeMounts:
- name: data-readonly
mountPath: /mnt/data-readonly
readOnly: true
- name: overlay-scratch
mountPath: /mnt/overlay-scratch
- name: data
mountPath: /mnt/data
mountPropagation: Bidirectional
- name: build-zddc-server
image: {{ printf "%s:%s" .Values.buildImage.repository .Values.buildImage.tag | quote }}
imagePullPolicy: Always
command: ["/bin/sh", "-c"]
args:
- |
set -eu
apk add --no-cache git
git clone --depth 1 --branch "$GIT_REF" "$GIT_REPO" /workspace
cd /workspace/zddc
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath \
-ldflags="-s -w -X main.version=$GIT_REF" \
-o /out/zddc-server \
./cmd/zddc-server
echo "built /out/zddc-server from $GIT_REF ($(git -C /workspace rev-parse --short HEAD))"
env:
- name: GIT_REPO
value: {{ .Values.zddc.gitRepo | quote }}
- name: GIT_REF
value: {{ .Values.zddc.gitRef | quote }}
volumeMounts:
- name: zddc-bin
mountPath: /out
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
containers:
- name: zddc-server
image: {{ printf "%s:%s" .Values.runtimeImage.repository .Values.runtimeImage.tag | quote }}
imagePullPolicy: IfNotPresent
command: ["/zddc/zddc-server"]
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: ZDDC_ROOT
value: {{ .Values.zddc.env.rootPath | quote }}
- name: ZDDC_ADDR
value: {{ .Values.zddc.env.addr | quote }}
- name: ZDDC_TLS_CERT
value: "none"
- name: ZDDC_INSECURE_DIRECT
value: "1"
- name: ZDDC_EMAIL_HEADER
value: {{ .Values.zddc.env.emailHeader | quote }}
- name: ZDDC_CORS_ORIGIN
value: {{ .Values.zddc.env.corsOrigin | quote }}
- name: ZDDC_LOG_LEVEL
value: {{ .Values.zddc.env.logLevel | quote }}
- name: ZDDC_INDEX_PATH
value: {{ .Values.zddc.env.indexPath | quote }}
volumeMounts:
- name: zddc-bin
mountPath: /zddc
- name: data
mountPath: {{ .Values.zddc.env.rootPath }}
{{- with .Values.data.subPath }}
subPath: {{ . | quote }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
# Tighter probe cadence than prod — fail fast in dev so issues
# surface immediately during testing.
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 3
periodSeconds: 10
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 1
periodSeconds: 5
timeoutSeconds: 2