How to let builders create IAM resources while improving security and agility for your organization

Many organizations restrict permissions to create and manage AWS Identity and Access Management (IAM) resources to a group of privileged users or a central team. This post explains how also granting these permissions to builders, that is the people who are developing, testing, launching, and managing cloud infrastructure, can speed up your development, increase your agility, and improve your application security. In addition, you will use an example application stack to see how IAM permissions boundaries can help establish a secure, yet agile, work environment for builders.

An example application stack

Defining and creating IAM resources within the application stack allows your builders to craft policies and roles that grant least privilege to application resources. When builders can assume identities with both IAM and application permissions, it is efficient for them to scope policies to specific resources by referencing them directly in a single template.

To illustrate our point, we’ll use a simple “hello world” serverless application. The application includes an AWS Step Functions state machine that, once executed, will invoke a “Hello World!” AWS Lambda function. We will use this example application along with some IAM policies and roles to illustrate how permissions boundaries can change the way IAM permissions are managed.

In this example application template, the Resource element in MyStateMachineExecutionRole which is specified as the role for MyStateMachine, includes a reference to the Amazon Resource Name (ARN) of MyLambdaFunction. This is a great example of the principle of least privilege: MyStateMachine will only have permissions to invoke MyLambdaFunction. Scoping this relationship is straightforward because the IAM, Step Functions, and Lambda resources are defined together in the same template.

Example application template

AWSTemplateFormatVersion: 2010-09-09
Description: builder-application Resources: MyLambdaFunctionExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole MyLambdaFunction: Type: AWS::Lambda::Function Properties: Runtime: nodejs12.x Role: !GetAtt MyLambdaFunctionExecutionRole.Arn Handler: index.handler Code: ZipFile: | exports.handler = (event, context, callback) => { callback(null, "Hello World!"); }; Timeout: 30 MyStateMachineExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - states.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: StateMachineExecutionPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - lambda:InvokeFunction Resource: !GetAtt MyLambdaFunction.Arn MyStateMachine: Type: AWS::StepFunctions::StateMachine Properties: DefinitionString: !Sub | { "StartAt": "HelloWorld", "States": { "HelloWorld": { "Type": "Task", "Resource": "${MyLambdaFunction.Arn}", "End": true } } } RoleArn: !GetAtt MyStateMachineExecutionRole.Arn MyLambdaFunctionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref MyLambdaFunction Principal: states.amazonaws.com SourceArn: !Ref MyStateMachine

In an organization that centrally manages IAM resources, a builder trying to deploy this example application may get an error. This is because the roles the builders are granted prohibit most IAM actions, particularly actions like iam:CreateRole and iam:UpdateRole. The stack will fail to deploy because it tries to create IAM resources and the builder lacks the permissions to do so. This creates three key challenges:

  1. Builders must rely on someone else to create IAM resources, or to launch an application stack that contains the IAM resources they need. This approach either slows down development while builders wait for the roles and policies to be created, or it encourages the central IAM team to grant overly-broad permissions so that they don’t have to be involved in day-to-day changes.
  2. IAM resources must be created before the rest of the stack, which makes it much more difficult to create least-privilege policies and roles.
  3. The code for IAM and application infrastructure is managed separately, which requires extra coordination when creating and updating workloads.

Removing these challenges can simplify your cloud development, and lower the amount of overhead and coordination required across your organization.

When IAM resources are created in a separately managed stack before the rest of the application, it encourages organizations to either overuse wildcards (“*”) in policies, or to specify custom physical names for resources.

For example:

Resource: arn:aws:lambda:us-east-1:999999999999:function:MyLambdaFunctionName

Sometimes pre-determined names make sense, but managing names of resources between teams adds a layer of configuration management across code that would otherwise be unnecessary. There are also cases where specifying a name prevents updates that require replacing the named resource (for example the Lambda function FunctionName). If you don’t manage custom names, your resources could be partially specified using the predictable part of the resulting name. For example, ARNs for Lambda functions created using AWS CloudFormation include a function name according to the pattern <stack name>-<resource name>-<random characters>, so a resulting resource specification might look like:

