From 7fdd5dc2772011ea96f972179b693a5102d3f5b9 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sat, 14 Nov 2026 16:34:35 -0800 Subject: [PATCH] fix(aws): explicitly deny Secrets Manager encryption with non-environment keys The AWS-managed aws/secretsmanager key (or a customer key whose key policy grants account principals) needs no identity-policy Allow, so the tagged-key Allow alone did not keep user secrets on environment keys. An explicit Deny on kms:Encrypt / kms:GenerateDataKey* via secretsmanager when the key lacks triple-c:component does. README claim corrected; verify-live entry added. Check.context doc now states the StringList rule as a requirement on the simulation builder. Co-Authored-By: Claude Opus 5.5 --- app/src-tauri/src/aws/policies.rs | 45 ++++++++++++++++++++++++++++++- aws/README.md | 13 ++++++--- aws/policies/workspace-user.json | 19 +++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/app/src-tauri/src/aws/policies.rs b/app/src-tauri/src/aws/policies.rs index bdc8596..7cb99d1 100644 --- a/app/src-tauri/src/aws/policies.rs +++ b/app/src-tauri/src/aws/policies.rs @@ -56,7 +56,9 @@ pub struct Check { /// An ARN template ([`render`]). pub resource: &'static str, /// Context entries. A key that appears more than once is a multi-valued - /// key (e.g. `aws:TagKeys`) and is sent as one `StringList` entry. + /// key (e.g. `aws:TagKeys`): whoever builds the simulation request must + /// send all its values as one `StringList` context entry (not enforced + /// here; the permission check's simulation builder owns it). pub context: &'static [(&'static str, ContextValue)], pub write: bool, } @@ -1693,4 +1695,45 @@ mod tests { ("aws:TagKeys", Literal("c")) ]))); } + + /// A key whose *key policy* grants account principals through Secrets + /// Manager (the AWS-managed `aws/secretsmanager` key, or any such customer + /// key) needs no identity-policy Allow, so only an explicit Deny keeps a + /// workspace user's secrets on environment keys. + #[test] + fn untagged_keys_are_explicitly_denied_through_secrets_manager() { + let tier = Tier::WorkspaceUser; + const VIA_SECRETS_MANAGER: Context = &[( + "kms:ViaService", + Literal("secretsmanager.{region}.amazonaws.com"), + )]; + const VIA_SECRETS_MANAGER_ENVIRONMENT_KEY: Context = &[ + ( + "kms:ViaService", + Literal("secretsmanager.{region}.amazonaws.com"), + ), + (COMPONENT_RESOURCE_TAG, Literal("environment")), + ]; + for action in [ + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + ] { + assert!( + explicitly_denied(tier, &write(action, PROBE_KEY, VIA_SECRETS_MANAGER)), + "{action} on an untagged key via Secrets Manager is not explicitly denied" + ); + assert!( + !explicitly_denied( + tier, + &write(action, PROBE_KEY, VIA_SECRETS_MANAGER_ENVIRONMENT_KEY) + ), + "{action} on an environment key must not be denied" + ); + } + // The checks themselves (environment key) stay allowed. + for c in checks(tier).iter().filter(|c| c.action.starts_with("kms:")) { + assert!(allowed(tier, c, ME, ME), "{}", c.action); + } + } } diff --git a/aws/README.md b/aws/README.md index 93d2e39..3ae6d19 100644 --- a/aws/README.md +++ b/aws/README.md @@ -31,8 +31,11 @@ following: `triple-c:component`. `CreateSecret` must name the key by its full ARN in `KmsKeyId`, and the user can use KMS (`Encrypt`/`GenerateDataKey`) only through Secrets Manager and only with environment keys. - - A secret on the default `aws/secretsmanager` key is refused. That key can't carry the tag, so - the workspace's own task role, which is limited by the boundary, couldn't decrypt it. + - A secret on any other key is refused by an **explicit Deny**: `kms:Encrypt` and + `kms:GenerateDataKey*` through Secrets Manager on a key without `triple-c:component`. The Deny + is needed because the AWS-managed `aws/secretsmanager` key, or any customer key whose key + policy grants account principals directly, needs no identity-policy Allow. A secret on such a + key would be unreadable by the workspace's own task role, which is limited by the boundary. - A secret tagged `triple-c:component` belongs to the environment. A workspace user can never tag, untag, overwrite or delete one; an explicit Deny enforces this. - **Manage their own access points.** They can create and delete EFS access points tagged with @@ -121,7 +124,8 @@ detection should flag any environment that breaks them. 4. **Every environment KMS key and EFS file system carries `triple-c:component`.** The boundary and the admin tier both rely on this tag. 5. **User secrets use the environment key.** The app must pass the environment KMS key's ARN as - `KmsKeyId` when it creates a secret. The policy refuses any other choice. + `KmsKeyId` when it creates a secret. The workspace policy explicitly denies Secrets Manager + encryption with any key that lacks `triple-c:component`, including `aws/secretsmanager`. ## One-time setup: the boundary policy @@ -209,6 +213,9 @@ a real account: - Check that Secrets Manager calls `GenerateDataKey`/`Encrypt` as the caller, so the KMS statement's `aws:ResourceTag/triple-c:component` rule is what enforces the environment key. - Check that `PutSecretValue` on such a secret passes. + - Check that a `CreateSecret` naming the AWS-managed `aws/secretsmanager` key's ARN is denied. + KMS should leave `aws:ResourceTag/triple-c:component` absent for that key, so the + `NeverEncryptSecretsWithOtherKeys` Deny fires even though the key policy allows the call. - **Tag keys.** Check that `aws:TagKeys` is populated for `RunTask`, for `CreateSecret` and secret `TagResource`, for `CreateAccessPoint`, and for the ECS and EFS tag-on-create `TagResource` calls. Check also whether ECS-managed (`aws:ecs:*`) or propagated task-definition tags appear in diff --git a/aws/policies/workspace-user.json b/aws/policies/workspace-user.json index f7ee2b4..9e5e6e7 100644 --- a/aws/policies/workspace-user.json +++ b/aws/policies/workspace-user.json @@ -292,6 +292,25 @@ } } }, + { + "Sid": "NeverEncryptSecretsWithOtherKeys", + "Effect": "Deny", + "Action": [ + "kms:Encrypt", + "kms:GenerateDataKey*" + ], + "Resource": [ + "arn:aws:kms:*:*:key/*" + ], + "Condition": { + "StringLike": { + "kms:ViaService": "secretsmanager.*.amazonaws.com" + }, + "Null": { + "aws:ResourceTag/triple-c:component": "true" + } + } + }, { "Sid": "ReadEcs", "Effect": "Allow",