If your organization is using IAM Identity Center (formerly AWS Single Sign-On, AWS SSO), there’s a good chance that most users assume the same IAM role when accessing AWS accounts.
But what if you still want to restrict access to EC2 instances on a per-user basis?
We ran into exactly this scenario and found a simple solution that may be useful for others — including our future selves.
Assumptions
We make the following assumptions:
- The IAM role is accessed exclusively via SSO and can not be assumed in any other way.
- EC2 instances are accessed exclusively through SSM Session Manager.
If EC2 instances are not exclusively accessed through SSM Session Manager in your environment, it’s worth considering. Session Manager removes the need for exposed SSH ports and centralizes access control in IAM, reducing both operational overhead and security risk.
Solution
The idea is straightforward: We tag each EC2 instance with a User tag and ensure that users can only start a session on instances where that tag matches their username.
A second user can be allowed access by setting the User2 tag; however, the User tag must still exist for this rule to apply.
To enforce this, we add an explicit Deny statement to the IAM policy attached to the shared role:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyStartSessionIfUserNotInSessionUserTags",
"Effect": "Deny",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"Null": {
"ssm:resourceTag/User": "false"
},
"StringNotLike": {
"aws:userid": [
"AROA*:${ssm:resourceTag/User}",
"AROA*:${ssm:resourceTag/User2}"
]
}
}
}
]
}
How this works
This statement denies ssm:StartSession when the instance has a User tag and the caller’s identity does not match that value.
Let’s break it down:
"Null": { "ssm:resourceTag/User": "false" }
This ensures the statement only applies if the User tag exists on the target instance. - If User is present → the condition evaluates to true and the rest of the statement is evaluated. - If User is not present → the statement does not apply. In other words: untagged instances are not affected by this deny rule.
"StringNotLike": { "aws:userid": [ "AROA*:${ssm:resourceTag/User}", "AROA*:${ssm:resourceTag/User2}" ] }
When a user logs in via IAM Identity Center, the aws:userid looks somewhat like AROAABCDEFGHIJKL:username.
- The
AROA...part identifies the role. - The part after the colon represents the session name, which, in the case of SSO, is typically derived from the Identity Center user identity (for example, username or email), depending on your configuration.
- It is very important that the role cannot be assumed in any other way, because otherwise the last part can be freely chosen.
- The condition checks that
aws:useriddoes not match the patternAROA*:<UserTagValue>. - If
aws:useriddoes not match either allowed pattern, the condition evaluates to true and the Deny applies, preventingssm:StartSession.
Prevent Tampering
Of course, this model only works if users cannot change the User tag themselves. To prevent tampering, we add a complementary deny statement:
{
"Sid": "PreventTamperingWithSessionUserTags",
"Effect": "Deny",
"Action": [
"ec2:CreateTags",
"ec2:DeleteTags"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"ForAnyValue:StringEquals": {
"aws:TagKeys": [
"User",
"User2"
]
}
}
}
How this works
This statement denies any attempt to create or delete the User tag on EC2 instances.
- ec2:CreateTags and ec2:DeleteTags are the API calls used to manage tags.
- aws:TagKeys contains the list of tag keys included in the request.
- ForAnyValue:StringEquals means this deny applies if any tag key in the request is User or User2.
In practical terms, any request that includes User or User2 is denied.
- This blocks adding, removing, or changing User/User2, even in mixed tag operations.
This ensures ownership is enforced consistently and cannot be bypassed by modifying the tag. These statements can be extended to cover additional tags.
More Secure Way
A more robust and future-proof approach would be to use Principal Tags instead of parsing identity information from aws:userid.
When a role is assumed, AWS allows you to attach session tags (also referred to as Principal Tags). These tags become part of the identity context and can be referenced in IAM policies using: ${aws:PrincipalTag/<TagKey>}
As part of the SSO process, we could add a tag like: User: alice
The policy would become much simpler:
"StringNotEquals": {
"ssm:resourceTag/User": "${aws:PrincipalTag/User}"
}
This has several advantages:
- no dependency on the internal structure of aws:userid
- less fragile if AWS ever changes role session formatting
- this tag is controlled by federation/session-tag configuration and is hard to spoof
Why we didn’t use it
In our case, we do not have the ability to attach custom Principal Tags to IAM Identity Center role sessions.
To make this work, you need:
- Attribute mappings in IAM Identity Center
- Permission set configuration that passes attributes as session tags
- The ability to control or standardize those mappings across accounts
Since that was not available in our setup, we opted for matching against aws:userid, which is present for assumed role sessions. If you do have control over Identity Center attribute mappings, using Principal Tags is the cleaner and more secure way.
Conclusion
With two small policy statements, we’ve protected our resources.
This approach can be extended further — for example, to prevent users from terminating, stopping, or modifying instances they do not own.
The result is an access model that scales with your organization, requires no key rotation or authorized_keys management, and keeps all access decisions in IAM
Written by

Thomas de Ruiter
Cloud Consultant with a passion for everything Cloud. Likes to automate all the things. Believes security is everyone's responsibility!
Our Ideas
Explore More Blogs
Contact



