---
cuppy_hub: aws
cuppy_path: patterns/iam-to-vpc-endpoints.md
cuppy_section: docs
cuppy_doc: TGER-J63A
cuppy_doc_uuid: 1339cd64-02ec-417c-890b-e7989af785a6
cuppy_permalink: https://cuppy.io/aws/d/TGER-J63A
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/patterns/iam-to-vpc-endpoints
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 57387efb90ebaa20e5ebb9f8535199cc0c517872bfe5f3345e634f96453066bd
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# IAM to VPC endpoints

**When traffic to an AWS service goes through a VPC endpoint, the endpoint's policy is one more layer that must allow the request.** It never grants anything: the caller's identity policy and any resource policy must still allow. With no policy set, AWS attaches a full-access default, so endpoint denials only start when someone narrows it.

## What crosses the handoff

| Side | Emits / expects |
|---|---|
| **Caller (IAM identity in the VPC)** | A signed request for an action on a resource, routed through the endpoint because of VPC routing (gateway endpoints) or DNS (interface endpoints). It carries the caller's principal and, because it used the endpoint, the endpoint ID as `aws:SourceVpce`. |
| **VPC endpoint** | One JSON policy per interface or gateway endpoint for an AWS service. It must contain a `Principal` element and be at most 20,480 characters including white space. It is evaluated against the principal, action and resource of each request passing through. |
| **Target resource** (bucket, queue, secret) | Its own resource policy, which can require the endpoint by testing `aws:SourceVpce`, or the VPC by testing `aws:SourceVpc`. |

Endpoints for services that are not AWS services (another account's service, Marketplace) have no policy and full access. Not every AWS service supports endpoint policies; where unsupported, the endpoint allows everything.

## Mismatches that bite

- **`aws:SourceIp` disappears.** It is absent when the request travels through a VPC endpoint. A bucket policy that allows by office or NAT IP range stops matching as soon as an endpoint is added: an Allow keyed on it no longer applies, and a Deny written with a negated IP operator now matches and denies.
- **`aws:SourceVpce` appears only on the endpoint path.** It is present only when the requester used a VPC endpoint. A resource policy that requires a specific endpoint therefore denies every request made without that endpoint, including requests from the console.
- **Account principals in a gateway endpoint policy.** For gateway endpoints the `Principal` must be `"*"`; to limit who can use it, test `aws:PrincipalArn` in a condition. Writing `"AWS": "<account-id>"` grants only that account's **root user**, not the users and roles in it.
- **Wildcards and system keys.** `*` and `?` and numeric operators cannot be used with system-generated global keys such as `aws:PrincipalAccount` or `aws:SourceVpc` in an endpoint policy. A string wildcard needs at least six consecutive characters before or after it. An ARN's account field is either an account ID or a wildcard, never both.
- **Propagation.** A changed endpoint policy can take a few minutes to take effect; test again before concluding a fix did not work.
- **CloudTrail gaps.** An access denial from an endpoint policy may not appear in CloudTrail when the endpoint belongs to a different account than the caller or the target role.

## Permissions on each side

- **Caller:** its identity-based policy must allow the action on the resource, exactly as without the endpoint.
- **Endpoint owner:** whoever runs `aws ec2 modify-vpc-endpoint --vpc-endpoint-id <id> --policy-document file://endpoint-policy.json` needs permission to modify the endpoint in the VPC's account.
- **Resource owner:** controls the resource policy, including any `aws:SourceVpce` / `aws:SourceVpc` requirement.

## Known-good

A gateway endpoint that only one role may use, combined with a bucket that only accepts requests through that endpoint:

```json
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "*",
    "Resource": "*",
    "Condition": { "StringEquals": { "aws:PrincipalArn": "arn:aws:iam::<account-id>:role/<app-role>" } }
  }]
}
```

On the bucket, a Deny for requests that did not come through `vpce-<id>`, tested with `StringNotEquals` on `aws:SourceVpce`, scoped so that it does not lock out the administrators who must manage the bucket from outside the VPC. Write that exception before applying the Deny.

## Known-bad

| Configuration | Symptom |
|---|---|
| Endpoint policy narrowed to a list of actions that omits one the application uses | `User: <arn> is not authorized to perform: <action> … because no VPC endpoint policy allows the <action> action` |
| Endpoint policy with an explicit Deny on an action | `… with an explicit deny in a VPC endpoint policy` |
| Gateway endpoint policy with `"Principal": {"AWS": "<account-id>"}` | Every role and user in that account is refused through the endpoint (only the account root user is granted) |
| Bucket policy requiring `aws:SourceVpce` with no exception for administrators | Console and out-of-VPC access to the bucket is denied, including for administrators; the message names the resource-based policy, not the endpoint |
| Bucket policy allowing by `aws:SourceIp` after an endpoint is added | Requests from inside the VPC are refused by the bucket policy, though nothing about the caller changed |

## Which side refused?

- **Message names a VPC endpoint policy** → the endpoint. Read its policy with `aws ec2 describe-vpc-endpoints --vpc-endpoint-ids <id>` (UNVERIFIED command form) and compare it with the action and principal.
- **Message names a resource-based policy** → the target's own policy, usually an `aws:SourceVpce`, `aws:SourceVpc` or `aws:SourceIp` condition; compare the condition with the path the request took.
- **Message names an identity-based policy** → the caller; the endpoint is not involved.
- **No denial in CloudTrail and the endpoint is in another account** → suspect the endpoint first.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
