Business Challenge
A team is asked to close an exfiltration risk. Their workload runs in private subnets, reaches S3 and DynamoDB through a NAT gateway, and the security review has flagged that outbound path. The fix is well understood: add a gateway endpoint for each service, remove the NAT gateway, save the per-gigabyte charge on the way.
They do it. Traffic to S3 no longer traverses the internet gateway. Flow logs show it going nowhere near the public path. The NAT gateway line item disappears. The review is updated to say data no longer leaves AWS, and the finding is closed.
Every factual statement in that paragraph is correct. The conclusion does not follow from them.
A compromised instance in that subnet can still write to an S3 bucket in an account nobody in the company has heard of. It goes through the endpoint, at endpoint speed, over AWS's own network, and appears in no flow log as internet-destined traffic. The team has not closed the exfiltration path. They have made it private, removed the NAT gateway charges that would have made it visible on a bill, and taken the internet gateway out of the picture that might have caught it.
"Does this traffic traverse the internet" and "which destinations can this traffic reach" are different questions with different answers, and a VPC endpoint only ever addresses the first. The second is decided by policy β and by a default that AWS applies on your behalf when you supply nothing.
Architecture
An endpoint policy is a resource-based policy that you attach to a VPC endpoint to control which AWS principals can use the endpoint to access an AWS service. That is the control. Here is the default it ships with.
The default is the whole story
The default endpoint policy grants full access to the endpoint, and it is exactly what you would write if you were trying to make a point:
{
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
]
}
And it is applied for you: if you don't attach an endpoint policy, we attach the default endpoint policy. So the console flow that creates an endpoint in four clicks produces a private path to every resource of that service type, in every AWS account, and reports success.
There is a second case with the same effect and less visibility. Not all AWS services support endpoint policies. If an AWS service doesn't support endpoint policies, we allow full access to any endpoint for the service. For those services the permissive outcome is not a default you failed to change β it is the only available behaviour, and the control has to live somewhere else entirely.
Three controls, three directions
The reason this is confusing in practice is that people reach for one control and expect it to answer all three questions. It cannot, because they face different ways.
What can be reached from here
The endpoint policy. It governs traffic leaving your network through this endpoint, and it is where aws:ResourceOrgID belongs if you want "this network may only reach our own buckets".
What can reach this
The resource policy β a bucket policy with aws:SourceVpce or aws:SourceVpc. It governs traffic arriving, from any network, and the endpoint policy has nothing to say about it.
The third is the one that catches what the other two miss: an organisation-level policy asserting that your identities may only reach your resources, on any path, endpoint or not. AWS frames the whole objective in one sentence: access can only be allowed if the identity is trusted, the resource is trusted, and the network is expected β and if any of these conditions are false, then the access inside the perimeter is unintended and should be denied.
Note that the circle being drawn is not the VPC. The circle that defines an AWS perimeter is typically represented as an AWS organization managed by AWS Organizations, and each AWS account you own is a logical container for AWS identities, resources, and networks. The network is one of three containers, which is why a network control alone closes one third of the problem.
It is another gate, not a replacement
One sentence in the documentation prevents a common misreading: an endpoint policy does not override or replace identity-based policies or resource-based policies. It joins the evaluation as another narrowing gate, in the sense established earlier in this series β it can take access away and it cannot grant any.
That has a practical consequence worth stating plainly. An endpoint policy allowing s3:* on * grants nobody anything; the identity policy still has to allow the call. And a restrictive endpoint policy cannot be relied on as the only control, because a principal reaching S3 by another path β a different VPC, a different account, the console β never passes through it.
The syntax trap, and it is the reverse of the one in #37
Gateway endpoints have their own principal rules. With gateway endpoints, the Principal element must be set to *, and to specify a principal you use the aws:PrincipalArn condition key. So far, ordinary.
Then this: if you specify the principal as an account ID in an endpoint policy, access is granted to the AWS account root user only, not all users and roles for the account.
Post #37 in this series established the opposite behaviour for a role trust policy, and AWS's wording there is equally direct: the account ARN and the shortened account ID behave the same way. Both delegate permissions to the account. Using the account ARN in the Principal element does not limit permissions to only the root user of the account. Same syntax, two policy types, opposite meanings. An author who learned the trust-policy rule and applies it here will write a policy they believe is broad and get one that admits a single principal nobody uses.
Why This Architecture Holds Up
It separates a cost decision from a security decision
Removing a NAT gateway is a good idea for reasons that have nothing to do with exfiltration: it removes a per-gigabyte charge and a component that can fail. Those benefits are real and they arrive the moment the endpoint exists. Treating them as a security outcome is what causes the finding to be closed prematurely, and separating the two lets a team take the cost win immediately while the policy work is still outstanding.
Each control has a scope you can state
Because the three face different directions, it is possible to say precisely what each covers and what it misses. An endpoint policy covers one endpoint in one VPC. A bucket policy covers one bucket from every network. An SCP covers every principal in an account on every path. Nobody has to guess which control an incident would have caught.
The default being permissive is at least consistent
A default that granted nothing would break every endpoint on creation and would be worked around by attaching a permissive policy β arriving at the same place with an extra step and a false sense of deliberation. The current default is honest about what it does, and the documentation states it in the same page as the mechanism. The gap is that creating an endpoint feels like a security action, so nobody goes looking.
The size budget is generous enough to enumerate
The size of an endpoint policy cannot exceed 20,480 characters, including white space β twice what a service control policy document gets. That matters because endpoint policies tend towards enumeration: a list of buckets, a list of accounts, a list of prefixes. There is room to be explicit here in a way there is not at the organisation level.
Key Architecture Decisions
An endpoint created without a policy is not unconfigured, it is configured permissively, and nothing distinguishes the two afterwards. In infrastructure as code that means the policy argument is not optional in your module — make it required, so a caller has to supply one or state why it is *.
Enumerate every VPC endpoint in the organisation and report which carry the default policy. That set is usually larger than expected, because endpoints get created during incidents and migrations by people solving a connectivity problem rather than an access one.
The two halves answer different questions and teams usually do one. aws:ResourceOrgID on the endpoint policy says this network may only reach resources we own. aws:SourceOrgID or aws:SourceVpce on the bucket policy says this bucket may only be reached from networks we own.
The first stops your instance writing to a stranger's bucket. The second stops a stranger's credential reading yours. Neither implies the other, and an exfiltration review that only asks about outbound traffic will find the first and miss the second entirely.
Where an AWS service doesn't support endpoint policies, AWS allows full access to any endpoint for the service. There is no configuration to get wrong and no finding to raise, which is precisely why this gap survives reviews.
Where the control goes insteadFor those services the perimeter has to be enforced at the identity and resource layers — an SCP with aws:ResourceOrgID covers every service and every path, including the ones with no endpoint policy support. Check the supported-service table when you add an endpoint, and record which of your endpoints are in this category rather than rediscovering it later.
Endpoint policies have syntax rules that do not apply to IAM policies generally, and each will fail in a way that is hard to read. You can't use wildcard characters or numeric condition operators with global context keys that reference system-generated identifiers, such as aws:PrincipalAccount or aws:SourceVpc. And when you use a string condition operator, you must use at least six consecutive characters before or after each wildcard character.
After you update an endpoint policy, it can take a few minutes for the changes to take effect. That is long enough to conclude a correct policy did not work and revert it. When testing, wait before deciding.
Which control catches which failure
| The failure | Endpoint policy | Resource policy | SCP or RCP |
|---|---|---|---|
| Our instance writes to a stranger's bucket, via our endpoint | Catches it | No | Catches it |
| Our instance writes to a stranger's bucket, via another path | No | No | Catches it |
| A stranger's credential reads our bucket | No | Catches it | RCP catches it |
| Our own credential reads our bucket from an unexpected network | No | Catches it | No |
| Traffic traverses the internet | Not a policy question — this is what the endpoint itself fixes | — | — |
Reference: the endpoint policy that makes the claim true
An S3 gateway endpoint policy restricted to resources inside your organisation. Note the Principal is * because a gateway endpoint requires it, and the narrowing is done entirely in the condition.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "OurOrganisationsResourcesOnly",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceOrgID": "o-exampleorgid"
}
}
}
]
}
Two things this does not do. It grants nothing, because an endpoint policy is a narrowing gate and the identity policy still has to allow the call. And it says nothing about who may reach your buckets β that needs aws:SourceVpce on the bucket policy, which is a separate change in a separate account, and is the half that gets forgotten.
Closing Thought
Network controls are satisfying because they produce a diagram you can point at. The line to the internet gateway disappears, the NAT gateway comes off the bill, and the picture looks materially safer than it did. Policy controls produce no diagram at all, which is part of why they get deferred.
What makes this particular gap durable is that the network change is genuinely worthwhile. It is not a mistake being corrected; it is a good change being credited with more than it did. The team that removed the NAT gateway improved their architecture and closed a finding that remained open.
The question to ask of any private-connectivity claim: if a compromised instance in this subnet wanted to send our data to an account we do not own, which control stops it? If the answer is the endpoint, check the policy on it. If there is no policy, AWS attached one, and it says Allow on everything.
Networking & Security — egress in the other direction: what a workload actually needs to reach on the internet, why an allowlist of domains is harder than it looks once TLS is involved, and where the inspection has to sit to be worth its cost.
Official AWS Reference
- AWS Documentation β Control access to VPC endpoints using endpoint policies
- AWS Whitepaper β Building a Data Perimeter on AWS
- AWS Documentation β Gateway endpoints for Amazon S3
- AWS Documentation β AWS services that integrate with AWS PrivateLink, and endpoint policy support
- AWS Documentation — AWS JSON policy elements: Principal
- AWS Documentation β AWS global condition context keys
- AWS Documentation β Resource control policies (RCPs)
Comments