ZDDC/helm/zddc-server-cache/values.yaml.example
ZDDC ac7553f940 fix(client): plug confused-deputy bind in client mode
A focused security review of phases 1-4 surfaced one MEDIUM finding
(confidence 9/10): in client mode (--upstream set) the cache layer
forwards the configured bearer to upstream on every incoming request
without authenticating the local caller, AND --addr defaulted to
:8443 (all interfaces). Together those mean a CLI user running
`zddc-server --upstream https://master --bearer-file ~/token` on a
laptop on hotel/cafe Wi-Fi exposes an open-proxy confused-deputy:
any attacker on the same L2 connects to https://<laptop-ip>:8443,
accepts the self-signed cert, issues GETs (or PUTs/DELETEs that
queue in the outbox), and the cache laundries each request through
upstream with the engineer's bearer. The full cached subtree leaks.

Two layers of defense in config.Load:

1. Loopback default in client mode. When cfg.Upstream is set and
   neither --addr nor ZDDC_ADDR was passed explicitly, --addr
   downgrades to "127.0.0.1:8443" (vs ":8443" in master mode). CLI
   users on a laptop get safe-by-default. Operators who want a
   non-loopback bind opt in explicitly.

2. Refuse non-loopback bind + bearer-file without acknowledgement.
   When cfg.Upstream is set, BearerFile is non-empty, the chosen
   addr is non-loopback, AND --insecure-direct is not set, the load
   fails with an error that names the bind, the threat (open-proxy
   confused-deputy laundering bearer credentials), and the
   acknowledgement flag. The helm zddc-server-cache/ chart already
   sets ZDDC_INSECURE_DIRECT=1 and relies on Kubernetes-namespaced
   pod networking for the gating, so the chart path is unaffected.
   The guard is bearer-file-conditional because proxy mode without a
   bearer doesn't have a credential to launder, and refusing it
   would needlessly block proxy-without-auth deployments.

Tests in internal/config/config_test.go lock down all four cases:
- --upstream with no explicit --addr → 127.0.0.1:8443
- --upstream + non-loopback --addr + --bearer-file (no IDirect) → refuse
- --upstream + non-loopback --addr + --bearer-file + --insecure-direct → ok
- --upstream + non-loopback --addr + NO bearer → ok (no credential to leak)

Doc updates: zddc/README.md client-mode "Flags" section gets a
WARNING block describing the loopback default + insecure-direct
escape hatch. AGENTS.md ZDDC_UPSTREAM row mentions the addr
downgrade. ARCHITECTURE.md gains a "Confused-deputy guard at
startup" subsection under "Master + proxy/cache/mirror" with the
two-layer defense rationale. helm/zddc-server-cache/values.yaml.example
adds an inline note next to addr: ":8080" explaining why the chart
sets ZDDC_INSECURE_DIRECT=1 and what the consequence is of removing
either side of the gating.

Master mode is unaffected — the client-mode validation block is
gated by `if cfg.Upstream != ""`. All existing tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:03:51 -05:00

159 lines
5.8 KiB
Text

