Guide · AWS VPC · Endpoint Security

VPC Endpoint Policies for MCP Servers — Resource-Scoped Access Control for S3, Secrets Manager, and Bedrock

A VPC endpoint policy is an IAM resource policy attached to a VPC endpoint that adds a second layer of access control on top of IAM identity policies — every API call through the endpoint must be allowed by both the caller's IAM policy and the endpoint policy. The default endpoint policy is permissive (Allow * on *), which means any principal with appropriate IAM permissions can call any resource in the service through the endpoint. Tightening endpoint policies closes a class of privilege-escalation risk: even if an MCP server's IAM role is compromised or over-provisioned, the endpoint policy can restrict which S3 buckets, which Secrets Manager secrets, or which Bedrock models are reachable from within that VPC. Combined with S3 bucket policies that deny access except via the VPC endpoint (the aws:SourceVpce condition), you get a network-enforced boundary that IAM alone cannot provide.

TL;DR

Set a custom endpoint policy on your S3 Gateway endpoint to restrict access to specific bucket ARNs. On your Secrets Manager Interface endpoint, restrict actions to only GetSecretValue and DescribeSecret for specific secret ARN patterns. On your S3 bucket, add a condition that denies access if the request does not come via the VPC endpoint (aws:SourceVpce). The endpoint policy does not grant permissions — it only restricts what's already allowed by IAM. Both policies must allow the call.

How endpoint policies layer with IAM

Endpoint policies follow the same evaluation logic as other IAM policy types, but they operate at the endpoint level, not the principal level. When a call goes through an endpoint, AWS evaluates: (1) the caller's IAM identity policy (does the role allow this action on this resource?), (2) the endpoint policy (does the endpoint allow this principal to call this action on this resource?), and (3) the resource policy if one exists (e.g., S3 bucket policy). All three must allow the call — any deny terminates evaluation.

# The default endpoint policy — allows everything:
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "*",
    "Resource": "*"
  }]
}

# Restrict an S3 Gateway endpoint to specific buckets only:
aws ec2 modify-vpc-endpoint \
  --vpc-endpoint-id vpce-0s3gateway \
  --policy-document '{
    "Statement": [{
      "Effect": "Allow",
      "Principal": "*",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket",
                 "s3:GetBucketLocation", "s3:DeleteObject"],
      "Resource": [
        "arn:aws:s3:::mcp-tool-outputs",
        "arn:aws:s3:::mcp-tool-outputs/*",
        "arn:aws:s3:::mcp-config-bucket",
        "arn:aws:s3:::mcp-config-bucket/*"
      ]
    },
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": ["s3:GetObject"],
      "Resource": [
        "arn:aws:s3:::amazonlinux.us-east-1.amazonaws.com/*",
        "arn:aws:s3:::prod-us-east-1-starport-layer-bucket/*"
      ]
    }]
  }'

The second statement in the S3 endpoint policy allows downloading from Amazon Linux repositories and Starport layer buckets — these are needed for ECS Fargate container startup and Lambda layer pulls in some regions. Without them, containers may fail to start or Lambda may fail to load layers. Always include these AWS-managed bucket patterns when restricting S3 endpoint policies in environments that run ECS Fargate or Lambda.

Secrets Manager endpoint policy — restrict to specific secrets

By default, any principal with the right IAM permissions can call any secret in any account through an Interface endpoint. A scoped endpoint policy limits which secret ARN patterns the endpoint serves, which actions are permitted, and which principal ARNs can use the endpoint. This prevents lateral movement: a compromised MCP server container cannot use the Secrets Manager endpoint to enumerate or read secrets belonging to other services.

# Restrict Secrets Manager endpoint to specific secrets and read-only actions
aws ec2 modify-vpc-endpoint \
  --vpc-endpoint-id vpce-0secretsmgr \
  --policy-document '{
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "AWS": [
            "arn:aws:iam::123456789012:role/MCPServerRole",
            "arn:aws:iam::123456789012:role/ECSTaskExecutionRole"
          ]
        },
        "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
        "Resource": [
          "arn:aws:secretsmanager:us-east-1:123456789012:secret:mcp/production/*",
          "arn:aws:secretsmanager:us-east-1:123456789012:secret:mcp/shared/*"
        ]
      },
      {
        "Effect": "Deny",
        "Principal": "*",
        "Action": [
          "secretsmanager:DeleteSecret",
          "secretsmanager:UpdateSecret",
          "secretsmanager:RotateSecret",
          "secretsmanager:CreateSecret",
          "secretsmanager:PutSecretValue"
        ],
        "Resource": "*"
      }
    ]
  }'

