Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/skills/k0s-sysext-ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ 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). `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:

```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`.
Expand Down
1 change: 1 addition & 0 deletions docs/skills/k0s-sysext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<k0s-version>.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. |
Expand Down
3 changes: 3 additions & 0 deletions elements/bluefin-server/os-stack.bst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 3 additions & 12 deletions elements/oci/k0s-sysext.bst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ description: |
Produce a systemd-sysext extension image for k0s.

Output: k0s-<k0s-version>.raw, k0s-<k0s-version>.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
Expand All @@ -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: /
Expand Down Expand Up @@ -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/
Expand All @@ -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

Expand Down
14 changes: 14 additions & 0 deletions files/k0s/sysext/k0s-kiosk-tls.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Generate persistent TLS certificate and key for KubeStellar kiosk
RequiresMountsFor=/var/lib/k0s
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; 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
12 changes: 11 additions & 1 deletion files/k0s/sysext/k0s-manifests.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions files/k0s/sysext/k0scontroller.service
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 63 additions & 3 deletions tests/unit/test_kubestellar_kiosk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -34,12 +37,35 @@ 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
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:
Expand Down Expand Up @@ -104,3 +130,37 @@ 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 "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
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