4 minutes
The Very Pattern of Security
Backstory
Typically, we developers, approach the world with an overly optimistic approach to solving certain problems, especially when it comes to security within our apps. “Oh, we’ll just use XYZ to make it secure” we’ll say. But this isn’t always true.
An interesting challenge popped up recently around the ‘best’ way of integrating an external build system with AWS; where ‘best’ was how to minimise the risk exposed by long lived credentials stored within an external system. THe fun part of this challenge was when I got to put on my pessimist’s hat to consider all the nefarious ways these credentials could be abused.
The silver lining in this situation, and something that is sadly missing from the external tools I typically use, was the ability to configure more than the usual access key
and secret key
. In addition, I could also specify the following:
role arn
role session name
; andexternal id
With these five values I pieced together a little demo CloudFormation template. It is such a nice pattern for these situations that I wanted to share it with the world! 😄
Interesting Bits
Rather than a long, boring, and unwieldy template, I’ll just highlight the interesting bits. The full template can be viewed in this GitHub repository.
The first snippet explicitly Denies
all actions except for the explicitly Allowed
sts:AssumeRole
. This is further restricted to only assume the specific IAM Role defined later in the template.
UserPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Version: 2012-10-17
Statement:
- Sid: 'ExplicitlyDenyAllActionsExceptAssumeRole'
Effect: Deny
NotAction: ['sts:AssumeRole']
Resource: '*'
- Sid: 'ExplicitlyAllowAssumeRole'
Effect: Allow
Action: ['sts:AssumeRole']
Resource: !GetAtt Role.Arn
The next restriction is applied in the role’s AssumeRolePolicyDocument
statement. The role can only be assumed by the specific IAM User defined in the template. Additionally, there is also a reference to a Permission Boundary. This restricts what the role can do. For example, you can use it to prevent privilege escalation, i.e., the role cannot by abused to create another role with full administration permissions. IAM Conditions are also used to ensure that a specific RoleSessionName
and ExternalId
are passed when assuming the role.
Role:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS:
- !GetAtt User.Arn
Action:
- 'sts:AssumeRole'
Condition:
StringLike:
'sts:RoleSessionName':
!Ref ParamRoleSessionName
'sts:ExternalId':
!Ref ParamExternalId
PermissionsBoundary: !Ref PermissionBoundary
The example permission boundary below explicitly denies all actions except for S3 Bucket actions. Keep in mind that all permissions are implicitly denied; so, the explicit deny needs to be paired with an appropriate explicit allow for S3.
PermissionBoundary:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: String
ManagedPolicyName: !Sub '${ParamBaseName}PermissionBoundaryPolicy'
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Deny
NotAction: 's3:*'
Resource: '*'
- Effect: Allow
NotAction:
- 's3:*'
Resource: '*'
Finally, there is the policy for the IAM Role. In this policy I explicitly deny all S3 actions that do not relate to buckets. I then add the explicit Allow
for allowed bucket actions. I also try and slip in an extra iam:ListRoles
permission. This will not be allowed due to the explicit deny in the preceding permissions boundary.
RolePolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Deny
NotAction: 's3:*Bucket*'
Resource: '*'
- Effect: Allow
NotAction:
- 's3:List*Bucket*'
- 's3:Get*Bucket*'
- 's3:Describe*Bucket*'
- 'iam:ListRoles'
Resource: '*'
Tests
As we are all good little developers who practice Test Driven Development, here is how to verify that the protection mechanisms work (the full scripts are included in the GitHub repository):
The following four assume-role
calls all fail as none of them pass a valid role-session-name
and external-id
pair:
aws sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "${MY_PARAM_ROLE_SESSION_NAME}"
aws sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "${MY_PARAM_ROLE_SESSION_NAME}_" --external-id "${MY_PARAM_EXTERNAL_ID}_"
aws sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "${MY_PARAM_ROLE_SESSION_NAME}_" --external-id "${MY_PARAM_EXTERNAL_ID}"
aws sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "${MY_PARAM_ROLE_SESSION_NAME}" --external-id "${MY_PARAM_EXTERNAL_ID}_"
The error returned will look like this:
An error occurred (AccessDenied) when calling the AssumeRole operation:
User: arn:aws:iam::111111111111:user/${MY_PARAM_BASE_NAME}
is not authorized to perform: sts:AssumeRole on resource:
arn:aws:iam::111111111111:role/${MY_PARAM_BASE_NAME}Role
Once the valid parameters are passed, we can then verify the permission boundary like so:
aws sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "${MY_PARAM_ROLE_SESSION_NAME}" --external-id "${MY_PARAM_EXTERNAL_ID}"
# Test: Allowed, lists all the S3 buckets in the account
aws s3api list-buckets
# Test: Attempts to create a new bucket, which is allowed by the permission boundary but denied by the IAM Role policy
aws s3api create-bucket --bucket "this-should-fail"
# Response: An error occurred (IllegalLocationConstraintException) when calling the CreateBucket operation: The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.
# Test: Attempts to list IAM Roles; this is denied by the permission boundary even though it is allowed by the IAM Role policy
aws iam list-roles
# Response: An error occurred (AccessDenied) when calling the ListRoles operation: User: arn:aws:sts::111111111111:assumed-role/${MY_PARAM_BASE_NAME}role/${MY_PARAM_BASE_NAME} is not authorized to perform: iam:ListRoles on resource: arn:aws:iam::111111111111:role/ with an explicit deny
Conclusion
In case you missed it, the full template and testing script can be found in the GitHub repository.
I hope you found this pattern useful! If you did please, or think it could be improved, let me know! All feedback welcome!