fix(secrets): backend-aware token checks; drop stale file-path assumptions

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.
This commit is contained in:
2026-07-07 07:44:20 -07:00
parent 5949543ace
commit 34061d3a68
4 changed files with 12 additions and 11 deletions

View File

@@ -103,7 +103,7 @@ export function SecretsEditor({ secrets, onSecretsChange }: SecretsEditorProps)
const handleAdd = () => { const handleAdd = () => {
const envKey = prompt("Environment variable name (e.g. MY_API_KEY):") const envKey = prompt("Environment variable name (e.g. MY_API_KEY):")
if (!envKey) return 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 if (!secretName) return
onSecretsChange({ ...secrets, [envKey]: secretName }) onSecretsChange({ ...secrets, [envKey]: secretName })
setStates((prev) => ({ setStates((prev) => ({

View File

@@ -281,8 +281,6 @@ def _check_runtime(config) -> list[Check]:
def _check_tls_exposure(config) -> list[Check]: def _check_tls_exposure(config) -> list[Check]:
from castle_core.config import SECRETS_DIR
checks: list[Check] = [] checks: list[Check] = []
gw = config.gateway gw = config.gateway
tls = (gw.tls or "off").lower() tls = (gw.tls or "off").lower()
@@ -329,15 +327,17 @@ def _check_tls_exposure(config) -> list[Check]:
token_name = {"cloudflare": "CLOUDFLARE_API_TOKEN"}.get( token_name = {"cloudflare": "CLOUDFLARE_API_TOKEN"}.get(
provider, f"{provider.upper()}_API_TOKEN" 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)) checks.append(Check(OK, "provider token present", detail=token_name))
else: else:
checks.append( checks.append(
Check( Check(
FAIL, FAIL,
"provider token missing", "provider token missing",
detail=f"~/.castle/secrets/{token_name}", detail=token_name,
hint=f"printf '%s' <token> > ~/.castle/secrets/{token_name} && chmod 600 $_", hint="set it via the dashboard Secrets page (or the file/vault backend)",
) )
) )

View File

@@ -252,7 +252,7 @@ def resolve_env_split(
partitioning lets callers keep secrets out of unit files and process argv partitioning lets callers keep secrets out of unit files and process argv
(routing them through a mode-0600 env file) while inlining the rest. (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 - ``${port}`` / ``${data_dir}`` / ``${name}`` / ``${public_url}`` (and
anything else in ``context``) expand to castle's computed values, so a anything else in ``context``) expand to castle's computed values, so a
service maps them to whatever env var its program reads (e.g. service maps them to whatever env var its program reads (e.g.

View File

@@ -15,7 +15,6 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from castle_core.config import ( from castle_core.config import (
SECRETS_DIR,
SPECS_DIR, SPECS_DIR,
CastleConfig, CastleConfig,
ensure_dirs, 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"Add to services/{_GATEWAY_NAME}.yaml → defaults.env: "
f"{token_env}: ${{secret:{token_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( messages.append(
f"Warning: secret '{token_env}' not found in {SECRETS_DIR} — place the " f"Warning: secret '{token_env}' is not set in the secret backend — add "
f"DNS-provider API token there (Cloudflare token scope: Zone:DNS:Edit)." f"the DNS-provider API token (Cloudflare token scope: Zone:DNS:Edit)."
) )