Creating conditional IAM policies in CloudFormation

Posted by Elliot Segler on Wed 28 April 2021

I though I'd write today about some syntax that doesn't appear to be well documented in the cloudformation template reference material. Many of our clients environments, and workloads, are complex in nature and end out wanting to bake lots of logic into to CloudFormation templates. This is particularly the case for named resources like IAM roles, where the nature of the role might change between workloads or specific environments (like dev through to prod).

While I'd argue it's good practice to limit use of conditional or programmatic logic in your cloudformation templates (which is a topic for another day), it's somewhat unavoidable in many circumstances. For those not aquanited with CloudFormation Conditons, they allow use to apply Boolean logic on the creation or applicability of resources. They can also be used in Intrinsic Functions to deterministically include, exclude or change the way attributes of a resource can be applied.

The use of these should be pretty common knowlegde and are a great tool for any CloudFormation user. What's far less obvious all of the places that you can use Intrinsic Functions. Specifically the Fn::If function can be used in the metadata attribute, update policy attribute, and property values in the Resources section and Outputs sections of a template.

Let's say that you want to optionally include or change the principal that can assume a role, based on the environment the workload is designated as (which is a very common pattern we see in the wild). You can do that inside the "body" of your AssumeRolePolicyDocument in your IAM resource.

Consider the the following below, which would check to see if this is a Prod environment:

Condition:
    IncludeProdConditionStatement: 
        !Equals 
            - !Ref Env 
            - "Prod"

Based on that, you can write a resource and make the policy something like the below:

MyExampleRole:
    Type: AWS::IAM::Role
    Properties:
        AssumeRolePolicyDocument:
            Version: 2012-10-17
            Statement:

                - Effect: Allow
                  Action:
                    - sts:AssumeRoleWithSAML
                  Principal:
                    Federated:
                        - !Sub "arn:aws:iam::${AWS::AccountId}:saml-provider/PROVIDER-NAME"
                  Condition:
                    StringEquals:
                        "SAML:aud": "https://signin.aws.amazon.com/saml"

                - !If IncludeProdConditionStatement
                - Effect: Allow
                  Action:
                    - sts:AssumeRole
                  # Some other properties ...
                - !AWS::NoValue

In your non-prod environments, the role would be assumable from your SAML provider (assuming you have one) but in Production, that could also be assumed by another account, say a security account or identity store account.

Writing templates using the conditions in this way, was definintely not obvious to me when I first started lookin at these reqirements and this certainly isn't the limit of what you could do.

Where I would though advise caution is, if you choose to do this you should definitely consider testing your templates. It's good practice anywhere there's programatic variability to apply testing for each possible outcome. If you haven't got tests for your CloudFormation, I'd encourage you to start. cfn-nag for linting/syntax and taskcat are great starting points.

If you are considering looking at this and you think you want help, please feel free to reach out as we are well placed to help! If you think you'd like to learn more or work at a place like ours also reach out because we are always looking for more talented people.