Found by an independent security review; verified against the code.
internal/policy/policy.go:449-481 handles a parse error by keeping the rules that were working. On the first load there are none: w.rules is still the zero Rules{} and w.loaded stays false. Rules() returns that empty set, Empty() reports "no policy", and EvaluateCreate short-circuits to Allow.
Failure: an operator saves a typo in policy.yaml, then the machine reboots — or the supervisor restarts for any reason. Every subsequent docker run is unjudged: --privileged, host namespaces, the bind allowlist, the registry allowlist, all off. The file on disk still looks like an enforced policy.
The mid-edit case the code was written for — a working rule set already loaded — behaves correctly. The startup case, which is the one that survives a reboot, does not.
Two things make this worse than a plain gap:
The package doc at policy.go:23-27 states the opposite as the package's reason for existing: "A rule set that cannot be read is an error the caller surfaces, not an empty policy: silently allowing everything because a file has a typo is the one failure mode a guardrail must not have." Nothing surfaces it — OnError only logs.
And the log line at cmd/skrog/supervise.go:238 reads "policy file is not valid; the previous rules stay in force", which is false in exactly this case. An operator reading the log is told the guardrail held.
docs/policy.md:113-118 is self-contradictory here too, and skrog doctor has no policy check, so nothing else catches it.
Fix shape: distinguish "no rules" from "rules unknown". A first load that fails should refuse to serve rather than serve nothing — or at minimum the supervisor should refuse to start with an unparseable policy file, which is the behaviour the package doc already promises.
Found by an independent security review; verified against the code.
internal/policy/policy.go:449-481handles a parse error by keeping the rules that were working. On the first load there are none:w.rulesis still the zeroRules{}andw.loadedstays false.Rules()returns that empty set,Empty()reports "no policy", andEvaluateCreateshort-circuits to Allow.Failure: an operator saves a typo in
policy.yaml, then the machine reboots — or the supervisor restarts for any reason. Every subsequentdocker runis unjudged:--privileged, host namespaces, the bind allowlist, the registry allowlist, all off. The file on disk still looks like an enforced policy.The mid-edit case the code was written for — a working rule set already loaded — behaves correctly. The startup case, which is the one that survives a reboot, does not.
Two things make this worse than a plain gap:
The package doc at
policy.go:23-27states the opposite as the package's reason for existing: "A rule set that cannot be read is an error the caller surfaces, not an empty policy: silently allowing everything because a file has a typo is the one failure mode a guardrail must not have." Nothing surfaces it —OnErroronly logs.And the log line at
cmd/skrog/supervise.go:238reads "policy file is not valid; the previous rules stay in force", which is false in exactly this case. An operator reading the log is told the guardrail held.docs/policy.md:113-118is self-contradictory here too, andskrog doctorhas no policy check, so nothing else catches it.Fix shape: distinguish "no rules" from "rules unknown". A first load that fails should refuse to serve rather than serve nothing — or at minimum the supervisor should refuse to start with an unparseable policy file, which is the behaviour the package doc already promises.