# values.yaml.example — zddc-server-cache
#
# Copy to values.yaml (or pass via --values) and customize for your
# environment. Contains NO secrets — the upstream bearer token MUST be
# provided via a separately-created Kubernetes Secret (see `bearer:`
# below). Do not paste the token value here.
# Source-build configuration. The init container clones the repo at
# `gitRef` and compiles cmd/zddc-server. Pin gitRef to a stable tag
# (zddc-server-vX.Y.Z) for production caches; tracking main is fine
# for dev mirrors.
zddc:
gitRepo: https://codeberg.org/VARASYS/ZDDC.git
gitRef: zddc-server-v0.0.7 # pin to a stable tag
# ZDDC environment-variable contract — see zddc/README.md "Client mode".
env:
# Local cache directory (mounted from the cache PVC; see `data:`
# below). The cache layer writes files here as they're fetched.
rootPath: /srv
# Listening address for incoming requests to this cache instance.
# Plain HTTP — ingress / mesh terminates TLS upstream of the pod.
#
# NOTE: in client mode the binary refuses to start with a non-
# loopback bind AND a configured bearer UNLESS ZDDC_INSECURE_DIRECT=1
# is also set. The cache forwards the bearer to upstream without
# authenticating the local caller, so a bare bind would be an open
# proxy. The chart's deployment.yaml sets ZDDC_INSECURE_DIRECT=1
# and relies on the Kubernetes-namespaced pod network + ingress
# auth proxy for that gating. If you remove either you must
# redirect the bind to 127.0.0.1.
addr: ":8080"
# Email-header convention from your authenticating reverse proxy.
# Used for AccessLog only in client mode (auth flows to upstream
# as a bearer; the cache layer doesn't enforce ACL locally when
# noAuth: true).
emailHeader: X-Auth-Request-Email
# CORS allowlist for the local instance. Same semantics as the
# master chart — empty disables CORS, which is the right default
# for embedded-tools / same-origin browsing.
corsOrigin: ""
# info / warn / error / debug.
logLevel: info
indexPath: ".archive"
# Skip ACL enforcement on incoming requests. Almost always true
# for a personal/field-engineer cache (the laptop is single-user-
# trust and the upstream master already filtered). Set to false
# only if you've put your own auth proxy in front of this cache
# AND want it to re-evaluate ACLs against cached `.zddc` files.
noAuth: true
# Upstream master configuration.
upstream:
# The master URL. Required. Don't include a trailing slash.
url: "https://zddc.example.com"
# proxy / cache / mirror.
# proxy — forward live, no disk persistence
# cache — persist responses on access (default; field-engineer use)
# mirror — cache + access-triggered subtree warmer (vendor /
# backup / complete-offline use)
mode: cache
# Accept self-signed / untrusted upstream TLS certs. Distinct from
# noAuth. Use only for dev masters with self-signed certs or for
# internal CAs your cluster's trust store doesn't yet have.
skipTLSVerify: false
# Mirror-mode only. Comma-separated URL subtrees the access-
# triggered walker keeps current. Empty + mode=mirror = full
# mirror ("/"). Ignored when mode != mirror.
mirrorSubtree: ""
# Mirror-mode only. Min gap between walks of the same subtree.
# Idle subtrees generate zero upstream traffic until next access.
# Default 1h.
mirrorMinInterval: 1h
# Bearer token — required when the upstream master enforces auth.
# Create a Secret separately (do NOT paste the token here):
#
# 1. On the master, sign in via your auth proxy and visit
# https://<master>/.tokens to issue a token.
# 2. Wrap it in a Kubernetes Secret:
#
# kubectl create secret generic zddc-cache-bearer \
# --from-literal=token=<paste-token-here>
#
# 3. Reference the Secret here.
#
# Set `secretName: ""` to disable bearer auth (only valid when the
# upstream is `--no-auth` or behind your own auth proxy that doesn't
# require bearer auth from internal callers).
bearer:
secretName: zddc-cache-bearer
secretKey: token
# Cache-storage PVC. Sized for the working set you expect to mirror —
# can be smaller than the master's data volume since only accessed
# files (or, in mirror mode, files under configured subtrees) get
# cached. Operators provision the PVC themselves; this chart only
# references it by name. ReadWriteOnce is fine — the cache is single-
# instance by design.
data:
pvcName: zddc-cache # name of an existing PersistentVolumeClaim
subPath: ""
# Service exposure. The cache listens on a plain HTTP port; ingress
# (or mesh sidecar) terminates TLS and forwards to this service.
service:
type: ClusterIP
port: 8080
# Ingress is optional — disabled by default since most cache
# deployments wire into an existing ingress / auth-proxy stack.
ingress:
enabled: false
className: ""
host: zddc-cache.example.com
tls:
enabled: false
secretName: zddc-cache-tls
# Pod resource limits. Cache instances are mostly I/O bound; the
# defaults below suit a small mirror (~1k files in working set).
# Bump cpu/memory for mirror mode against larger trees.
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
# Replicas. Cache instances are single-instance by design — multiple
# replicas would race on writes to the same cache directory and
# duplicate the upstream walker traffic. Use a separate cache
# deployment per region/tenant if you need fan-out.
replicaCount: 1
# Build-stage Go image (init container).
buildImage:
repository: docker.io/golang
tag: 1.24-alpine
# Runtime image (main container).
runtimeImage:
repository: docker.io/alpine
tag: "3.19"
# Image pull credentials, if your registry requires them.
imagePullSecrets: []
# - name: regcred