From c1011f107c58856aed9e2ffdea685688d2ba6629 Mon Sep 17 00:00:00 2001 From: Danathar Date: Fri, 11 Sep 2026 06:05:40 -0400 Subject: [PATCH 1/2] fix(sec): remove hardcoded kubestellar console secrets and auto-applied manifest Delete files/k0s/manifests/kubestellar/01-kubestellar-console-github-oauth.yaml so no hardcoded JWT secrets or dummy OAuth credentials are automatically seeded into /var/lib/k0s/manifests/ or applied by the k0s manifest deployer. Make JWT_SECRET secretKeyRef in 40-kubestellar-console.yaml optional so the console starts cleanly in local development mode without requiring a pre-seeded Secret manifest (in-container console startup generates an ephemeral secret when unset). Update unit test contracts in test_k0s_manifests.py and test_kubestellar_kiosk.py to assert the secret manifest is not present and all secret references are optional. Document operator-provided OAuth Secret configuration in docs/skills/k0s-sysext-ops.md. Closes #72 Signed-off-by: Danathar --- docs/skills/k0s-sysext-ops.md | 27 +++++++++++++++++++ .../01-kubestellar-console-github-oauth.yaml | 15 ----------- .../kubestellar/40-kubestellar-console.yaml | 2 +- tests/unit/test_k0s_manifests.py | 1 + tests/unit/test_kubestellar_kiosk.py | 3 +++ 5 files changed, 32 insertions(+), 16 deletions(-) delete mode 100644 files/k0s/manifests/kubestellar/01-kubestellar-console-github-oauth.yaml diff --git a/docs/skills/k0s-sysext-ops.md b/docs/skills/k0s-sysext-ops.md index 7b806f8..e6a0127 100644 --- a/docs/skills/k0s-sysext-ops.md +++ b/docs/skills/k0s-sysext-ops.md @@ -46,6 +46,33 @@ k0s automatically applies all `.yaml` files under `/var/lib/k0s/manifests/argocd k0s kubectl get pods -A ``` +## Configuring KubeStellar Console OAuth and JWT Secret + +By default, the KubeStellar console runs in local development mode without requiring OAuth credentials or a static JWT secret. + +To configure production GitHub OAuth credentials and a persistent JWT signing key, apply an operator-provided Secret to the `kubestellar-console` namespace: + +```bash +cat <<'EOF' | k0s kubectl apply -f - +apiVersion: v1 +kind: Secret +metadata: + name: kubestellar-console-github-oauth + namespace: kubestellar-console +type: Opaque +stringData: + client-id: "" + client-secret: "" + jwt-secret: "" +EOF +``` + +Restart the console deployment to pick up the configured credentials: + +```bash +k0s kubectl rollout restart deployment/kubestellar-console -n kubestellar-console +``` + ## 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/files/k0s/manifests/kubestellar/01-kubestellar-console-github-oauth.yaml b/files/k0s/manifests/kubestellar/01-kubestellar-console-github-oauth.yaml deleted file mode 100644 index 3e58dc3..0000000 --- a/files/k0s/manifests/kubestellar/01-kubestellar-console-github-oauth.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: kubestellar-console ---- -apiVersion: v1 -kind: Secret -metadata: - name: kubestellar-console-github-oauth - namespace: kubestellar-console -type: Opaque -stringData: - client-id: "" - client-secret: "" - jwt-secret: default-jwt-secret-123456789012345678901234 diff --git a/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml b/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml index fc502bd..c312415 100644 --- a/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml +++ b/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml @@ -65,7 +65,7 @@ spec: secretKeyRef: name: kubestellar-console-github-oauth key: jwt-secret - optional: false + optional: true ports: - containerPort: 8080 name: http diff --git a/tests/unit/test_k0s_manifests.py b/tests/unit/test_k0s_manifests.py index 149ca42..76104f4 100644 --- a/tests/unit/test_k0s_manifests.py +++ b/tests/unit/test_k0s_manifests.py @@ -38,6 +38,7 @@ def test_k0s_manifest_files(): assert (ks_dir / "30-kubestellar-core.yaml").is_file() assert (ks_dir / "40-kubestellar-console.yaml").is_file() assert (ks_dir / "41-kubestellar-kiosk-proxy.yaml").is_file() + assert not (ks_dir / "01-kubestellar-console-github-oauth.yaml").exists() def test_postgres_password_not_hardcoded(): diff --git a/tests/unit/test_kubestellar_kiosk.py b/tests/unit/test_kubestellar_kiosk.py index ec608c7..dcb578e 100644 --- a/tests/unit/test_kubestellar_kiosk.py +++ b/tests/unit/test_kubestellar_kiosk.py @@ -89,10 +89,13 @@ def test_console_provides_local_and_oauth_login_options() -> None: assert "hostPort:" not in console assert "name: GITHUB_CLIENT_ID" in console assert "name: GITHUB_CLIENT_SECRET" in console + assert "name: JWT_SECRET" in console assert "name: kubestellar-console-github-oauth" in console assert "key: client-id" in console assert "key: client-secret" in console + assert "key: jwt-secret" in console assert "optional: true" in console + assert "optional: false" not in console def test_proxy_is_the_only_public_console_endpoint() -> None: From 63ded6dad7e7beff2c5d17345ff6bb6e49c8405c Mon Sep 17 00:00:00 2001 From: Danathar Date: Sat, 12 Sep 2026 10:52:20 +0000 Subject: [PATCH 2/2] fix(console): pin image by digest, cite upstream JWT_SECRET fallback hanthor's review on #90 confirmed the optional: true change is correct but flagged two things to resolve first: 1. The safety argument ("the console generates an ephemeral random secret when JWT_SECRET is unset") rests on upstream behaviour this repo can't see. Fetched kubestellar/console's start.sh at the exact pinned tag (v0.3.34) and confirmed it directly: when JWT_SECRET is unset, it generates `openssl rand -hex 32` (falling back to /dev/urandom, never an empty or compiled-in default), persists it to $INSTALL_DIR/data/.jwt_secret so it survives a plain container restart, and exports it. Since this Deployment mounts no volume for that path, it's lost on pod recreation -- so "ephemeral" is accurate at the pod-lifetime granularity, just not on every single restart. Added a comment citing the exact upstream lines so a future image bump that changes this can't invalidate the claim silently. 2. The image was on a mutable tag while the sibling kiosk-proxy nginx image in the same manifest set is already digest-pinned. Resolved ghcr.io/kubestellar/console:v0.3.34's current digest via skopeo (confirmed it's an OCI image index, same multi-arch-safe shape as a tag reference) and pinned it. Left item 3 (DEV_MODE/admin login already on main) alone, per the review's own framing -- pre-existing, out of scope, worth a follow-up issue rather than scope creep here. Verified: all 8 relevant unit tests pass, full suite 201/201 + 1 xfailed, YAML parses clean. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LNgJ8VMdoqzGequbxsP2Kc --- .../manifests/kubestellar/40-kubestellar-console.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml b/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml index c312415..76baf7e 100644 --- a/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml +++ b/files/k0s/manifests/kubestellar/40-kubestellar-console.yaml @@ -40,7 +40,9 @@ spec: spec: containers: - name: console - image: ghcr.io/kubestellar/console:v0.3.34 + # v0.3.34, pinned by digest for supply-chain integrity (same pattern + # as the kiosk proxy's nginx image in 41-kubestellar-kiosk-proxy.yaml). + image: ghcr.io/kubestellar/console@sha256:f27adaf8d097a5403f60041615525497e0970151fcd10b90b7b6e903b395be32 env: - name: DEV_MODE value: "true" @@ -60,6 +62,13 @@ spec: name: kubestellar-console-github-oauth key: client-secret optional: true + # When unset, the image's own start.sh generates a random + # openssl rand -hex 32 secret (persisted to $INSTALL_DIR/data/ + # .jwt_secret so it survives a plain container restart, but lost on + # pod recreation since no volume is mounted here) rather than + # falling back to an empty or compiled-in default -- verified + # against the pinned v0.3.34 tag: + # https://github.com/kubestellar/console/blob/v0.3.34/start.sh#L524-L541 - name: JWT_SECRET valueFrom: secretKeyRef: