Programster's Blog

Tutorials focusing on Linux, programming, and open-source

AWS IAM Examples

Below are some example configurations that I find useful in restricting applications to doing only what they need to do.

Lock To Region

If you want a quick way to lock an IAM account down to a single region such as London:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:RequestedRegion": "eu-west-2"
                }
            }
        }
    ]
}

I have not tested this against things like Cloudfront, but I assume one will not be able to deploy such a distribution because CDN's are inherently cross-region.

SES

SES Send Email From Specific IP

The following configuration only allows sending emails, and only if the requests for sending those emails came from the specified IP addresses. This means that even if your credentials leaked out somehow, people wouldn't be able to use them to send emails. They would have to have access to your server.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "ses:SendRawEmail"
            ],
            "Resource": "*",
            "Condition": {
                "ForAnyValue:IpAddress": {
                    "aws:SourceIp": [
                        "x.x.x.x/32",
                        "x.x.x.x/32"
                    ]
                }
            }
        }
    ]
}

The above worked in the AWS cli for sending an email thorugh SES, but would not work when plugging details in for SMTP. For being able to send an SMTP request I created SMTP credentials in the AWS console (not IAM), and added the condition below to the generated user retrospectively.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ses:SendRawEmail",
            "Resource": "*",
            "Condition": {
                "ForAnyValue:IpAddress": {
                    "aws:SourceIp": [
                        "x.x.x.x/32",
                        "x.x.x.x/32"
                    ]
                }
            }
        }
    ]
}

If you wish to further restrict it, so that emails can only come from a specified email address, then you can add this like so:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "ses:SendRawEmail"
            ],
            "Resource": "*",
            "Condition": {
                "ForAnyValue:IpAddress": {
                    "aws:SourceIp": [
                        "x.x.x.x/32",
                        "x.x.x.x/32"
                    ]
                },
                "StringEquals": {
                    "ses:FromAddress": "my-email@my.domain.com"
                }
            }
        }
    ]
}

EC2

EC2 Snapshotting

If you need a user that just has the ability to create AMI images (which requires snapshotting of the ebs volumes), then this is what you need (courtesy of a post on serverfault):

{     
  "Effect": "Allow",
  "Action": [
    "ec2:Describe*",
    "ec2:CreateSnapshot",
    "ec2:CreateImage"
  ],
  "Resource": [
    "*"
  ]
}

S3

S3 Access To Specific Bucket

The following configuration will give full access to everything in the my-example-bucket bucket in S3 (as long as you own it).

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::my-example-bucket",
                "arn:aws:s3:::my-example-bucket/*"
            ]
        }
    ]
}

Below is the version I used to use:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::my-example-bucket"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:PutObjectVersionAcl",
        "s3:PutObjectAcl"
      ],
      "Resource": ["arn:aws:s3:::my-example-bucket/*"]
    }
  ]
}

The above (outdated) configuration won't let you upload to it through the web console. This alternative version from here did:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListAllMyBuckets"
            ],
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::my-bucket-name",
                "arn:aws:s3:::my-bucket-name/*"
            ]
        }
    ]
}

Terraform S3 Backend Permissions

This is the policy required for an IAM user/credentials that Terraform would need in order to use S3 as a backend (store your Terraform state).

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::mybucket"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::mybucket/path/to/my/key"
    }
  ]
}

This configuration was copied directly from the Terraform docs which you may wish to refer to instead.

Elastic Transcoder

Elastic Transcoder - Restricted To Single Pipeline

Use the JSON configuration below, just swapping out the identifier for the pipeline.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "elastictranscoder:ListPipelines",
                "elastictranscoder:ListJobsByStatus",
                "elastictranscoder:ListPresets"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "elastictranscoder:Read*",
                "elastictranscoder:*Job",
                "elastictranscoder:*Preset",
                "elastictranscoder:List*"
            ],
            "Resource": [
                "arn:aws:elastictranscoder:*:*:job/*",
                "arn:aws:elastictranscoder:*:*:pipeline/2457257325632-wysdbs",
                "arn:aws:elastictranscoder:*:*:preset/*"
            ]
        }
    ]
}

I made up the pipeline ID 2457257325632-wysdbs in the example configuration, but it should be close enough to a real name that you should recognize that you want the short identifier, not the really long ARN.

Route 53

Nginx Proxy Manager - Let's Encrypt DNS Challenge

If you are using Nginx Proxy Manager, and wish for it to be able to perform DNS challenges, then you can create a strict IAM policy for it like so:

{
    "Version": "2012-10-17",
    "Statement": [
       {
            "Effect": "Allow",
            "Action": "route53:GetChange",
            "Resource": "arn:aws:route53:::change/*"
        },
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "route53:ChangeResourceRecordSets"
            ],
            "Resource": "arn:aws:route53:::hostedzone/Z07XXXXXXXXXXXXXX",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx/32"
                },
                "ForAllValues:StringLike": {
                    "route53:ChangeResourceRecordSetsNormalizedRecordNames": [
                        "_acme-challenge.*"
                    ],
                    "route53:ChangeResourceRecordSetsRecordTypes": ["TXT"]
                }
            }
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "route53:ListHostedZones",
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx/32"
                }
            }
        }
    ]
}

This will require the requests to come from your server, and the only types of records that can be changed are TXT records, whose values must start with _acme-challenge., which all the DNS challenges do.

You will need to change all instances of xxx.xxx.xxx.xxx/32 to the CIDR of your Nginx Proxy Manager server, and arn:aws:route53:::hostedzone/Z07XXXXXXXXXXXXXX to the ARN of the hosted zone for the domain you wish to change. If you wish to manage multiple zones, then you need to change this to an array of ARNs.

References

Last updated: 3rd October 2023
First published: 18th August 2020