IAM policies

Before we release our web app to the world, we want to make sure it is as secure as possible. Amazon provides Identity and Access Management (IAM) functionality to ensure that even if our lambda functions are somehow compromised access to our AWS account is limited.

The way we do this is by specifying what resources our web app can access in one or more IAM policies. After deploying a zappa app, we can find the default IAM configuration by going to the IAM roles console and searching for the name of our app. This is what the default policy attached to that role looks like:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:*"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "xray:PutTraceSegments",
                "xray:PutTelemetryRecords"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AttachNetworkInterface",
                "ec2:CreateNetworkInterface",
                "ec2:DeleteNetworkInterface",
                "ec2:DescribeInstances",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DetachNetworkInterface",
                "ec2:ModifyNetworkInterfaceAttribute",
                "ec2:ResetNetworkInterfaceAttribute"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kinesis:*"
            ],
            "Resource": "arn:aws:kinesis:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "sns:*"
            ],
            "Resource": "arn:aws:sns:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "sqs:*"
            ],
            "Resource": "arn:aws:sqs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:*"
            ],
            "Resource": "arn:aws:dynamodb:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "route53:*"
            ],
            "Resource": "*"
        }
    ]
}

As you can see, there are a significant number of permissions that we probably don't need for a simple web app. Also, some large companies have blanket rejections of policies with wildcard (*) resources, so I'll show you ways to restrict the way those resources are specified. Sometimes getting the exact permissions we do need can be tricky, so I'm going to step through each element of the IAM policy above.

Before we step through the example, however, you should know how resources are referenced in IAM. All Resource elements (the thing the permissions are being provided for) are denoted with Amazon Resource Names (ARNs). The thing to understand about ARNs is that their structure is mostly the same across different resource types. Here's a list of possible ARN formats:

arn:partition:service:region:account-id:resource arn:aws:service:region:account-id:resourcetype/resource arn:aws:service:region:account-id:resourcetype/resource/qualifier arn:aws:service:region:account-id:resourcetype/resource:qualifier arn:aws:service:region:account-id:resourcetype:resource arn:aws:service:region:account-id:resourcetype:resource:qualifier

Unfortunately this isn't all that clear! The inevitable question is "but which ARN formats go with which service?" One way to find that out is to search for individual services on this page: Amazon Resource Names (ARNs) and AWS Service Namespaces. However, for the resources listed in the policy above I'm going to do that work for you.

Let's start at the beginning: the logs resource.

{
    "Effect": "Allow",
    "Action": [
        "logs:*"
    ],
    "Resource": "arn:aws:logs:*:*:*"
}

Here the default Zappa policy is allowing us to do every possible action that the logs service has (logs:*, the star meaning 'everything') and the resource that we can do those things to is every single log resource available. This is clearly less than ideal. What we want to look for is a way to only specify the logs from the resource types that zappa creates. Here are two more levels of additional security here that you could implement, depending on your needs:

  1. "Resource": "arn:aws:logs:[region]:[account-id]:log-group:/aws/lambda/*" will give you permissions to all log groups for Lambda. This is less breakable than specifying the specific log groups for your lambda, but also less secure. And please note one more confusing thing here: log-group is a literal string, not a placeholder for anything else (like region and account-id are).
  2. You can also use the more restricted format "Resource": "arn:aws:logs:[region]:[account-id]:log-group:/aws/lambda/[project_name]-[stage]", where project_name is literally whatever is in the project_name field in your zappa_settings.json file, and stage is the stage in that file (like dev or production in the example file we gave in the chapter on deploying Zappa). If you go this route you'll need to make a separate Statement element for each stage. This is more secure, but more breakable — if you ever change the stage or project_name values in your settings file, logging will break. As always, what you'll want to do will depend on the needs of your specific application.

Now let's look at the Lambda permissions:

{
    "Effect": "Allow",
    "Action": [
        "lambda:InvokeFunction"
    ],
    "Resource": [
        "*"
    ]
}

This statement is less permissive than the logs statement, since it limits the allowed actions to only lambda:InvokeFunction. However, it still allows InvokeFunction to be performed on any resource; one way to limit this statement further would be to only allow invocation of the specific lambdas created by Zappa. Here's one way to do this:

{
    "Effect": "Allow",
    "Action": [
        "lambda:InvokeFunction"
    ],
    "Resource": [
        "arn:aws:lambda:[region]:[account-id]:function:[project_name]-[stage]"
    ]
}

Where the values in square brackets are replaced appropriately.

