fix(aws): document register-into-family residual risk, correct sizes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d1883310dd
commit
7320aaa5d4
2 files changed
+36
-27
No files matched your search
@@ -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 {
|
||||
|
||||
+14
-7
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user