Skip to content

fix(identity): make team guardrails the permission set boundary and scope team access - #188

Merged
Alexanderamiri merged 3 commits into
mainfrom
fix/team-permission-scope
Oct 31, 2026
Merged

Alexanderamiri merged 3 commits into
mainfrom
fix/team-permission-scope

Conversation

@Alexanderamiri

@Alexanderamiri Alexanderamiri commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Warning

Security-critical, changes live human access. Please review carefully. After merge and the platform apply, the next team-provisioner run (registry CI) re-provisions every javabin-team-{team} permission set with the new boundary and policies.

Problem

Finding 18 said the per-team permission set's inline policy (Action *, Resource *, StringEqualsIfExists aws:ResourceTag/team) allows untagged resources. Live AWS shows it is worse than that. javabin-team-{team}-guardrails is attached to the permission set as a customer-managed policy (AWSReservedSSO_javabin-team-javazoneweb_* has it in AttachedPolicies and has no PermissionsBoundary). That policy is the developer boundary rendered with the team name, and its first statement is Allow * *. As an identity policy, that statement grants every action the guardrail Denies don't cover. The inline tag scoping never limited anything.

What this meant in practice: a member of any team could overwrite the platform and other teams' Terraform state, read the Vaultwarden data bucket, delete or modify the untagged dbmoresleep RDS instance and the legacy EC2/Elastic Beanstalk instances, change records in the javazone.no hosted zone, attach policies to untagged IAM roles, update untagged Lambda code, delete platform SSM parameters (override-signing-key), and tag untagged resources team=<own> to take them over. See rows 1-42 below.

Change

