The cluster plugin wrote the provider's cloud-config with
O_CREATE|O_WRONLY|O_TRUNC and nothing else, at a path that holds the
cluster token in cleartext and is rewritten as root on every boot.
It never created the parent directory, so a node whose cloud-config
directory is not there yet, or a provider that points
cluster_config_path somewhere the image does not ship, failed the whole
boot event on ENOENT.
The open itself trusted the path. A symlink redirected the write and
O_TRUNC emptied whatever it pointed at. A FIFO parked the boot in
open(2) for good. A file somebody else had already created kept its own
mode, because O_CREATE applies perm only when it creates the file, and
then received the cluster token.
MkdirAll the parent, then open with O_NOFOLLOW and O_NONBLOCK, reject
anything that is not a regular file, and set the mode to 0600 after the
open rather than trusting O_CREATE to have done it. Only the last path
element is protected by O_NOFOLLOW, so a symlinked cloud-config
directory keeps working.
Every error past the JSON unmarshal in onBoot also said "failed to parse
boot event", which points the reader at the payload instead of at the
filesystem. Each step now says what it was doing, and the two errnos
that mean "the path is not a file" name what is in the way.
Fixes kairos-io#4867
Signed-off-by: Ettore Di Giacinto <mudler@kairos.io>
Fixes #4867.
sdk/clusterplugin/plugin.gowrote the provider's cloud-config withO_CREATE|O_WRONLY|O_TRUNCand nothing else, at a path that holds the clustertoken in cleartext and is rewritten as root on every boot.
MkdirAllon the parentcluster_config_pathunder a directory the image does not ship, failed the whole boot event on ENOENTO_NOFOLLOWO_TRUNCemptied its targetO_CREATEappliespermonly when it creates the file, so a file somebody else had pre-created kept its own mode and received the tokenO_NONBLOCKopen(2), with no writer ever arrivingThe fix creates the parent, opens with
O_NOFOLLOW|O_NONBLOCK, rejectsanything the
fstatsays is not a regular file, and sets the mode to 0600after the open instead of trusting
O_CREATE. Only the last path element isprotected by
O_NOFOLLOW, so a symlinked cloud-config directory still works.On the misleading error: all four returns past the JSON unmarshal said
failed to parse boot event. Each step now says what it was doing, and thetwo errnos that mean "this path is not a file", ELOOP and ENXIO, name what is
in the way instead of surfacing a bare
no such device or address.Planting any of these needs write access to a persistent path, so this is
robustness rather than a privilege boundary, as the issue says. What it buys
is that the failure is reported, instead of being a truncated stranger's file,
a boot that never finishes, or a token anyone can read.
Tests
New suite, ten specs, in
sdk/clusterplugin(the package had none). Eachguard is pinned by a spec that fails when only that guard is removed:
MkdirAllcreates the directory the cluster config goes inO_NOFOLLOWrefuses a symlink at the config path, and leaves its target aloneO_NONBLOCKdoes not block on a named pipe nobody is reading(10s bound, so it fails instead of hanging the suite)IsRegularguardrefuses a named pipe somebody is reading, instead of writing the token into itChmodtakes the mode down to 0600 on a file somebody else createdsays what went wrong with the file, not that the event failed to parseThe symlink spec asserts the target keeps its contents, and the FIFO-with-a-reader
spec asserts nothing reached the pipe, so neither passes just because an error
came back.
No boot test.
clusterpluginhas no consumer inside this repository; theproviders that import it live in their own repositories, and the contract they
use,
ClusterPlugin.Run, is unchanged.AI was used to write this change. No human has read it yet.