Two P0 items with one shape: the instance refused to check the state it was about
to run in. Together they make vole serve fail on a configuration that cannot be
safe and fix the file modes an earlier version left behind.
#7 — Validate the configuration and guard dev_login
Config.Validate() runs before anything is opened and reports every problem
at once. It refuses:
dev_loginwithoutdev_only, withhttps_addr, with a non-loopbackpublic_url, or with a non-loopbackhttp_addropen_registrationwithout an HTTPSpublic_url, andopen_registration_rolethat namesadminor no role at all- a
public_urlwith a user, path, query or fragment, or another scheme - a
pages_domainthat is not a bare DNS suffix, and apages_refgit would refuse - a negative size, lease or limit;
receive_max_bytesbelowmax_blob_bytes https_addrwith no name to certify, a non-addressacme_email, anacme_directory_urlthat is not HTTPS
dev_only is the explicit opt-in the issue asked for: a credential-free session
has to be a decision, not a line copied from an example.
The proxy hole is closed at the request too. isLoopbackRequest trusted Host,
so a proxy rewriting it to 127.0.0.1 inherited a dev session. It is now
loopbackBrowser: the peer must be loopback, the request must name the configured
http_addr (same port; same host or another loopback name, so localhost still
works), and any request carrying Forwarded, X-Forwarded-For,
X-Forwarded-Host, X-Forwarded-Proto or X-Real-IP is refused.
#8 — Harden deployment file modes and the service unit
internal/secure is one place that creates a secret and checks the mode of one
that already exists: 0700 directories, 0600 files, group access retained
because 0750 with a dedicated group is a deliberate deployment, world access
not. Startup hardens the data directory, users.db and its -wal/-shm, the
index and run databases, the audit log, the SSH host key and the ACME cache, logs
the paths whose mode it changed, and fails when a mode cannot be tightened —
an instance that cannot protect its keys should not serve them. Every audit
writer now uses secure.OpenAppend, so an existing log inherited at 0644
stops being readable by everyone the next time it is written.
vole admin account backup tightens its copy too: VACUUM INTO created it with
the process umask, and that file is every account, key and passkey.
The systemd unit gains UMask=0027, LimitCORE=0 (a core dump of this process is
the host key), an empty CapabilityBoundingSet= with a documented drop-in for the
one capability port 80/443 needs, PrivateDevices, ProtectProc=invisible,
ProcSubset=pid, ProtectKernelLogs, RemoveIPC, RestrictAddressFamilies=,
and MemoryDenyWriteExecute. systemd-analyze verify accepts it; the forge runs
no JIT, and the container runtime a CI runner needs is a separate process.
docs/operations.md gains a production baseline (loopback web behind TLS,
firewalled SSH, conservative limits, audited administration, a restore that has
been tested) and docs/packaging.md documents the capability drop-in.
Tests
forge/config: every refusal above, a development instance that must pass, a production instance that must pass, multi-error reporting, and a check that the duplicated account-name rule still matches the roster’s.internal/secure: inherited0644tightened,0750left alone,0777losing world access, append keeping earlier lines, only changed paths reported, directory/file mismatches refused.server: a fresh instance has no world-readable roster, host key or audit log; an instance whose secrets were0644is tightened by a restart and still starts; a backup is not readable beyond its owner.server/web: a proxy-rewrittenHost, a missing port, a forwarded header and a remote peer all get no session; the opt-out cookie still works.
gofmt, go vet ./..., go test ./..., and vole workflow run verify (which
includes go test -race ./... and govulncheck) are clean.