Back to blog
AWSBest PracticesCloud SecurityIdentity & AccessNetworking

VPC Endpoint Allows Cross-Account Access: Risks and Remediation

Learn why a VPC endpoint policy granting cross-account access is risky, how to detect it, and step-by-step CLI and Terraform fixes to lock it down.

TL;DR

This check flags VPC endpoints whose endpoint policy grants access to AWS principals outside your own account. Cross-account access on an endpoint can let other accounts route traffic to your services or reach data they should not. Lock the policy down to your own account IDs, or scope it tightly to the specific external principals that genuinely need access.

VPC endpoints are one of those features that quietly do a lot of heavy lifting. They keep traffic to AWS services like S3, DynamoDB, and your own internal APIs off the public internet and on the AWS backbone. But because an endpoint sits between your network and a service, the policy attached to it is a real access control boundary, not just a routing detail. When that policy allows principals from another AWS account, you have effectively opened a door that most people never check.

This check, vpc_endpointcrossaccount, looks at every VPC endpoint in your AWS accounts and inspects the endpoint policy document. If the Principal element references an account ID, role, or user that does not belong to the account being scanned, the endpoint is flagged.


What this check detects

A VPC endpoint policy is an IAM resource policy attached to a gateway or interface endpoint. It controls which principals can use the endpoint and what actions they can perform through it. By default, gateway endpoints come with a wide-open policy ("Principal": "*" with full access), which is permissive but scoped to the VPC route table.

This check parses the policy and compares the principals named in each Allow statement against the account that owns the endpoint. It raises a finding when it sees something like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::999988887777:root"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-internal-bucket/*"
    }
  ]
}

Here, account 999988887777 is not the account that owns the endpoint. The check surfaces that as cross-account access so you can confirm it was intentional.

Note: An endpoint policy does not grant permissions on its own. It works as a filter on top of the IAM and resource policies that already exist. But because it is a filter, an overly broad endpoint policy removes a layer of defense rather than adding one. The check is about that missing layer.


Why it matters

The risk here is subtle, which is exactly why it slips past reviews. Cross-account access on a VPC endpoint can show up in a few ways, and each one has a different blast radius.

Data exfiltration through a shared service

Say you run an interface endpoint for an internal API or a gateway endpoint for an S3 bucket holding sensitive data. If the endpoint policy trusts an external account, a compromised role in that account can reach your service over the private AWS network, bypassing any expectation that "private" meant "only us." Network controls like security groups will not catch this because the traffic is legitimate from the endpoint's point of view.

Confused deputy and over-trusting partners

Cross-account endpoint policies are often added during a partner integration or a migration, then forgotten. The external account that was a trusted partner two years ago might now belong to a team you no longer work with, or worse, an account that has been recycled. The endpoint keeps trusting it regardless.

Warning: A wildcard principal combined with a wildcard resource ("Principal": "*" and "Resource": "*") on a gateway endpoint is the default for S3 and DynamoDB gateway endpoints. That default is broad by design, but it means any IAM principal that can reach the endpoint, including roles in other accounts that have network access, is only limited by the underlying resource policies. Tighten it.

Compliance and audit findings

Frameworks like SOC 2, PCI DSS, and internal data-residency rules generally expect you to demonstrate that access to sensitive systems is scoped to known, authorized identities. An endpoint policy that names an external account, especially without documentation, is an easy finding for an auditor and a hard one to explain after the fact.


How to fix it

Start by confirming whether the cross-account access is intentional. Some of these findings are legitimate, for example a shared services account or a deliberate partner integration. The goal is not to delete every external principal, it is to make sure each one is known, scoped, and documented.

Step 1: Find the endpoint and read its policy

List your endpoints and inspect the policy on each one.

aws ec2 describe-vpc-endpoints \
  --query 'VpcEndpoints[].{Id:VpcEndpointId,Service:ServiceName,Type:VpcEndpointType}' \
  --output table

Then pull the full policy for the endpoint in question.

aws ec2 describe-vpc-endpoints \
  --vpc-endpoint-ids vpce-0abc123def4567890 \
  --query 'VpcEndpoints[0].PolicyDocument' \
  --output text | jq .

Step 2: Identify the cross-account principals

Look at every Principal block. Any account ID that is not your own, any role ARN from another account, and any "AWS": "*" that is not scoped by a condition deserves scrutiny. Note them down with the resources and actions they can reach.

Step 3: Rewrite the policy with least privilege

