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>
175 lines
6.8 KiB
YAML
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
|