Back to Checks
aws

VPC endpoint policy allows access only from trusted AWS accounts

vpc_endpoint_connections_trust_boundaries
Service: vpcSeverity: high
OfficialBuilt-inVerified
Categories:trust-boundariesidentity-access
Resource Type:AwsEc2VpcEndpointService

VPC endpoint policies are assessed for restriction to configured trusted AWS accounts. If Principal values (including *) or account ARNs permit non-trusted principals, or conditions aren't sufficiently restrictive, the endpoint is identified. Endpoints without editable policies are excluded.

Risk

Non-trusted principals using your endpoint can access AWS services as if from your VPC, weakening segmentation. This enables unauthorized reads/writes and data exfiltration from resources tied to the endpoint, harming confidentiality and integrity, and potentially increasing costs.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks vpc_endpoint_connections_trust_boundaries

Recommendation

Apply least privilege: restrict endpoint policies to your account and an explicit allowlist of trusted accounts. Avoid * principals unless coupled with strict conditions. Prevent transitive trust across network links, and use resource policies and monitoring as defense in depth to limit endpoint use.

Remediation

CLI

aws ec2 modify-vpc-endpoint --vpc-endpoint-id <example_resource_id> --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"*","Resource":"*","Condition":{"StringEquals":{"aws:PrincipalAccount":["<TRUSTED_ACCOUNT_ID_1>","<TRUSTED_ACCOUNT_ID_2>"]}}}]}'

Terraform

# Add this to the existing VPC endpoint resource to restrict access
resource "aws_vpc_endpoint" "<example_resource_name>" {
  # ...existing required arguments for the endpoint...

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = "*"
      Action    = "*"
      Resource  = "*"
      Condition = {                       # CRITICAL: restrict by trusted accounts
        StringEquals = {
          "aws:PrincipalAccount" = [      # CRITICAL: only these accounts can use the endpoint
            "<TRUSTED_ACCOUNT_ID_1>",
            "<TRUSTED_ACCOUNT_ID_2>"
          ]
        }
      }
    }]
  })
}

NativeIaC

# CloudFormation: restrict VPC endpoint access to trusted accounts only
Resources:
  <example_resource_name>:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: <example_resource_id>
      ServiceName: com.amazonaws.<region>.<service>
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: "*"
            Action: "*"
            Resource: "*"
            Condition:                  # CRITICAL: restrict by trusted accounts
              StringEquals:             # CRITICAL: only allow specified accounts
                "aws:PrincipalAccount":   # CRITICAL: limits usage to these accounts
                  - "<TRUSTED_ACCOUNT_ID_1>"
                  - "<TRUSTED_ACCOUNT_ID_2>"

Other

1Open the AWS Console and go to VPC > Endpoints
2Select the endpoint and choose Actions > Manage policy
3Select Custom and paste this minimal policy, replacing with your trusted account IDs:
4```json
5{
6"Version": "2012-10-17",
7"Statement": [
8{
9"Effect": "Allow",
10"Principal": "*",
11"Action": "*",
12"Resource": "*",
13"Condition": {
14"StringEquals": {
15"aws:PrincipalAccount": [
16"<TRUSTED_ACCOUNT_ID_1>",
17"<TRUSTED_ACCOUNT_ID_2>"
18]
19}
20}
21}
22]
23}
24```
25Click Save

Check Code

from re import compile

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.iam.lib.policy import is_condition_block_restrictive
from prowler.providers.aws.services.vpc.vpc_client import vpc_client


class vpc_endpoint_connections_trust_boundaries(Check):
    def execute(self):
        findings = []
        # Get trusted account_ids from prowler.config.yaml
        trusted_account_ids = vpc_client.audit_config.get("trusted_account_ids", [])
        # Always include the same account as trusted
        trusted_account_ids.append(vpc_client.audited_account)
        for endpoint in vpc_client.vpc_endpoints:
            # Check VPC endpoint policy and  avoid "com.amazonaws.vpce" endpoints since the policy cannot be modified
            if (
                endpoint.policy_document
                and "com.amazonaws.vpce." not in endpoint.service_name
            ):
                access_from_trusted_accounts = True
                for statement in endpoint.policy_document["Statement"]:
                    # If one policy allows access from a non-trusted account
                    if not access_from_trusted_accounts:
                        break
                    if "*" == statement["Principal"]:
                        access_from_trusted_accounts = False
                        report = Check_Report_AWS(
                            metadata=self.metadata(), resource=endpoint
                        )

                        if "Condition" in statement:
                            for account_id in trusted_account_ids:
                                if is_condition_block_restrictive(
                                    statement["Condition"], account_id
                                ):
                                    access_from_trusted_accounts = True
                                else:
                                    access_from_trusted_accounts = False
                                    break

                        if not access_from_trusted_accounts:
                            report.status = "FAIL"
                            report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} can be accessed from non-trusted accounts."
                        else:
                            report.status = "PASS"
                            report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} can only be accessed from trusted accounts."

                        findings.append(report)
                        if not access_from_trusted_accounts:
                            break

                    else:
                        if "AWS" in statement["Principal"]:
                            if isinstance(statement["Principal"]["AWS"], str):
                                principals = [statement["Principal"]["AWS"]]
                            else:
                                principals = statement["Principal"]["AWS"]
                        else:
                            # If the principal is not an AWS principal, we don't need to check it since it could be a service or a federated principal
                            principals = []
                        for principal_arn in principals:
                            report = Check_Report_AWS(
                                metadata=self.metadata(), resource=endpoint
                            )

                            if principal_arn == "*":
                                access_from_trusted_accounts = False
                                if "Condition" in statement:
                                    for account_id in trusted_account_ids:
                                        if is_condition_block_restrictive(
                                            statement["Condition"], account_id
                                        ):
                                            access_from_trusted_accounts = True
                                        else:
                                            access_from_trusted_accounts = False
                                            break

                                if not access_from_trusted_accounts:
                                    report.status = "FAIL"
                                    report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} can be accessed from non-trusted accounts."
                                else:
                                    report.status = "PASS"
                                    report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} can only be accessed from trusted accounts."

                                findings.append(report)
                                if not access_from_trusted_accounts:
                                    break
                            else:
                                # Account ID can be an ARN or just a 12-digit string
                                pattern = compile(r"^[0-9]{12}$")
                                match = pattern.match(principal_arn)
                                if not match:
                                    account_id = principal_arn.split(":")[4]
                                else:
                                    account_id = match.string

                                if account_id not in trusted_account_ids:
                                    access_from_trusted_accounts = False

                                if "Condition" in statement:
                                    for account_id in trusted_account_ids:
                                        if is_condition_block_restrictive(
                                            statement["Condition"], account_id
                                        ):
                                            access_from_trusted_accounts = True
                                        else:
                                            access_from_trusted_accounts = False
                                            break

                                if not access_from_trusted_accounts:
                                    report.status = "FAIL"
                                    report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} can be accessed from non-trusted accounts."
                                else:
                                    report.status = "PASS"
                                    report.status_extended = f"VPC Endpoint {endpoint.id} in VPC {endpoint.vpc_id} can only be accessed from trusted accounts."

                                findings.append(report)
                                if not access_from_trusted_accounts:
                                    break

        return findings