From 0464192ac6b6f9fa47b0bed5e9bdd4972a2299b9 Mon Sep 17 00:00:00 2001 From: Danathar Date: Fri, 11 Sep 2026 06:14:47 -0400 Subject: [PATCH 1/2] fix(sec): generate kiosk TLS cert and key at first boot instead of sysext 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 #87 Signed-off-by: Danathar --- docs/skills/k0s-sysext-ops.md | 17 +++++++++++ docs/skills/k0s-sysext.md | 1 + elements/bluefin-server/os-stack.bst | 3 ++ elements/oci/k0s-sysext.bst | 15 ++-------- files/k0s/sysext/k0s-kiosk-tls.service | 14 +++++++++ files/k0s/sysext/k0scontroller.service | 4 +-- tests/unit/test_kubestellar_kiosk.py | 41 ++++++++++++++++++++++++-- 7 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 files/k0s/sysext/k0s-kiosk-tls.service diff --git a/docs/skills/k0s-sysext-ops.md b/docs/skills/k0s-sysext-ops.md index 7b806f8..bd2edc2 100644 --- a/docs/skills/k0s-sysext-ops.md +++ b/docs/skills/k0s-sysext-ops.md @@ -46,6 +46,23 @@ k0s automatically applies all `.yaml` files under `/var/lib/k0s/manifests/argocd k0s kubectl get pods -A ``` +## KubeStellar Kiosk TLS Management + +The KubeStellar kiosk proxy TLS certificate (`cert.pem`) and private key (`key.pem`) are generated on first boot by `k0s-kiosk-tls.service` before `k0scontroller.service` starts. The private key is stored persistently in `/var/lib/k0s/kiosk/key.pem` with mode `0600`, and the certificate is configured with the host's actual IP addresses in the Subject Alternative Names (SAN). + +To rotate or regenerate the TLS certificate and private key: + +```bash +# Remove the existing certificate and key from persistent storage +rm -f /var/lib/k0s/kiosk/key.pem /var/lib/k0s/kiosk/cert.pem + +# Trigger regeneration via the oneshot unit +systemctl restart k0s-kiosk-tls.service + +# Restart the controller service to reload kiosk assets +systemctl restart k0scontroller.service +``` + ## Troubleshooting - **Extension not merged**: Check `systemd-sysext status`. Verify the persistent image is `/var/lib/k0s/k0s.raw`; the boot activation unit copies it into `/run/extensions/k0s.raw`. diff --git a/docs/skills/k0s-sysext.md b/docs/skills/k0s-sysext.md index 1e38f94..8793076 100644 --- a/docs/skills/k0s-sysext.md +++ b/docs/skills/k0s-sysext.md @@ -57,6 +57,7 @@ Design choices: | `elements/k0s/k0s-bin.bst` | Pins the upstream `k0s` binary SHA256; the release URL is derived from `include/k0s.yml`. | | `elements/oci/k0s-sysext.bst` | Builds the EROFS sysext image (`k0s-.raw`). | | `files/k0s/sysext/k0scontroller.service` | systemd unit for the k0s single-node controller/worker. Not enabled by default. | +| `files/k0s/sysext/k0s-kiosk-tls.service` | Oneshot systemd unit generating persistent kiosk TLS cert and key on first boot. | | `files/k0s/sysext/extension-release.k0s` | Static sysext identity (`ID=_any`); `VERSION_ID=`/`ARCHITECTURE=` are appended at build time. | | `files/k0s/sysext/k0s-manifests.conf` | tmpfiles rule that copies declarative stacks to `/var/lib/k0s/manifests/`. | | `files/k0s/manifests/argocd/` | Raw YAML manifests for Argo CD. | diff --git a/elements/bluefin-server/os-stack.bst b/elements/bluefin-server/os-stack.bst index 004c09f..a2ef5d2 100644 --- a/elements/bluefin-server/os-stack.bst +++ b/elements/bluefin-server/os-stack.bst @@ -53,6 +53,9 @@ depends: # gpg binary required by systemd-sysupdate to verify SHA256SUMS.gpg - freedesktop-sdk.bst:components/gnupg.bst + # openssl CLI required for first-boot TLS certificate generation + - freedesktop-sdk.bst:components/openssl.bst + # First-Boot Credentials Provisioning - bluefin-server/os-creds-prov.bst diff --git a/elements/oci/k0s-sysext.bst b/elements/oci/k0s-sysext.bst index b980c9d..05ca194 100644 --- a/elements/oci/k0s-sysext.bst +++ b/elements/oci/k0s-sysext.bst @@ -3,8 +3,8 @@ description: | Produce a systemd-sysext extension image for k0s. Output: k0s-.raw, k0s-.raw.zst, and SHA256SUMS. - Contains /usr/bin/k0s, k0scontroller.service, tmpfiles.d/k0s-manifests.conf, - and /usr/share/k0s/manifests/ (Argo CD and KubeStellar). + Contains /usr/bin/k0s, k0scontroller.service, k0s-kiosk-tls.service, + tmpfiles.d/k0s-manifests.conf, and /usr/share/k0s/manifests/ (Argo CD and KubeStellar). (@): - include/arch.yml @@ -15,7 +15,6 @@ build-depends: - freedesktop-sdk.bst:components/erofs-utils.bst - freedesktop-sdk.bst:components/xz.bst - freedesktop-sdk.bst:components/zstd.bst - - freedesktop-sdk.bst:components/openssl.bst - filename: k0s/k0s-bin.bst config: location: / @@ -64,6 +63,7 @@ config: # Stage systemd service cp -a sysext-src/k0scontroller.service sysext/usr/lib/systemd/system/ + cp -a sysext-src/k0s-kiosk-tls.service sysext/usr/lib/systemd/system/ # Stage extension release metadata cp -a sysext-src/extension-release.k0s sysext/usr/lib/extension-release.d/ @@ -82,15 +82,6 @@ config: # Stage Console kiosk proxy assets. cp -a kiosk-src/. sysext/usr/share/k0s/kiosk/ - # Generate self-signed TLS certificate for local kiosk proxy - openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ - -keyout sysext/usr/share/k0s/kiosk/key.pem \ - -out sysext/usr/share/k0s/kiosk/cert.pem \ - -subj "/CN=KubeStellar Console" \ - -addext "subjectAltName=DNS:localhost,DNS:*.local,IP:127.0.0.1,IP:10.0.2.15" - chmod 0600 sysext/usr/share/k0s/kiosk/key.pem - chmod 0644 sysext/usr/share/k0s/kiosk/cert.pem - # Build EROFS image mkfs.erofs -d0 "${OUT}/${FNAME}" sysext diff --git a/files/k0s/sysext/k0s-kiosk-tls.service b/files/k0s/sysext/k0s-kiosk-tls.service new file mode 100644 index 0000000..539b211 --- /dev/null +++ b/files/k0s/sysext/k0s-kiosk-tls.service @@ -0,0 +1,14 @@ +[Unit] +Description=Generate persistent TLS certificate and key for KubeStellar kiosk +RequiresMountsFor=/var/lib/k0s +After=network-online.target +Before=k0scontroller.service + +[Service] +Type=oneshot +RemainAfterExit=yes +StateDirectory=k0s +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; SAN="DNS:localhost,DNS:*.local,IP:127.0.0.1"; if command -v hostname >/dev/null 2>&1; then HN=$$(hostname 2>/dev/null || true); if [ -n "$$HN" ] && [ "$$HN" != "localhost" ]; then SAN="$${SAN},DNS:$${HN}"; fi; fi; if command -v ip >/dev/null 2>&1; then for addr in $$(ip -o addr show scope global 2>/dev/null | awk "{print $$4}" | cut -d/ -f1); do SAN="$${SAN},IP:$${addr}"; done; fi; openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/k0s/kiosk/key.pem -out /var/lib/k0s/kiosk/cert.pem -subj "/CN=KubeStellar Console" -addext "subjectAltName=$${SAN}"; chmod 0600 /var/lib/k0s/kiosk/key.pem; chmod 0644 /var/lib/k0s/kiosk/cert.pem' + +[Install] +WantedBy=k0scontroller.service multi-user.target diff --git a/files/k0s/sysext/k0scontroller.service b/files/k0s/sysext/k0scontroller.service index 618f2a6..7ea3458 100644 --- a/files/k0s/sysext/k0scontroller.service +++ b/files/k0s/sysext/k0scontroller.service @@ -2,8 +2,8 @@ Description=k0s - Zero Friction Kubernetes Documentation=https://docs.k0sproject.io ConditionFileIsExecutable=/usr/bin/k0s -After=network-online.target -Wants=network-online.target +After=network-online.target k0s-kiosk-tls.service +Wants=network-online.target k0s-kiosk-tls.service [Service] Type=simple diff --git a/tests/unit/test_kubestellar_kiosk.py b/tests/unit/test_kubestellar_kiosk.py index 5cffd7e..63b5d51 100644 --- a/tests/unit/test_kubestellar_kiosk.py +++ b/tests/unit/test_kubestellar_kiosk.py @@ -25,6 +25,9 @@ / "kubestellar" / "41-kubestellar-kiosk-proxy.yaml" ) +KIOSK_TLS_SERVICE = ROOT / "files" / "k0s" / "sysext" / "k0s-kiosk-tls.service" +K0S_CONTROLLER_SERVICE = ROOT / "files" / "k0s" / "sysext" / "k0scontroller.service" +OS_STACK = ROOT / "elements" / "bluefin-server" / "os-stack.bst" def test_kiosk_assets_are_packaged_and_seeded() -> None: @@ -34,8 +37,9 @@ def test_kiosk_assets_are_packaged_and_seeded() -> None: assert KIOSK_CONF.is_file() assert KIOSK_JS.is_file() assert KIOSK_CSS.is_file() - assert "freedesktop-sdk.bst:components/openssl.bst" in sysext - assert "keyout sysext/usr/share/k0s/kiosk/key.pem" in sysext + assert "freedesktop-sdk.bst:components/openssl.bst" not in sysext + assert "keyout" not in sysext + assert "cp -a sysext-src/k0s-kiosk-tls.service sysext/usr/lib/systemd/system/" in sysext assert "path: files/k0s/kiosk" in sysext assert "directory: kiosk-src" in sysext assert "cp -a kiosk-src/. sysext/usr/share/k0s/kiosk/" in sysext @@ -104,3 +108,36 @@ def test_proxy_is_the_only_public_console_endpoint() -> None: "nginx@sha256:62223d644fa234c3a1cc785ee14242ec47a77364226f1c811d2f669f96dc2ac8" in proxy ) + + +def test_k0s_kiosk_tls_service_contract() -> None: + assert KIOSK_TLS_SERVICE.is_file(), "k0s-kiosk-tls.service is missing" + content = KIOSK_TLS_SERVICE.read_text(encoding="utf-8") + + assert "Type=oneshot" in content + assert "Before=k0scontroller.service" in content + assert "RequiresMountsFor=/var/lib/k0s" in content + assert "StateDirectory=k0s" in content + assert "chmod 0600 /var/lib/k0s/kiosk/key.pem" in content + assert "chmod 0644 /var/lib/k0s/kiosk/cert.pem" in content + assert "/CN=KubeStellar Console" in content + assert "DNS:localhost,DNS:*.local,IP:127.0.0.1" in content + assert "ip -o addr show scope global" in content + assert ( + "test -s /var/lib/k0s/kiosk/key.pem && " + "test -s /var/lib/k0s/kiosk/cert.pem && exit 0" + ) in content + + +def test_k0scontroller_orders_after_kiosk_tls() -> None: + assert K0S_CONTROLLER_SERVICE.is_file(), "k0scontroller.service is missing" + content = K0S_CONTROLLER_SERVICE.read_text(encoding="utf-8") + + assert "k0s-kiosk-tls.service" in content + assert "After=network-online.target k0s-kiosk-tls.service" in content + assert "Wants=network-online.target k0s-kiosk-tls.service" in content + + +def test_os_stack_includes_openssl() -> None: + content = OS_STACK.read_text(encoding="utf-8") + assert "freedesktop-sdk.bst:components/openssl.bst" in content From 6c2a436c503c6ce2d026700baeaebb7c5bdfc24d Mon Sep 17 00:00:00 2001 From: Danathar Date: Sat, 12 Sep 2026 10:48:59 +0000 Subject: [PATCH 2/2] fix(kiosk): stop the cert-persistence tmpfiles rule from matching cert.pem/key.pem hanthor's review on #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 Claude-Session: https://claude.ai/code/session_01LNgJ8VMdoqzGequbxsP2Kc --- docs/skills/k0s-sysext-ops.md | 4 +++- files/k0s/sysext/k0s-kiosk-tls.service | 4 ++-- files/k0s/sysext/k0s-manifests.conf | 12 +++++++++++- tests/unit/test_kubestellar_kiosk.py | 25 ++++++++++++++++++++++++- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/skills/k0s-sysext-ops.md b/docs/skills/k0s-sysext-ops.md index bd2edc2..b26d54f 100644 --- a/docs/skills/k0s-sysext-ops.md +++ b/docs/skills/k0s-sysext-ops.md @@ -48,7 +48,9 @@ k0s kubectl get pods -A ## KubeStellar Kiosk TLS Management -The KubeStellar kiosk proxy TLS certificate (`cert.pem`) and private key (`key.pem`) are generated on first boot by `k0s-kiosk-tls.service` before `k0scontroller.service` starts. The private key is stored persistently in `/var/lib/k0s/kiosk/key.pem` with mode `0600`, and the certificate is configured with the host's actual IP addresses in the Subject Alternative Names (SAN). +The KubeStellar kiosk proxy TLS certificate (`cert.pem`) and private key (`key.pem`) are generated on first boot by `k0s-kiosk-tls.service` before `k0scontroller.service` starts. The private key is stored persistently in `/var/lib/k0s/kiosk/key.pem` with mode `0600`, and the certificate is configured with the host's actual IP addresses in the Subject Alternative Names (SAN). `files/k0s/sysext/k0s-manifests.conf` seeds the kiosk static assets (`nginx.conf`, `kiosk-gate.js`, `kiosk-gate.css`) into this same directory on every boot, file-by-file rather than as a whole-directory copy, specifically so it never deletes `cert.pem`/`key.pem`. + +The SAN is computed once, at generation time, from whatever IP addresses the host has at that moment (3650-day validity, no periodic refresh). On a DHCP host whose address later changes, the certificate will not include the new address — rotate manually (below) after an address change if browser cert warnings start appearing. To rotate or regenerate the TLS certificate and private key: diff --git a/files/k0s/sysext/k0s-kiosk-tls.service b/files/k0s/sysext/k0s-kiosk-tls.service index 539b211..c6feadb 100644 --- a/files/k0s/sysext/k0s-kiosk-tls.service +++ b/files/k0s/sysext/k0s-kiosk-tls.service @@ -1,14 +1,14 @@ [Unit] Description=Generate persistent TLS certificate and key for KubeStellar kiosk RequiresMountsFor=/var/lib/k0s -After=network-online.target +After=network-online.target systemd-tmpfiles-setup.service Before=k0scontroller.service [Service] Type=oneshot RemainAfterExit=yes StateDirectory=k0s -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; SAN="DNS:localhost,DNS:*.local,IP:127.0.0.1"; if command -v hostname >/dev/null 2>&1; then HN=$$(hostname 2>/dev/null || true); if [ -n "$$HN" ] && [ "$$HN" != "localhost" ]; then SAN="$${SAN},DNS:$${HN}"; fi; fi; if command -v ip >/dev/null 2>&1; then for addr in $$(ip -o addr show scope global 2>/dev/null | awk "{print $$4}" | cut -d/ -f1); do SAN="$${SAN},IP:$${addr}"; done; fi; openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/k0s/kiosk/key.pem -out /var/lib/k0s/kiosk/cert.pem -subj "/CN=KubeStellar Console" -addext "subjectAltName=$${SAN}"; chmod 0600 /var/lib/k0s/kiosk/key.pem; chmod 0644 /var/lib/k0s/kiosk/cert.pem' +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; SAN="DNS:localhost,DNS:*.local,IP:127.0.0.1"; if command -v hostname >/dev/null 2>&1; then HN=$$(hostname 2>/dev/null || true); if [ -n "$$HN" ] && [ "$$HN" != "localhost" ]; then SAN="$${SAN},DNS:$${HN}"; fi; else echo "k0s-kiosk-tls: hostname command not found, SAN has no host DNS name" >&2; fi; if command -v ip >/dev/null 2>&1; then for addr in $$(ip -o addr show scope global 2>/dev/null | awk "{print $$4}" | cut -d/ -f1); do SAN="$${SAN},IP:$${addr}"; done; else echo "k0s-kiosk-tls: ip command not found, SAN has no host IP address" >&2; fi; openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/k0s/kiosk/key.pem -out /var/lib/k0s/kiosk/cert.pem -subj "/CN=KubeStellar Console" -addext "subjectAltName=$${SAN}"; chmod 0600 /var/lib/k0s/kiosk/key.pem; chmod 0644 /var/lib/k0s/kiosk/cert.pem' [Install] WantedBy=k0scontroller.service multi-user.target diff --git a/files/k0s/sysext/k0s-manifests.conf b/files/k0s/sysext/k0s-manifests.conf index 33f9929..d43dd2a 100644 --- a/files/k0s/sysext/k0s-manifests.conf +++ b/files/k0s/sysext/k0s-manifests.conf @@ -2,4 +2,14 @@ d /var/lib/k0s/manifests 0755 root root - - C+ /var/lib/k0s/manifests/argocd - - - - /usr/share/k0s/manifests/argocd C+ /var/lib/k0s/manifests/kubestellar - - - - /usr/share/k0s/manifests/kubestellar -C+ /var/lib/k0s/kiosk - - - - /usr/share/k0s/kiosk +# Per-file, not whole-directory: k0s-kiosk-tls.service also writes +# cert.pem/key.pem into this directory, and they must survive every boot. +# A directory-level `C+` here would delete-then-recopy the whole tree from +# /usr/share/k0s/kiosk (which never contains cert.pem/key.pem), wiping the +# generated TLS material on every boot. Targeting each static asset file +# individually still propagates sysext upgrades to nginx.conf/kiosk-gate.* +# without touching anything else in the directory. +d /var/lib/k0s/kiosk 0755 root root - - +C+ /var/lib/k0s/kiosk/nginx.conf - - - - /usr/share/k0s/kiosk/nginx.conf +C+ /var/lib/k0s/kiosk/kiosk-gate.js - - - - /usr/share/k0s/kiosk/kiosk-gate.js +C+ /var/lib/k0s/kiosk/kiosk-gate.css - - - - /usr/share/k0s/kiosk/kiosk-gate.css diff --git a/tests/unit/test_kubestellar_kiosk.py b/tests/unit/test_kubestellar_kiosk.py index 63b5d51..2a211ee 100644 --- a/tests/unit/test_kubestellar_kiosk.py +++ b/tests/unit/test_kubestellar_kiosk.py @@ -43,7 +43,29 @@ def test_kiosk_assets_are_packaged_and_seeded() -> None: assert "path: files/k0s/kiosk" in sysext assert "directory: kiosk-src" in sysext assert "cp -a kiosk-src/. sysext/usr/share/k0s/kiosk/" in sysext - assert "C+ /var/lib/k0s/kiosk - - - - /usr/share/k0s/kiosk" in tmpfiles + assert "C+ /var/lib/k0s/kiosk/nginx.conf - - - - /usr/share/k0s/kiosk/nginx.conf" in tmpfiles + assert "C+ /var/lib/k0s/kiosk/kiosk-gate.js - - - - /usr/share/k0s/kiosk/kiosk-gate.js" in tmpfiles + assert "C+ /var/lib/k0s/kiosk/kiosk-gate.css - - - - /usr/share/k0s/kiosk/kiosk-gate.css" in tmpfiles + + +def test_tmpfiles_never_wipes_the_generated_tls_material() -> None: + """A directory-level `C+ /var/lib/k0s/kiosk` would delete-then-recopy the + whole tree from /usr/share/k0s/kiosk on every boot, which never contains + cert.pem/key.pem -- wiping the TLS material k0s-kiosk-tls.service + generates into that same directory. Regression guard for that collision. + """ + directives = [ + line for line in TMPFILES.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.strip().startswith("#") + ] + + assert not any("cert.pem" in line or "key.pem" in line for line in directives) + # No directive targets the bare directory (as opposed to a file inside it). + for line in directives: + if line.startswith("C+"): + assert line.split()[1] != "/var/lib/k0s/kiosk", ( + f"whole-directory C+ rule would delete cert.pem/key.pem: {line!r}" + ) def test_proxy_injects_only_csp_safe_same_origin_assets() -> None: @@ -117,6 +139,7 @@ def test_k0s_kiosk_tls_service_contract() -> None: assert "Type=oneshot" in content assert "Before=k0scontroller.service" in content assert "RequiresMountsFor=/var/lib/k0s" in content + assert "After=network-online.target systemd-tmpfiles-setup.service" in content assert "StateDirectory=k0s" in content assert "chmod 0600 /var/lib/k0s/kiosk/key.pem" in content assert "chmod 0644 /var/lib/k0s/kiosk/cert.pem" in content