feat(secrets): encrypt egress tokens at rest with per-bottle ENV_VAR_SECRET
tracker-policy-pr / check-pr (pull_request) Successful in 13s
test / integration-docker (pull_request) Successful in 20s
test / integration-firecracker (pull_request) Successful in 3m25s
test / unit (pull_request) Failing after 13m4s
test / coverage (pull_request) Has been skipped
test / publish-infra (pull_request) Has been skipped
lint / lint (push) Has been cancelled
tracker-policy-pr / check-pr (pull_request) Successful in 13s
test / integration-docker (pull_request) Successful in 20s
test / integration-firecracker (pull_request) Successful in 3m25s
test / unit (pull_request) Failing after 13m4s
test / coverage (pull_request) Has been skipped
test / publish-infra (pull_request) Has been skipped
lint / lint (push) Has been cancelled
Implements the interim secret-provider design (PRD prd-new-secret-provider): each agent receives a random ENV_VAR_SECRET injected into its container env at launch. The host uses this key to encrypt each egress auth token value (HMAC-SHA256 CTR mode, stdlib-only) and store it in a new bottled_agent_secrets table (one row per env var, key column plaintext for auditing). The key never touches the DB. On infra container restart the in-memory token map is lost. launch_consolidated now calls _reprovision_running_bottles after ensure_running: for each registered bottle still alive on the gateway network it execs `printenv ENV_VAR_SECRET` into the agent container and posts the result to the new POST /bottles/<id>/reprovision_gateway control-plane endpoint, which decrypts the stored rows and restores _tokens — no manual intervention needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,22 @@ _MIGRATIONS = TableMigrations(
|
||||
# egress allowlist / routes / git config selected by source IP. The
|
||||
# multi-tenant gateway resolves it per request via `attribute`.
|
||||
"ALTER TABLE orchestrator_bottles ADD COLUMN policy TEXT NOT NULL DEFAULT ''",
|
||||
# v4 — per-bottle encrypted egress secrets (PRD prd-new-secret-provider).
|
||||
# One row per env-var: key (env-var name) is plaintext for auditing;
|
||||
# value is the encrypted token string. The encryption key (ENV_VAR_SECRET)
|
||||
# lives only in the agent's environment — a row alone cannot recover the
|
||||
# credential.
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS bottled_agent_secrets (
|
||||
bottled_agent_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'injected_env_var'
|
||||
)
|
||||
""",
|
||||
# v5 — index for fast per-bottle lookups and bulk DELETE on teardown.
|
||||
"CREATE INDEX IF NOT EXISTS idx_bottled_agent_secrets_id "
|
||||
"ON bottled_agent_secrets (bottled_agent_id, type)",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -326,6 +342,57 @@ class RegistryStore(DbStore):
|
||||
return None
|
||||
return rec
|
||||
|
||||
# --- encrypted egress secret store ------------------------------------
|
||||
|
||||
def store_agent_secrets(
|
||||
self,
|
||||
bottle_id: str,
|
||||
encrypted_values: dict[str, str],
|
||||
secret_type: str = "injected_env_var",
|
||||
) -> None:
|
||||
"""Replace all stored secrets for *bottle_id* with *encrypted_values*
|
||||
(env-var name → encrypted ciphertext). Deletes then re-inserts so a
|
||||
re-registration is always consistent with the current token set."""
|
||||
with self._connection() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM bottled_agent_secrets "
|
||||
"WHERE bottled_agent_id = ? AND type = ?",
|
||||
(bottle_id, secret_type),
|
||||
)
|
||||
conn.executemany(
|
||||
"INSERT INTO bottled_agent_secrets "
|
||||
"(bottled_agent_id, key, value, type) VALUES (?, ?, ?, ?)",
|
||||
[(bottle_id, k, v, secret_type) for k, v in encrypted_values.items()],
|
||||
)
|
||||
self._chmod()
|
||||
|
||||
def get_agent_secrets(
|
||||
self,
|
||||
bottle_id: str,
|
||||
secret_type: str = "injected_env_var",
|
||||
) -> dict[str, str]:
|
||||
"""Return {env_var_name: encrypted_value} for *bottle_id*, or {} if none."""
|
||||
with self._connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT key, value FROM bottled_agent_secrets "
|
||||
"WHERE bottled_agent_id = ? AND type = ?",
|
||||
(bottle_id, secret_type),
|
||||
).fetchall()
|
||||
return {row[0]: row[1] for row in rows}
|
||||
|
||||
def delete_agent_secrets(
|
||||
self,
|
||||
bottle_id: str,
|
||||
secret_type: str = "injected_env_var",
|
||||
) -> None:
|
||||
"""Remove all stored secrets for *bottle_id* (e.g. on teardown)."""
|
||||
with self._connection() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM bottled_agent_secrets "
|
||||
"WHERE bottled_agent_id = ? AND type = ?",
|
||||
(bottle_id, secret_type),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BottleRecord",
|
||||
|
||||
Reference in New Issue
Block a user