API Gateway Resource Policies

Thi API Gateway Resource policy troubleshooting guide is based on How API Gateway resource policy affect authorization workflow https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-authorization-flow.html

How to use this guide

1. Identify the Authorization workflow applicable for your Use case

2. Based on authorization workflow and If API is accessed from same account or cross account , refer the Table A or Table B (If needed)

3. Refer to the policy examples based on use case

API Gateway resource policy only

API Gateway Resource Policy

IAM Authentication + Resource Policy

API Gateway Resource Policy

Lambda Authorizer + Resource Policy

API Gateway Resource Policy

Amazon Cognito Authentication + Resource Policy

API Gateway Resource Policy

POLICY EVALUATION TABLES

TABLE A

SAME ACCOUNT : When access to an API Gateway API is controlled by an IAM policy (or a Lambda or Amazon Cognito user pools authorizer) and an API Gateway resource policy, both of which are in the same AWS account.
  
 A few examples:
 1. If IAM User/Role policy ALLOWS but In API Gateway resource policy an Explicit Allow could not be found then as per Row 2, access would be Allowed.
  
 2. If IAM User/Role policy DENY but In API Gateway resource policy an Explicit Allow could not be found then as per Row 8, access would be Explicitly Denied.
 {"Message": "User: arn:aws:iam::ACCOUNT_ID:user/USER_ID is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/dev/GET/ with an explicit deny"}

  3. If IAM User/Role policy does not have an ALLOW/DENY  and  In API Gateway resource policy an ALLOW/DENY could not be found then as per Row 5, access would be Implicit Denied.
 {"Message": "User: arn:aws:iam::ACCOUNT_ID:user/USER_ID is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/dev/GET/"}

Note: If the error does not specify about Explicity Deny, This would indicate an Implicit Deny and would be worth checking for Allow conditions exist or not for the resource.
IAM Entity(User/Role) (or Lambda or Amazon Cognito user pools authorizer)API Gateway resource policyResulting behavior
AllowAllowAllow
AllowNeither Allow nor DenyAllow
AllowDenyExplicit Deny
Neither Allow nor DenyAllowAllow
Neither Allow nor DenyNeither Allow nor DenyImplicit Deny
Neither Allow nor DenyDenyExplicit Deny
DenyAllowExplicit Deny
DenyNeither Allow nor DenyExplicit Deny
DenyDenyExplicit Deny

TABLE B

CROSS ACCOUNT : When access to an API Gateway API is controlled by an IAM policy (or a Lambda or Amazon Cognito user pools authorizer) and an API Gateway resource policy, which are in different AWS accounts.

IAM Entity(User/Role) (or Lambda or Amazon Cognito user pools authorizer)API Gateway resource policyResulting behavior
AllowAllowAllow
AllowNeither Allow nor DenyImplicit Deny
AllowDenyExplicit Deny
Neither Allow nor DenyAllowImplicit Deny
Neither Allow nor DenyNeither Allow nor DenyImplicit Deny
Neither Allow nor DenyDenyExplicit Deny
DenyAllowExplicit Deny
DenyNeither Allow nor DenyExplicit Deny
DenyDenyExplicit Deny

POLICY EXAMPLES

Allow a Single IAM User

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT_ID:user/USER_ID"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Allow an IAM Role

For assumed roles as well use, the Actual IAM role ARN not the STS assumed role ARN.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT_ID:role/testrole"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Allow an account

If you allow an Account in the Principal, All the IAM entities for that account would be allowed as long as the IAM entities(USER/ROLES) are having permissions to access

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT_ID:root"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Deny all except an IAM User

https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html#specifying-notprincipal

As a best practice, you should include the ARNs for the account in your policy. Some services require the account ARN, although this is not required in all cases. Any existing policies without the required ARN will continue to work, but new policies that include these services must meet this requirement. IAM does not track these services, and therefore recommends that you always include the account ARN.