team_provisioner/handler.py (_ensure_team_permission_set and two new helpers; nothing else in the handler):

  • Boundary, not policy. put_permissions_boundary_to_permission_set sets javabin-team-{team}-guardrails as the boundary. Only after that succeeds, the old customer-managed references are detached (javabin-team-{team}-guardrails and the legacy javabin-developer-boundary). If the boundary call fails, the permission set is left as it was and the result reports an error.
  • ViewOnlyAccess (AWS managed, the same policy javabin-readonly uses) is attached for read-only console browsing: List/Describe only, no object, parameter or secret reads. A failure is reported as view_only and does not block the rest.
  • New inline policy (_team_inline_policy):
    • AllowTeamTaggedResources: * on resources tagged with the team, using StringEquals (no IfExists). Requests without a resource tag (untagged resources, S3 objects, Route53, creates) no longer match.
    • Name-scoped allows for team resources that can't be matched by tag:
      • /javabin/apps/{team}/* SSM: get and overwrite (PutParameter has no aws:ResourceTag)
      • /ecs/{team}/* log reads and live tail
      • {team}-*-{account} buckets (the app.yaml bucket naming, so never a legacy bucket like javazone-backup)
      • ecs:ExecuteCommand on the javabin-platform cluster. Tasks carry the team tag through propagateTags=SERVICE, so the task side is covered by the tag allow.
    • AllowUntaggedReads, so javabin status and javabin secret don't depend on the managed attachment.
    • The two existing SSM Denies are unchanged.
  • terraform/platform/lambdas/main.tf changes the team-provisioner role:
    • Adds sso:AttachManagedPolicyToPermissionSet and sso:PutPermissionsBoundaryToPermissionSet.
    • Adds iam:PutRolePermissionsBoundary on aws-reserved/sso.amazonaws.com/* roles, conditioned on the boundary being a javabin-team-*-guardrails policy. CloudTrail shows that in this management account, SSO provisioning makes its IAM calls with the provisioner's own credentials.
    • Lets the role read ViewOnlyAccess.
    • Drops sso:AttachCustomerManagedPolicyReferenceToPermissionSet, which is no longer used.
  • Docs: docs/lambda-functions.md has a new "Team permission set" section. Also updated: the GuardDuty note in docs/platform-modules.md, CLAUDE.md, and the iam/boundary.tf comment ("never attach it as a policy").

Verification

Simulation. aws iam simulate-custom-policy for team javazoneweb, using:

  • the live guardrails (javabin-team-javazoneweb-guardrails v3, from IAM)
  • the live inline policy (sso-admin get-inline-policy-for-permission-set)
  • the live ViewOnlyAccess
  • the new inline policy rendered from this branch's _team_inline_policy("javazoneweb")

The resources are real and their team tags were read from AWS. aws:ResourceTag/team is only put in the context for actions whose service evaluates it; S3 objects and Route53 never get it.

  • Before = the live setup: guardrails + inline as identity policies.
  • After = new inline + ViewOnlyAccess as identity policies, guardrails as --permissions-boundary-policy-input-list.
  • After w/o ViewOnly = the case where the managed attach failed.

DENY is an explicit deny; deny is an implicit deny.

# Case Action Before After After w/o ViewOnly
1 state bucket: read platform state s3:GetObject allow deny deny
2 state bucket: overwrite platform state s3:PutObject allow deny deny
3 state bucket: overwrite other team's app state s3:PutObject allow deny deny
4 plan-artifacts bucket: write s3:PutObject allow deny deny
5 vaultwarden data bucket (untagged): read s3:GetObject allow deny deny
6 legacy bucket javazone-backup: delete object s3:DeleteObject allow deny deny
7 legacy website bucket javazone.no: put bucket policy s3:PutBucketPolicy allow deny deny
8 TF lock table: delete lock item dynamodb:DeleteItem allow deny deny
9 legacy DynamoDB datomic table (untagged): scan dynamodb:Scan allow deny deny
10 SSM other team secret (team=cupcake): read ssm:GetParameter DENY DENY DENY
11 SSM other team secret: overwrite ssm:PutParameter DENY DENY DENY
12 SSM platform signing key (untagged): read ssm:GetParameter DENY DENY DENY
13 SSM platform signing key (untagged): delete ssm:DeleteParameter allow deny deny
14 SSM /infrastructure/alb/listener-arn (untagged): delete ssm:DeleteParameter allow deny deny
15 Secrets Manager untagged secret: read secretsmanager:GetSecretValue allow deny deny
16 KMS untagged CMK (datomic): schedule deletion kms:ScheduleKeyDeletion allow deny deny
17 ECR other team repo (cupcake): push ecr:PutImage DENY DENY DENY
18 ECR CI image repo (platform): push ecr:PutImage DENY DENY DENY
19 ECR CDK repo (untagged): delete images ecr:BatchDeleteImage allow deny deny
20 RDS dbmoresleep (untagged): delete rds:DeleteDBInstance allow deny deny
21 RDS dbmoresleep (untagged): reset master password rds:ModifyDBInstance allow deny deny
22 platform ALB listener (shared): modify elasticloadbalancing:ModifyListener DENY DENY DENY
23 platform ALB listener (shared): add rule elasticloadbalancing:CreateRule allow deny deny
24 legacy Default-ALB (untagged): delete elasticloadbalancing:DeleteLoadBalancer allow deny deny
25 legacy EC2 instance (untagged, cakeredux prod): terminate ec2:TerminateInstances allow deny deny
26 legacy EC2 instance (untagged): claim by tagging team=javazoneweb ec2:CreateTags allow deny deny
27 vaultwarden ECS service (untagged): scale/enable exec ecs:UpdateService allow deny deny
28 vaultwarden task (untagged): exec ecs:ExecuteCommand allow deny deny
29 other team ECS service (cupcake): update ecs:UpdateService DENY DENY DENY
30 other team task (cupcake): exec ecs:ExecuteCommand DENY DENY DENY
31 IAM role javabin-ci-infra (platform): put policy iam:PutRolePolicy DENY DENY DENY
32 IAM role javabinAktiv (untagged): attach AdministratorAccess iam:AttachRolePolicy allow deny deny
33 IAM role javabinAktiv (untagged): rewrite trust policy iam:UpdateAssumeRolePolicy allow deny deny
34 IAM create user iam:CreateUser DENY DENY DENY
35 Lambda javabin-team-provisioner (platform): update code lambda:UpdateFunctionCode DENY DENY DENY
36 Lambda javabin-team-provisioner (platform): invoke lambda:InvokeFunction DENY DENY DENY
37 Lambda AutomaticBlogPoster (untagged): update code lambda:UpdateFunctionCode allow deny deny
38 CloudTrail javabin-trail: stop logging cloudtrail:StopLogging DENY DENY DENY
39 Route53 javazone.no zone: change records route53:ChangeResourceRecordSets allow deny deny
40 Route53: create hosted zone route53:CreateHostedZone allow deny deny
41 SQS legacy queue javazone-talk-events (untagged): purge sqs:PurgeQueue allow deny deny
42 EC2: launch instance (no resource tag in request) ec2:RunInstances allow deny deny
43 own SSM param: read (javabin secret list) ssm:GetParameter allow allow allow
44 own SSM param: overwrite (javabin secret set) ssm:PutParameter allow allow allow
45 own SSM path: get by path ssm:GetParametersByPath allow allow allow
46 own SSM param: create new (no overwrite) ssm:PutParameter DENY DENY DENY
47 own SSM cognito-client-id: overwrite ssm:PutParameter DENY DENY DENY
48 SSM describe parameters ssm:DescribeParameters allow allow allow
49 own logs: filter (aws logs tail) logs:FilterLogEvents allow allow allow
50 own logs: get events logs:GetLogEvents allow allow allow
51 own logs: live tail logs:StartLiveTail allow allow allow
52 own logs: Insights start query logs:StartQuery allow allow allow
53 logs: describe log groups logs:DescribeLogGroups allow allow allow
54 other team logs (cupcake): filter logs:FilterLogEvents DENY DENY DENY
55 ECS list services (javabin status) ecs:ListServices allow allow allow
56 ECS describe own service (javabin status) ecs:DescribeServices allow allow allow
57 ECS describe other team's service (guardrails DenyXTeam, unchanged) ecs:DescribeServices DENY DENY DENY
58 Logs Insights get results (guardrails DenyXLogs, unchanged) logs:GetQueryResults DENY DENY DENY
59 own ECS service: force new deployment ecs:UpdateService allow allow allow
60 own task: exec (task half) ecs:ExecuteCommand allow allow allow
61 own task: exec (cluster half, cluster team=shared) ecs:ExecuteCommand allow allow allow
62 own task: stop ecs:StopTask allow allow allow
63 own task definition: describe ecs:DescribeTaskDefinition allow allow deny
64 ECR auth token ecr:GetAuthorizationToken allow allow allow
65 own ECR repo: push ecr:PutImage allow allow allow
66 own ECR repo: pull ecr:BatchGetImage allow allow allow
67 own ECR repo: list images ecr:DescribeImages allow allow allow
68 own bucket {team}-data-{acct}: read object s3:GetObject allow allow allow
69 own bucket: list s3:ListBucket allow allow allow
70 Cost Explorer (javabin status) ce:GetCostAndUsage allow allow allow
71 console: EC2 describe instances ec2:DescribeInstances allow allow deny
72 console: RDS describe rds:DescribeDBInstances allow allow deny
73 console: Lambda list functions lambda:ListFunctions allow allow deny
74 console: CloudWatch metric data cloudwatch:GetMetricData allow allow allow
75 console: ALB rules elasticloadbalancing:DescribeRules allow allow allow
76 console: list buckets s3:ListAllMyBuckets allow allow deny
77 console: Route53 list record sets route53:ListResourceRecordSets allow allow deny

Rows 57-58 are unchanged pre-existing guardrail denies, outside this PR:

  • Row 57: DenyXTeam denies ecs:DescribeServices on other teams' services. javabin status describes every service in the cluster in one call, so it probably fails for team SSO sessions today. The CLI should describe only {team}-* services.
  • Row 58: DenyXLogs denies logs:GetQueryResults, which has no resource, so Logs Insights results can't be read.

Also unchanged: javabin status --resources uses the Athena workgroup javabin-cost-analytics, which is tagged team=platform, so it was already denied for team sessions.

Tests:

  • python3 -m unittest discover -s terraform/lambda-src/tests: 222 OK. New and updated tests cover the boundary call, the detaches, ViewOnly, the boundary-failure path, the ViewOnly-failure path, and the inline policy scoping.
  • python3 -m unittest discover -s scripts/tests: 133 OK.
  • terraform fmt -check -recursive and terraform validate in terraform/platform: OK.

Not verified live: the simulator does not know which actions populate aws:ResourceTag, so I set the context from the Service Authorization Reference. I had read-only access and could not assume a team role.

Reviewer checklist after deploy:

  • After the registry CI runs team-provisioner, aws iam get-role --role-name AWSReservedSSO_javabin-team-<team>_* shows PermissionsBoundary = javabin-team-<team>-guardrails, and list-attached-role-policies shows only ViewOnlyAccess.
  • A team member checks javabin secret list / set, aws logs tail /ecs/<team>/<app>, an ECR push, and aws ecs update-service --force-new-deployment, and confirms an s3 cp from the state bucket fails.
  • javabin-team-platform has no guardrails policy (pending). It now gets read-only access: list-attached-role-policies on AWSReservedSSO_javabin-team-platform_* shows only ViewOnlyAccess and no javabin-developer-boundary. Its two members are also in drift (javabin-admin), so they keep admin access through that permission set. See the PR comment for the simulation.

🤖 Generated with Claude Code

…cope team access

The javabin-team-{team} permission sets attached javabin-team-{team}-guardrails
as a customer-managed policy. Its baseline "Allow *" therefore granted every
action the guardrail Denies miss, so team members could write the Terraform
state bucket, legacy buckets, untagged RDS/EC2/Lambda/IAM roles and Route53
zones. The inline StringEqualsIfExists allow added the same for untagged
resources.

Set the guardrails as the permissions boundary instead, detach the old
policy references, attach ViewOnlyAccess for console browsing, and replace
the inline policy with StringEquals on the team tag plus name-scoped allows
for the team's parameters, log groups, buckets and the ECS Exec cluster half.
Grant team-provisioner the SSO and IAM calls this needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 31, 2026 12:34
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 0 to add, 2 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.lambdas.aws_iam_role_policy.team_provisioner will be updated in-place
  ~ resource "aws_iam_role_policy" "team_provisioner" {
        id     = "javabin-team-provisioner:javabin-team-provisioner"
        name   = "javabin-team-provisioner"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (4 unchanged elements hidden)
                    {
                        Action   = [
                            "identitystore:CreateGroup",
                            "identitystore:DescribeGroup",
                            "identitystore:DescribeUser",
                            "identitystore:ListGroups",
                            "identitystore:CreateGroupMembership",
                            "identitystore:ListGroupMemberships",
                            "identitystore:DeleteGroupMembership",
                            "identitystore:ListUsers",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "IdentityStoreSync"
                    },
                  ~ {
                      ~ Action   = [
                            # (5 unchanged elements hidden)
                            "sso:PutInlinePolicyToPermissionSet",
                          - "sso:AttachCustomerManagedPolicyReferenceToPermissionSet",
                            "sso:DetachCustomerManagedPolicyReferenceFromPermissionSet",
                          + "sso:AttachManagedPolicyToPermissionSet",
                          + "sso:PutPermissionsBoundaryToPermissionSet",
                            "sso:ProvisionPermissionSet",
                            # (1 unchanged element hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails" -> [
                          + "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                          + "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "iam:GetSAMLProvider",
                            "iam:GetRole",
                            "iam:CreateRole",
                            "iam:AttachRolePolicy",
                            "iam:PutRolePolicy",
                            "iam:UpdateRole",
                            "iam:UpdateRoleDescription",
                            "iam:ListAttachedRolePolicies",
                            "iam:ListRolePolicies",
                            "iam:DeleteRole",
                            "iam:DeleteRolePolicy",
                            "iam:DetachRolePolicy",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*",
                            "arn:aws:iam::553637109631:saml-provider/AWSSSO_*",
                        ]
                        Sid      = "IAMForSSOAssignment"
                    },
                  + {
                      + Action    = "iam:PutRolePermissionsBoundary"
                      + Condition = {
                          + ArnLike = {
                              + "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*"
                      + Sid       = "SetTeamGuardrailsBoundary"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_lambda_function.team_provisioner will be updated in-place
  ~ resource "aws_lambda_function" "team_provisioner" {
        id                             = "javabin-team-provisioner"
      ~ last_modified                  = "2026-10-26T14:09:21.000+0000" -> (known after apply)
      ~ source_code_hash               = "i+0n0A3gn0SSOJWZx1C2Aq4/eSSczvrK8hXWYOqCpqs=" -> "1Awumzex28IbJ1Ec+4wTdhcHCAgfKfWsfpXYHD9py/Q="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

In-place update of the team-provisioner Lambda's IAM role policy and function code: 0 to add, 2 to change, 0 to destroy.

  • 🔒 [security] New statement SetTeamGuardrailsBoundary grants iam:PutRolePermissionsBoundary (Effect = Allow) on sso.amazonaws.com reserved roles, constrained by a condition that the boundary must match arn:aws:iam::553637109631:policy/javabin-team-*-guardrails. This lets the provisioner change the permissions boundary on SSO-created roles, which is a privilege-escalation-relevant capability. The condition limits which boundary can be set but not which roles' boundaries are changed. Verify this is intended and consider restricting the resource or adding a condition on the boundary being set.
  • 🔒 [security] Existing SSO statement now allows sso:AttachManagedPolicyToPermissionSet and sso:PutPermissionsBoundaryToPermissionSet, and drops sso:AttachCustomerManagedPolicyReferenceToPermissionSet. The new permissions-boundary-on-permission-set action lets the provisioner alter boundaries on permission sets. Confirm this is needed.
  • 🔒 [security] Resource list for the guardrails statement adds arn:aws:iam::aws:policy/job-function/ViewOnlyAccess. This is a read-only AWS managed policy, so the risk is low, but it widens the set of policies that can be attached to permission sets.
  • ✅ [routine] Lambda function javabin-team-provisioner: source_code_hash changes, so the deployment package is being replaced in-place. No infrastructure is created or destroyed; last_modified is computed.
  • 💥 [destruction] No resources are created or destroyed (0 to add, 0 to destroy). Both changes are in-place updates.

… access

javabin-team-platform has no guardrails policy, so the provisioner left it
as it was: javabin-developer-boundary attached as a policy, whose Allow *
granted everything its Denies miss. A team whose guardrails are pending or
refused now gets ViewOnlyAccess plus a read-only inline policy, with the
legacy attached policies removed. The happy-path test also checks that the
boundary is set before the old policies are detached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri

Copy link
Copy Markdown
Member Author

Follow-up to the review, commit 132f326.

Finding 1 (javabin-team-platform keeps javabin-developer-boundary attached): accepted and fixed. Live state confirms it. The permission set has javabin-developer-boundary as a customer-managed policy, no boundary, and the old inline Allow * with StringEqualsIfExists team=platform. The fix is in this PR rather than a follow-up, because leaving it would keep the Finding 18 exposure open for that group.

  • If the guardrails state is not ok (pending or refused), _ensure_team_permission_set no longer returns early. It sets no boundary, detaches javabin-team-{team}-guardrails and javabin-developer-boundary, attaches ViewOnlyAccess, and puts _read_only_inline_policy() (only the AllowUntaggedReads statement, now shared as _UNTAGGED_READS). Then it assigns and provisions as usual and reports guardrails: <state>.
  • The team inline policy can't be used without a boundary: its tag allow would cover every team=platform resource (CI roles, the team-provisioner Lambda, CloudTrail).
  • Side effect: a brand-new team now gets read-only access until platform CI creates its guardrails, where before it got nothing. The next provisioner run switches it to full access.
  • Impact: the platform group (alexander.amiri, stian.nygaard) are both also in drift, which has javabin-admin, so neither loses access.
  • No new provisioner IAM permissions are needed. Every SSO and IAM call involved was already allowed.

Simulation for javabin-team-platform (simulate-custom-policy). Before = live javabin-developer-boundary + live inline policy. After = live ViewOnlyAccess + _read_only_inline_policy():

# Case Action Before After
1 state bucket: read platform state s3:GetObject allow deny
2 state bucket: overwrite platform state s3:PutObject allow deny
3 vaultwarden data bucket (untagged): read s3:GetObject allow deny
4 SSM platform signing key (untagged): delete ssm:DeleteParameter allow deny
5 SSM platform signing key (untagged): read ssm:GetParameter DENY deny
6 SSM other team secret (cupcake): overwrite ssm:PutParameter DENY deny
7 IAM role javabin-ci-infra (platform): put policy iam:PutRolePolicy DENY deny
8 IAM role javabinAktiv (untagged): attach policy iam:AttachRolePolicy allow deny
9 Lambda javabin-team-provisioner (platform): update code lambda:UpdateFunctionCode DENY deny
10 Lambda AutomaticBlogPoster (untagged): update code lambda:UpdateFunctionCode allow deny
11 RDS dbmoresleep (untagged): delete rds:DeleteDBInstance allow deny
12 Route53: create hosted zone route53:CreateHostedZone allow deny
13 EC2: launch instance ec2:RunInstances allow deny
14 CloudTrail javabin-trail (platform): stop logging cloudtrail:StopLogging DENY deny
15 ECS list services (javabin status) ecs:ListServices allow allow
16 ECS describe services (any team) ecs:DescribeServices DENY allow
17 logs: describe log groups logs:DescribeLogGroups allow allow
18 console: EC2 describe instances ec2:DescribeInstances allow allow
19 Cost Explorer (javabin status) ce:GetCostAndUsage allow allow

Row 16 is metadata only, the same access javabin-readonly already has. Before, the developer boundary's PrincipalTag Denies blocked it because SSO sessions have no team tag.

Finding 2 (happy-path test doesn't check order): accepted and fixed. test_team_guardrails_are_the_boundary_not_an_attached_policy now asserts through sso.mock_calls that put_permissions_boundary_to_permission_set runs before the first detach, and the detach runs before provision_permission_set. test_missing_guardrails_leave_the_permission_set_untouched is replaced by test_missing_guardrails_give_read_only_access, which covers both pending and refused.

No findings rejected.

Docs updated: docs/lambda-functions.md (Team permission set, order paragraph) and CLAUDE.md. The PR body checklist item for javabin-team-platform is updated too.

Tests: python3 -m unittest discover -s terraform/lambda-src/tests 222 OK; python3 -m unittest discover -s scripts/tests 133 OK. No Terraform changed in this commit.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 0 to add, 2 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.lambdas.aws_iam_role_policy.team_provisioner will be updated in-place
  ~ resource "aws_iam_role_policy" "team_provisioner" {
        id     = "javabin-team-provisioner:javabin-team-provisioner"
        name   = "javabin-team-provisioner"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (4 unchanged elements hidden)
                    {
                        Action   = [
                            "identitystore:CreateGroup",
                            "identitystore:DescribeGroup",
                            "identitystore:DescribeUser",
                            "identitystore:ListGroups",
                            "identitystore:CreateGroupMembership",
                            "identitystore:ListGroupMemberships",
                            "identitystore:DeleteGroupMembership",
                            "identitystore:ListUsers",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "IdentityStoreSync"
                    },
                  ~ {
                      ~ Action   = [
                            # (5 unchanged elements hidden)
                            "sso:PutInlinePolicyToPermissionSet",
                          - "sso:AttachCustomerManagedPolicyReferenceToPermissionSet",
                            "sso:DetachCustomerManagedPolicyReferenceFromPermissionSet",
                          + "sso:AttachManagedPolicyToPermissionSet",
                          + "sso:PutPermissionsBoundaryToPermissionSet",
                            "sso:ProvisionPermissionSet",
                            # (1 unchanged element hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails" -> [
                          + "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                          + "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "iam:GetSAMLProvider",
                            "iam:GetRole",
                            "iam:CreateRole",
                            "iam:AttachRolePolicy",
                            "iam:PutRolePolicy",
                            "iam:UpdateRole",
                            "iam:UpdateRoleDescription",
                            "iam:ListAttachedRolePolicies",
                            "iam:ListRolePolicies",
                            "iam:DeleteRole",
                            "iam:DeleteRolePolicy",
                            "iam:DetachRolePolicy",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*",
                            "arn:aws:iam::553637109631:saml-provider/AWSSSO_*",
                        ]
                        Sid      = "IAMForSSOAssignment"
                    },
                  + {
                      + Action    = "iam:PutRolePermissionsBoundary"
                      + Condition = {
                          + ArnLike = {
                              + "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*"
                      + Sid       = "SetTeamGuardrailsBoundary"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_lambda_function.team_provisioner will be updated in-place
  ~ resource "aws_lambda_function" "team_provisioner" {
        id                             = "javabin-team-provisioner"
      ~ last_modified                  = "2026-10-26T14:09:21.000+0000" -> (known after apply)
      ~ source_code_hash               = "i+0n0A3gn0SSOJWZx1C2Aq4/eSSczvrK8hXWYOqCpqs=" -> "PtdS86AE3A0vp1F1OmKBKInGGYr0MkDJpK9OuMX/fWI="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

Plan updates two resources in place (0 add, 0 destroy): the team-provisioner Lambda's IAM role policy is modified to change SSO permission-set and guardrail-boundary permissions, and the Lambda code is redeployed.

  • 🔒 [security] New statement SetTeamGuardrailsBoundary (Effect Allow) grants iam:PutRolePermissionsBoundary on AWSReservedSSO roles, conditioned on iam:PermissionsBoundary matching arn:aws:iam::553637109631:policy/javabin-team-*-guardrails. This lets the provisioner change the boundary on SSO roles, which is a privilege-escalation-relevant permission. The condition limits which boundary can be set, but it does not restrict which boundary can be removed or replaced on a role. Confirm this is intended and that the wildcard scope is tight.
  • 🔒 [security] Resource on the guardrails statement expands from only javabin-team-*-guardrails to also include arn:aws:iam::aws:policy/job-function/ViewOnlyAccess (AWS managed policy). This widens the set of policies the provisioner can attach or reference. Read-only in itself, but it is a scope change worth confirming.
  • 🔒 [security] SSO permission-set actions change: sso:AttachCustomerManagedPolicyReferenceToPermissionSet is removed, while sso:AttachManagedPolicyToPermissionSet and sso:PutPermissionsBoundaryToPermissionSet are added. Net effect is broader capability to attach AWS managed policies and set permission-set boundaries. The removed action is narrower. Review against least privilege.
  • ✅ [routine] Lambda aws_lambda_function.team_provisioner updated in place with a new source_code_hash only; no config, runtime, or role changes shown. Routine code deployment.
  • 💰 [cost] No new billable resources are created. No NAT gateways, load balancers, or databases are involved.
  • 💥 [destruction] No resources are destroyed or replaced. Plan is 0 add, 2 change, 0 destroy.

Resolve conflicts with #180 (team access sync) and the ECS exec change:
keep the boundary-based team permission set and scoped inline policy,
keep main's SCIM/access-group docs and sync_team_access, and describe
sync_team_access as switching pending teams from read-only to full access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri

Copy link
Copy Markdown
Member Author

Merged origin/main (#175, #178, #180, #184, #185, #186, #187) in da3fffe. Conflicts and how I resolved them:

  • team_provisioner/handler.py, _team_inline_policy: main (fix(ecs-service): make /tmp writable for non-root apps, add grace period and ECS Exec #184) added AllowExecOnPlatformCluster and kept the old AllowUntaggedReads inline. This branch already has both: the same exec statement, plus AllowUntaggedReads moved into the shared _UNTAGGED_READS. I kept this branch's version, so neither statement is duplicated. Main's _ensure_team_permission_set from fix(team-provisioner): grant new teams AWS access, report missing IC users #180 (early return when guardrails are pending, guardrails attached as a policy) merged into this branch's version without a conflict: boundary when ok, read-only otherwise. _guardrails_state, _create_account_assignment (the already_existed lookup) and sync_team_access come from main and are unchanged.
  • tests/test_team_provisioner_memberships.py: kept this branch's policy assertions and the 32 KB size check. I also kept main's exec checks (action, no condition), pointed at AllowTeamTaggedResources. Main's StringEqualsIfExists assertion is dropped because this PR removes that condition on purpose.
  • CLAUDE.md: kept main's new "Google SCIM provisions only direct members" bullet and this branch's "SSO sessions carry no team tag" bullet (boundary, read-only for pending teams). I also reworded the sync-team-access.sh row: it now switches teams from read-only to full access.
  • docs/lambda-functions.md: kept main's GitHub team, Team AWS access and member-access sections, and this branch's Team permission set section. I rewrote main's Team AWS access text to match this branch. Pending teams now get read-only access instead of nothing, and sync_team_access sets the guardrails as the boundary instead of attaching them.

Checks: lambda-src 252 tests OK, scripts 184 OK, terraform fmt -check -recursive OK, terraform validate (platform) OK.

Behaviour change reviewers haven't seen: with #180, a pending team got no account assignment until sync_team_access ran after platform CI. That run then saw already_existed: False and posted "got access" to #javabin-infra-alerts. With this PR, the registry sync already creates the read-only assignment, so when sync_team_access later upgrades the team to full access, the assignment already exists. That upgrade is not posted to Slack. Access itself is correct; only the notification is lost. Fixing it would mean checking the existing boundary first, which needs sso:GetPermissionsBoundaryForPermissionSet. I left that out of this merge.

@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 3 to add, 11 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.iam.aws_iam_role.ci_infra will be updated in-place
  ~ resource "aws_iam_role" "ci_infra" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub"              = [
                                    # (1 unchanged element hidden)
                                    "repo:javaBin/platform:environment:production",
                                  + "repo:javaBin@95047/platform@1175354559:ref:refs/heads/main",
                                  + "repo:javaBin@95047/platform@1175354559:environment:production",
                                ]
                                # (1 unchanged attribute hidden)
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-infra"
        name                  = "javabin-ci-infra"
        tags                  = {
            "Name" = "javabin-ci-infra"
        }
        # (9 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_infra_plan will be updated in-place
  ~ resource "aws_iam_role" "ci_infra_plan" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub" = "repo:javaBin/platform:*" -> [
                                  + "repo:javaBin/platform:*",
                                  + "repo:javaBin@95047/platform@1175354559:*",
                                ]
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-infra-plan"
        name                  = "javabin-ci-infra-plan"
        tags                  = {
            "Name" = "javabin-ci-infra-plan"
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_registry will be updated in-place
  ~ resource "aws_iam_role" "ci_registry" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub" = "repo:javaBin/registry:ref:refs/heads/main" -> [
                                  + "repo:javaBin/registry:ref:refs/heads/main",
                                  + "repo:javaBin@95047/registry@1175355770:ref:refs/heads/main",
                                ]
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-registry"
        name                  = "javabin-ci-registry"
        tags                  = {
            "Name" = "javabin-ci-registry"
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["cupcake"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-cupcake:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["javazoneweb"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-javazoneweb:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["jzwatch"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-jzwatch:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-testteam:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.team_provisioner_access_schedule will be created
  + resource "aws_cloudwatch_event_rule" "team_provisioner_access_schedule" {
      + arn                 = (known after apply)
      + description         = "Retry Identity Center group memberships for members Google SCIM has since provisioned"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-team-provisioner-access-schedule"
      + name_prefix         = (known after apply)
      + schedule_expression = "rate(3 hours)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.team_provisioner_access will be created
  + resource "aws_cloudwatch_event_target" "team_provisioner_access" {
      + arn            = "arn:aws:lambda:eu-central-1:553637109631:function:javabin-team-provisioner"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "sync_access_members"
            }
        )
      + rule           = "javabin-team-provisioner-access-schedule"
      + target_id      = "invoke-team-provisioner-access"
    }

  # module.lambdas.aws_iam_role_policy.budget_enforcer will be updated in-place
  ~ resource "aws_iam_role_policy" "budget_enforcer" {
        id     = "javabin-budget-enforcer:javabin-budget-enforcer"
        name   = "javabin-budget-enforcer"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (2 unchanged elements hidden)
                    {
                        Action   = "ecs:ListServices"
                        Effect   = "Allow"
                        Resource = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                        Sid      = "ECSListServicesUnconstrained"
                    },
                  ~ {
                      ~ Action   = "ecs:UpdateService" -> [
                          + "ecs:UpdateService",
                          + "ecs:TagResource",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_iam_role_policy.team_provisioner will be updated in-place
  ~ resource "aws_iam_role_policy" "team_provisioner" {
        id     = "javabin-team-provisioner:javabin-team-provisioner"
        name   = "javabin-team-provisioner"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (4 unchanged elements hidden)
                    {
                        Action   = [
                            "identitystore:CreateGroup",
                            "identitystore:DescribeGroup",
                            "identitystore:DescribeUser",
                            "identitystore:ListGroups",
                            "identitystore:CreateGroupMembership",
                            "identitystore:ListGroupMemberships",
                            "identitystore:DeleteGroupMembership",
                            "identitystore:ListUsers",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "IdentityStoreSync"
                    },
                  ~ {
                      ~ Action   = [
                            # (1 unchanged element hidden)
                            "sso:DescribeAccountAssignmentCreationStatus",
                          + "sso:ListAccountAssignments",
                            "sso:ListPermissionSets",
                            # (2 unchanged elements hidden)
                            "sso:PutInlinePolicyToPermissionSet",
                          - "sso:AttachCustomerManagedPolicyReferenceToPermissionSet",
                            "sso:DetachCustomerManagedPolicyReferenceFromPermissionSet",
                          + "sso:AttachManagedPolicyToPermissionSet",
                          + "sso:PutPermissionsBoundaryToPermissionSet",
                            "sso:ProvisionPermissionSet",
                            # (1 unchanged element hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails" -> [
                          + "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                          + "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "iam:GetSAMLProvider",
                            "iam:GetRole",
                            "iam:CreateRole",
                            "iam:AttachRolePolicy",
                            "iam:PutRolePolicy",
                            "iam:UpdateRole",
                            "iam:UpdateRoleDescription",
                            "iam:ListAttachedRolePolicies",
                            "iam:ListRolePolicies",
                            "iam:DeleteRole",
                            "iam:DeleteRolePolicy",
                            "iam:DetachRolePolicy",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*",
                            "arn:aws:iam::553637109631:saml-provider/AWSSSO_*",
                        ]
                        Sid      = "IAMForSSOAssignment"
                    },
                  + {
                      + Action    = "iam:PutRolePermissionsBoundary"
                      + Condition = {
                          + ArnLike = {
                              + "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*"
                      + Sid       = "SetTeamGuardrailsBoundary"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_lambda_function.budget_enforcer will be updated in-place
  ~ resource "aws_lambda_function" "budget_enforcer" {
        id                             = "javabin-budget-enforcer"
      ~ last_modified                  = "2026-03-26T20:01:54.976+0000" -> (known after apply)
      ~ source_code_hash               = "i3E4tI5mT0qCS601izSv52QOgwAndKfcZ3JkLzxTNis=" -> "lM6wzIndpyMp1ZmU5nft0SteLorI7ppyEyOYIL/2hjU="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.team_provisioner will be updated in-place
  ~ resource "aws_lambda_function" "team_provisioner" {
        id                             = "javabin-team-provisioner"
      ~ last_modified                  = "2026-10-31T13:03:32.000+0000" -> (known after apply)
      ~ source_code_hash               = "3I4td1T2ENiRLUUEjeBLPinRaiXc/sZrwOT+Ai83lOY=" -> "tpW4OZM/7/EBE9sKi6c6DAazYfsyHZV2rG/JJUYTTng="
        tags                           = {}
        # (21 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "AWS_ACCESS_GROUP"             = "javabin-developer@java.no"
                # (19 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.team_provisioner_access_schedule will be created
  + resource "aws_lambda_permission" "team_provisioner_access_schedule" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-team-provisioner"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridgeAccessSchedule"
      + statement_id_prefix = (known after apply)
    }

Plan: 3 to add, 11 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🔴 HIGH

Plan adds 3 resources and updates 11 in-place (0 destroyed): it widens GitHub OIDC trust for CI roles to a new foreign repo path (repo:javaBin@95047/...), extends ECS and Identity Center/IAM permissions including a permissions-boundary-setting grant, and updates two Lambdas.

  • 🔒 [security] GitHub OIDC trust policies on the ci_infra, ci_infra_plan and ci_registry roles gained new subject claims 'repo:javaBin@95047/platform@1175354559:' and 'repo:javaBin@95047/registry@1175355770:'. These use the GitHub repository-ID-suffixed format, which matches a repository that was renamed/recreated or is a different repo entirely. Verify that the repository IDs belong to javaBin's own repos before apply, since this lets whatever workflows run in those repos assume production CI roles. The ci_infra role also gains 'environment:production' and 'ref:refs/heads/main' for the new repo IDs; ci_infra_plan's wildcard now matches the new repo too.
  • 🔒 [security] team_provisioner Lambda role: adds iam:PutRolePermissionsBoundary on aws-reserved SSO roles, conditioned on the permissions boundary matching javabin-team-*-guardrails. This is a privilege-escalation-relevant grant; the condition limits it, but confirm it is intended. Also adds Resource 'arn:aws:iam::aws:policy/job-function/ViewOnlyAccess' to the SSO permission-set actions and sso:ListAccountAssignments, PutPermissionsBoundaryToPermissionSet and AttachManagedPolicyToPermissionSet, and removes sso:AttachCustomerManagedPolicyReferenceToPermissionSet. Net effect is broader Identity Center control. The AWS-managed policy ARN is attached to a Resource list that also covers IAM actions, so review the scope.
  • 🔒 [security] ECS deploy policies for cupcake, javazoneweb, jzwatch and testteam add ecs:TagResource (only on RegisterTaskDefinition), ecs:ListTasks (scoped to the javabin-platform cluster), and ecs:ListTagsForResource / ecs:DescribeTasks. The TagResource and ListTasks statements are Allow grants. The budget_enforcer role adds ecs:TagResource to ecs:UpdateService with Resource on the cluster. These are modest, but they are Allow grants on Resource '*' for some statements.
  • ✅ [routine] Two Lambda updates (budget_enforcer code hash change; team_provisioner code hash change plus new environment variable AWS_ACCESS_GROUP=javabin-developer@java.no). The team_provisioner last_modified shows a future-looking timestamp, which is worth checking against the source. No destruction.
  • ✅ [routine] New scheduled EventBridge rule (rate 3 hours) invoking team_provisioner with action sync_access_members, plus a matching lambda permission. Low cost (EventBridge scheduled invocations and Lambda runs are negligible), no public exposure. Note the rule name 'javabin-team-provisioner-access-schedule' is clearly paired with the Lambda's new action.

@Alexanderamiri
Alexanderamiri merged commit 5d2b2dc into main Oct 31, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the fix/team-permission-scope branch October 31, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant