Repository navigation
fix(secrets): gate /api/secrets behind operator auth (#79) - #122
Conversation
The /api/secrets routes exposed full CRUD over cluster secrets with no authentication. Build/run pods call the platform API directly (bypassing the forward-auth proxy), so any LLM-built app could read the operator's sus-git-token / sus-anthropic-api-key in plaintext and create, overwrite, or delete any secret — an app→platform privilege escalation. - Add a require_operator dependency and apply it at the /api/secrets router level. Pods resolve to the anonymous "guest" identity and are now rejected (403). No-op in single-user/no-auth deployments. - Defence in depth: a denylist of platform-owned secrets (sourced from the git-token/api-key managers, extensible via SUS_PROTECTED_SECRETS) is never listed, read-with-values, or mutated through this API, even for an authenticated operator. This protects those credentials in single-user / open-proxy modes where a pod can appear authenticated. - Remove the pod-facing secrets docs from build-pod/CLAUDE.md. - Add tests: guests rejected on every verb; operator listings exclude protected secrets; protected names blocked before any K8s call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvLgS2ry8vNHC6sW8aDZsY
|
Claude finished @dev-dull's task in 1m 21s —— View job Review: fix(secrets): gate /api/secrets behind operator auth
Overall: The core fix is solid. 1. The same credentials are still writable through
2. Outdated docs (minor) – these still describe
Notes:
|
| # Platform-owned secrets that must never be exposed or mutated through this | ||
| # generic API, even to an authenticated operator. Sourced from the managers | ||
| # that own them so a rename can't silently un-protect them. Extend via the | ||
| # ``SUS_PROTECTED_SECRETS`` env var (comma-separated). | ||
| _DEFAULT_PROTECTED_SECRETS = frozenset({ | ||
| _GIT_TOKEN_SECRET, | ||
| _API_KEY_SECRET, | ||
| }) |
There was a problem hiding this comment.
These same secrets can still be overwritten through /setup, so the denylist doesn't fully deliver "never mutated through this API".
landing/app/routes/setup.py has POST /setup/api-key, POST /setup/git-token and POST /setup/repo-url. They write sus-anthropic-api-key and sus-git-token through the managers and call clone_or_pull(). The setup_router is included in main.py:46 with no dependency, so a build/run pod that resolves to guest can still:
- replace the operator's git token or API key with its own (a write rather than a read, but still a takeover of platform credentials), and
- point
SUS_GIT_REPO_URLat a repo it controls. The platform would then sync its catalog and skills from that repo, which is arguably worse than reading the token.
This is the same app→platform gap as #79, one router over. The cheapest fix fits in this PR: give the setup router the same gate, APIRouter(prefix="/setup", dependencies=[Depends(require_operator)]), at least for the POST routes. If you'd rather keep this PR narrow, please open a follow-up. Either way, the PR description's "never … mutated … even for an operator" claim only holds for /api/secrets.
Review (GHA Claude + mAIndmeld) surfaced that the fix was incomplete: - G1: the /setup router was unauthenticated, so a build/run pod could POST /setup/git-token, /setup/api-key, or /setup/repo-url and overwrite the very credentials /api/secrets now protects (and repoint catalog sync at an attacker repo) — the same #79 escalation via another router. Gate the /setup router with require_operator. Safe re: bootstrap: /setup is reached from the authenticated landing page, so login precedes setup. - F2: in single-user / open-proxy modes the auth gate is bypassed and the denylist is the sole defence, so it must cover every platform-managed secret. Add a test that discovers all modules defining SECRET_NAME and asserts each is in _DEFAULT_PROTECTED_SECRETS. - Add a test that /setup routes reject guests. - Docs: correct README, Platform Design, and the networkpolicy comment that still described /api/secrets as app-accessible / #79 as open. 34 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvLgS2ry8vNHC6sW8aDZsY
|
Claude finished @dev-dull's task in 28s —— View job Re-review after
|
Summary
Closes #79. The
/api/secretsroutes exposed full CRUD over cluster secrets in the workloads namespace with no authentication. Build/run pods call the platform API directly (bypassing the Authelia forward-auth proxy), so any LLM-built app could read the operator'ssus-git-token/sus-anthropic-api-keyin plaintext and create, overwrite, or delete any secret — an app→platform privilege escalation.Posture (confirmed with maintainer): operator-only. Pods get no secrets self-service.
Changes
require_operatordependency, applied at the/api/secretsrouter level so every current and future route is covered. Pods resolve to the anonymousguestidentity and are now rejected with 403. No-op in single-user / no-auth deployments, where every request is the local operator by design.SUS_PROTECTED_SECRETS— is never listed, read-with-values, or mutated through this API, even for an authenticated operator. This is what protects those credentials in single-user / open-proxy modes, where a pod can appear authenticated.build-pod/CLAUDE.md.landing/tests/test_secrets_auth.py: guests rejected on every verb; operator listings exclude protected secrets; protected names blocked before any K8s call. Full landing suite: 32 passed.Not in scope (follow-ups)
🤖 Generated with Claude Code