Therefore, When using NotPrincipal, Always specify root account ARN in the AWS principal along with specific IAM User/Roles.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "NotPrincipal": {
                "AWS": [
                    "arn:aws:iam::ACCOUNT_ID:root",
             "arn:aws:iam::ACCOUNT_ID:user/USER_ID"
                ]
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Allow a Cross-Account user

Unlike NotPrincipal element, When using Principal JSON element It is not needed to specify the the ARNs for the account in your policy If an Explicit Allow is not found, It will result in Implicit Deny.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT_ID:user/USER"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Deny all except a specific Cross-account user/role

If an Explicit Allow is not found, It will result in Implicit Deny.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "NotPrincipal": {
                "AWS": [
                    "arn:aws:iam::ACCOUNT_ID:root",
             "arn:aws:iam::ACCOUNT_ID:user/burner"
                ]
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Allow an IP Range

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition" : {
                "IpAddress": {
                    "aws:SourceIp": [ "{{sourceIpOrCIDRBlock}}", "{{sourceIpOrCIDRBlock}}" ]
                }
            }
        } 
}

Deny all except an IP Range

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition" : {
                "NotIpAddress": {
                    "aws:SourceIp": [ "{{sourceIpOrCIDRBlock}}", "{{sourceIpOrCIDRBlock}}" ]
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:API_ACCOUNT:API_ID/*/*/*"
        }
    ]
}

Allow VPC

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:sourceVpc": "{{vpcID}}"
                }
            }
        }
    ]
}

Deny all except a VPC

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:sourceVpc": "{{vpcID}}"
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Allow VPC Endpoint

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:sourceVpce": "{{vpceID}}"
                }
            }
        }
    ]
}

Deny all except a VPC Endpoint

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:sourceVpce": "{{vpceID}}"
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}

Private API : Deny API traffic based on source vpc IP address or range

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": [
                "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
            ]
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": [
               "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
            ],
            "Condition" : {
                "IpAddress": {
                    "aws:VpcSourceIp": ["192.0.2.0/24", "198.51.100.0/24"]
                }
            }
        }
    ]
}

aws:PrincipalAccount

This example policy would Deny all the requests If the requesting principal belongs to other account than specified in aws:PrincipalAccount

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": {
                "AWS": "*"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:PrincipalAccount": "ACCOUNT_ID"
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:1234567890:API_ID/*/*/*"
        }
    ]
}
Access Logs of Failed Request(Principal Account does not match) "authorizeError": "The client is not authorized to perform this operation.",
"identityAccountIdIAM": "ACCOUNT_ID",
"identityCallerIAM": "ACCESS_ID",
"identityPrincipalOrgId": "o-xxxxxxxxxx",
"IAMuserArn": "arn:aws:iam::ACCOUNT_ID:user/USER_ID",
"ownerAccountId": "ACCOUNT_ID",
Access Logs of Passing Request (Principal Account matches) "authorizeError": "-",
"identityAccountIdIAM": "ACCOUNT_ID",
"identityCallerIAM": "ACCESS_ID",
"identityPrincipalOrgId": "o-xxxxxxxxxx",
"IAMuserArn": "arn:aws:iam::ACCOUNT_ID:user/USER_ID",
"ownerAccountId": "ACCOUNT_ID",

aws:PrincipalArn

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": {
                "AWS": "*"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:PrincipalArn": "arn:aws:iam::ACCOUNT_ID:user/burner"
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*/*"
        }
    ]
}
Access Logs of Failed Request(Principal Arn does not match) "authorizeError": "The client is not authorized to perform this operation.",
"identityAccountIdIAM": "ACCOUNT_ID",
"identityCallerIAM": "ACCESS_ID",
"identityPrincipalOrgId": "o-XXXX",
"IAMuserArn": "arn:aws:iam::ACCOUNT_ID:user/USER_ID",
"ownerAccountId": "ACCOUNT_ID",   Access Logs of Passing Request (Principal Arn matches) "authorizeError": "-",
"identityAccountIdIAM": "ACCOUNT_ID",
"identityCallerIAM": "ACCESS_ID",
"identityPrincipalOrgId": "o-XXXXX",
"IAMuserArn": "arn:aws:iam::ACCOUNT_ID:user/burner",
"ownerAccountId": "ACCOUNT_ID",