The README grew alongside the tool and ended up in the order the work happened rather than the order a reader needs. Three things were wrong with it. The two-tier split was implicit, so a reader had to sort out which paragraphs applied to the host they were building. The opening defined the whole tool as "no Docker socket in job containers, and deny-by-default egress", which describes one tier only - someone building the other was told it does things it deliberately will not do. And long-form justification sat between the reader and the steps. So the README is now tier-first: pick a tier from a comparison table, then follow only that tier's half. Everything that is background rather than instruction moved to docs/reference.md, with docs/README.md indexing it and the Proxmox runbook. The runbook stays in proxmox/ beside the firewall template it explains. The tier names changed with it. `ci` and `runtime` were dejima's words for these hosts: `ci` distinguishes nothing on a tool whose whole subject is CI, and `runtime` only means something if you already know dejima's runtime tests are the workload that forced the tier to exist. They are `confined` and `privileged` now, after what a job on each is granted. The distinction that generalises is where the security boundary sits - inside the host for one, on the hypervisor for the other - and the table leads with it. `unprivileged`/`privileged` would have been the more standard pairing and was rejected: two characters apart on a flag where a misread silently produces a host that looks hardened and is not. The old --role values still work and say what they are now called rather than translating silently. examples/ is renamed to match on both sides, so no file's tier is implied by the absence of a suffix, and the config mounts in the compose files follow. Written down explicitly for the first time: the privileged tier has no egress allowlist by design. Its jobs hold the socket and run in the host's network namespace, so any filter installed there is one they can flush. Installing it anyway would leave a host that looks like it has an allowlist, which is worse than one that plainly does not. Also adds GPL-3.0-or-later to match dejima: LICENSE plus SPDX headers on the scripts. bats test/ passes, 21/21. All four --role values and an invalid one behave as intended; the compose and act_runner examples still parse.
62 lines
3.1 KiB
Plaintext
62 lines
3.1 KiB
Plaintext
# The static half of the job network's resolver. Install as
|
|
# /etc/dnsmasq.d/ci-allow.conf - written by install.sh. See this repository's README.
|
|
#
|
|
# There are no `server=` lines here on purpose. Every allowed name is rendered
|
|
# into a SECOND file, /etc/dnsmasq.d/ci-allow.conf, by
|
|
# refresh-allowlist.example.sh, from .dejima/allowed-domains on origin/main.
|
|
# dnsmasq reads both and merges them. Keeping the generated names out of this
|
|
# file is what stops a hand-edited copy drifting from the one that is actually
|
|
# derived from a branch you control - a `server=` line added here is a grant
|
|
# nobody merged.
|
|
#
|
|
# Same construction as the sandbox's own docker/dns-proxy.sh, one layer out:
|
|
# allowlist the names, and let the answers populate the set of allowed
|
|
# addresses. A name with no `server=` line anywhere gets REFUSED - not
|
|
# resolved and then blocked at the firewall, refused - which is what closes
|
|
# off encoding data into query names to a domain you never allowed.
|
|
#
|
|
# Not runnable as written: the addresses below are RFC 5737 documentation
|
|
# ranges. Substitute your own, and read the subnet note in etc/config.example before
|
|
# picking one - "private" is load-bearing here, not tidiness.
|
|
|
|
# bind-dynamic, not bind-interfaces. Both keep this resolver off every other
|
|
# address on the host - it answers for untrusted code and has no business
|
|
# being reachable from anywhere else - but bind-interfaces binds the device
|
|
# once, at startup, and does not survive that device being recreated.
|
|
#
|
|
# A Docker bridge is recreated whenever its network is, which re-running the
|
|
# installer does. dnsmasq then holds a socket on an interface that no longer
|
|
# exists: `ss` still lists it as bound, and nothing answers. Every job fails
|
|
# at apt with "Temporary failure resolving", which says nothing about any of
|
|
# this. bind-dynamic picks the interface back up when it reappears.
|
|
listen-address=203.0.113.1
|
|
bind-dynamic
|
|
no-hosts
|
|
|
|
# No system resolvers. Without this, /etc/resolv.conf silently becomes an
|
|
# upstream for every name and the allowlist stops meaning anything.
|
|
#
|
|
# Note this applies to the whole dnsmasq instance, so this instance serves the
|
|
# job network and nothing else. If the host needs a resolver for its own sake,
|
|
# that is a different instance or a different daemon.
|
|
no-resolv
|
|
|
|
# Every answer served is added to the firewall's set, so a destination that
|
|
# rotates its edge addresses stays reachable without a resolver cron chasing
|
|
# it. Only allowlisted names get an upstream to answer them in the first
|
|
# place, which is what makes a wildcard safe here.
|
|
ipset=/#/ci-allow
|
|
|
|
# Answers claiming a private address are rejected: a name you do not control
|
|
# resolving to one is how a job reaches something on your network that was
|
|
# never on any list.
|
|
#
|
|
# This is keyed on the private ranges, which is one of the reasons the job
|
|
# bridge itself should use one - see this repository's README.
|
|
stop-dns-rebind
|
|
|
|
# A self-hosted forge legitimately resolves to a private address, which
|
|
# stop-dns-rebind would otherwise reject. Exempt it by name - not by turning
|
|
# the protection off. One line per internal name you allow.
|
|
rebind-domain-ok=/gitea.example.com/
|