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.