fix(aws): pin the state bucket to the account, tag-scope network parents, drop v1-unused grants
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7dbf568696
commit
02c8616512
6 files changed
+431
-193
No files matched your search
@@ -140,10 +140,27 @@ const COMPONENT_REQUESTED: Context =
|
||||
&[("aws:RequestTag/triple-c:component", Literal("environment"))];
|
||||
const COMPONENT_RESOURCE_TAG: &str = "aws:ResourceTag/triple-c:component";
|
||||
const COMPONENT_TAGGED: Context = &[(COMPONENT_RESOURCE_TAG, Literal("environment"))];
|
||||
/// A call a service makes on the caller's behalf.
|
||||
const VIA_AWS_SERVICE: Context = &[("aws:ViaAWSService", Literal("true"))];
|
||||
/// A call EFS makes on the caller's behalf (a mount target's interface).
|
||||
const VIA_EFS: Context = &[(
|
||||
"aws:CalledViaLast",
|
||||
Literal("elasticfilesystem.amazonaws.com"),
|
||||
)];
|
||||
/// Created inside a Triple-C (component-tagged) VPC, subnet or address.
|
||||
const REQUESTED_IN_TRIPLE_C_PARENT: Context = &[
|
||||
("aws:RequestTag/triple-c:component", Literal("environment")),
|
||||
(COMPONENT_RESOURCE_TAG, Literal("environment")),
|
||||
];
|
||||
/// The state bucket is in the caller's own account.
|
||||
const IN_MY_ACCOUNT: Context = &[
|
||||
("s3:ResourceAccount", Literal("{account}")),
|
||||
("aws:PrincipalAccount", Literal("{account}")),
|
||||
];
|
||||
/// The state bucket is created tagged `triple-c:component=state`.
|
||||
const STATE_REQUESTED: Context = &[("aws:RequestTag/triple-c:component", Literal("state"))];
|
||||
const STATE_REQUESTED: Context = &[
|
||||
("aws:RequestTag/triple-c:component", Literal("state")),
|
||||
("s3:ResourceAccount", Literal("{account}")),
|
||||
("aws:PrincipalAccount", Literal("{account}")),
|
||||
];
|
||||
|
||||
const PROBE_CLUSTER: &str = "arn:aws:ecs:{region}:{account}:cluster/triple-c-probe";
|
||||
const PROBE_TASK: &str =
|
||||
@@ -408,16 +425,6 @@ const ENVIRONMENT_ADMIN_CHECKS: &[Check] = &[
|
||||
write("ecs:UntagResource", PROBE_CLUSTER, &[]),
|
||||
read("ecs:DescribeClusters", PROBE_CLUSTER, &[]),
|
||||
read("ecs:ListTagsForResource", PROBE_CLUSTER, &[]),
|
||||
write(
|
||||
"ecs:CreateCapacityProvider",
|
||||
"arn:aws:ecs:{region}:{account}:capacity-provider/triple-c-probe",
|
||||
&[],
|
||||
),
|
||||
write(
|
||||
"ecs:DeleteCapacityProvider",
|
||||
"arn:aws:ecs:{region}:{account}:capacity-provider/triple-c-probe",
|
||||
&[],
|
||||
),
|
||||
write("ecs:RegisterTaskDefinition", "*", &[]),
|
||||
write("ecs:DeregisterTaskDefinition", "*", &[]),
|
||||
// The network (environment-admin-network.json). Creates carry the
|
||||
@@ -425,7 +432,7 @@ const ENVIRONMENT_ADMIN_CHECKS: &[Check] = &[
|
||||
// its parents; tags go on only through the creating call.
|
||||
write("ec2:CreateVpc", PROBE_VPC, COMPONENT_REQUESTED),
|
||||
write("ec2:CreateSubnet", PROBE_SUBNET, COMPONENT_REQUESTED),
|
||||
write("ec2:CreateSubnet", PROBE_VPC, COMPONENT_REQUESTED),
|
||||
write("ec2:CreateSubnet", PROBE_VPC, REQUESTED_IN_TRIPLE_C_PARENT),
|
||||
write(
|
||||
"ec2:CreateInternetGateway",
|
||||
PROBE_INTERNET_GATEWAY,
|
||||
@@ -436,24 +443,38 @@ const ENVIRONMENT_ADMIN_CHECKS: &[Check] = &[
|
||||
PROBE_ROUTE_TABLE,
|
||||
COMPONENT_REQUESTED,
|
||||
),
|
||||
write("ec2:CreateRouteTable", PROBE_VPC, COMPONENT_REQUESTED),
|
||||
write(
|
||||
"ec2:CreateRouteTable",
|
||||
PROBE_VPC,
|
||||
REQUESTED_IN_TRIPLE_C_PARENT,
|
||||
),
|
||||
write("ec2:AllocateAddress", PROBE_ELASTIC_IP, COMPONENT_REQUESTED),
|
||||
write(
|
||||
"ec2:CreateNatGateway",
|
||||
PROBE_NAT_GATEWAY,
|
||||
COMPONENT_REQUESTED,
|
||||
),
|
||||
write("ec2:CreateNatGateway", PROBE_SUBNET, COMPONENT_REQUESTED),
|
||||
write(
|
||||
"ec2:CreateNatGateway",
|
||||
PROBE_SUBNET,
|
||||
REQUESTED_IN_TRIPLE_C_PARENT,
|
||||
),
|
||||
write(
|
||||
"ec2:CreateNatGateway",
|
||||
PROBE_ELASTIC_IP,
|
||||
COMPONENT_REQUESTED,
|
||||
REQUESTED_IN_TRIPLE_C_PARENT,
|
||||
),
|
||||
write(
|
||||
"ec2:CreateSecurityGroup",
|
||||
PROBE_SECURITY_GROUP,
|
||||
COMPONENT_REQUESTED,
|
||||
),
|
||||
write(
|
||||
"ec2:CreateNatGateway",
|
||||
PROBE_VPC,
|
||||
REQUESTED_IN_TRIPLE_C_PARENT,
|
||||
),
|
||||
// The one create that may name an untagged (existing-network) VPC.
|
||||
write("ec2:CreateSecurityGroup", PROBE_VPC, COMPONENT_REQUESTED),
|
||||
write(
|
||||
"ec2:CreateTags",
|
||||
@@ -505,11 +526,6 @@ const ENVIRONMENT_ADMIN_CHECKS: &[Check] = &[
|
||||
COMPONENT_TAGGED,
|
||||
),
|
||||
write("ec2:AssociateRouteTable", PROBE_SUBNET, COMPONENT_TAGGED),
|
||||
write(
|
||||
"ec2:ReplaceRouteTableAssociation",
|
||||
PROBE_ROUTE_TABLE,
|
||||
COMPONENT_TAGGED,
|
||||
),
|
||||
write(
|
||||
"ec2:DisassociateRouteTable",
|
||||
PROBE_ROUTE_TABLE,
|
||||
@@ -560,18 +576,23 @@ const ENVIRONMENT_ADMIN_CHECKS: &[Check] = &[
|
||||
PROBE_SECURITY_GROUP,
|
||||
COMPONENT_TAGGED,
|
||||
),
|
||||
write(
|
||||
"ec2:UpdateSecurityGroupRuleDescriptionsEgress",
|
||||
PROBE_SECURITY_GROUP,
|
||||
COMPONENT_TAGGED,
|
||||
),
|
||||
// EFS creates and deletes a mount target's interface with the caller's
|
||||
// EC2 permissions.
|
||||
write(
|
||||
"ec2:CreateNetworkInterface",
|
||||
PROBE_NETWORK_INTERFACE,
|
||||
VIA_AWS_SERVICE,
|
||||
VIA_EFS,
|
||||
),
|
||||
write("ec2:CreateNetworkInterface", PROBE_SUBNET, VIA_AWS_SERVICE),
|
||||
write("ec2:CreateNetworkInterface", PROBE_SUBNET, VIA_EFS),
|
||||
write(
|
||||
"ec2:DeleteNetworkInterface",
|
||||
PROBE_NETWORK_INTERFACE,
|
||||
VIA_AWS_SERVICE,
|
||||
VIA_EFS,
|
||||
),
|
||||
read("ec2:DescribeSecurityGroups", "*", &[]),
|
||||
read("ec2:DescribeVpcs", "*", &[]),
|
||||
@@ -679,17 +700,6 @@ const ENVIRONMENT_ADMIN_CHECKS: &[Check] = &[
|
||||
write("logs:DeleteRetentionPolicy", PROBE_LOG_GROUP, &[]),
|
||||
read("logs:ListTagsForResource", PROBE_LOG_GROUP, &[]),
|
||||
read("logs:DescribeLogGroups", "*", &[]),
|
||||
write(
|
||||
"ecr:CreateRepository",
|
||||
"arn:aws:ecr:{region}:{account}:repository/triple-c/probe",
|
||||
&[],
|
||||
),
|
||||
write(
|
||||
"ecr:DeleteRepository",
|
||||
"arn:aws:ecr:{region}:{account}:repository/triple-c/probe",
|
||||
&[],
|
||||
),
|
||||
write("ecr:CreatePullThroughCacheRule", "*", &[]),
|
||||
write("ssm:PutParameter", PROBE_PARAMETER, COMPONENT_REQUESTED),
|
||||
write(
|
||||
"ssm:AddTagsToResource",
|
||||
@@ -705,16 +715,24 @@ const ENVIRONMENT_ADMIN_CHECKS: &[Check] = &[
|
||||
// The state bucket (the app creates it; OpenTofu's S3 backend reads and
|
||||
// writes the state and lock objects).
|
||||
write("s3:CreateBucket", PROBE_STATE_BUCKET, STATE_REQUESTED),
|
||||
write("s3:PutBucketVersioning", PROBE_STATE_BUCKET, &[]),
|
||||
write("s3:PutEncryptionConfiguration", PROBE_STATE_BUCKET, &[]),
|
||||
write("s3:PutBucketPublicAccessBlock", PROBE_STATE_BUCKET, &[]),
|
||||
write("s3:PutBucketTagging", PROBE_STATE_BUCKET, &[]),
|
||||
write("s3:PutBucketPolicy", PROBE_STATE_BUCKET, &[]),
|
||||
read("s3:GetBucketVersioning", PROBE_STATE_BUCKET, &[]),
|
||||
read("s3:ListBucket", PROBE_STATE_BUCKET, &[]),
|
||||
read("s3:GetObject", PROBE_STATE_OBJECT, &[]),
|
||||
write("s3:PutObject", PROBE_STATE_OBJECT, &[]),
|
||||
write("s3:DeleteObject", PROBE_STATE_OBJECT, &[]),
|
||||
write("s3:PutBucketVersioning", PROBE_STATE_BUCKET, IN_MY_ACCOUNT),
|
||||
write(
|
||||
"s3:PutEncryptionConfiguration",
|
||||
PROBE_STATE_BUCKET,
|
||||
IN_MY_ACCOUNT,
|
||||
),
|
||||
write(
|
||||
"s3:PutBucketPublicAccessBlock",
|
||||
PROBE_STATE_BUCKET,
|
||||
IN_MY_ACCOUNT,
|
||||
),
|
||||
write("s3:PutBucketTagging", PROBE_STATE_BUCKET, IN_MY_ACCOUNT),
|
||||
write("s3:PutBucketPolicy", PROBE_STATE_BUCKET, IN_MY_ACCOUNT),
|
||||
read("s3:GetBucketVersioning", PROBE_STATE_BUCKET, IN_MY_ACCOUNT),
|
||||
read("s3:ListBucket", PROBE_STATE_BUCKET, IN_MY_ACCOUNT),
|
||||
read("s3:GetObject", PROBE_STATE_OBJECT, IN_MY_ACCOUNT),
|
||||
write("s3:PutObject", PROBE_STATE_OBJECT, IN_MY_ACCOUNT),
|
||||
write("s3:DeleteObject", PROBE_STATE_OBJECT, IN_MY_ACCOUNT),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1676,45 +1694,76 @@ mod tests {
|
||||
};
|
||||
assert!(allowed(tier, &tag_at("CreateVpc"), ME, ME));
|
||||
assert!(!allowed(tier, &tag_at("RunInstances"), ME, ME));
|
||||
// A mount target's interface is EFS's doing, never a direct call.
|
||||
// A mount target's interface is EFS's doing, never a direct call, nor
|
||||
// another service's.
|
||||
for action in ["ec2:CreateNetworkInterface", "ec2:DeleteNetworkInterface"] {
|
||||
assert!(
|
||||
!allowed(tier, &write(action, PROBE_NETWORK_INTERFACE, &[]), ME, ME),
|
||||
"{action}"
|
||||
);
|
||||
let via_lambda: Context = &[("aws:CalledViaLast", Literal("lambda.amazonaws.com"))];
|
||||
assert!(
|
||||
!allowed(
|
||||
tier,
|
||||
&write(action, PROBE_NETWORK_INTERFACE, via_lambda),
|
||||
ME,
|
||||
ME
|
||||
),
|
||||
"{action} via another service"
|
||||
);
|
||||
}
|
||||
// No bucket may be deleted, and only the state bucket is managed.
|
||||
// Only the state bucket is managed, and only in the caller's account
|
||||
// (the name is predictable, so another account could pre-create it).
|
||||
const OTHER_BUCKET: &str = "arn:aws:s3:::someone-elses-bucket";
|
||||
const OTHER_ACCOUNT: Context = &[
|
||||
("s3:ResourceAccount", Literal("999999999999")),
|
||||
("aws:PrincipalAccount", Literal("{account}")),
|
||||
];
|
||||
const OTHER_ACCOUNT_REQUESTED: Context = &[
|
||||
("aws:RequestTag/triple-c:component", Literal("state")),
|
||||
("s3:ResourceAccount", Literal("999999999999")),
|
||||
("aws:PrincipalAccount", Literal("{account}")),
|
||||
];
|
||||
// No bucket may be deleted.
|
||||
for bucket in [PROBE_STATE_BUCKET, OTHER_BUCKET] {
|
||||
for context in [&[][..], COMPONENT_TAGGED] {
|
||||
for context in [&[][..], COMPONENT_TAGGED, IN_MY_ACCOUNT] {
|
||||
assert!(
|
||||
!allowed(tier, &write("s3:DeleteBucket", bucket, context), ME, ME),
|
||||
"s3:DeleteBucket on {bucket}"
|
||||
);
|
||||
}
|
||||
}
|
||||
for action in [
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:PutBucketVersioning",
|
||||
"s3:PutEncryptionConfiguration",
|
||||
"s3:PutBucketPublicAccessBlock",
|
||||
"s3:PutBucketTagging",
|
||||
] {
|
||||
let state_calls = [
|
||||
("s3:PutBucketPolicy", PROBE_STATE_BUCKET),
|
||||
("s3:PutBucketVersioning", PROBE_STATE_BUCKET),
|
||||
("s3:PutEncryptionConfiguration", PROBE_STATE_BUCKET),
|
||||
("s3:PutBucketPublicAccessBlock", PROBE_STATE_BUCKET),
|
||||
("s3:PutBucketTagging", PROBE_STATE_BUCKET),
|
||||
("s3:ListBucket", PROBE_STATE_BUCKET),
|
||||
("s3:GetObject", PROBE_STATE_OBJECT),
|
||||
("s3:PutObject", PROBE_STATE_OBJECT),
|
||||
("s3:DeleteObject", PROBE_STATE_OBJECT),
|
||||
];
|
||||
for (action, resource) in state_calls {
|
||||
assert!(
|
||||
allowed(tier, &write(action, PROBE_STATE_BUCKET, &[]), ME, ME),
|
||||
allowed(tier, &write(action, resource, IN_MY_ACCOUNT), ME, ME),
|
||||
"{action}"
|
||||
);
|
||||
assert!(
|
||||
!allowed(tier, &write(action, OTHER_BUCKET, &[]), ME, ME),
|
||||
"{action}"
|
||||
for context in [&[][..], OTHER_ACCOUNT] {
|
||||
assert!(
|
||||
!allowed(tier, &write(action, resource, context), ME, ME),
|
||||
"{action} on a state bucket with {context:?}"
|
||||
);
|
||||
}
|
||||
let elsewhere = resource.replace(
|
||||
"triple-c-tfstate-{account}-{region}",
|
||||
"someone-elses-bucket",
|
||||
);
|
||||
let elsewhere: &'static str = Box::leak(elsewhere.into_boxed_str());
|
||||
assert!(
|
||||
!allowed(tier, &write(action, elsewhere, IN_MY_ACCOUNT), ME, ME),
|
||||
"{action} on {elsewhere}"
|
||||
);
|
||||
}
|
||||
for action in ["s3:PutObject", "s3:DeleteObject", "s3:GetObject"] {
|
||||
let check = Check {
|
||||
write: action != "s3:GetObject",
|
||||
..write(action, "arn:aws:s3:::someone-elses-bucket/key", &[])
|
||||
};
|
||||
assert!(!allowed(tier, &check, ME, ME), "{action}");
|
||||
}
|
||||
assert!(allowed(
|
||||
tier,
|
||||
@@ -1722,15 +1771,88 @@ mod tests {
|
||||
ME,
|
||||
ME
|
||||
));
|
||||
assert!(!allowed(
|
||||
for (bucket, context) in [
|
||||
(PROBE_STATE_BUCKET, IN_MY_ACCOUNT),
|
||||
(PROBE_STATE_BUCKET, OTHER_ACCOUNT_REQUESTED),
|
||||
(OTHER_BUCKET, STATE_REQUESTED),
|
||||
] {
|
||||
assert!(
|
||||
!allowed(tier, &write("s3:CreateBucket", bucket, context), ME, ME),
|
||||
"s3:CreateBucket {bucket} with {context:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Network resources are created only inside the environment's own
|
||||
/// (component-tagged) VPC, subnets and addresses: a tagged subnet,
|
||||
/// route table, gateway or NAT gateway in someone else's VPC would
|
||||
/// re-route it. The one exception is a security group, which an
|
||||
/// `existing`-network environment creates in the user's own VPC.
|
||||
#[test]
|
||||
fn network_creates_need_a_triple_c_parent() {
|
||||
let tier = Tier::EnvironmentAdmin;
|
||||
for (action, parent) in [
|
||||
("ec2:CreateSubnet", PROBE_VPC),
|
||||
("ec2:CreateRouteTable", PROBE_VPC),
|
||||
("ec2:CreateNatGateway", PROBE_VPC),
|
||||
("ec2:CreateNatGateway", PROBE_SUBNET),
|
||||
("ec2:CreateNatGateway", PROBE_ELASTIC_IP),
|
||||
] {
|
||||
assert!(
|
||||
allowed(
|
||||
tier,
|
||||
&write(action, parent, REQUESTED_IN_TRIPLE_C_PARENT),
|
||||
ME,
|
||||
ME
|
||||
),
|
||||
"{action} in a Triple-C {parent}"
|
||||
);
|
||||
assert!(
|
||||
!allowed(tier, &write(action, parent, COMPONENT_REQUESTED), ME, ME),
|
||||
"{action} in an untagged {parent}"
|
||||
);
|
||||
}
|
||||
// Attaching a gateway (and routing through it) needs the VPC tagged.
|
||||
for (action, resource) in [
|
||||
("ec2:AttachInternetGateway", PROBE_VPC),
|
||||
("ec2:CreateRoute", PROBE_ROUTE_TABLE),
|
||||
("ec2:AssociateRouteTable", PROBE_ROUTE_TABLE),
|
||||
("ec2:AssociateRouteTable", PROBE_SUBNET),
|
||||
] {
|
||||
assert!(
|
||||
!allowed(tier, &write(action, resource, COMPONENT_REQUESTED), ME, ME),
|
||||
"{action} on an untagged {resource}"
|
||||
);
|
||||
}
|
||||
assert!(allowed(
|
||||
tier,
|
||||
&write("s3:CreateBucket", PROBE_STATE_BUCKET, &[]),
|
||||
&write("ec2:CreateSecurityGroup", PROBE_VPC, COMPONENT_REQUESTED),
|
||||
ME,
|
||||
ME
|
||||
));
|
||||
assert!(!allowed(
|
||||
tier,
|
||||
&write("s3:CreateBucket", OTHER_BUCKET, STATE_REQUESTED),
|
||||
&write("ec2:CreateSecurityGroup", PROBE_VPC, &[]),
|
||||
ME,
|
||||
ME
|
||||
));
|
||||
// A VPC's main route table association names no subnet, so swapping
|
||||
// it would re-route a VPC by its route table alone: never allowed.
|
||||
assert!(!allowed(
|
||||
tier,
|
||||
&write(
|
||||
"ec2:ReplaceRouteTableAssociation",
|
||||
PROBE_ROUTE_TABLE,
|
||||
COMPONENT_TAGGED
|
||||
),
|
||||
ME,
|
||||
ME
|
||||
));
|
||||
// Triple-C roles are passed only to ECS tasks (Fargate, v1).
|
||||
let pass_to_ec2: Context = &[("iam:PassedToService", Literal("ec2.amazonaws.com"))];
|
||||
assert!(!allowed(
|
||||
tier,
|
||||
&write("iam:PassRole", PROBE_ROLE, pass_to_ec2),
|
||||
ME,
|
||||
ME
|
||||
));
|
||||
|
||||
@@ -201,8 +201,6 @@ pub(crate) fn conditions_hold(stmt: &Value, req: &Request) -> bool {
|
||||
// otherwise when no value matches.
|
||||
let (matches, negated): (fn(&str, &str) -> bool, bool) = match base {
|
||||
"StringEquals" => (|p, v| p == v, false),
|
||||
// A boolean key's value is the string "true" or "false".
|
||||
"Bool" => (|p, v| p == v, false),
|
||||
"StringLike" | "ArnLike" => (glob, false),
|
||||
"StringNotEquals" => (|p, v| p == v, true),
|
||||
"StringNotLike" => (glob, true),
|
||||
@@ -290,31 +288,4 @@ mod tests {
|
||||
("aws:TagKeys", Literal("c"))
|
||||
])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bool_compares_the_values_string() {
|
||||
let stmt: Value = serde_json::json!({
|
||||
"Sid": "T", "Effect": "Allow", "Action": "x:Y", "Resource": "*",
|
||||
"Condition": { "Bool": { "aws:ViaAWSService": "true" } }
|
||||
});
|
||||
let holds = |context| {
|
||||
let c = Check {
|
||||
action: "x:Y",
|
||||
resource: "*",
|
||||
context,
|
||||
write: true,
|
||||
};
|
||||
conditions_hold(
|
||||
&stmt,
|
||||
&Request {
|
||||
check: &c,
|
||||
caller: ME,
|
||||
owner: ME,
|
||||
},
|
||||
)
|
||||
};
|
||||
assert!(holds(&[("aws:ViaAWSService", Literal("true"))]));
|
||||
assert!(!holds(&[("aws:ViaAWSService", Literal("false"))]));
|
||||
assert!(!holds(&[]));
|
||||
}
|
||||
}
|
||||
@@ -1340,7 +1340,9 @@ fn the_record_is_format_checked_before_use() {
|
||||
// it shows missing is added here, to the policy, to the check table
|
||||
// (`policies.rs`) and to `aws/README.md` together.
|
||||
|
||||
use crate::aws::policies::{permissions_boundary_json, policy_files, Check, ContextValue, Tier};
|
||||
use crate::aws::policies::{
|
||||
checks, permissions_boundary_json, policy_files, Check, ContextValue, Tier,
|
||||
};
|
||||
use crate::aws::policy_eval as eval;
|
||||
|
||||
type Context = &'static [(&'static str, ContextValue)];
|
||||
@@ -1404,8 +1406,17 @@ const BOUNDED: Context = &[
|
||||
),
|
||||
("aws:PrincipalAccount", Literal("{account}")),
|
||||
];
|
||||
/// Created inside an environment-module VPC, subnet or address: the new
|
||||
/// resource carries the request tag, its parent the component tag.
|
||||
const ENV_CREATE_IN_ENV: Context = &[
|
||||
(REQUEST_COMPONENT, Literal("environment")),
|
||||
(RESOURCE_COMPONENT, Literal("environment")),
|
||||
];
|
||||
/// A call EFS makes on the caller's behalf (a mount target's interface).
|
||||
const VIA_SERVICE: Context = &[("aws:ViaAWSService", Literal("true"))];
|
||||
const VIA_SERVICE: Context = &[(
|
||||
"aws:CalledViaLast",
|
||||
Literal("elasticfilesystem.amazonaws.com"),
|
||||
)];
|
||||
const NONE: Context = &[];
|
||||
|
||||
macro_rules! created_by {
|
||||
@@ -1414,18 +1425,24 @@ macro_rules! created_by {
|
||||
};
|
||||
}
|
||||
|
||||
/// The state bucket is in the caller's own account.
|
||||
const IN_ACCOUNT: Context = &[
|
||||
("s3:ResourceAccount", Literal("{account}")),
|
||||
("aws:PrincipalAccount", Literal("{account}")),
|
||||
];
|
||||
|
||||
/// The S3 backend's calls on one deployment's state and lock objects.
|
||||
const BACKEND: &[Check] = &[
|
||||
call("s3:ListBucket", STATE_BUCKET, NONE),
|
||||
call("s3:GetObject", ENV_STATE, NONE),
|
||||
call("s3:PutObject", ENV_STATE, NONE),
|
||||
call("s3:GetObject", ENV_LOCK, NONE),
|
||||
call("s3:PutObject", ENV_LOCK, NONE),
|
||||
call("s3:DeleteObject", ENV_LOCK, NONE),
|
||||
call("s3:GetObject", USER_STATE, NONE),
|
||||
call("s3:PutObject", USER_STATE, NONE),
|
||||
call("s3:PutObject", USER_LOCK, NONE),
|
||||
call("s3:DeleteObject", USER_LOCK, NONE),
|
||||
call("s3:ListBucket", STATE_BUCKET, IN_ACCOUNT),
|
||||
call("s3:GetObject", ENV_STATE, IN_ACCOUNT),
|
||||
call("s3:PutObject", ENV_STATE, IN_ACCOUNT),
|
||||
call("s3:GetObject", ENV_LOCK, IN_ACCOUNT),
|
||||
call("s3:PutObject", ENV_LOCK, IN_ACCOUNT),
|
||||
call("s3:DeleteObject", ENV_LOCK, IN_ACCOUNT),
|
||||
call("s3:GetObject", USER_STATE, IN_ACCOUNT),
|
||||
call("s3:PutObject", USER_STATE, IN_ACCOUNT),
|
||||
call("s3:PutObject", USER_LOCK, IN_ACCOUNT),
|
||||
call("s3:DeleteObject", USER_LOCK, IN_ACCOUNT),
|
||||
];
|
||||
|
||||
const MODULE_CALLS: &[(&str, &[Check])] = &[
|
||||
@@ -1462,6 +1479,8 @@ const MODULE_CALLS: &[(&str, &[Check])] = &[
|
||||
"aws_security_group",
|
||||
&[
|
||||
call("ec2:CreateSecurityGroup", SG, ENV_CREATE),
|
||||
// The one create that may name an untagged VPC: an
|
||||
// `existing`-network environment's is the user's own.
|
||||
call("ec2:CreateSecurityGroup", VPC, ENV_CREATE),
|
||||
call("ec2:CreateTags", SG, created_by!("CreateSecurityGroup")),
|
||||
call("ec2:DescribeSecurityGroups", "*", NONE),
|
||||
@@ -1582,7 +1601,7 @@ const MODULE_CALLS: &[(&str, &[Check])] = &[
|
||||
"aws_subnet",
|
||||
&[
|
||||
call("ec2:CreateSubnet", SUBNET, ENV_CREATE),
|
||||
call("ec2:CreateSubnet", VPC, ENV_CREATE),
|
||||
call("ec2:CreateSubnet", VPC, ENV_CREATE_IN_ENV),
|
||||
call("ec2:CreateTags", SUBNET, created_by!("CreateSubnet")),
|
||||
call("ec2:DescribeSubnets", "*", NONE),
|
||||
call("ec2:ModifySubnetAttribute", SUBNET, ENV_TAGGED),
|
||||
@@ -1596,7 +1615,7 @@ const MODULE_CALLS: &[(&str, &[Check])] = &[
|
||||
"aws_route_table",
|
||||
&[
|
||||
call("ec2:CreateRouteTable", RTB, ENV_CREATE),
|
||||
call("ec2:CreateRouteTable", VPC, ENV_CREATE),
|
||||
call("ec2:CreateRouteTable", VPC, ENV_CREATE_IN_ENV),
|
||||
call("ec2:CreateTags", RTB, created_by!("CreateRouteTable")),
|
||||
call("ec2:DescribeRouteTables", "*", NONE),
|
||||
call("ec2:CreateTags", RTB, ENV_TAGGED),
|
||||
@@ -1615,13 +1634,14 @@ const MODULE_CALLS: &[(&str, &[Check])] = &[
|
||||
),
|
||||
(
|
||||
// Untaggable: authorized against the route table and the subnet.
|
||||
// Replacing an association is never allowed (it could swap a VPC's
|
||||
// main route table), so a changed route_table_id fails rather than
|
||||
// re-associating; the modules never change it.
|
||||
"aws_route_table_association",
|
||||
&[
|
||||
call("ec2:AssociateRouteTable", RTB, ENV_TAGGED),
|
||||
call("ec2:AssociateRouteTable", SUBNET, ENV_TAGGED),
|
||||
call("ec2:DescribeRouteTables", "*", NONE),
|
||||
call("ec2:ReplaceRouteTableAssociation", RTB, ENV_TAGGED),
|
||||
call("ec2:ReplaceRouteTableAssociation", SUBNET, ENV_TAGGED),
|
||||
call("ec2:DisassociateRouteTable", RTB, ENV_TAGGED),
|
||||
call("ec2:DisassociateRouteTable", SUBNET, ENV_TAGGED),
|
||||
],
|
||||
@@ -1642,9 +1662,9 @@ const MODULE_CALLS: &[(&str, &[Check])] = &[
|
||||
"aws_nat_gateway",
|
||||
&[
|
||||
call("ec2:CreateNatGateway", NAT, ENV_CREATE),
|
||||
call("ec2:CreateNatGateway", SUBNET, ENV_CREATE),
|
||||
call("ec2:CreateNatGateway", EIP, ENV_CREATE),
|
||||
call("ec2:CreateNatGateway", VPC, ENV_CREATE),
|
||||
call("ec2:CreateNatGateway", SUBNET, ENV_CREATE_IN_ENV),
|
||||
call("ec2:CreateNatGateway", EIP, ENV_CREATE_IN_ENV),
|
||||
call("ec2:CreateNatGateway", VPC, ENV_CREATE_IN_ENV),
|
||||
call("ec2:CreateTags", NAT, created_by!("CreateNatGateway")),
|
||||
call("ec2:DescribeNatGateways", "*", NONE),
|
||||
call("ec2:CreateTags", NAT, ENV_TAGGED),
|
||||
@@ -1948,3 +1968,65 @@ fn every_onboarding_role_stays_inside_the_boundary() {
|
||||
}
|
||||
assert!(seen >= 15, "only {seen} role grants checked");
|
||||
}
|
||||
|
||||
/// Admin write checks for calls the app makes itself, not the modules.
|
||||
const APP_ONLY_WRITES: &[&str] = &[
|
||||
// The state bucket's setup (the backend only reads and writes objects).
|
||||
"s3:CreateBucket",
|
||||
"s3:PutBucketVersioning",
|
||||
"s3:PutEncryptionConfiguration",
|
||||
"s3:PutBucketPublicAccessBlock",
|
||||
"s3:PutBucketTagging",
|
||||
"s3:PutBucketPolicy",
|
||||
// Workspaces (phase 5): task definitions and the roles they name.
|
||||
"ecs:RegisterTaskDefinition",
|
||||
"ecs:DeregisterTaskDefinition",
|
||||
"iam:PassRole",
|
||||
"iam:AttachRolePolicy",
|
||||
"iam:DetachRolePolicy",
|
||||
// The permission check itself.
|
||||
"iam:SimulatePrincipalPolicy",
|
||||
];
|
||||
|
||||
fn is_write(action: &str) -> bool {
|
||||
let verb = action.split(':').nth(1).unwrap();
|
||||
!["Describe", "List", "Get"]
|
||||
.iter()
|
||||
.any(|p| verb.starts_with(p))
|
||||
}
|
||||
|
||||
/// The module table and the check table can't drift: every write a module
|
||||
/// makes is something the permission check asks about, and every admin write
|
||||
/// check is a module's call or one of the app's own.
|
||||
#[test]
|
||||
fn every_module_write_has_an_admin_check_and_vice_versa() {
|
||||
let checked: BTreeSet<&str> = checks(Tier::EnvironmentAdmin)
|
||||
.iter()
|
||||
.map(|c| c.action)
|
||||
.filter(|a| is_write(a))
|
||||
.collect();
|
||||
let module: BTreeSet<&str> = MODULE_CALLS
|
||||
.iter()
|
||||
.flat_map(|(_, calls)| calls.iter().map(|c| c.action))
|
||||
.filter(|a| is_write(a))
|
||||
.collect();
|
||||
let unchecked: Vec<_> = module.difference(&checked).collect();
|
||||
assert!(
|
||||
unchecked.is_empty(),
|
||||
"module writes without an admin check: {unchecked:?}"
|
||||
);
|
||||
let app_only: BTreeSet<&str> = APP_ONLY_WRITES.iter().copied().collect();
|
||||
let stray: Vec<_> = checked
|
||||
.iter()
|
||||
.filter(|a| !module.contains(*a) && !app_only.contains(*a))
|
||||
.collect();
|
||||
assert!(
|
||||
stray.is_empty(),
|
||||
"admin write checks no module or app call needs: {stray:?}"
|
||||
);
|
||||
let stale: Vec<_> = app_only
|
||||
.iter()
|
||||
.filter(|a| module.contains(*a) || !checked.contains(*a))
|
||||
.collect();
|
||||
assert!(stale.is_empty(), "APP_ONLY_WRITES is stale: {stale:?}");
|
||||
}
|
||||
+40
-26
@@ -90,12 +90,15 @@ and nothing runs with more than the admin holds. It covers:
|
||||
- the per-account state bucket `triple-c-tfstate-<account>-<region>` (created by the app, tagged
|
||||
`triple-c:component=state`, then read and written by OpenTofu's S3 backend, lock files included);
|
||||
- `triple-c-*` IAM roles, and reading the `triple-c-permissions-boundary` policy;
|
||||
- ECS clusters (and capacity providers and task definitions);
|
||||
- ECS clusters (Fargate and Fargate Spot only) and task definitions;
|
||||
- the network: VPCs, subnets, an internet gateway, route tables and routes, an Elastic IP and NAT
|
||||
gateway, security groups and their rules (`environment-admin-network.json`);
|
||||
- EFS file systems, mount targets and file-system policies, and KMS keys and aliases;
|
||||
- `/triple-c/*` log groups and `/triple-c/*` SSM parameters;
|
||||
- ECR repositories and pull-through cache rules.
|
||||
- `/triple-c/*` log groups and `/triple-c/*` SSM parameters.
|
||||
|
||||
Not in v1, and so not in the policy: ECR (tasks pull from GHCR), custom ECS capacity providers,
|
||||
and passing roles to EC2 (`iam:PassRole` is to `ecs-tasks.amazonaws.com` only). Phase 7's EC2
|
||||
capacity adds them back.
|
||||
|
||||
### What each module resource needs
|
||||
|
||||
@@ -116,25 +119,32 @@ to the table, the policy, the check table in `policies.rs` and this list togethe
|
||||
| `aws_ssm_parameter` | `ssm:PutParameter`, `AddTagsToResource`, `GetParameter`, `ListTagsForResource`, `RemoveTagsFromResource`, `DeleteParameter` (`/triple-c/*`), `DescribeParameters` (`*`) |
|
||||
| `aws_vpc` | `ec2:CreateVpc` (request tag), `CreateTags` (at create), `ModifyVpcAttribute` (DNS hostnames), `CreateTags`/`DeleteTags`, `DeleteVpc` (tagged), `Describe*` |
|
||||
| `aws_internet_gateway` | `ec2:CreateInternetGateway`, `CreateTags` (at create), `Attach`/`DetachInternetGateway` (tagged gateway and VPC), `DeleteInternetGateway`, `Describe*` |
|
||||
| `aws_subnet` | `ec2:CreateSubnet` (subnet and VPC, request tag), `CreateTags` (at create), `ModifySubnetAttribute`, `DeleteSubnet` (tagged), `Describe*` |
|
||||
| `aws_route_table` | `ec2:CreateRouteTable` (table and VPC, request tag), `CreateTags` (at create), `DeleteRouteTable` (tagged), `Describe*` |
|
||||
| `aws_subnet` | `ec2:CreateSubnet` (subnet: request tag; VPC: tagged), `CreateTags` (at create), `ModifySubnetAttribute`, `DeleteSubnet` (tagged), `Describe*` |
|
||||
| `aws_route_table` | `ec2:CreateRouteTable` (table: request tag; VPC: tagged), `CreateTags` (at create), `DeleteRouteTable` (tagged), `Describe*` |
|
||||
| `aws_route` | `ec2:CreateRoute`, `ReplaceRoute`, `DeleteRoute` (the tagged route table; untaggable) |
|
||||
| `aws_route_table_association` | `ec2:AssociateRouteTable`, `ReplaceRouteTableAssociation`, `DisassociateRouteTable` (the tagged route table and subnet; untaggable) |
|
||||
| `aws_route_table_association` | `ec2:AssociateRouteTable`, `DisassociateRouteTable` (the tagged route table and subnet; untaggable). Never `ReplaceRouteTableAssociation`: it names no subnet when it swaps a VPC's main route table, so it could re-route a VPC that isn't Triple-C's; the modules never change an association's table |
|
||||
| `aws_eip` | `ec2:AllocateAddress` (request tag), `CreateTags` (at create), `ReleaseAddress` (tagged), `Describe*` |
|
||||
| `aws_nat_gateway` | `ec2:CreateNatGateway` (gateway, subnet, address and VPC, request tag), `CreateTags` (at create), `DeleteNatGateway` (tagged), `Describe*` |
|
||||
| `aws_nat_gateway` | `ec2:CreateNatGateway` (gateway: request tag; its subnet, address and VPC: tagged), `CreateTags` (at create), `DeleteNatGateway` (tagged), `Describe*` |
|
||||
| `aws_efs_file_system` | `elasticfilesystem:CreateFileSystem` (`*`, request tag), `TagResource` (at create), `UpdateFileSystem`, `PutLifecycleConfiguration`, `TagResource`/`UntagResource`, `DeleteFileSystem` (tagged), `Describe*`, `ListTagsForResource` |
|
||||
| `aws_efs_mount_target` | `elasticfilesystem:CreateMountTarget`, `ModifyMountTargetSecurityGroups`, `DeleteMountTarget` (the tagged file system; untaggable), and the interface EFS makes with the caller's permissions: `ec2:CreateNetworkInterface`, `ec2:DeleteNetworkInterface` (only with `aws:ViaAWSService`) |
|
||||
| `aws_efs_mount_target` | `elasticfilesystem:CreateMountTarget`, `ModifyMountTargetSecurityGroups`, `DeleteMountTarget` (the tagged file system; untaggable), and the interface EFS makes with the caller's permissions: `ec2:CreateNetworkInterface`, `ec2:DeleteNetworkInterface` (only with `aws:CalledViaLast = elasticfilesystem.amazonaws.com`; in the environment's subnets, which in `existing` mode are the user's own) |
|
||||
| `aws_efs_file_system_policy` | `elasticfilesystem:PutFileSystemPolicy`, `DeleteFileSystemPolicy` (the tagged file system; untaggable), `DescribeFileSystemPolicy` |
|
||||
| `aws_iam_role` | `iam:CreateRole` (with the boundary), `TagRole`, `UntagRole`, `GetRole`, `ListRolePolicies`, `ListAttachedRolePolicies`, `ListInstanceProfilesForRole`, `UpdateRole`, `UpdateRoleDescription`, `DeleteRole` (`triple-c-*`) |
|
||||
| `aws_iam_role_policy` | `iam:PutRolePolicy`, `DeleteRolePolicy` (with the boundary), `GetRolePolicy` (the role; untaggable) |
|
||||
| data sources | `sts:GetCallerIdentity`, `ec2:DescribeAvailabilityZones`, `ec2:DescribeSubnets`, `ssm:GetParameter` |
|
||||
| S3 backend | `s3:ListBucket` (the state bucket), `GetObject`/`PutObject`/`DeleteObject` (`triple-c-tfstate-*/*`, the `.tflock` objects included) |
|
||||
| S3 backend | `s3:ListBucket` (the state bucket), `GetObject`/`PutObject`/`DeleteObject` (`triple-c-tfstate-*/*`, the `.tflock` objects included), in the caller's own account |
|
||||
|
||||
The app also creates the state bucket: `s3:CreateBucket` with a `triple-c:component` request tag,
|
||||
then `PutBucketVersioning`, `PutEncryptionConfiguration`, `PutBucketPublicAccessBlock`,
|
||||
`PutBucketTagging`, `PutBucketPolicy`, `GetBucket*` and `ListBucket`, all on
|
||||
`triple-c-tfstate-*` only. No bucket can be deleted.
|
||||
|
||||
Every state-bucket statement also requires `s3:ResourceAccount = ${aws:PrincipalAccount}`: the
|
||||
bucket name is predictable, so another account could create `triple-c-tfstate-<your account>-…`
|
||||
first and collect (or feed) your state. With the condition, the admin's calls on a bucket owned by
|
||||
any other account are denied, so the app fails rather than writes state there. (The Service
|
||||
Authorization Reference lists `s3:ResourceAccount` for `CreateBucket` as well as the bucket and
|
||||
object actions; Task 8's live run confirms it.)
|
||||
|
||||
It is fenced in the following ways.
|
||||
|
||||
**Roles are bounded.** The admin can create a `triple-c-*` role, or change its policies, only when
|
||||
@@ -163,7 +173,10 @@ administrator.
|
||||
**Destruction is tag-scoped.** VPCs, subnets, internet gateways, route tables, Elastic IPs, NAT
|
||||
gateways, security groups, EFS file systems and KMS keys can be created only with a
|
||||
`triple-c:component` tag (the modules' `default_tags` put it on every create request). They can be
|
||||
changed or deleted only if they carry that tag. Tagging is allowed only as part of the creating
|
||||
changed or deleted only if they carry that tag. Subnets, route tables and NAT gateways can be
|
||||
created only *inside* a Triple-C (tagged) VPC, subnet and address, and an internet gateway
|
||||
attached, a route added or a route table associated only on tagged ones, so the admin cannot
|
||||
re-route a VPC that isn't Triple-C's. Tagging is allowed only as part of the creating
|
||||
call (`ec2:CreateAction`, `elasticfilesystem:CreateAction`), or on a resource that is already
|
||||
Triple-C's. Resources that cannot carry a tag are authorized through one that does: routes and
|
||||
route-table associations through the route table (and subnet), mount targets and the file-system
|
||||
@@ -177,23 +190,24 @@ account-wide:
|
||||
- `ecs:RegisterTaskDefinition` and `ecs:DeregisterTaskDefinition` on `*`. The admin can deregister
|
||||
*any* task definition revision in the account. `DeregisterTaskDefinition` has no resource type
|
||||
(IAM authorizes it against `*`), so it cannot be narrowed.
|
||||
- `ecr:CreatePullThroughCacheRule` on `*`, which also has no resource type. The admin can add
|
||||
pull-through cache rules for any prefix. This cannot delete or overwrite anything.
|
||||
- `elasticfilesystem:CreateFileSystem` on `*` (no resource type), still only with a
|
||||
`triple-c:component` request tag.
|
||||
- Security groups, subnets, route tables and NAT gateways can be created in **any** VPC or subnet,
|
||||
as long as the new resource is tagged: the request tag, not the parent's tag, is what the create
|
||||
statement checks (an `existing`-network environment's VPC is the user's own and untagged).
|
||||
- `ec2:CreateSecurityGroup` in **any** VPC, as long as the new group is tagged: an
|
||||
`existing`-network environment creates its tasks group and each user's `-efs` group in the
|
||||
user's own, untagged VPC. It is the only create that may name an untagged parent.
|
||||
- `ec2:AuthorizeSecurityGroupIngress`/`Egress` on `security-group-rule/*`: authorizing names the
|
||||
new, untagged rule as well as the group, and the group must still be tagged.
|
||||
- `ec2:CreateNetworkInterface` and `ec2:DeleteNetworkInterface` on `*`, but only when an AWS
|
||||
service makes the call for the admin (`aws:ViaAWSService`): EFS creates and deletes a mount
|
||||
target's interface with the caller's EC2 permissions.
|
||||
- `ec2:CreateNetworkInterface` and `ec2:DeleteNetworkInterface` on `*`, but only when EFS makes
|
||||
the call for the admin (`aws:CalledViaLast = elasticfilesystem.amazonaws.com`): EFS creates and
|
||||
deletes a mount target's interface with the caller's EC2 permissions, in any subnet (an
|
||||
`existing` environment's are untagged). **Unverified:** whether EFS sets `aws:CalledVia` for
|
||||
these calls is for Task 8's live run to show; if it does not, this statement needs another
|
||||
condition.
|
||||
- Reads on `*`: `ec2:Describe*`, `elasticfilesystem:Describe*`, the KMS key reads and
|
||||
`ListAliases`, `logs:DescribeLogGroups` and `ssm:DescribeParameters`.
|
||||
- `s3:PutBucketPolicy` on the state bucket: an admin could open their own account's state bucket to
|
||||
others. The state holds no secrets (the modules create none), but it maps the account's
|
||||
Triple-C resources.
|
||||
- `s3:PutBucketPolicy` on the state bucket (only a bucket in the admin's own account, enforced by
|
||||
`s3:ResourceAccount`): an admin could open that bucket to other accounts. The state holds no
|
||||
secrets (the modules create none), but it maps the account's Triple-C resources.
|
||||
|
||||
## Invariants the modules must keep (phase 4)
|
||||
|
||||
@@ -235,8 +249,8 @@ Create each file as a **customer managed policy** and attach it. In the console,
|
||||
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
|
||||
may hold 6,144 characters, not counting whitespace. Without whitespace, `workspace-user.json` is
|
||||
about 6,040 (roughly 100 characters of headroom), `environment-admin.json` about 6,080 (60) and
|
||||
`environment-admin-network.json` about 2,370; a unit test (`every_policy_fits_a_managed_policy`)
|
||||
about 6,040 (roughly 100 characters of headroom), `environment-admin.json` about 5,790 (350)
|
||||
and `environment-admin-network.json` about 3,280; a unit test (`every_policy_fits_a_managed_policy`)
|
||||
fails before any file outgrows the limit. A tier that outgrows its files gains another file, which
|
||||
the app then lists with the others. For IAM Identity Center, add the files as inline policies on the permission set
|
||||
(the "up to 32,768 characters" limit is unverified) or reference the managed policies.
|
||||
@@ -261,7 +275,8 @@ aws iam attach-user-policy --user-name <user> \
|
||||
is derived from the provider's behaviour and the Service Authorization Reference, not yet from a
|
||||
real `tofu apply`/`destroy`. In particular unverified: whether tag-on-create needs
|
||||
`logs:TagResource` / `ssm:AddTagsToResource` / `ecs:TagResource` as listed, whether EFS's
|
||||
interface calls carry `aws:ViaAWSService`, and whether `s3:CreateBucket` honours
|
||||
interface calls carry `aws:CalledViaLast = elasticfilesystem.amazonaws.com`, whether
|
||||
`s3:ResourceAccount` is evaluated for `s3:CreateBucket`, and whether `s3:CreateBucket` honours
|
||||
`aws:RequestTag`. A trust-policy change on a role (`iam:UpdateAssumeRolePolicy`) is never
|
||||
allowed, so a module version that changes a role's trust must replace the role.
|
||||
- **Admin access to user data.** The admin can retag `triple-c-*` roles (`iam:TagRole`). The
|
||||
@@ -338,5 +353,4 @@ Still open for the phases that create the resources: `RunTask`'s tag keys and `i
|
||||
points above. The simulator does not apply session policies, so the first real create is the final
|
||||
check. The phase-4 additions (the network, the state bucket, the module table above, the
|
||||
owner-scoped `CreateAccessPoint`) are verified by Task 8's live apply and destroy; until then they
|
||||
are unit-tested only. (Checked with the simulator: a `Bool` condition matches a context entry sent
|
||||
as a string, which is how the permission check sends `aws:ViaAWSService`.)
|
||||
are unit-tested only.
|
||||
@@ -2,23 +2,62 @@
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "CreateTaggedNetwork",
|
||||
"Sid": "CreateTaggedVpcs",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:CreateVpc"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ec2:*:*:vpc/*"
|
||||
],
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"aws:RequestTag/triple-c:component": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "CreateTaggedSubnets",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:CreateSubnet"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ec2:*:*:subnet/*"
|
||||
],
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"aws:RequestTag/triple-c:component": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "AllocateTaggedAddresses",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:AllocateAddress"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ec2:*:*:elastic-ip/*"
|
||||
],
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"aws:RequestTag/triple-c:component": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "CreateOtherTaggedNetwork",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:CreateVpc",
|
||||
"ec2:CreateSubnet",
|
||||
"ec2:CreateInternetGateway",
|
||||
"ec2:CreateRouteTable",
|
||||
"ec2:AllocateAddress",
|
||||
"ec2:CreateNatGateway",
|
||||
"ec2:CreateSecurityGroup"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ec2:*:*:vpc/*",
|
||||
"arn:aws:ec2:*:*:subnet/*",
|
||||
"arn:aws:ec2:*:*:internet-gateway/*",
|
||||
"arn:aws:ec2:*:*:route-table/*",
|
||||
"arn:aws:ec2:*:*:elastic-ip/*",
|
||||
"arn:aws:ec2:*:*:natgateway/*",
|
||||
"arn:aws:ec2:*:*:security-group/*"
|
||||
],
|
||||
@@ -28,6 +67,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "CreateOnlyInTripleCParents",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:CreateSubnet",
|
||||
"ec2:CreateRouteTable",
|
||||
"ec2:CreateNatGateway"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ec2:*:*:vpc/*",
|
||||
"arn:aws:ec2:*:*:subnet/*",
|
||||
"arn:aws:ec2:*:*:elastic-ip/*"
|
||||
],
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"aws:ResourceTag/triple-c:component": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "CreateSecurityGroupsInAnyVpc",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:CreateSecurityGroup"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ec2:*:*:vpc/*"
|
||||
],
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"aws:RequestTag/triple-c:component": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "TagNetworkAtCreate",
|
||||
"Effect": "Allow",
|
||||
@@ -73,7 +146,6 @@
|
||||
"ec2:ReplaceRoute",
|
||||
"ec2:DeleteRoute",
|
||||
"ec2:AssociateRouteTable",
|
||||
"ec2:ReplaceRouteTableAssociation",
|
||||
"ec2:DisassociateRouteTable",
|
||||
"ec2:ReleaseAddress",
|
||||
"ec2:DeleteNatGateway",
|
||||
@@ -120,8 +192,8 @@
|
||||
"*"
|
||||
],
|
||||
"Condition": {
|
||||
"Bool": {
|
||||
"aws:ViaAWSService": "true"
|
||||
"StringEquals": {
|
||||
"aws:CalledViaLast": "elasticfilesystem.amazonaws.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,10 +50,7 @@
|
||||
],
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"iam:PassedToService": [
|
||||
"ecs-tasks.amazonaws.com",
|
||||
"ec2.amazonaws.com"
|
||||
]
|
||||
"iam:PassedToService": "ecs-tasks.amazonaws.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -136,17 +133,6 @@
|
||||
"arn:aws:ecs:*:*:cluster/triple-c-*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sid": "ManageCapacityProviders",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ecs:CreateCapacityProvider",
|
||||
"ecs:DeleteCapacityProvider"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ecs:*:*:capacity-provider/triple-c-*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sid": "ManageTaskDefinitions",
|
||||
"Effect": "Allow",
|
||||
@@ -278,28 +264,6 @@
|
||||
"arn:aws:logs:*:*:log-group:/triple-c/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sid": "ManageRepositories",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ecr:CreateRepository",
|
||||
"ecr:DeleteRepository"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:ecr:*:*:repository/triple-c-*",
|
||||
"arn:aws:ecr:*:*:repository/triple-c/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sid": "CreatePullThroughCacheRules",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ecr:CreatePullThroughCacheRule"
|
||||
],
|
||||
"Resource": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sid": "ManageEnvironmentParameters",
|
||||
"Effect": "Allow",
|
||||
@@ -346,6 +310,9 @@
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"aws:RequestTag/triple-c:component": "false"
|
||||
},
|
||||
"StringEquals": {
|
||||
"s3:ResourceAccount": "${aws:PrincipalAccount}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -363,7 +330,12 @@
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::triple-c-tfstate-*"
|
||||
]
|
||||
],
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:ResourceAccount": "${aws:PrincipalAccount}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "ReadAndWriteState",
|
||||
@@ -375,7 +347,12 @@
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::triple-c-tfstate-*/*"
|
||||
]
|
||||
],
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:ResourceAccount": "${aws:PrincipalAccount}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user