From 34061d3a6899f5bf3b081a10fad915454ff547c1 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Tue, 7 Jul 2026 07:44:20 -0700 Subject: [PATCH] fix(secrets): backend-aware token checks; drop stale file-path assumptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-migration sweep of legacy file-based secret code: - deploy.py + doctor.py checked for the token *file* → false 'secret not found' warnings now that tokens live in the vault; use read_secret (backend-aware) - drop now-unused SECRETS_DIR imports - refresh stale docstring/prompt text (${secret} 'reads ~/.castle/secrets' → 'via the active backend'; SecretsEditor add-prompt) Kept (legitimate file paths): the bootstrap tier (OPENBAO_* token/unseal, cloudflared creds dir) + the rendered 0600 env files. --- app/src/components/SecretsEditor.tsx | 2 +- cli/src/castle_cli/commands/doctor.py | 10 +++++----- core/src/castle_core/config.py | 2 +- core/src/castle_core/deploy.py | 9 +++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/components/SecretsEditor.tsx b/app/src/components/SecretsEditor.tsx index 945bfcf..4e864af 100644 --- a/app/src/components/SecretsEditor.tsx +++ b/app/src/components/SecretsEditor.tsx @@ -103,7 +103,7 @@ export function SecretsEditor({ secrets, onSecretsChange }: SecretsEditorProps) const handleAdd = () => { const envKey = prompt("Environment variable name (e.g. MY_API_KEY):") if (!envKey) return - const secretName = prompt("Secret file name (stored in ~/.castle/secrets/):", envKey) + const secretName = prompt("Secret name (in the active backend — file or vault):", envKey) if (!secretName) return onSecretsChange({ ...secrets, [envKey]: secretName }) setStates((prev) => ({ diff --git a/cli/src/castle_cli/commands/doctor.py b/cli/src/castle_cli/commands/doctor.py index 9a16986..61fb448 100644 --- a/cli/src/castle_cli/commands/doctor.py +++ b/cli/src/castle_cli/commands/doctor.py @@ -281,8 +281,6 @@ def _check_runtime(config) -> list[Check]: def _check_tls_exposure(config) -> list[Check]: - from castle_core.config import SECRETS_DIR - checks: list[Check] = [] gw = config.gateway tls = (gw.tls or "off").lower() @@ -329,15 +327,17 @@ def _check_tls_exposure(config) -> list[Check]: token_name = {"cloudflare": "CLOUDFLARE_API_TOKEN"}.get( provider, f"{provider.upper()}_API_TOKEN" ) - if (SECRETS_DIR / token_name).exists(): + from castle_core.config import read_secret + + if read_secret(token_name): checks.append(Check(OK, "provider token present", detail=token_name)) else: checks.append( Check( FAIL, "provider token missing", - detail=f"~/.castle/secrets/{token_name}", - hint=f"printf '%s' > ~/.castle/secrets/{token_name} && chmod 600 $_", + detail=token_name, + hint="set it via the dashboard Secrets page (or the file/vault backend)", ) ) diff --git a/core/src/castle_core/config.py b/core/src/castle_core/config.py index 6dae35c..c063b3f 100644 --- a/core/src/castle_core/config.py +++ b/core/src/castle_core/config.py @@ -252,7 +252,7 @@ def resolve_env_split( partitioning lets callers keep secrets out of unit files and process argv (routing them through a mode-0600 env file) while inlining the rest. - - ``${secret:NAME}`` reads `~/.castle/secrets/NAME`. + - ``${secret:NAME}`` resolves via the active secret backend (file or OpenBao). - ``${port}`` / ``${data_dir}`` / ``${name}`` / ``${public_url}`` (and anything else in ``context``) expand to castle's computed values, so a service maps them to whatever env var its program reads (e.g. diff --git a/core/src/castle_core/deploy.py b/core/src/castle_core/deploy.py index ecfa090..64885f8 100644 --- a/core/src/castle_core/deploy.py +++ b/core/src/castle_core/deploy.py @@ -15,7 +15,6 @@ from dataclasses import dataclass, field from pathlib import Path from castle_core.config import ( - SECRETS_DIR, SPECS_DIR, CastleConfig, ensure_dirs, @@ -362,10 +361,12 @@ def _acme_preflight(config: CastleConfig, messages: list[str]) -> None: f"Add to services/{_GATEWAY_NAME}.yaml → defaults.env: " f"{token_env}: ${{secret:{token_env}}}" ) - if not (SECRETS_DIR / token_env).exists(): + from castle_core.config import read_secret + + if not read_secret(token_env): messages.append( - f"Warning: secret '{token_env}' not found in {SECRETS_DIR} — place the " - f"DNS-provider API token there (Cloudflare token scope: Zone:DNS:Edit)." + f"Warning: secret '{token_env}' is not set in the secret backend — add " + f"the DNS-provider API token (Cloudflare token scope: Zone:DNS:Edit)." )