Repository navigation
fix(identity): make team guardrails the permission set boundary and scope team access - #188
Conversation
…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>
Terraform Plan🚧 Changes detected — Plan: 0 to add, 2 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟡 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.
|
… 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>
|
Follow-up to the review, commit 132f326. Finding 1 (
Simulation for
Row 16 is metadata only, the same access Finding 2 (happy-path test doesn't check order): accepted and fixed. No findings rejected. Docs updated: Tests: 🤖 Generated with Claude Code |
Terraform Plan🚧 Changes detected — Plan: 0 to add, 2 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟡 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.
|
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>
|
Merged
Checks: lambda-src 252 tests OK, scripts 184 OK, Behaviour change reviewers haven't seen: with #180, a pending team got no account assignment until |
Terraform Plan🚧 Changes detected — Plan: 3 to add, 11 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🔴 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.
|
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}-guardrailsis attached to the permission set as a customer-managed policy (AWSReservedSSO_javabin-team-javazoneweb_*has it inAttachedPoliciesand has noPermissionsBoundary). That policy is the developer boundary rendered with the team name, and its first statement isAllow * *. 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
dbmoresleepRDS instance and the legacy EC2/Elastic Beanstalk instances, change records in thejavazone.nohosted zone, attach policies to untagged IAM roles, update untagged Lambda code, delete platform SSM parameters (override-signing-key), and tag untagged resourcesteam=<own>to take them over. See rows 1-42 below.Change
team_provisioner/handler.py(_ensure_team_permission_setand two new helpers; nothing else in the handler):put_permissions_boundary_to_permission_setsetsjavabin-team-{team}-guardrailsas the boundary. Only after that succeeds, the old customer-managed references are detached (javabin-team-{team}-guardrailsand the legacyjavabin-developer-boundary). If the boundary call fails, the permission set is left as it was and the result reports anerror.ViewOnlyAccess(AWS managed, the same policyjavabin-readonlyuses) is attached for read-only console browsing: List/Describe only, no object, parameter or secret reads. A failure is reported asview_onlyand does not block the rest._team_inline_policy):AllowTeamTaggedResources:*on resources tagged with the team, usingStringEquals(noIfExists). Requests without a resource tag (untagged resources, S3 objects, Route53, creates) no longer match./javabin/apps/{team}/*SSM: get and overwrite (PutParameterhas noaws:ResourceTag)/ecs/{team}/*log reads and live tail{team}-*-{account}buckets (theapp.yamlbucket naming, so never a legacy bucket likejavazone-backup)ecs:ExecuteCommandon thejavabin-platformcluster. Tasks carry the team tag throughpropagateTags=SERVICE, so the task side is covered by the tag allow.AllowUntaggedReads, sojavabin statusandjavabin secretdon't depend on the managed attachment.terraform/platform/lambdas/main.tfchanges the team-provisioner role:sso:AttachManagedPolicyToPermissionSetandsso:PutPermissionsBoundaryToPermissionSet.iam:PutRolePermissionsBoundaryonaws-reserved/sso.amazonaws.com/*roles, conditioned on the boundary being ajavabin-team-*-guardrailspolicy. CloudTrail shows that in this management account, SSO provisioning makes its IAM calls with the provisioner's own credentials.ViewOnlyAccess.sso:AttachCustomerManagedPolicyReferenceToPermissionSet, which is no longer used.docs/lambda-functions.mdhas a new "Team permission set" section. Also updated: the GuardDuty note indocs/platform-modules.md,CLAUDE.md, and theiam/boundary.tfcomment ("never attach it as a policy").Verification
Simulation.
aws iam simulate-custom-policyfor teamjavazoneweb, using:javabin-team-javazoneweb-guardrailsv3, from IAM)sso-admin get-inline-policy-for-permission-set)ViewOnlyAccess_team_inline_policy("javazoneweb")The resources are real and their
teamtags were read from AWS.aws:ResourceTag/teamis only put in the context for actions whose service evaluates it; S3 objects and Route53 never get it.--permissions-boundary-policy-input-list.DENYis an explicit deny;denyis an implicit deny.s3:GetObjects3:PutObjects3:PutObjects3:PutObjects3:GetObjects3:DeleteObjects3:PutBucketPolicydynamodb:DeleteItemdynamodb:Scanssm:GetParameterssm:PutParameterssm:GetParameterssm:DeleteParameterssm:DeleteParametersecretsmanager:GetSecretValuekms:ScheduleKeyDeletionecr:PutImageecr:PutImageecr:BatchDeleteImagerds:DeleteDBInstancerds:ModifyDBInstanceelasticloadbalancing:ModifyListenerelasticloadbalancing:CreateRuleelasticloadbalancing:DeleteLoadBalancerec2:TerminateInstancesec2:CreateTagsecs:UpdateServiceecs:ExecuteCommandecs:UpdateServiceecs:ExecuteCommandiam:PutRolePolicyiam:AttachRolePolicyiam:UpdateAssumeRolePolicyiam:CreateUserlambda:UpdateFunctionCodelambda:InvokeFunctionlambda:UpdateFunctionCodecloudtrail:StopLoggingroute53:ChangeResourceRecordSetsroute53:CreateHostedZonesqs:PurgeQueueec2:RunInstancesssm:GetParameterssm:PutParameterssm:GetParametersByPathssm:PutParameterssm:PutParameterssm:DescribeParameterslogs:FilterLogEventslogs:GetLogEventslogs:StartLiveTaillogs:StartQuerylogs:DescribeLogGroupslogs:FilterLogEventsecs:ListServicesecs:DescribeServicesecs:DescribeServiceslogs:GetQueryResultsecs:UpdateServiceecs:ExecuteCommandecs:ExecuteCommandecs:StopTaskecs:DescribeTaskDefinitionecr:GetAuthorizationTokenecr:PutImageecr:BatchGetImageecr:DescribeImagess3:GetObjects3:ListBucketce:GetCostAndUsageec2:DescribeInstancesrds:DescribeDBInstanceslambda:ListFunctionscloudwatch:GetMetricDataelasticloadbalancing:DescribeRuless3:ListAllMyBucketsroute53:ListResourceRecordSetsRows 57-58 are unchanged pre-existing guardrail denies, outside this PR:
DenyXTeamdeniesecs:DescribeServiceson other teams' services.javabin statusdescribes every service in the cluster in one call, so it probably fails for team SSO sessions today. The CLI should describe only{team}-*services.DenyXLogsdenieslogs:GetQueryResults, which has no resource, so Logs Insights results can't be read.Also unchanged:
javabin status --resourcesuses the Athena workgroupjavabin-cost-analytics, which is taggedteam=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 -recursiveandterraform validateinterraform/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:
aws iam get-role --role-name AWSReservedSSO_javabin-team-<team>_*showsPermissionsBoundary=javabin-team-<team>-guardrails, andlist-attached-role-policiesshows onlyViewOnlyAccess.javabin secret list/set,aws logs tail /ecs/<team>/<app>, an ECR push, andaws ecs update-service --force-new-deployment, and confirms ans3 cpfrom the state bucket fails.javabin-team-platformhas no guardrails policy (pending). It now gets read-only access:list-attached-role-policiesonAWSReservedSSO_javabin-team-platform_*shows onlyViewOnlyAccessand nojavabin-developer-boundary. Its two members are also indrift(javabin-admin), so they keep admin access through that permission set. See the PR comment for the simulation.🤖 Generated with Claude Code