The explicit Deny statement for write/mutation operations is belt-and-suspenders: even if a Lambda rotation function runs in the same VPC and needs to call PutSecretValue through this endpoint, the endpoint policy blocks it. Rotation Lambdas should use their own distinct endpoint (or a NAT path) if needed — this separation means an exploit in the MCP server container cannot be used to modify secrets even if the container's IAM role somehow gained temporary write access via a confused-deputy scenario.

Enforcing VPC endpoint usage in S3 bucket policies

The inverse of an endpoint policy: require that all access to an S3 bucket comes via a specific VPC endpoint. This denies access from outside the VPC — including from other AWS accounts, from the console, from CLI calls on EC2 instances without the endpoint, and from the internet. The condition aws:SourceVpce matches on the endpoint ID that served the request.

# S3 bucket policy that enforces VPC endpoint access only
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/MCPServerRole"
      },
      "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::mcp-tool-outputs",
        "arn:aws:s3:::mcp-tool-outputs/*"
      ],
      "Condition": {
        "StringEquals": {
          "aws:SourceVpce": "vpce-0s3gateway"
        }
      }
    },
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::mcp-tool-outputs",
        "arn:aws:s3:::mcp-tool-outputs/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:SourceVpce": "vpce-0s3gateway"
        }
      }
    }
  ]
}

The Deny statement with StringNotEquals is critical — without it, access from outside the VPC is still permitted if IAM allows it. The explicit Deny overrides any Allow in IAM. One caveat: aws:SourceVpce is not available in all services or request types — S3 supports it, but some console operations (S3 Select in the AWS console, for example) may not set it and will be denied. Test console access patterns before enforcing this in production. Use aws:SourceVpc (VPC ID, not endpoint ID) as a looser alternative that allows all Interface endpoints and Gateway endpoints in the VPC.

Cross-account endpoint policies

When MCP servers in one account access resources in another account through a VPC endpoint, the endpoint policy must explicitly allow the cross-account access. The default endpoint policy (Principal: "*") allows any principal including cross-account ones — but specifying a Principal restricts to that exact ARN. Use aws:PrincipalAccount as a condition to restrict to your own account while still supporting cross-account scenarios for specific resources.

# Endpoint policy that allows own account only (blocks cross-account):
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "secretsmanager:GetSecretValue",
    "Resource": "*",
    "Condition": {
      "StringEquals": {
        "aws:PrincipalAccount": "123456789012"
      }
    }
  }]
}

# Endpoint policy that allows own account + a specific cross-account role:
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "AWS": [
        "arn:aws:iam::123456789012:role/MCPServerRole",
        "arn:aws:iam::999988887777:role/DataPipelineRole"
      ]
    },
    "Action": ["s3:GetObject"],
    "Resource": "arn:aws:s3:::shared-mcp-data/*"
  }]
}

# Verify current endpoint policy
aws ec2 describe-vpc-endpoints \
  --vpc-endpoint-ids vpce-0abc123 \
  --query 'VpcEndpoints[0].PolicyDocument'

A common mistake: setting a restrictive endpoint policy with specific Principal ARNs but forgetting to update it when roles change (role name changes, account migration, new services). Endpoint policies are infrequently inspected and rot silently — add a rotation reminder or store them in CDK/Terraform with the role ARN as a variable so changes propagate automatically. Use aws:PrincipalOrgID if you want to allow any principal in your AWS Organization without enumerating account ARNs individually.

Failure modes reference

FailureSymptomFix
AccessDenied despite correct IAM policySDK returns AccessDeniedException with no clear reasonEndpoint policy is blocking the call — endpoint policy AND IAM policy must both allow; check endpoint policy with describe-vpc-endpoints; default policy allows all
S3 bucket denies console access after adding vpc endpoint conditionS3 console shows "Access Denied" for bucket operationsaws:SourceVpce condition blocks console access (requests from browser/console don't go through the endpoint); use aws:SourceVpc instead, or add a separate Allow statement for the role ARN without the endpoint condition
Lambda in VPC cannot call Secrets Manager after policy restrictionGetSecretValue fails with AccessDenied; Lambda execution role has the permissionLambda execution role ARN not in the endpoint policy Principal list; add the role ARN or use aws:PrincipalAccount condition to allow all principals in your account
Endpoint policy change not taking effectNew restrictive policy applied but old access still worksSDK clients cache credentials and endpoint connections; in-flight connections may persist for up to 90 seconds; allow time for TCP connection pool to expire after policy change
ECS Fargate fails to pull container image after restricting S3 endpointTask fails at image pull with S3 AccessDeniedECR image layers are stored in S3; endpoint policy must allow s3:GetObject on AWS-managed ECR layer buckets (prod-us-east-1-starport-layer-bucket/*); add the S3 bucket ARN for the ECR layer cache in the endpoint policy