Config discovery & hardening
Where config files come from, in what order they merge, and the gates every one of them passes before a single byte is parsed. The theme throughout: a config problem is a loud startup error, never a silent skip — silent misconfiguration is the worst bug class.
The three locations#
Searched in order for an applet whose primary alias is srv; later
files override earlier ones field by field:
| # | Location | Unix | Windows |
|---|---|---|---|
| 1 | next to the real binary | <binary-dir>/srv-config.<ext> | same, .exe resolved |
| 2 | system config | /etc/srv/config.<ext> | %ProgramData%\srv\config.<ext> (C:\ProgramData when unset) |
| 3 | user config (XDG) | ~/.config/srv/config.<ext> | %AppData%\srv\config.<ext> |
So a user setting overrides a system setting, which overrides the
shipped companion — and arguments and environment override them all
(defaults < config files < environment < arguments).
An explicit --config path (or SRV__CONFIG) replaces the search
entirely: that one file is the configuration. The single exception is
--write-config with a missing target — see
Core config & arguments.
What the search will and won't see#
The search only probes the extensions it knows: json plus whatever the
accepted format providers claim. It never
enumerates directories — so config.json.bak, editor droppings and
package-manager leftovers are simply outside its view and can never
break startup.
Two files at the same location — say both config.json and
config.yaml — are ambiguous, and ambiguity is a startup error, not a
precedence rule. --config is checked the same way: if you pass
--config app.toml and no registered provider claims .toml, startup
fails and says so — the file is never half-read or quietly ignored.
The pinned companion location#
Location 1 is special: it configures whatever ships next to the binary, so it gets the strictest treatment.
- "Next to the binary" means next to the real binary — the
executable path from
os.Executable()with every symlink resolved. Busybox-style applet symlinks never relocate the companion: a symlink to the binary sitting in an attacker-writable directory must not get to choose the configuration. - The companion itself must be a regular file physically in that
directory: it is opened refusing a symlink at the final path
component (
O_NOFOLLOW, enforced atomically by the kernel — no check-then-open race; the Windows variant rejects reparse points). - A symlinked companion is a loud startup error — someone put it there.
That includes a dangling symlink: the stat probe follows links and
sees nothing, so pinned candidates get an
Lstatcross-check that catches the squatter before it can become a live redirect.
/etc and the XDG location are deliberately not pinned —
symlink-overlay distros (OpenWrt and friends) keep working — and
--config is exempt: an explicit user path is the user's business.
Regular files only#
Every config source must resolve to a regular file. The stat probe — which follows symlinks, so a symlink to a regular file still passes at the unpinned locations — refuses FIFOs before any open could block on them, and gives devices and directories a clean startup error instead of whatever reading one would produce.
The size cap#
A config file larger than the cap — default 1 MiB, which covers any
sane configuration — is refused. The size is checked on the same stat,
before the file is even opened: an oversized config is never opened,
read or parsed. A capped reader underneath is defense in depth against
stat races and lying sizes, and it never truncates silently.
Like feature suppression, the cap is a build-time
property of the binary: fw.MaxConfigSize(bytes) before Main — a
non-positive limit is itself a collected startup violation.
Every gate is loud#
The full list of things that fail startup rather than pass quietly:
an unknown key anywhere in a file, a key for a suppressed feature or a
run-scoped (dump:"-") field, trailing data after the top-level JSON
object, an ambiguous location, an unhandled explicit extension, a
symlinked or dangling-symlinked companion, a non-regular file, and an
oversized file. If a config file is wrong, the operator finds out at
startup — not three weeks later when the "ignored" setting finally
matters.
One more gate is on the roadmap (designed, not yet built): refusing
group/world-writable configs at the system locations — the
read-side sibling of the pinning rules, the same check sudoers and
sshd make.