From 546e68ccb22e043777668e38e92f0fb1edcd0f18 Mon Sep 17 00:00:00 2001 From: Travis Wu Date: Wed, 16 Sep 2026 18:35:36 +0800 Subject: [PATCH] feat(enterprise): issue the console CA and name the account it authorises A chart-deployed Advisor logged "console: disabled (no -console-account)" and no console session could open. The chart gained the two values it needs (cube-ai-advisor#223); this supplies them. The CA is generated once and passed back on every upgrade, exactly as the enrollment CA already is, and for a sharper reason than symmetry: advisor-api generates one at startup when the chart passes none, and a CubeCOS node pins that CA in sshd and keeps it across firmware upgrades. A CA regenerated on the next pod restart therefore stops every already-enrolled node from accepting console certificates -- silently, because sshd simply stops accepting them and nothing on the Advisor notices. An unreadable existing key is refused rather than replaced. The account defaults to "advisor", which CubeCOS already provisions for exactly this; passing an empty one leaves the console off, which is what every install did before. Nothing pushes the CA to a node: the Advisor mints certificates and the node decides whether to accept them. So the install ends by printing the CA and the hex_cli command that installs it, rather than leaving an operator to discover that the console silently authenticates nobody. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PZ5umjjCedZwWtbAbiMjfj Signed-off-by: Travis Wu --- internal/enterprise/assets/install-advisor.sh | 42 ++++++++++++++++++- internal/enterprise/assets_test.go | 41 ++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/internal/enterprise/assets/install-advisor.sh b/internal/enterprise/assets/install-advisor.sh index cda5ada..1d89fec 100644 --- a/internal/enterprise/assets/install-advisor.sh +++ b/internal/enterprise/assets/install-advisor.sh @@ -3,7 +3,7 @@ # advisor_register only pushes the chart + prereqs to Harbor; this installs # the chart and confirms the advisor serves. Idempotent: safe to re-run. # -# args: [base_url] [console_pool] +# args: [base_url] [console_pool] [console_account] set -uo pipefail FRAMEWORK="${1:?framework name required}" ADVISOR_LB_IP="${2:?advisor lb ip required}" @@ -23,6 +23,10 @@ BASE_URL="${4:-https://$ADVISOR_LB_IP}" # port), and a path scheme needs URL rewriting that breaks the OIDC flow. # Empty leaves the console off, which is what every install did before. CONSOLE_POOL="${5:-}" +# The login account console certificates authorise on a node. CubeCOS +# provisions "advisor" for exactly this, so that is the default; empty leaves +# the console disabled, which is what every install did before. +CONSOLE_ACCOUNT="${6:-advisor}" NS=cube-advisor fail() { echo "ERROR: $*" >&2; exit 1; } @@ -255,6 +259,29 @@ elif [ -n "$CONSOLE_POOL" ]; then echo "warning: the console pool has fewer than 2 addresses; leaving the web console off" >&2 fi +# --- the console's SSH user CA --- +# advisor-api generates one at startup when the chart passes no key, which +# suits a dev run and breaks a deployment: a node pins this CA in sshd and +# keeps it across firmware upgrades, so a CA regenerated on the next pod +# restart silently stops every already-enrolled node accepting console +# certificates. Generated once here and passed back on every upgrade, exactly +# like the enrollment CA above. +CONSOLE_ARGS=() +CONSOLEDIR="$(mktemp -d)"; TMPDIRS+=("$CONSOLEDIR") +if $K -n "$NS" get secret cube-advisor-console-ca >/dev/null 2>&1; then + $K -n "$NS" get secret cube-advisor-console-ca -o jsonpath='{.data.ca\.key}' 2>/dev/null | base64 -d > "$CONSOLEDIR/ca.key" + [ -s "$CONSOLEDIR/ca.key" ] \ + || fail "cube-advisor-console-ca exists but its key is unreadable — refusing to upgrade and strand every node that trusts it" + echo "carrying the existing console CA through the upgrade." +else + # ed25519: what console/ca.go generates for itself, and what sshd expects in + # a TrustedUserCAKeys line. + ssh-keygen -q -t ed25519 -N "" -C cube-advisor-console -f "$CONSOLEDIR/ca.key" \ + || fail "could not generate the console CA" + echo "issued a console CA for account ${CONSOLE_ACCOUNT}." +fi +CONSOLE_ARGS+=(--set console.account="$CONSOLE_ACCOUNT" --set-file console.caKey="$CONSOLEDIR/ca.key") + helm upgrade --install cube-advisor "oci://$RURL/$RPROJ/cube-advisor" --version "$CHART_VER" \ -n "$NS" --create-namespace --kubeconfig "$KC" \ --set lbIP="$ADVISOR_LB_IP" \ @@ -265,6 +292,7 @@ helm upgrade --install cube-advisor "oci://$RURL/$RPROJ/cube-advisor" --version --set-file web.tls.key="$TLSDIR/tls.key" \ "${ENROLL_ARGS[@]}" \ "${WC_ARGS[@]}" \ + "${CONSOLE_ARGS[@]}" \ --kube-insecure-skip-tls-verify --insecure-skip-tls-verify --timeout 20m --wait=false \ || fail "helm upgrade failed — the release is unchanged; do not uninstall to retry, that deletes the database" @@ -304,4 +332,16 @@ case "$code" in *) echo "warning: POST /api/v1/enroll answered $code; expected 401" >&2 ;; esac +# The node half of the console: sshd has to trust this CA before a console +# session can authenticate, and nothing pushes it — the Advisor mints +# certificates, the node decides whether to accept them. Print it where the +# operator installing this will see it. +if [ -s "$CONSOLEDIR/ca.key.pub" ]; then + echo + echo "Console CA for account ${CONSOLE_ACCOUNT}. On each node, run:" + echo " hex_cli -c advisor -c console_trust " + cat "$CONSOLEDIR/ca.key.pub" + echo +fi + echo "cube-advisor installed and verified: https://${ADVISOR_LB_IP}/" diff --git a/internal/enterprise/assets_test.go b/internal/enterprise/assets_test.go index 17d1e20..b27c1e7 100644 --- a/internal/enterprise/assets_test.go +++ b/internal/enterprise/assets_test.go @@ -187,3 +187,44 @@ func TestAdvisorConsoleStaysOffWithoutAPool(t *testing.T) { "node's own dashboard") } } + +// advisor-api generates a console CA at startup when the chart passes none, +// which is right for a dev run and destructive for a deployment: a node pins +// this CA in sshd and keeps it across firmware upgrades, so a CA regenerated +// on the next pod restart stops every already-enrolled node from accepting +// console certificates — silently, because sshd simply stops accepting them. +func TestAdvisorInstallCarriesTheConsoleCAThroughAnUpgrade(t *testing.T) { + for _, want := range []string{ + "get secret cube-advisor-console-ca", + "--set-file console.caKey=", + "refusing to upgrade and strand every node that trusts it", + } { + if !contains(installAdvisorScript, want) { + t.Errorf("the advisor installer does not %q; an upgrade would issue a new "+ + "console CA and every node trusting the old one would stop "+ + "accepting console sessions", want) + } + } +} + +// The account is what a certificate authorises on the node. CubeCOS +// provisions "advisor" for exactly this. +func TestAdvisorInstallNamesTheConsoleAccount(t *testing.T) { + if !contains(installAdvisorScript, "--set console.account=") { + t.Error("the installer never sets console.account, so the deployed Advisor " + + "logs \"console: disabled (no -console-account)\" and no session can open") + } + if !contains(installAdvisorScript, `CONSOLE_ACCOUNT="${6:-advisor}"`) { + t.Error("the console account is not the 6th argument defaulting to advisor") + } +} + +// Nothing pushes the CA to a node — the Advisor mints certificates, the node +// decides whether to accept them — so the install has to end by telling the +// operator what to install and how. +func TestAdvisorInstallPrintsTheConsoleCAForTheNodes(t *testing.T) { + if !contains(installAdvisorScript, "hex_cli -c advisor -c console_trust") { + t.Error("the installer does not tell the operator how to make nodes trust " + + "the console CA, so the console cannot work end to end") + } +}