AWS Design Pattern

Cross-Account PrivateLink + SigV4 Auth

Lambda (Account A) → API Gateway (Account B) — Zero public internet exposure

Account A resources
Account B resources
AWS internal / PrivateLink
SigV4 auth layer
PrivateLink
AWS backbone
No public internet
Request Traffic Flow
01
Lambda Invocation
Lambda function triggered. AWS SDK signs request with SigV4 using execution role credentials.
02
Private DNS Resolution
VPC resolver returns ENI private IPs for the execute-api hostname. No public DNS query.
03
SG Allow Check
Lambda SG egress → VPCE SG inbound verified. Port 443 matched between the two SGs only.
04
Endpoint Policy
VPC Endpoint IAM policy evaluated. Only requests targeting the allowed API ARN pass through.
05
PrivateLink Transit
Traffic traverses AWS internal backbone from ENI to API GW service front-end. Never leaves AWS.
06
Resource Policy
API GW denies by default. Checks aws:sourceVpce matches Account A's endpoint ID exactly.
07
SigV4 Verification
AWS_IAM authorizer validates signature, timestamp, and that the principal has execute-api:Invoke.
08
API Handler
Request accepted. Backend integration (Lambda, HTTP, etc.) executes. Response returned privately.
Defense-in-Depth — Four Security Layers
Layer 1 · Account A
Security Group Peering
Only traffic from sg-lambda-caller can reach the VPC Endpoint ENIs on port 443. No IP-range rules — group-to-group reference ensures only the Lambda SG qualifies. Eliminates lateral movement from within the VPC.
Layer 2 · Account A
VPC Endpoint Policy
IAM resource policy on the endpoint restricts which API ARN can be called through it. Can scope to resource tags (e.g. Project = [PROJECT]). Prevents endpoint re-use to reach unintended APIs even within the same region.
Layer 3 · Account B
API Gateway Resource Policy
Default-deny. Explicit allow requires aws:sourceVpce to match Account A's endpoint ID. Even with valid SigV4 credentials, requests not originating from that endpoint are rejected at the API layer.
Layer 4 · Account B · SigV4
AWS IAM / SigV4 Authentication
authorizationType: AWS_IAM on every route. Requests must carry a valid HMAC-SHA256 signature derived from Account A's Lambda execution role. Replay protection via timestamp skew window (±5 min). Cross-account Invoke grant scoped to specific routes.
Reference Policies
VPC Endpoint Policy — Account A
IAM
// Attach to the Interface VPC Endpoint
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSpecificAPI",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCT-A::role/lambda-role"
      },
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:REGION:ACCT-B:API-ID/*",
      "Condition": {
        "StringEquals": {
          "aws:resourceTag/Project": "[PROJECT]"
        }
      }
    },
    {
      "Sid": "DenyAll",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:resourceTag/Project": "[PROJECT]"
        }
      }
    }
  ]
}
API Gateway Resource Policy — Account B
Deny-first
// Applied to Private API Gateway in Account B
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyPublicAccess",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:REGION:ACCT-B:API-ID/*",
      "Condition": {
        "StringNotEquals": {
          "aws:sourceVpce": "vpce-0abc1234def56789"
        }
      }
    },
    {
      "Sid": "AllowFromVPCEndpoint",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCT-A::role/lambda-role"
      },
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:REGION:ACCT-B:API-ID/prod/*",
      "Condition": {
        "StringEquals": {
          "aws:sourceVpce": "vpce-0abc1234def56789"
        }
      }
    }
  ]
}
Lambda — SigV4 Request Signing (Python)
boto3
# Uses boto3 AWSRequest + SigV4Auth — no manual HMAC
import boto3, requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import get_credentials

session = boto3.Session()
credentials = session.get_credentials()
region = "us-east-1"

url = "https://API-ID.execute-api.REGION.amazonaws.com/prod/items"

req = AWSRequest(method="GET", url=url)
SigV4Auth(credentials, "execute-api", region).add_auth(req)

response = requests.get(
    url,
    headers=dict(req.headers)  # Authorization, X-Amz-Date, X-Amz-Security-Token
)
Lambda Execution Role — Cross-Account Invoke Grant
IAM
// Add inline policy to Lambda role in Account A
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAPIInvoke",
      "Effect": "Allow",
      "Action": "execute-api:Invoke",
      "Resource": [
        // Scope tightly — method and route, not wildcard
        "arn:aws:execute-api:REGION:ACCT-B:API-ID/prod/GET/items",
        "arn:aws:execute-api:REGION:ACCT-B:API-ID/prod/POST/items"
      ]
    }
  ]
}

// Never use "Resource": "*" for execute-api:Invoke
// Scope to the minimum set of stage/method/route combos
Implementation Prerequisites & Gotchas
⚠️
Private DNS Conflict
Enabling Private DNS on the VPCE overrides all execute-api resolution in the VPC. All API GW calls in the VPC route through the endpoint — not just Account B's API.
⚠️
Dual Policy Requirement
Both the API GW Resource Policy (Account B) and the VPCE Endpoint Policy (Account A) must allow the call. Either alone is insufficient — both are evaluated.
⚠️
SigV4 Timestamp Skew
AWS rejects requests with X-Amz-Date more than ±5 minutes from server time. Ensure Lambda execution environment clock is synced (it is by default; watch for custom runtimes).
⚠️
Lambda VPC Cold Start
VPC-attached Lambda has higher cold start latency due to ENI provisioning. Use Provisioned Concurrency if latency SLA is strict. Lambda SnapStart (Java) may also help.
Route Table — No IGW
Confirm the private subnets housing Lambda and VPCE ENIs have no route to an Internet Gateway or NAT Gateway in the route table. Traffic must stay local.
Multi-AZ VPCE ENIs
Create the endpoint in at least two AZs for HA. Each AZ gets its own ENI with a distinct private IP. Route table entries are per-subnet.
CloudTrail + VPC Flow Logs
Enable CloudTrail Data Events for execute-api and VPC Flow Logs on VPCE ENIs. Provides full audit trail for all cross-account API calls and network flows.
Least-Privilege IAM
Scope the Lambda execution role's execute-api:Invoke to specific stage/method/route ARNs. Never use a wildcard Resource for cross-account API authorization.