Skip to content

[sec-check] fix: remove hardcoded kubestellar console secrets and auto-applied manifest - #90

Open
Danathar wants to merge 2 commits into
projectbluefin:mainfrom
Danathar:sec/fix-kubestellar-console-secrets
Open

[sec-check] fix: remove hardcoded kubestellar console secrets and auto-applied manifest#90
Danathar wants to merge 2 commits into
projectbluefin:mainfrom
Danathar:sec/fix-kubestellar-console-secrets

Conversation

@Danathar

Copy link
Copy Markdown

Security Fix

Addresses finding in #72: files/k0s/manifests/kubestellar/01-kubestellar-console-github-oauth.yaml committed dummy GitHub OAuth credentials and a static JWT secret that was automatically seeded into /var/lib/k0s/manifests/ and deployed by the k0s manifest deployer on default installations.

Changes

  1. Remove Auto-Applied OAuth Secret Manifest: Deleted files/k0s/manifests/kubestellar/01-kubestellar-console-github-oauth.yaml so no hardcoded secrets or dummy credentials are baked into the sysext or auto-applied by k0s.
  2. Make JWT_SECRET Optional in Console Deployment: In files/k0s/manifests/kubestellar/40-kubestellar-console.yaml, set optional: true on the JWT_SECRET Secret reference (matching the existing optional: true on client-id and client-secret). When unset, the console container automatically generates an ephemeral random 32-byte secret at startup (console/start.sh).
  3. Regression Tests: Updated tests/unit/test_k0s_manifests.py to assert 01-kubestellar-console-github-oauth.yaml is not present in manifests, and updated tests/unit/test_kubestellar_kiosk.py to verify all Secret references are optional.
  4. Documentation: Documented how operators can configure production GitHub OAuth credentials and a persistent JWT secret via an operator-created Secret in docs/skills/k0s-sysext-ops.md.

Closes #72

— hive: backend=agy

…ed 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 projectbluefin#72

Signed-off-by: Danathar <Danathar@users.noreply.github.com>

@hanthor hanthor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix. Two things to confirm before it lands, and one much larger issue in the same file.

Deleting 01-kubestellar-console-github-oauth.yaml is clearly right — jwt-secret: default-jwt-secret-123456789012345678901234 was a fixed signing key auto-applied on every default install, and shipping it in the sysext meant every Bluefin Server signed tokens with the same value. Removing it is the correct call.

I checked the obvious way this could break and it does not: deleting that file does not orphan the namespace, because 40-kubestellar-console.yaml declares it itself.

$ git show pr90:files/k0s/manifests/kubestellar/40-kubestellar-console.yaml | head -4
apiVersion: v1
kind: Namespace
metadata:
  name: kubestellar-console

So the Service and Deployment still have a namespace to land in. Good.

1. The central safety claim is not verifiable from this repo

The body states:

When unset, the console container automatically generates an ephemeral random 32-byte secret at startup (console/start.sh).

console/start.sh is not in this repository:

$ git ls-tree -r --name-only pr90 | grep -i 'start.sh'
(no output)

It lives inside ghcr.io/kubestellar/console:v0.3.34. The whole security argument for optional: true rests on that behaviour. If the image instead falls back to an empty or compiled-in default signing key when JWT_SECRET is unset, this change makes forgery easier than the status quo, because the bad-but-known secret is replaced by a bad-and-silent one. Please link the upstream start.sh line, or pin the behaviour with a comment in the manifest, so a future image bump can't quietly invalidate it.

2. The console image is on a mutable tag

image: ghcr.io/kubestellar/console:v0.3.34

No digest. This is the same class of issue #81 is fixing for argocd, and the kiosk proxy in the sibling file is already pinned (nginx@sha256:62223d644fa234c3a1cc785ee14242ec47a77364226f1c811d2f669f96dc2ac8). Since this PR is already editing this file for supply-chain reasons and the behaviour in item 1 is image-dependent, pinning by digest here would be cheap and would make the claim durable.

3. The bigger hole is three lines above the one you changed

Unchanged by this PR, but in the same container spec:

        env:
        - name: DEV_MODE
          value: "true"
        - name: ALLOW_DEV_MODE_IN_CLUSTER
          value: "true"
        - name: DEV_USER_LOGIN
          value: "admin"

To be explicit about baseline: this is pre-existing on main, not introduced here. But it substantially undercuts what this PR achieves. Hardening the JWT signing key matters only if authentication is enforced; with dev mode explicitly enabled in-cluster and a hardcoded admin login, anyone who reaches the console is already an administrator, regardless of how the JWT is signed. Worth a follow-up issue — and it's arguably the finding that should be at the top of #72's queue.

CI status

No checks have run on this PR at all:

mcp__github__pull_request_read(method="get_check_runs", pullNumber=90)
  -> {"total_count":0,"check_runs":[]}

mergeable_state is unstable; it's a fork PR (Danathar/server) and looks like it needs maintainer approval for workflows to run. I ran the gates locally on main + this branch: 201 passed, 1 xfailed, 41/41 bats, all three .github/scripts checkers exit 0 — identical to the main baseline at 7f376fa. Nothing regressed. The two new assertions are cheap and correctly aimed.

Merge-order note

This and #91 both edit docs/skills/k0s-sysext-ops.md and tests/unit/test_kubestellar_kiosk.py. Each merges cleanly into main alone, but not into each other:

$ git merge pr90   # onto main+pr91
CONFLICT (content): Merge conflict in docs/skills/k0s-sysext-ops.md

Whichever goes second needs a rebase.

Marking this as a comment rather than a block — items 1 and 2 are worth resolving first, but the change itself is an improvement over what's on main.


Generated by Claude Code

hanthor's review on projectbluefin#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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LNgJ8VMdoqzGequbxsP2Kc
@Danathar

Copy link
Copy Markdown
Author

@hanthor — pushed in 2de9d42:

  1. Verified the upstream claim directly. Fetched kubestellar/console's start.sh at the exact pinned tag (v0.3.34): when JWT_SECRET is unset, it runs openssl rand -hex 32 (falling back to /dev/urandom, never an empty/compiled-in default), persists it to $INSTALL_DIR/data/.jwt_secret, and exports it. Since this Deployment mounts no volume for that path, it survives a plain container restart but is lost on pod recreation — so "ephemeral" holds at pod-lifetime granularity. Added a comment citing the exact upstream lines (https://github.com/kubestellar/console/blob/v0.3.34/start.sh#L524-L541) so a future image bump that changes this doesn't invalidate the claim silently.

  2. Pinned the image by digest — resolved ghcr.io/kubestellar/console:v0.3.34's current digest via skopeo (it's an OCI image index, same multi-arch-safe shape a tag gives you) and pinned it, matching the kiosk-proxy nginx image's existing pattern in the sibling manifest.

Left item 3 alone per your own framing — pre-existing on main, worth a follow-up issue rather than scope creep here.

Verified: all 8 relevant unit tests pass, full suite 201/201 + 1 xfailed.

On the merge-order note with #91: pushed both independently; whichever lands second will need the small rebase on docs/skills/k0s-sysext-ops.md / tests/unit/test_kubestellar_kiosk.py you flagged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] kubestellar console ships hardcoded JWT secret + dummy GitHub OAuth credentials, auto-applied by default

2 participants