feat(security): RFC#523 3-layer forbidden-env guardrail for tenant workspaces (task #146) #1555
Reference in New Issue
Block a user
Delete Branch "feat/146-forbidden-env-guard"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements RFC#523 (internal#523) — task #146. Refuse to start a tenant workspace if any operator-fleet-scope env var name is present.
Threat model: a leaked
GITEA_TOKEN/CP_ADMIN_API_TOKEN/RAILWAY_TOKEN/INFISICAL_OPERATOR_TOKEN/MOLECULE_OPERATOR_*in a tenant container would let a compromised agent escalate from "compromise of one workspace" to "compromise of the whole platform."3-layer defense-in-depth
L1 — provisioner-side fail-closed abort (Go)
workspace_provision_forbidden_env.go+ hook inprepareProvisionContextloadWorkspaceSecrets, BEFORE per-agentapplyAgentGitHTTPCreds(which legitimately sets a fallbackGITEA_TOKEN)global_secrets,workspace_secretsprovisioner.buildContainerEnvstays as defense-in-depthL2 —
workspace/entrypoint.shtop-of-file env-grepexit 1with explicit error naming the offending keysMOLECULE_TENANT_GUARD_DISABLE=1escape hatch for local-dev (NEVER in tenant containers)L3 —
.gitea/workflows/lint-forbidden-env-keys.ymlworkspace-server/internal/**.gofor new code hardcoding a forbidden env-var nameOpen-source-template compatibility
Deny set lives in Go and YAML constants — NOT hardcoded in any open-source template's
start.sh. Per memoryfeedback_open_source_templates_no_hardcoded_org_internals, templates published as separate repos (template-codex / template-hermes / template-openclaw) get their L2 in follow-up template PRs with a fork-friendly default deny set (noMOLECULE_-specific literal). TheMOLECULE_OPERATOR_prefix appears only in the internal claude-code template'sentrypoint.shhere.Test plan
TestIsForbiddenTenantEnvKey_ExactMatches— 25 cases (all 16 forbidden + 9 allowed)TestIsForbiddenTenantEnvKey_PrefixMatches— 8 cases (4 MOLECULE_OPERATOR_*, 4 adjacent-allowed)TestFindForbiddenTenantEnvKeys_NoneAndEmptyTestFindForbiddenTenantEnvKeys_SingleAndMultipleSortedTestFormatForbiddenTenantEnvError_Phrasing— singular vs pluralworkspace/tests/test_entrypoint_forbidden_env_guard.sh): 12 cases — clean / per-agent-vars-pass / 5 forbidden-blocks / 2 MOLECULE_OPERATOR_* blocks / 2 adjacent-pass / disable-flag-bypassenvVars["GITEA_TOKEN"] = "x") is caughtGITEA_TOKENin payload → 4xx withforbidden_env_keysextradocker run -e GITEA_TOKEN=foo <image>→ exit 1 within 5sRefs
feedback_passwords_in_chat_are_burnedfeedback_per_agent_gitea_identity_defaultfeedback_open_source_templates_no_hardcoded_org_internalsfeedback_check_vendor_docs_and_actual_source_before_guess_api_shape(verified POSIXenvsemantics + Goos.Environ/ map contract before writing)Out of scope (per RFC#523)
reference_prod_team_infisical_identities)🤖 Generated with Claude Code
5-axis review on RFC#523 3-layer forbidden-env guardrail: correctness OK (L1 fail-closed at prepareProvisionContext, L2 entrypoint env-grep, L3 PR-time grep lint with exempt allowlist); readability OK (each layer's purpose + threat model documented inline); arch OK (drift detection via the test TestIsForbiddenTenantEnvKey_ExactMatches as SoT, layers cross-reference each other); security OK (operator-scope key NAMES blocked from tenant env writers, prefix scan covers MOLECULE_OPERATOR_*, exempt list is narrow + justified); perf OK (lint runs sub-second, L1 runs once per provision). APPROVED.
DevOps review: workflow has no paths: filter (feedback_path_filtered_workflow_cant_be_required compliant for required-ability), exempt list is narrow with per-class justification, grep -F + grep -E prefix pass cover both shapes. EXEMPT_PATHS reviewer-signoff comment is enforceable. APPROVED.