Resource: arn:aws:lambda:us-east-1:999999999999:function:*-MyLambdaFunction-*

Note: the reason a wildcard is used in place of <stack name> is because that field is truncated when the character limit is reached, so it is not a reliable field to match.

It is possible to update IAM policies with the known resource names after stack creation, but in practice, that is difficult to manage at scale. Also, naming schemas vary across infrastructure as code (IaC) tools, so allowing IAM resources to be created in the application stack makes configuration management more efficient for teams.

Fortunately, by granting builders IAM permissions, your organization will be able to create more secure policies and improve your DevOps posture.

Use permissions boundaries to enforce security perimeters

Organizations that exclude IAM permissions from all but privileged users are trying to enforce security perimeters around identities and resources. So, how can your organization permit actions like iam:CreateRole and iam:UpdateRole in builder policies, without allowing builders to create roles with elevated permissions that go beyond those perimeters? This is where permissions boundaries come in. You can use a managed policy attached to a builder role as a permissions boundary, which will effectively set the maximum permissions that a builder could be granted.

Following the steps in this section, we will create a permissions boundary around our builders. This will allow them to create IAM roles for their resources (like Lambda functions or EC2 instances) with IAM policies attached. But they will only deploy successfully if the policies the create are inside the permissions boundaries.

Create the builder policy and trust policy

In the first procedure, you create a policy that grants builders both the application-related permissions they need, and the permissions to create and manage policies, roles, and instance-profiles.

Note: This procedure assumes the partition in all ARNs is aws, and the URL suffix in principals is amazonaws.com. If you are deploying in another partition, you will need to make the appropriate substitutions.

The builders can be granted these permissions if they meet two conditions:

  • The name of the resource starts with the word builder.
  • Roles have a builder-boundary policy applied as a permissions boundary.

To create the builder policy

  1. Open or install the AWS Command Line Interface (CLI) and assume a role that has permissions to create IAM resources.
  2. Download the builder policy from an S3 bucket to your local machine using the following command:
    aws s3 cp s3://awsiammedia/public/sample/993-grant-IAM-permissions-for-builders/builder-policy.json .

  3. Open the builder-policy.json policy and replace <account> with your account number.
  4. Create the builder policy using the following command:
    aws iam create-policy --policy-name builder-policy --policy-document file://builder-policy.json

Create the permissions boundary

In this procedure, you create a builder boundary policy that can be used as the permissions boundary. This example uses several statements and policy elements to deny permissions both for specific services (such as AWS Organizations and AWS Account Management) and specific actions (such as AWS CloudTrail DeleteTrail), and allows everything else.

Note: In this case, you use a wildcard for the Resource element because you want to deny these permissions for all resources related to these services and actions.

To create the permissions boundary

  1. Download the builder boundary policy from an S3 bucket to a file named builder-boundary.json by using the following command:
    aws s3 cp s3://awsiammedia/public/sample/993-grant-IAM-permissions-for-builders/builder-boundary.json .

  2. Create the boundary policy by using the following command:
    aws iam create-policy --policy-name builder-boundary --policy-document file://builder-boundary.json

Create the builder role and set the permissions boundary

Finally, you can create a builder-role, set a builder-trust as the trust policy and a builder-boundary as the permissions boundary, and attach the builder-policy as the permissions policy. The effective permissions for the builder-role will then be the intersection of the two policies: a permission granted in the permissions policy must also be granted in the permissions boundary, or else it will be implicitly denied according to the policy evaluation logic.