Now let's discuss X-Ray permissions. X-Ray is a very cool introspection tool that allows you to more easily debug distributed applications. It basically lets you send traces from all the different components in a distributed system to X-Ray so you can see your whole application flow in one place — the X-Ray SDK helps you collect metadata from calls to both AWS services and non-AWS HTTP(S)-based services. However, this is not enabled by default in zappa. To use this AWS feature you'll need to set "xray_tracing": true in your zappa settings file. If you don't want to use it, you can just pull it out of your IAM policy. If you do want to use it, you may want to note the ARN of the X-Ray resource when it gets created and then restrict the relevant IAM statement in your policy.

Next let's take a look at ec2 permissions. These are only required if your application is reaching out to ec2 instances (for instance, an RDS database in your AWS account). If your application does not need network access to ec2 instances then feel free to remove this statement. However, if you do use this statement, I would suggest keeping the wildcard in the resource specification — in this particular case trying to specify all the precise ec2 resources acted upon is probably counterproductive. If you have blanket restrictions against wildcards in your organization, please speak to your security group about this. My guess is that they'll make an exception for this case.

{
    "Effect": "Allow",
    "Action": [
        "ec2:AttachNetworkInterface",
        "ec2:CreateNetworkInterface",
        "ec2:DeleteNetworkInterface",
        "ec2:DescribeInstances",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DetachNetworkInterface",
        "ec2:ModifyNetworkInterfaceAttribute",
        "ec2:ResetNetworkInterfaceAttribute"
    ],
    "Resource": "*"
}

The next service in the list is S3:

{
    "Effect": "Allow",
    "Action": [
        "s3:*"
    ],
    "Resource": "arn:aws:s3:::*"
}

My suggestion here would be to at least restrict access to the specific bucket(s) you will be accessing from your application. Ideally you'll know the locations within your bucket(s) that you'll be accessing, and you can further restrict access. For instance:

{
    "Effect": "Allow",
    "Action": [
        "s3:*"
    ],
    "Resource": "arn:aws:s3:::[bucket_name]/*"
}

The next service to look at is AWS Kinesis. Kinesis can be used to analyze realtime streaming data, but unless you have a specific use case for it, you don't need it to run Zappa applications. This statement can safely be removed from your locked-down config. The reason it is included in the default IAM policy for Zappa is that Zappa allows you to configure event sources that will trigger a lambda function — please see the Zappa documentation Executing in Response to AWS Events for more information about this.

{
    "Effect": "Allow",
    "Action": [
        "kinesis:*"
    ],
    "Resource": "arn:aws:kinesis:*:*:*"
}

SNS use is also allowed by the IAM policy, but you only need this statement if you're using Zappa's async feature in a specific way. In default operation, when you add an @task decorator to a function Zappa will directly spawn another Lambda to run the function and return immediately. You can read more about this feature here. However, if you choose to use the @task_sns decorator for the async feature, Zappa will need permissions to write to an SNS topic to spawn the async job. If you're not going to use the @task_sns decorator in your application (and you're not going to be using SNS as an event source as described in the Kinesis section above) you can remove this statement:

{
    "Effect": "Allow",
    "Action": [
        "sns:*"
    ],
    "Resource": "arn:aws:sns:*:*:*"
}

Next in the statement list is blanket SQS permissions. This statement is also optional unless you need SQS for your specific use case — it exists because SQS can be used as an event source, similar to Kinesis, SNS, S3, and DynamoDB. Again, for more info on this you can read the Zappa documentation section Executing in Response to AWS Events.

{
    "Effect": "Allow",
    "Action": [
        "sqs:*"
    ],
    "Resource": "arn:aws:sqs:*:*:*"
}

The DynamoDB statement is the last of the optional "event source" permissions. The reason it exists in the default IAM policy is so that DynamoDB can be used as an event source. However, even though we don't use it as an event source for our Profile App, we can't get rid of this permission statement entirely — we need it to write author profiles to the database. We can, however, only give permissions to the table used by our app:

{
    "Effect": "Allow",
    "Action": [
        "dynamodb:*"
    ],
    "Resource": "arn:aws:dynamodb:[region]:[account-id]:table/[table-name]"
}

Where [table-name] is the DynamoDB table you'll be accessing from your web app.

We're now finally to the last statement to evaluate in the default IAM policy: route53 permissions. There's really not much to say about this one. It can be removed from the IAM policy. It appears to be in the default policy because the Zappa CLI uses it to configure hosted zones.

{
    "Effect": "Allow",
    "Action": [
        "route53:*"
    ],
    "Resource": "*"
}


If you're finding this guide useful, you may want to sign up to receive more of my writing at cloudconsultant.dev.