Web & config generators¶
Beyond systemd units, a stack can declare typed config for the services it fronts: nginx vhosts, a Prometheus scrape config, a Litestream replication config. Each is rendered deterministically and merged into the deploy's config-file set. These generators are backend-independent — nginx runs as a systemd service in the target topology, but its config does not depend on the supervisor.
Every value that lands in a path, an nginx directive, or a YAML scalar is validated at compose and again at render (defense-in-depth), through one shared validator module. See the injection-guard discipline below.
nginx vhosts¶
All HTTP vhosts render to /etc/nginx/conf.d/<serverName>.conf. A duplicate serverName (case-folded — nginx matches case-insensitively) across any vhost type is a compose error, so two generators can never clobber one file.
Static sites¶
Serve a built package's store-path tree (not a bin/). root names a package module; compose resolves it to the content-addressed store path, pins it in the closure (so it is pushed and GC-rooted), and renders root <store-path>;.
Reverse proxies¶
Forward to a validated http(s)://host[:port][/path] upstream with the standard X-Forwarded-* headers. tls = true renders a 443 ssl server referencing the predictable cert paths plus an 80→443 redirect; websocket = true adds the Upgrade/Connection headers.
reverseProxies = [
{ serverName = "app.example.com"
proxyPass = "http://127.0.0.1:8080"
tls = true
websocket = true }
]
Reverse proxies also take access controls (all optional, default unrestricted):
{ serverName = "admin.example.com"
proxyPass = "http://127.0.0.1:9000"
allowMethods = ["GET", "POST"] # any other method → 405
allowIps = ["10.0.0.0/8"] # allow these, then deny all
denyIps = ["192.0.2.13"] # checked first
blockUserAgents = ["BadBot", "scraper"] } # 403 via an nginx map
allowMethods is a fixed-set allowlist; allowIps/denyIps are validated through the stdlib ipaddress (so a normalised value can carry no injection); blockUserAgents patterns are a quantifier-free substring set (no catastrophic-backtracking ReDoS — nginx evaluates them against every request's User-Agent).
L4 stream (TCP) proxies¶
A layer-4 TCP proxy (e.g. SSH :22 → a backend host). Because nginx permits only one top-level stream {} block, all stream proxies render into a single /etc/nginx/stream.conf. The operator must include /etc/nginx/stream.conf; at the top level of nginx.conf (Punix does not own the main config). Listen ports must be unique.
Redirect-only vhosts¶
redirects = [
{ serverName = "old.example.com" to = "https://new.example.com"
preservePath = true permanent = true } # 301 …$request_uri
]
preservePath appends $request_uri; permanent picks 301 vs 302. to is a validated http(s) URL.
Prometheus¶
A fixed-shape prometheus.yml, hand-rendered (no YAML dependency):
prometheus = {
scrapeInterval = "15s"
scrapeConfigs = [
{ jobName = "node" targets = ["10.0.0.1:9100", "10.0.0.2:9100"] }
]
}
→ /etc/prometheus/prometheus.yml. Job names, targets, and the interval are validated and emitted as quoted scalars.
Litestream¶
Replicate SQLite databases to s3 or file targets → /etc/litestream.yml:
litestream = {
dbs = [
{ path = "/var/lib/app/app.db"
replicas = [ { type = "s3" bucket = "my-backups" path = "app" } ] }
]
}
Credentials are not in the file. S3 keys are environment variables on the Litestream service (LITESTREAM_ACCESS_KEY_ID, …) delivered via the secret oracle, so the rendered YAML carries nothing sensitive.
Why everything is validated twice¶
Configuration is data, but these generators turn it into things a host parses — paths, nginx directives, YAML. A semi-trusted PCL string that reaches a directive unguarded is an injection. Every validator:
- uses an allowlist character set, never a denylist;
- anchors with
.fullmatch(Python's$also matches before a trailing newline — a real bypass we've fixed;\Z/fullmatchdoes not); - runs at compose (fail-fast, located) and at render (so a stack built another way is still safe).
Where in the code¶
src/punix/deploy/generators/nginx.py,prometheus.py,litestream.py.src/punix/deploy/validate.py— the single audit home for the injection guards.
Related¶
- Config safety — the path-confinement boundary every config write (generator output and raw
configFiles) passes. - TLS certificates — what
tls = truereferences. - Stateful services — the Postgres provisioning generator.
- Reference: decisions — ADR-015 (config-generator ecosystem).