To create the builder role and set the permissions boundary

  1. Download the trust policy from an S3 bucket to a file named builder-trust.json by using the following command:
    aws s3 cp s3://awsiammedia/public/sample/993-grant-IAM-permissions-for-builders/builder-trust.json .

    This policy allows anyone in the account, as well as the CloudFormation and Amazon Elastic Compute Cloud (EC2) services, to assume the builder-role on an Amazon EC2 instance. You can use a combination of the Principal and Condition attributes to reduce its scope.

  2. Create the role and trust, and set the permissions boundary by using the following command, replacing <account> with your account number:
    aws iam create-role --role-name builder-role --assume-role-policy-document file://builder-trust.json --permissions-boundary arn:aws:iam::<account>:policy/builder-boundary

  3. Attach the builder-policy to the role by using the following command, replacing <account> with your account number:
    aws iam attach-role-policy --role-name builder-role --policy-arn arn:aws:iam::<account>:policy/builder-policy

Test the permissions boundary

In this procedure, you will demonstrate that the permissions boundaries work. You test the new builder-role by trying to launch a stack that includes IAM resources — first without the required permissions boundary attached, and then retrying it after the permissions boundary has been attached.

To test the permissions boundary

  1. To test, assume the builder-role and try to deploy an example application. Download the example application template shown above and save it as builder-application.yml. Do not make any changes yet — you want to test without it first, to confirm that the guardrails you have put in place are working. Use the following command.
    aws s3 cp s3://awsiammedia/public/sample/993-grant-IAM-permissions-for-builders/builder-application.yml .

  2. Assume the builder-role by using the following command:
    role=builder-role; \
    aws_role=$(aws iam get-role --role-name $role); \
    role_arn=$(echo $aws_role|jq '.Role.Arn'|tr -d '"'); \
    aws_credentials=$(aws sts assume-role --role-arn $role_arn --role-session-name builder-test); \
    export AWS_ACCESS_KEY_ID=$(echo $aws_credentials|jq '.Credentials.AccessKeyId'|tr -d '"'); \
    export AWS_SECRET_ACCESS_KEY=$(echo $aws_credentials|jq '.Credentials.SecretAccessKey'|tr -d '"'); \
    export AWS_SESSION_TOKEN=$(echo $aws_credentials|jq '.Credentials.SessionToken'|tr -d '"'); \
    

  3. Check that the you have now assumed the builder-role by using the following command:
    aws sts get-caller-identity

  4. Launch the stack with CloudFormation by using the following command:
    aws cloudformation create-stack --stack-name builder-application --template-body file://builder-application.yml --capabilities CAPABILITY_NAMED_IAM

  5. If things are set up correctly, the stack will fail. To confirm, check the StackStatus by using the following command– it will show ROLLBACK_IN_PROGRESS or ROLLBACK_COMPLETE.
    aws cloudformation describe-stacks --stack-name builder-application

  6. The stack failed to create because the builder-role you assumed does not have permissions to create the two execution roles without satisfying the conditions of the guardrails you created. To see the details, use the following command:
    aws cloudformation describe-stack-events --stack-name builder-application

    Look for the section with LogicalResourceId: MyLambdaFunctionExecutionRole and review the output for ResourceStatusReason.

  7. Because the original stack did not create successfully, you will not be able to update it, so you will need to delete it instead by using the following command:
    aws cloudformation delete-stack --stack-name builder-application

  8. To fix your stack, modify the builder-application.yml file by uncommenting the lines with the PermissionsBoundary and RoleName properties in the MyLambdaFunctionExecutionRole and MyStateMachineExecutionRole resources.

    For example, the MyLambdaFunctionExecutionRole resource should look like this with your account number for <account>:

     MyLambdaFunctionExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole PermissionsBoundary: arn:aws:iam::<account>:policy/builder-boundary RoleName: builder-lambda-role

  9. Launch a stack with your updated template by using the following command:
    aws cloudformation create-stack --stack-name builder-application --template-body file://builder-application.yml --capabilities CAPABILITY_NAMED_IAM

  10. Confirm the StackStatus is CREATE_COMPLETE by using the following command:
    aws cloudformation describe-stacks --stack-name builder-application

