[sec-check] fix: generate kiosk TLS cert and key at first boot instead of sysext build time - #91
[sec-check] fix: generate kiosk TLS cert and key at first boot instead of sysext build time#91Danathar wants to merge 2 commits into
Conversation
…sext build time Move KubeStellar kiosk TLS certificate and private key generation from build time in elements/oci/k0s-sysext.bst to a first-boot oneshot systemd service (files/k0s/sysext/k0s-kiosk-tls.service) ordered before k0scontroller.service. - Remove openssl req and build-depends from elements/oci/k0s-sysext.bst so no private key or static certificate is baked into the public sysext release asset. - Add files/k0s/sysext/k0s-kiosk-tls.service to generate key.pem (0600) and cert.pem (0644) under persistent /var/lib/k0s/kiosk/ on first boot. - Dynamically populate Subject Alternative Names (SAN) using localhost, *.local, loopback, hostname, and the host's actual global IP addresses. - Add Wants= and After= dependency on k0s-kiosk-tls.service to k0scontroller.service. - Add freedesktop-sdk.bst:components/openssl.bst to elements/bluefin-server/os-stack.bst. - Update documentation and unit test contracts in test_kubestellar_kiosk.py. Closes projectbluefin#87 Signed-off-by: Danathar <Danathar@users.noreply.github.com>
hanthor
left a comment
There was a problem hiding this comment.
The vulnerability fix is right. The "persistent" half does not hold.
Removing the build-time openssl req is correct and worth doing — a private key baked into a published sysext is the same key on every install, and that is a real finding. No argument with the direction.
But the certificate this PR generates is regenerated on every boot, not persisted, because nothing removed the tmpfiles rule that owns the destination directory.
The collision
files/k0s/sysext/k0s-manifests.conf is unchanged by this PR and still contains:
C+ /var/lib/k0s/kiosk - - - - /usr/share/k0s/kiosk
The new unit writes into that exact path:
ExecStart=/bin/bash -eu -c 'test -s /var/lib/k0s/kiosk/key.pem && test -s /var/lib/k0s/kiosk/cert.pem && exit 0; mkdir -p /var/lib/k0s/kiosk; ... -keyout /var/lib/k0s/kiosk/key.pem -out /var/lib/k0s/kiosk/cert.pem ...'
In tmpfiles.d, the + suffix on C means remove the destination first, then copy. So on every boot:
systemd-tmpfiles-setup.serviceruns (ordered early, beforesysinit.target) and deletes/var/lib/k0s/kiosk/wholesale, re-copying it from/usr/share/k0s/kiosk— which, after this PR, no longer containscert.pemorkey.pem.k0s-kiosk-tls.serviceruns much later (After=network-online.target, well pastsysinit.target), finds no key, and itstest -s … && exit 0fast path falls through.- A brand-new self-signed cert is minted.
The unit is named "Generate persistent TLS certificate and key", the docs added here say the key "is stored persistently in /var/lib/k0s/kiosk/key.pem", and the idempotence guard in item 4 of your summary is written to skip regeneration — none of those are true as long as that C+ line points at the same directory. Operators get a new certificate fingerprint on every reboot, so any pinned or hand-trusted cert breaks and the browser warning returns each time.
Note this is new with this PR: before it, cert.pem/key.pem shipped inside /usr/share/k0s/kiosk, so the C+ re-copy reproduced the same (shared, baked-in) cert each boot. That was the vulnerability — but it was at least stable. This PR fixes the sharing and trades it for churn.
Suggested fix
Cleanest is to stop overlapping the two owners. Generate into a directory tmpfiles does not manage, e.g. /var/lib/kubestellar-kiosk/, and point the proxy's hostPath at it:
# files/k0s/manifests/kubestellar/41-kubestellar-kiosk-proxy.yaml
path: /var/lib/kubestellar-kiosk(nginx.conf needs no change — it reads /etc/kubestellar-kiosk/cert.pem, which is the mount point, not the host path.) Dropping the + from the kiosk line would also stop the deletion, but then sysext upgrades stop propagating new kiosk assets, which is presumably why + is there.
Whichever way you go, please add ordering — After=systemd-tmpfiles-setup.service — so the two can never race, and a test asserting the generated path is not under a C+ destination.
Smaller points
ip/hostnamemay be absent. The SAN loop is guarded bycommand -v, so ifipis not in the OS stack the cert silently getsDNS:localhost,DNS:*.local,IP:127.0.0.1and no host IP — the console is then unreachable by address without a cert error, with nothing in the log saying why.opensslwas correctly added toos-stack.bst; consider assertingipis there too, or logging when the lookup yields nothing.After=network-online.targetdelaysk0scontroller.service(viaBefore=) purely to enumerate addresses. That is a real boot-time cost on a host with a slow link, for a cert that this PR intends to generate once.- First-boot SAN is frozen for 3650 days. On DHCP the address can move and the cert does not follow. Worth documenting in the runbook next to the rotation steps you added.
CI status
No checks have run on this PR at all:
mcp__github__pull_request_read(method="get_check_runs", pullNumber=91)
-> {"total_count":0,"check_runs":[]}
mergeable_state is unstable. It is a fork PR (Danathar/server) and appears to need maintainer approval to run workflows. The build job is the only thing that would exercise k0s-sysext.bst after the openssl build-depend was removed, so that removal is currently unverified by CI. I ran the local gates on main + this branch and they pass — 204 passed, 1 xfailed, 41/41 bats, all three checkers exit 0 — against a main baseline of 201 passed, 1 xfailed, so the three new tests are additive and nothing regressed. But the local suite cannot build the sysext.
Merge-order note
This and #90 both edit docs/skills/k0s-sysext-ops.md and tests/unit/test_kubestellar_kiosk.py. Each merges cleanly into main on its own, but they conflict with each other:
$ git merge pr90 # onto main+pr91
CONFLICT (content): Merge conflict in docs/skills/k0s-sysext-ops.md
Whichever lands second needs a rebase.
Generated by Claude Code
…t.pem/key.pem hanthor's review on projectbluefin#91 argued that the single whole-directory `C+ /var/lib/k0s/kiosk - - - - /usr/share/k0s/kiosk` tmpfiles rule deletes the directory wholesale on every boot and re-copies it from the sysext image (which never contains cert.pem/key.pem), so the persistent-cert fix this PR makes would trade a shared baked-in cert for a fresh one every reboot. I tried to verify the failure mechanism before just taking the suggested fix on faith: ran `systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev` (the exact flags systemd-tmpfiles-setup.service uses) twice against a sandboxed root with the original single-line config, planting fake cert.pem/key.pem between runs to simulate a reboot with an already-generated cert. On this host's systemd 259 (Fedora 44, matching the target OS), they survive both runs -- `C+` does not delete pre-existing destination content; per man(5) tmpfiles.d (Arch Wiki mirror, upstream docs returned 403): plain `C` skips the whole copy if the destination directory already exists and is non-empty, and `C+` relaxes only that skip -- it still won't overwrite/delete files that already exist at the destination. So the specific "deletes wholesale and re-copies" mechanism in the review does not reproduce here. That said, the suggested fix is still worth making regardless of the exact mechanism: relying on an undocumented-by-the-obvious-docs, version-dependent "C+ happens to leave extra files alone" behavior as the only thing standing between a private key and deletion is fragile for something this sensitive, and hanthor asked for the ordering guarantee either way ("whichever way you go, please add After=systemd-tmpfiles-setup.service"). Changed the rule from one whole-directory C+ to three per-file C+ lines (nginx.conf, kiosk-gate.js, kiosk-gate.css) so cert.pem/key.pem are structurally never named by any tmpfiles directive, added the After= ordering, and added logging when `hostname`/`ip` are unavailable (smaller point from the same review) plus a docs note on SAN staleness under DHCP. Added a regression test asserting no tmpfiles line targets the bare kiosk directory and that cert.pem/key.pem never appear in the config. Full suite: 205 passed, 1 xfailed (unchanged from main). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LNgJ8VMdoqzGequbxsP2Kc
|
@hanthor — pushed in I couldn't reproduce the "deletes wholesale every boot" claim. I ran Checked the actual semantics (upstream docs 403'd, used the Arch Wiki mirror): plain Made the fix anyway. Regardless of whether the exact mechanism reproduces, relying on an undocumented, version-dependent "
Full suite: 205 passed, 1 xfailed (unchanged). Left the |
Security Fix
Addresses architecture finding in #87: the k0s sysext build previously generated a self-signed TLS certificate and private key (
key.pem) at build time and baked them directly into the published sysext image.Changes
openssl reqandfreedesktop-sdk.bst:components/openssl.bstbuild-depends fromelements/oci/k0s-sysext.bstso no private key or static certificate is baked into the public sysext release asset.files/k0s/sysext/k0s-kiosk-tls.service, a oneshot systemd unit packaged into the sysext that generateskey.pem(mode0600) andcert.pem(mode0644) under persistent/var/lib/k0s/kiosk/on first boot beforek0scontroller.servicestarts.DNS:localhost,DNS:*.local,IP:127.0.0.1, hostname, and all global IP addresses discovered on the host (ip -o addr show scope global).key.pemandcert.pemexist. Operators can rotate the certificate at any time by removing them from/var/lib/k0s/kiosk/and restarting the unit.After=network-online.target k0s-kiosk-tls.serviceandWants=network-online.target k0s-kiosk-tls.servicetofiles/k0s/sysext/k0scontroller.service.freedesktop-sdk.bst:components/openssl.bsttoelements/bluefin-server/os-stack.bstto provide/usr/bin/opensslat runtime.docs/skills/k0s-sysext-ops.mdand repository layout indocs/skills/k0s-sysext.md. Added unit test contracts intests/unit/test_kubestellar_kiosk.py.Closes #87
— hive: backend=agy