diff --git a/app/src-tauri/src/aws/policies.rs b/app/src-tauri/src/aws/policies.rs index f418465..40aed77 100644 --- a/app/src-tauri/src/aws/policies.rs +++ b/app/src-tauri/src/aws/policies.rs @@ -248,6 +248,12 @@ const WORKSPACE_USER_CHECKS: &[Check] = &[ ), // `workspace-user-tasks.json`: a user registers, tags and retires their // own task definitions, and reads their own secrets. + // Residual risk the policy cannot close: the family carries the owner + // key, which IAM can't condition on, so any user may register a revision + // into another owner's family. The app's mitigation: it runs only the + // exact `family:revision` ARN its own register call returned, and when it + // looks up an existing revision it accepts only one tagged with the + // caller's own owner ID (never "latest of the family"). write( "ecs:RegisterTaskDefinition", PROBE_TASK_DEFINITION, @@ -1140,27 +1146,23 @@ mod tests { #[test] fn registering_a_task_definition_needs_the_owner_tag_and_only_the_three_keys() { let reg = ws_check("ecs:RegisterTaskDefinition", "task-definition/"); - let with_keys = |extra: Option<&'static str>| -> Check { - let ctx: Context = match extra { - None => &[ - (OWNER_REQUEST_TAG, OwnerId), - ("aws:TagKeys", Literal("triple-c:owner")), - ("aws:TagKeys", Literal("triple-c:project")), - ("aws:TagKeys", Literal("triple-c:environment")), - ], - Some(_) => &[ - (OWNER_REQUEST_TAG, OwnerId), - ("aws:TagKeys", Literal("triple-c:owner")), - ("aws:TagKeys", Literal("triple-c:project")), - ("aws:TagKeys", Literal("triple-c:environment")), - ("aws:TagKeys", Literal("extra")), - ], - }; - Check { context: ctx, ..*reg } - }; - assert!(allowed(Tier::WorkspaceUser, &with_keys(None), ME, ME)); + const THREE_KEYS: Context = &[ + (OWNER_REQUEST_TAG, OwnerId), + ("aws:TagKeys", Literal("triple-c:owner")), + ("aws:TagKeys", Literal("triple-c:project")), + ("aws:TagKeys", Literal("triple-c:environment")), + ]; + const FOUR_KEYS: Context = &[ + (OWNER_REQUEST_TAG, OwnerId), + ("aws:TagKeys", Literal("triple-c:owner")), + ("aws:TagKeys", Literal("triple-c:project")), + ("aws:TagKeys", Literal("triple-c:environment")), + ("aws:TagKeys", Literal("extra")), + ]; + let with_keys = |context: Context| Check { context, ..*reg }; + assert!(allowed(Tier::WorkspaceUser, &with_keys(THREE_KEYS), ME, ME)); assert!( - !allowed(Tier::WorkspaceUser, &with_keys(Some("extra")), ME, ME), + !allowed(Tier::WorkspaceUser, &with_keys(FOUR_KEYS), ME, ME), "an extra tag key must be refused" ); let no_owner = Check { diff --git a/aws/README.md b/aws/README.md index 46d9711..d250679 100644 --- a/aws/README.md +++ b/aws/README.md @@ -12,10 +12,11 @@ tier the signed-in identity has under **Settings → Hosts → Check**. | `policies/environment-admin-network.json` | The **environment admin** tier, part 2: the network those modules create (VPC, subnets, gateways, routes, security groups) | | `policies/permissions-boundary.json` | The **permissions boundary** every Triple-C role must carry. It is never attached to a person | -The workspace tier and the admin tier are each **two** files, because one managed policy holds only -6,144 characters; both files of a tier are required, and the app's **Copy** lists every file of a -tier. An environment admin attaches all four tier files (both workspace user files and both admin -files): the admin files add environment management on top of everything a workspace user can do. Explicit Deny statements in any file apply to anyone +The workspace tier and the admin tier are each **two** files, because one managed policy holds +only 6,144 characters; both files of a tier are required, and the app's **Copy** lists every +file of a tier. An environment admin attaches all four tier files (both workspace user files and +both admin files): the admin files add environment management on top of everything a workspace +user can do. Explicit Deny statements in any file apply to anyone who has it attached. ## Workspace user @@ -36,8 +37,14 @@ following: execution role is also authorized with `iam:PassRole` on it, which `PassOwnTaskRoles` already covers (owner-tagged `triple-c-*` roles, to ECS tasks only; a unit test checks both roles). `RunOwnTasks` still requires the definition's owner tag, so a user can run only definitions they - registered. The statements use the revision-free and revision forms alike: the `*` in - `task-definition/triple-c-*` matches `family:revision`. + registered. The `*` in `task-definition/triple-c-*` matches `family:revision` too. + - **Residual risk: polluting another owner's family.** The family name carries the owner key, + which IAM cannot condition on, so any workspace user can register a *revision* into another + owner's `triple-c-*` family and make it the family's latest. It cannot run or steal anything + (`RunOwnTasks` needs the owner tag, and the new revision carries the attacker's), and the + policy cannot close this. The app's mitigation: it always runs the exact `family:revision` + ARN its own register call returned, and when it looks up an existing revision it accepts + only one tagged with the caller's own owner ID, never "latest of the family". - **Read their own secrets.** `secretsmanager:GetSecretValue` on `triple-c/*` secrets tagged with their owner ID, so a second machine can read the agent token and attach to a running task (`ReadOwnSecrets`). An explicit Deny (`NeverReadEnvironmentSecrets`) refuses it on any secret @@ -357,7 +364,7 @@ module creates the same policy, applied by an account administrator; the admin t Create each file as a **customer managed policy** and attach it. In the console, go to **IAM → Policies → Create policy → JSON**, paste the file, and attach the policy to a user, group or role. Inline policies do not fit: a role's inline policies share 10,240 characters and an -environment admin needs every tier file (about 14,500), and a user's share 2,048. A managed policy +environment admin needs every tier file (about 16,600), and a user's share 2,048. A managed policy may hold 6,144 characters, not counting whitespace. Without whitespace, `workspace-user.json` is about 6,040 (roughly 100 characters of headroom), `workspace-user-tasks.json` about 1,460, `environment-admin.json` about 5,800 (340)