The builder-role you assumed does not have permissions to view the AWS Management Console and verify the application you deployed is working. However, you may do so by logging in to the console using another role with the appropriate permissions.

To verify the application you deployed is working

  1. Log in to the AWS Management Console using another role with the appropriate permissions.
  2. Navigate to the Step Functions console, choose State machines.
  3. Choose the state machine you just created, choose Start execution, choose Start execution again, and view the output of the Lambda function.

Now builders can safely assume the new builder-role and launch application stacks that include IAM resources.

Clean up

After you have finished testing, clean up the resources created in this example. Because the builder-role does not have permissions to delete IAM policies, you will need to assume a different role that can manage IAM resources to complete steps 3-6 below.

To clean up

  1. Delete the builder-application stack by using the following command:
    aws cloudformation delete-stack --stack-name builder-application

  2. Detach the builder-policy from builder-role by using the following command, replacing <account> with your account number:
    aws iam detach-role-policy --role-name builder-role --policy-arn arn:aws:iam::<account>:policy/builder-policy

  3. Delete the builder-role by using the following command:
    aws iam delete-role --role-name builder-role

  4. Delete the builder-policy by using the following command, replacing <account> with your account number:
    aws iam delete-policy --policy-arn arn:aws:iam::<account>:policy/builder-policy

  5. Delete the builder-boundary by using the following command, replacing <account> with your account number:
    aws iam delete-policy --policy-arn arn:aws:iam::<account>:policy/builder-boundary 

Service control policies

Permissions boundaries are applied to individual IAM users or roles within an account. So, if your organization has multiple accounts, you must create and maintain these boundaries in each account. But what if you’d like to apply a set of rules across some or all of your accounts? In this case, you could use service control policies (SCPs), which are a feature of AWS Organizations, to provide central control over the maximum available permissions for multiple accounts in your organization. Like permissions boundaries, SCPs use the same IAM policy language with fine-grained control, but do not actually grant any permissions.

SCPs provide a way for you to simplify control and management of permissions across multiple accounts. And like permissions boundaries, different SCPs can be crafted to be more or less restrictive depending on the account type (for example, dev, test, or prod), and can serve as another guardrail to provide builders the capabilities they need while restricting access to specific services, features, and actions. To learn more about creating SCPs, see the example service control policies section in the AWS Organizations User Guide.

Additional tools

Creating and managing tightly scoped policies and roles is an ongoing process that requires a lot of thought and attention to detail. AWS IAM enables fine-grained access control to AWS services, and permissions boundaries are an advanced feature. Managing IAM permissions boundaries, roles, and policies has its own development, testing, and deployment lifecycle. We recommend learning to use the IAM Policy Simulator to test policies and determine whether or not specific actions are allowed for a given user, group, or role. In addition, you can create and update policies using the Visual editor or JSON tab in the IAM console where you will get insights, warnings, and suggestions related to security and errors in IAM policies.

After policies have been created, it is important to continuously monitor and audit them. Two tools you can use for this are:

  • Access Advisor – to review when services and actions were last accessed.
  • Access Analyzer – to identify resources that are shared with an external identity.

Lastly, the AWS Cloud Development Kit (CDK) has built-in convenience methods to help you follow best practices, including the ability to generate least-privilege policies for cloud applications with a single line of code.

Conclusion

In this post, you learned how to put policies and guardrails in place that will allow your organization to grant IAM permissions to builders. These changes will enable your builders to develop and deploy cloud infrastructure and applications more rapidly, and will help strengthen your organization’s security culture by extending the responsibility to a broader group. To learn more about creating, testing, and refining IAM policies, see Creating IAM policies, Testing IAM policies, and Refining permissions using access information in the IAM User Guide.

 
If you have feedback about this post, submit comments in the Comments section below. If you have questions about this post, contact AWS Support.

Want more AWS Security news? Follow us on Twitter.

Jeb Benson

Jeb Benson

Jeb is a senior consultant with AWS WWPS Professional Services based in Colorado.