SCS Lab 04 - Cap a role with a permission boundary
Scaffold: 3/5. The demo bucket, the over-granted developer role, and the managed policy that sits as that role’s permission boundary are all built and wired. What is missing is the one thing that makes the boundary worth having: the boundary’s own policy document. You write it.
The scenario
A developer role was handed s3:* and iam:* on every resource. That is the
access someone reaches for when they are in a hurry, and it is exactly the
access you do not want a role to keep. The obvious fix is to rewrite the
identity policy, but you cannot always do that cleanly: the policy is shared,
or generated, or owned by a team that will not narrow it this quarter, and in
the meantime the role is dangerous.
A permission boundary caps a role without touching its identity policy. It is a
managed policy you attach as the role’s PermissionsBoundary, and it sets a
ceiling: the role’s effective permissions become the intersection of what
its identity policy allows and what the boundary allows. Leave s3:DeleteObject
out of the boundary and the role cannot delete objects, no matter that its
identity policy still says s3:*. The identity policy stays broad; the boundary
makes the broad part unreachable.
The requirement
DemoDeveloperRole keeps its broad identity policy (s3:* and iam:* on *).
Its boundary, DeveloperBoundary, must cap the effective permissions to a safe
set: read and list the demo bucket, put objects into it, emit CloudWatch
metrics, and write logs. Nothing else. Specifically, no iam:* and no
s3:Delete*.
What’s provided
src/template.yaml- the demo S3 bucket, theDemoDeveloperRolewith its broad identity policy and itsPermissionsBoundaryalready set toDeveloperBoundary, and theDeveloperBoundarymanaged policy itself. The boundary ships with a placeholder document that allows*on*, marked with aTODO. The stack Outputs are all there.scripts/- deploy, test, and teardown.solution/template.yaml- the complete, correct template.
Deploying src/template.yaml as shipped succeeds, but the boundary caps
nothing: a boundary that allows everything imposes no ceiling, so the role’s
effective permissions equal its dangerous identity policy. That is the failure
the lab starts from.
Your task
Open src/template.yaml and replace the placeholder PolicyDocument in
DeveloperBoundary so it allows exactly:
s3:Get*ands3:List*on the demo bucket and its objects (the bucket ARN and the bucket ARN with/*).s3:PutObjecton the bucket’s objects (the/*ARN).cloudwatch:PutMetricDataandlogs:*on*.
Do not add iam:* and do not add s3:Delete*. A boundary is a ceiling, not a
grant: anything you leave out is capped away even though the identity policy
still lists it. Use !GetAtt DemoBucket.Arn for the bucket and
!Sub '${DemoBucket.Arn}/*' for its objects.
If you want the mechanics first, read the walk-through post linked at the bottom.
Run it
# Defaults: stack scs-lab-04, region ap-southeast-2.
./scripts/deploy.sh # deploys src/template.yaml
./scripts/test.sh # simulates the role's effective permissions
./scripts/teardown.sh # empties the bucket and deletes everything
The stack creates a named role and a named managed policy, so deploy.sh
passes CAPABILITY_NAMED_IAM.
What success looks like
test.sh never assumes the role or touches an object. It calls the IAM policy
simulator, which evaluates a principal’s identity policies and its
permission boundary together, so it reports the role’s effective decision for
each action. It asks three questions and, once the boundary is tightened,
prints:
ok: s3:GetObject is allowed (allowed)
ok: s3:DeleteObject is denied (implicitDeny) - the boundary caps it
ok: iam:CreateUser is denied (implicitDeny) - the boundary caps it
PASS: the boundary lets the role read the demo bucket but caps away
s3:DeleteObject and iam:CreateUser, even though the identity policy grants
s3:* and iam:*. Effective permissions are the intersection of the two.
With the unedited src/template.yaml, the boundary allows * on *, so
s3:DeleteObject and iam:CreateUser both come back allowed and the test fails.
If it fails
- DeleteObject or CreateUser comes back allowed. The boundary is still too
wide. Every action the boundary allows stays reachable if the identity policy
also allows it, and the identity policy allows everything. Remove
iam:*ands3:Delete*from the boundary; only list the safe actions. - GetObject comes back denied. The boundary is too tight, or the resource
ARN does not match.
s3:GetObjectacts on objects, so the boundary needs the/*object ARN, not just the bucket ARN. The intersection removes anything the boundary omits, so a read that should work fails when the boundary forgets it. - The stack will not update. A permission boundary is just a managed policy;
editing its
PolicyDocumentand redeploying replaces it in place. Make sure you editedDeveloperBoundaryand not the role.
Reveal the solution
Deploy the complete reference template without editing anything:
SRC=solution ./scripts/deploy.sh && ./scripts/test.sh
What you just learned
- A permission boundary sets effective permissions to the intersection of the identity policy and the boundary. Both have to allow an action for it to be allowed; leave it out of either and it is denied.
- A boundary is a ceiling, not a grant. Adding an action to the boundary never gives the role anything; it only stops removing what the identity policy already grants. The identity policy is what grants.
- This is how you delegate safely. Hand a team the ability to create roles, but
require every role they create to carry your boundary, and they can never
mint a role more powerful than the ceiling you set, even with
iam:*. - A boundary is not an SCP. An SCP is a Organizations guardrail that caps every principal in an account, including the root user, and lives above IAM. A boundary is attached to one role or user and caps only that principal. Both are ceilings that grant nothing; they operate at different scopes.
Next
The rest of the SCS lab track is listed in labs/README-scs.md.