Hand Over Permissions Without Long-Lived Keys
Goal
It is true that roles are safer than long-term keys, but if you write the trust policy loosely, that safety disappears entirely. In this lab, without a cloud account, you write trust policies yourself and build by hand a checker that catches the dangerous forms, to confirm those spots.
Trust policies and permission policies
If you mix up the two documents, nothing matches.
- Permission policy — what this role can do
- Trust policy — who can assume this role
What you write in this lab is on the trust policy side. Only the last step is a permission policy.
Conditions
- The application runs on an EC2 instance.
- You must open read permission to the partner account
210987654321. - CI deploys only from the repository
acme/platform, and only from itsrefs/heads/main. - Developers must be able to attach a task role when creating an ECS task.
Checking syntax
Policies usually go in as code, not through the console. If the JSON is broken, the error message appears unrelated to the policy content, so it is faster to check every time.
python3 -m json.tool /root/roles/trust-ec2.json
What you make
| File | Contents |
|---|---|
/root/roles/trust-ec2.json |
The role the instance assumes |
/root/roles/trust-partner.json |
The role opened to an external account |
/root/roles/trust-ci.json |
The role assumed through federation |
/root/roles/lint_trust.py |
A checker for dangerous trust policies |
/root/roles/passrole.json |
The permission to pass a role |
/root/roles/06-session.md |
Session duration calculation |
/root/roles/07-notes.md |
Summary |
Notes
The checker in step 4 is run against four hidden policies and the three policies you made. Missing a dangerous one is a failure, and flagging a safe one is also a failure.
Let the instance get credentials on its own
Write the trust policy for a role that an EC2 instance can assume in /root/roles/trust-ec2.json. Version is 2012-10-17.
A trust policy decides only "who can assume this role." What it can do is in a separate permission policy.
The key is that the principal is a service, not a person. In Principal, write Service, and the action is the single action of assuming the role.
If you attach this role to an instance, the programs running inside obtain credentials without a key file. They are refreshed automatically before they expire, so no person has to touch anything.
Fill in one more field when opening to an external account
Write the trust policy for a role that the partner account 210987654321 can assume in /root/roles/trust-partner.json. Writing only the account number is not enough.
If you write only the account number, anyone inside that account can assume this role. If a single partner employee's credentials leak, our role is open.
So attach a value known only to both sides as a condition. Compare sts:ExternalId with StringEquals, and choose a value of at least eight characters that is hard to guess.
Without this condition, a third party that has entrusted work to that partner can reach our role through the partner.
Get rid of keys in CI
Write the trust policy for a role that a CI runner assumes through OIDC in /root/roles/trust-ci.json. It must be assumable only from the acme/platform repository, and only on refs/heads/main.
When assuming through federation, the action is not sts:AssumeRole but sts:AssumeRoleWithWebIdentity, and the principal is the identity provider written in Principal.Federated.
If you leave out the condition, any repository that uses that CI service can assume our role. It is the most common mistake, and one line of condition blocks it.
Pin :sub with StringEquals and also check :aud. If you use StringLike with * and write only the repository, it can be assumed from any branch of that repository, which is not very different from having no condition.
A checker that catches dangerous trust policies
Create /root/roles/lint_trust.py. When called as python3 /root/roles/lint_trust.py <정책.json> (policy file), it must print what is dangerous and exit with code 1 for a dangerous policy, and exit with code 0 for a safe policy.
Look at three things.
- Is the principal open —
Principalitself is*, orPrincipal.AWSis* - An external account is put in
Principal.AWSwith nosts:ExternalIdcondition - It is
sts:AssumeRoleWithWebIdentity, but there is no:subcondition or the value contains*
The grader also feeds the policies you made in the first three steps to this checker. Flagging a safe one is also a failure — once false alarms begin, people turn the checker off.
Narrow the permission to pass a role
Developers must be able to attach a task role when creating an ECS task. Write that permission in /root/roles/passrole.json, but narrow it down to the roles that can be attached and the service they are passed to.
The action of attaching a role to a resource when creating it is effectively a permission to "make something do what I cannot do." If you leave Resource as *, a low-privilege user can create a resource with an administrator role attached and escalate privileges.
In Resource, write the ARN of the roles that may be attached. You can also fix only the beginning of the name and leave the rest open.
Also pin down with the iam:PassedToService condition which service it may be passed to. Then the path of passing the same role to a different service is blocked.
Session duration is the response speed
Assume that credentials leaked as soon as they were issued. It takes 3 hours to notice and 1 hour to decide on a response. Calculate the time that remains even after the permissions are revoked for a 12-hour session and for a 1-hour session, and write at least three lines in /root/roles/06-session.md.
Even if you reduce the role's permissions, sessions that have already been issued are not cancelled. They can still be used for the remaining validity time.
Revocation takes 3 + 1 hours. With a 12-hour session, how much remains after that? What happens with a 1-hour session?
If you write down the numbers, "keep sessions short" becomes a calculation, not a preference.
Summarize the three points
Write at least three lines in /root/roles/07-notes.md: the difference between long-term keys and temporary credentials, what a trust policy decides, and what you must not leave out for external accounts and federation.
The text must include 만료 (expiry), 신뢰 정책 (trust policy) and 조건 (condition).