fix(agent,secret): enforce CRYPTO_SECRET for agent deploy (#31) and guard unscoped vault purge (#33)
#31: HeicodeDeployAgent now refuses to deploy unless CRYPTO_SECRET is explicitly configured, so the per-agent access_token is sealed with a key that survives a container restart. common.CryptoSecret is never literally "" (defaults to uuid/SessionSecret), so the sealAgentToken plaintext fallback was effectively unreachable; the real hazard is an ephemeral random seal key making tokens undecryptable after restart. Dev-only override: HEICODE_ALLOW_PLAINTEXT_AGENT_TOKEN_IN_DEV=true. Verified prod container has CRYPTO_SECRET set (64 chars) -> deploy stays allowed. #33: StartSecretPurgeTask refuses to start a whole-vault purge when HEICODE_SECRET_PURGE_NAME_PREFIX is empty unless HEICODE_SECRET_PURGE_VAULT_EXCLUSIVE=true, so HM never permanently purges another tenant's soft-deleted secrets in a shared vault. Logs the resolved purge scope at startup. Both gates extracted into pure, unit-tested helpers (agentTokenSealKeyConfigured, secretPurgeScopeAllowed). Affects: Manager only (Agent deploy + Secret lifecycle). No Client/Swarm/billing/audit schema change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -214,6 +214,19 @@ func StartSecretPurgeTask() {
|
||||
if strings.TrimSpace(os.Getenv("AZURE_KEY_VAULT_URL")) == "" {
|
||||
return // no vault configured — nothing to purge
|
||||
}
|
||||
// #33: a destructive purge that scans the WHOLE vault is only safe when
|
||||
// HM owns the vault exclusively. If no name prefix scopes purging to
|
||||
// HM-managed secrets, require an explicit opt-in (HEICODE_SECRET_PURGE_
|
||||
// VAULT_EXCLUSIVE=true) so HM never permanently purges another tenant's
|
||||
// soft-deleted secrets that happen to live in a shared vault.
|
||||
purgePrefix := strings.TrimSpace(common.GetEnvOrDefaultString("HEICODE_SECRET_PURGE_NAME_PREFIX", ""))
|
||||
vaultExclusive := common.GetEnvOrDefaultBool("HEICODE_SECRET_PURGE_VAULT_EXCLUSIVE", false)
|
||||
allowed, scopeDesc := secretPurgeScopeAllowed(purgePrefix, vaultExclusive)
|
||||
if !allowed {
|
||||
common.SysLog("secret purge task NOT started: " + scopeDesc)
|
||||
return
|
||||
}
|
||||
common.SysLog("secret purge scope: " + scopeDesc)
|
||||
intervalHours := common.GetEnvOrDefault("HEICODE_SECRET_PURGE_INTERVAL_HOURS", 24)
|
||||
if intervalHours < 1 {
|
||||
intervalHours = 24
|
||||
@@ -231,6 +244,23 @@ func StartSecretPurgeTask() {
|
||||
})
|
||||
}
|
||||
|
||||
// secretPurgeScopeAllowed decides whether the destructive vault purge may run,
|
||||
// given the configured name prefix and the vault-exclusive opt-in (#33). A purge
|
||||
// that scans the WHOLE vault (empty prefix) is only safe when HM owns the vault
|
||||
// exclusively, so it must be explicitly opted in. Returns the decision plus a
|
||||
// human-readable scope/refusal description for the startup log. Pure helper for
|
||||
// unit testing without touching process env.
|
||||
func secretPurgeScopeAllowed(namePrefix string, vaultExclusive bool) (bool, string) {
|
||||
prefix := strings.TrimSpace(namePrefix)
|
||||
if prefix == "" {
|
||||
if !vaultExclusive {
|
||||
return false, "HEICODE_SECRET_PURGE_NAME_PREFIX is empty and HEICODE_SECRET_PURGE_VAULT_EXCLUSIVE!=true — refusing to purge an entire (possibly shared) vault. Set a name prefix to scope to HM-managed secrets, or set HEICODE_SECRET_PURGE_VAULT_EXCLUSIVE=true only if this vault is exclusive to HM."
|
||||
}
|
||||
return true, "ENTIRE vault (HEICODE_SECRET_PURGE_VAULT_EXCLUSIVE=true, no name prefix)"
|
||||
}
|
||||
return true, "secrets with name prefix '" + prefix + "'"
|
||||
}
|
||||
|
||||
func secretPurgeRetentionDays() int {
|
||||
d := common.GetEnvOrDefault("HEICODE_SECRET_PURGE_RETENTION_DAYS", 30)
|
||||
if d < 1 {
|
||||
|
||||
Reference in New Issue
Block a user