If the external access is not needed, remove it and scope the policy to your own account. A tightened gateway endpoint policy for S3 might look like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:root"
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::my-internal-bucket/*"
    }
  ]
}

If the cross-account access is genuinely required, scope it down to the specific role rather than the whole account, and constrain the actions and resources. Naming the exact role removes the implicit trust of every identity in the partner account:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::999988887777:role/partner-ingest-role"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-shared-bucket/exports/*"
    }
  ]
}

Step 4: Apply the updated policy

Danger: Updating an endpoint policy takes effect immediately and can break live traffic. If a workload depends on the endpoint, removing a principal or narrowing actions can cause sudden access-denied errors. Validate the change in a staging account or during a maintenance window, and confirm which principals are actually using the endpoint before you cut anything.

aws ec2 modify-vpc-endpoint \
  --vpc-endpoint-id vpce-0abc123def4567890 \
  --policy-document file://endpoint-policy.json

Step 5: Verify

aws ec2 describe-vpc-endpoints \
  --vpc-endpoint-ids vpce-0abc123def4567890 \
  --query 'VpcEndpoints[0].PolicyDocument' \
  --output text | jq .

Tip: Before you tighten an endpoint policy, enable VPC endpoint flow logging or check CloudTrail data events for the underlying service to see which principals have actually used the endpoint recently. That turns guesswork into a clear allowlist and prevents the access-denied surprises.


Fixing it in infrastructure as code

If your endpoints are managed by Terraform or CloudFormation, change them there so the fix sticks and survives the next deploy. A console-only edit will be reverted the next time someone runs terraform apply.

Terraform example with a scoped policy:

resource "aws_vpc_endpoint" "s3" {
  vpc_id          = aws_vpc.main.id
  service_name    = "com.amazonaws.us-east-1.s3"
  route_table_ids = [aws_route_table.private.id]

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { AWS = "arn:aws:iam::111122223333:root" }
        Action    = ["s3:GetObject", "s3:PutObject"]
        Resource  = "arn:aws:s3:::my-internal-bucket/*"
      }
    ]
  })
}

How to prevent it from happening again

One-off fixes do not hold. The accounts that drift are the ones where the next engineer adds an external principal under deadline pressure. Catch it earlier.

Gate it in CI/CD with policy as code

Scan your Terraform plans before they reach AWS. A tool like Checkov, OPA/Conftest, or tfsec can flag endpoint policies that reference foreign account IDs. Here is a Conftest/Rego policy that fails when an endpoint policy names a principal outside an approved account list:

package main

approved_accounts := {"111122223333", "444455556666"}

deny[msg] {
  resource := input.resource.aws_vpc_endpoint[name]
  policy := json.unmarshal(resource.policy)
  statement := policy.Statement[_]
  statement.Effect == "Allow"
  principal := statement.Principal.AWS
  account := regex.find_n("[0-9]{12}", principal, 1)[0]
  not approved_accounts[account]
  msg := sprintf("VPC endpoint '%s' trusts non-approved account %s", [name, account])
}

Wire that into your pull request pipeline so a plan that introduces cross-account endpoint access fails the build and requires a documented exception.

Use an SCP guardrail

For a stronger control, a service control policy at the organization level can prevent endpoint policies from being modified outside an approved process, or you can restrict who can call ec2:ModifyVpcEndpoint at all. Keep that permission with a small, audited set of roles.

Continuously monitor with Lensix

Manual reviews miss endpoints created in regions nobody looks at. Lensix runs vpc_endpointcrossaccount across every account and region on a schedule, so a new cross-account endpoint surfaces as a finding within the next scan rather than during an audit a year later. Pair it with alerting so the owning team hears about it the same day.


Best practices

  • Default to your own account. Endpoint policies should reference your account or specific internal roles unless there is a documented reason to do otherwise.
  • Scope to roles, not root. When external access is required, name the exact role ARN instead of account:root. Trusting an account trusts every identity in it.
  • Constrain actions and resources. Replace "Action": "*" and "Resource": "*" with the minimum the workload needs. The endpoint policy is a filter, so make it a tight one.
  • Use conditions. Add aws:PrincipalOrgID conditions to restrict access to identities within your organization, or aws:SourceVpc to bind access to a specific VPC.
  • Document every exception. If a partner account is allowed, record why, who owns it, and when it should be reviewed. Undocumented trust is the thing auditors and attackers both find first.
  • Review on a cadence. Endpoint policies are write-once-forget-forever by nature. Schedule a quarterly review of every cross-account grant and remove what no longer applies.

Tip: The aws:PrincipalOrgID condition is the cleanest way to keep an endpoint open enough for internal cross-account use without naming every account by hand. It automatically covers new accounts you add to your org and excludes everything outside it.

Cross-account access on a VPC endpoint is rarely a deliberate decision that someone is proud of. It is usually a leftover from an integration or a too-broad default that nobody circled back to. Treat the endpoint policy as the access boundary it actually is, scope it to known identities, and put a check in your pipeline so the next broad policy never makes it to production.