Follow-up to #146
Problem
BootcNode is controller-owned: the pool controller writes spec.desiredImage
and spec.desiredImageState, and the docs say users manage the pool, not the
node. Nothing enforces that — anyone with write access to bootcnodes can patch
the field:
$ kubectl patch bootcnode <node> --type=merge \
-p '{"spec":{"desiredImage":"foo.com/img@sha256:<previous-digest>"}}'
bootcnode.node.bootc.dev/<node> patched
The controller repairs the field on the next reconcile, but the repair is not the
whole story. The daemon watches BootcNode spec changes and staging is not gated
on the pool, so it starts bootc switch against the edited digest before the
repair lands. On a live cluster the repair took ~10ms — the switch was already
issued and had reached the registry. Because the daemon runs plain bootc switch
(no --apply), the rogue image becomes the node's next-boot default.
End state: spec.desiredImage reads correctly, the pool reports upToDate, and
the node boots an image nobody asked for at the next reboot (EC2 maintenance,
kernel panic, admin drain). The mutation is transient; the boot-order side effect
is not, and nothing in the API records that it happened.
Request
Reject writes to controller-owned BootcNode spec fields from any identity other
than the operator's own ServiceAccount, at admission. It could be either a
ValidatingAdmissionPolicy or a validating webhook, whichever fits the project
better.
A cluster-admin can still delete or bypass it, and status stays daemon-writable.
This is a guard against user error and over-broad GitOps selectors, not a security
boundary.
Follow-up to #146
Problem
BootcNodeis controller-owned: the pool controller writesspec.desiredImageand
spec.desiredImageState, and the docs say users manage the pool, not thenode. Nothing enforces that — anyone with write access to
bootcnodescan patchthe field:
The controller repairs the field on the next reconcile, but the repair is not the
whole story. The daemon watches
BootcNodespec changes and staging is not gatedon the pool, so it starts
bootc switchagainst the edited digest before therepair lands. On a live cluster the repair took ~10ms — the switch was already
issued and had reached the registry. Because the daemon runs plain
bootc switch(no
--apply), the rogue image becomes the node's next-boot default.End state:
spec.desiredImagereads correctly, the pool reportsupToDate, andthe node boots an image nobody asked for at the next reboot (EC2 maintenance,
kernel panic, admin drain). The mutation is transient; the boot-order side effect
is not, and nothing in the API records that it happened.
Request
Reject writes to controller-owned
BootcNodespec fields from any identity otherthan the operator's own ServiceAccount, at admission. It could be either a
ValidatingAdmissionPolicyor a validating webhook, whichever fits the projectbetter.
A cluster-admin can still delete or bypass it, and
statusstays daemon-writable.This is a guard against user error and over-broad GitOps selectors, not a security
boundary.