AWS PrivateLink for Bedrock: Keep Coding-Agent Model Calls Off the Public Internet

20 views

Your coding-agent platform locks sandboxes down with “no internet” security groups. Then someone notices that the planner service reaches Amazon Bedrock through a NAT gateway, because nobody created a VPC endpoint for bedrock-runtime. Prompts, tool schemas and chunks of private source code all leave the VPC over the public Bedrock endpoint. That’s still TLS-encrypted, but it means the planner subnet needs a route to the internet, and that route is exactly what you were trying to take away from untrusted code.

AWS PrivateLink fixes this. An interface VPC endpoint places network interfaces for Bedrock inside your own subnets, so your code can call Bedrock without an internet gateway, a NAT device, a VPN or Direct Connect, and without public IP addresses. This guide covers which Bedrock endpoints a coding agent needs, how to create them, how to restrict them with endpoint policies, how to prove traffic really uses them, which other endpoints an agent runner needs, how to keep secrets out of prompts and logs, and what to check when it doesn’t work. It merges our earlier posts on private Bedrock endpoints and secret hygiene into this guide.

⚡ TL;DR
  • Bedrock has separate endpoint services: bedrock (control plane), bedrock-runtime (InvokeModel, Converse), bedrock-agent (build-time APIs such as knowledge base and prompt management), bedrock-agent-runtime (Retrieve, InvokeAgent), plus bedrock-mantle and FIPS variants. Create only the ones your code calls.
  • Turn on private DNS (it needs VPC DNS hostnames and DNS resolution), and your SDK code doesn’t change at all.
  • Replace the default “allow everything” endpoint policy with one that allows bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream for your organization’s principals. Converse and ConverseStream are authorized through those two actions.
  • Prove it: DNS returns private IPs, the call succeeds with no NAT route, and the CloudTrail event shows your vpcEndpointId.

How an interface endpoint works

When you create an interface endpoint, AWS creates an endpoint network interface in each subnet you choose, with a private IP address from that subnet. These are requester-managed interfaces: you can see them in your account but can’t modify them. Traffic to Bedrock goes to those private IPs and stays on the AWS network.

Three controls decide who can use the endpoint:

  • Security group on the endpoint interfaces. It must allow inbound HTTPS (443) from your callers.
  • Endpoint policy, a resource policy on the endpoint that limits which principals, actions and resources can pass through it. The default allows full access.
  • IAM policies on the calling role, which still apply. An endpoint policy doesn’t grant anything by itself. It can only narrow what IAM already allows.

With private DNS enabled, the normal Regional hostname (for example bedrock-runtime.us-east-1.amazonaws.com) resolves to the endpoint’s private IPs inside your VPC. That’s why existing SDK code keeps working without an endpoint_url change.

Figure 1. Planner calls reach Bedrock through endpoint interfaces in each AZ. Sandboxes keep no internet path. Diagram by CheatCoders.
Figure 1. Planner calls reach Bedrock through endpoint interfaces in each AZ. Sandboxes keep no internet path. Diagram by CheatCoders.

Which Bedrock endpoints a coding agent needs

Bedrock publishes several endpoint services, and each one covers a different group of APIs. If you miss one, you get the classic “PrivateLink works for chat, but my RAG tool times out” problem.

Service nameCoversCreate it if your VPC code calls…
com.amazonaws.region.bedrock-runtimeRuntime APIsConverse, ConverseStream, InvokeModel
com.amazonaws.region.bedrock-agent-runtimeAgents runtime APIsRetrieve, RetrieveAndGenerate, InvokeAgent
com.amazonaws.region.bedrock-agentAgents build-time APIsStartIngestionJob, GetPrompt, CreatePromptVersion
com.amazonaws.region.bedrockControl plane APIsControl-plane calls from inside the VPC, such as listing models or managing guardrails
bedrock-mantle, bedrock-fips, bedrock-runtime-fipsMantle APIs; FIPS endpointsFIPS endpoints are available in us-east-1, us-east-2, us-west-2, ca-central-1 and the two GovCloud (US) Regions

A typical agent platform needs bedrock-runtime for the model loop and bedrock-agent-runtime for knowledge base retrieval. If the agent host also reads managed prompts at startup (GetPrompt), add bedrock-agent. Your other dependencies need their own endpoints too: an “air-gapped” planner that pulls secrets or container images still needs endpoints for those services.

Before you start

  • The VPC has DNS hostnames and DNS resolution enabled. Private DNS doesn’t work without them.
  • One private subnet per Availability Zone where your callers run. You can choose one subnet per AZ for an endpoint.
  • A security group for the endpoint that allows inbound TCP 443 from your callers’ security groups. Check network ACLs too.
  • Budget for the cost: interface endpoints are billed per hour and per GB of data processed (see the PrivateLink pricing page).

Step 1: Create the runtime endpoint

Start with bedrock-runtime. Here is the AWS CLI version. The same flags apply to the other Bedrock service names.

bash
REGION=us-east-1
VPC_ID=vpc-0123456789abcdef0
SUBNETS="subnet-0aaa1111bbbb2222c subnet-0ddd3333eeee4444f"   # one per AZ
VPCE_SG=sg-0123456789abcdef0                               # allows 443 from planner SG

aws ec2 create-vpc-endpoint \
  --region "$REGION" \
  --vpc-id "$VPC_ID" \
  --vpc-endpoint-type Interface \
  --service-name "com.amazonaws.${REGION}.bedrock-runtime" \
  --subnet-ids $SUBNETS \
  --security-group-ids "$VPCE_SG" \
  --private-dns-enabled \
  --policy-document file://bedrock-runtime-endpoint-policy.json

If you manage infrastructure with the AWS CDK, the same endpoint looks like this. Using the service name string avoids depending on a specific CDK constant.

typescript
import * as ec2 from "aws-cdk-lib/aws-ec2";

const runtimeEndpoint = new ec2.InterfaceVpcEndpoint(this, "BedrockRuntimeEndpoint", {
  vpc,
  service: new ec2.InterfaceVpcEndpointService(`com.amazonaws.${this.region}.bedrock-runtime`, 443),
  subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
  securityGroups: [endpointSg],
  privateDnsEnabled: true,
});
endpointSg.addIngressRule(plannerSg, ec2.Port.tcp(443), "planner to Bedrock endpoint");

Step 2: Replace the default endpoint policy

The default endpoint policy allows any action by any principal. A tighter policy limits the endpoint to model invocation by principals in your AWS Organization. Converse requires bedrock:InvokeModel and ConverseStream requires bedrock:InvokeModelWithResponseStream, so those two actions cover the usual agent loop.

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ModelCallsFromOurOrgOnly",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "bedrock:InvokeModel",
        "bedrock:InvokeModelWithResponseStream"
      ],
      "Resource": [
        "arn:aws:bedrock:*::foundation-model/*",
        "arn:aws:bedrock:*:*:inference-profile/*",
        "arn:aws:bedrock:*:*:application-inference-profile/*"
      ],
      "Condition": {
        "StringEquals": { "aws:PrincipalOrgID": "o-exampleorgid" }
      }
    }
  ]
}

Two details matter here. First, the Region wildcard in the foundation-model ARN is deliberate: cross-Region inference profiles run the model in other Regions, and the AWS example policy for inference profiles uses arn:aws:bedrock:*::foundation-model/* for that reason. Narrow the model part (for example to one provider) once you know which models you use. Second, policy updates take a few minutes to apply, so wait before you decide a change didn’t work. Endpoint policies must include a Principal element and are limited to 20,480 characters.

Step 3: Point code at the endpoint (usually no change)

With private DNS enabled, nothing changes in your code. The SDK resolves the standard hostname, and inside the VPC that name points to the endpoint.

python
import boto3

# Private DNS on: the default hostname resolves to the endpoint's private IPs.
client = boto3.client("bedrock-runtime", region_name="us-east-1")

resp = client.converse(
    modelId="your-model-or-inference-profile-id",
    messages=[{"role": "user", "content": [{"text": "Say hello from inside the VPC."}]}],
)
print(resp["output"]["message"]["content"][0]["text"])

If you didn’t enable private DNS (for example, during a staged migration), pass the endpoint-specific URL explicitly:

python
client = boto3.client(
    "bedrock-runtime",
    region_name="us-east-1",
    endpoint_url="https://vpce-0abc123def4567890.bedrock-runtime.us-east-1.vpce.amazonaws.com",
)

Step 4: Prove the traffic uses PrivateLink

Don’t stop at “the call worked”. It also works through a NAT gateway. Run these three checks from a task or instance in the private subnet:

bash
# 1. DNS should return private addresses from your subnets, not public IPs
dig +short bedrock-runtime.us-east-1.amazonaws.com

# 2. The endpoint port is reachable (interface endpoints don't answer ping; use nc)
nc -vz bedrock-runtime.us-east-1.amazonaws.com 443

# 3. The internet is NOT reachable from this subnet (should time out)
curl -sS --max-time 5 https://example.com || echo "no internet path: good"

Then confirm on the AWS side. Bedrock logs Converse, ConverseStream and InvokeModel to CloudTrail as management events, so they show up in event history without extra setup. Open the event for your test call and check that vpcEndpointId matches your endpoint ID. (Agents runtime calls such as InvokeAgent are logged as data events, which you have to enable separately.)

Step 5 (optional): Deny Bedrock calls that bypass the endpoint

An endpoint policy controls what passes through the endpoint. It doesn’t stop a role from calling Bedrock some other way, such as from a laptop with leaked credentials. To enforce “agent roles may only call Bedrock through our endpoints”, add a deny on the identity side, either in a service control policy or in the role’s permissions, using the aws:SourceVpce condition key.

json
{
  "Sid": "AgentRolesUseBedrockOnlyViaOurEndpoints",
  "Effect": "Deny",
  "Action": [
    "bedrock:InvokeModel",
    "bedrock:InvokeModelWithResponseStream"
  ],
  "Resource": "*",
  "Condition": {
    "StringNotEquals": { "aws:SourceVpce": ["vpce-0abc123def4567890", "vpce-0fed987cba6543210"] },
    "ArnLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/agent-*" },
    "Bool": { "aws:PrincipalIsAWSService": "false" }
  }
}

Be careful with this one. aws:SourceVpce is only present when a request comes through a VPC endpoint, and AWS notes that network context is removed when one AWS service calls another on your behalf. That’s why the statement is scoped to your agent roles and excludes AWS service principals. Test it in a non-production account first and keep a documented break-glass role.

Step 6: Add the endpoints the rest of the runner needs

A private Bedrock endpoint is only half the job. An agent runner in a subnet without a NAT route also has to pull its container image, write logs, get credentials and read secrets. Each of those is a separate AWS service with its own endpoint, and a missing one shows up as a task that hangs at startup or a tool that times out.

NeedEndpoint(s)Notes
Pull images from Amazon ECRecr.api and ecr.dkr (interface), plus an S3 gateway endpointECR stores image layers in S3, so pulls need the S3 gateway endpoint too. Fargate platform version 1.4.0 or later needs both ECR endpoints. Private DNS must be on for ecr.dkr.
Container logs with the awslogs driverlogs (interface)ECR’s documentation says tasks without an internet gateway that use the awslogs driver need this endpoint.
Secrets for toolssecretsmanager (interface)Lets tools fetch credentials at call time instead of carrying them in environment variables.
Assume roles from inside the VPCsts (interface)Needed when code calls STS directly, for example to assume a per-tenant role.

Give these endpoints the same treatment as the Bedrock ones: a security group that allows 443 only from the runner, and endpoint policies that limit what can pass through. The S3 gateway endpoint uses a policy too; ECR’s documentation lists the bucket ARN pattern that holds image layers, so you can allow that bucket and your own artifact buckets instead of all of S3.

Keep secrets out of prompts, traces and logs

Private networking stops prompts from crossing the internet. It does nothing about what’s in the prompt. Anything you put in a model’s context can come back out: in a response, in a tool argument the model was tricked into writing, or in a log. Prompt injection from a malicious README or issue comment is the usual route. So the rule is simple: credentials never go into prompts, system messages or tool descriptions.

  • Fetch secrets inside the tool, at call time. A tool that opens a pull request reads its GitHub credential from Secrets Manager inside the sandbox, makes the call, and returns only safe fields such as the PR URL. The model never sees the token.
  • Don’t keep long-lived third-party keys in plain environment variables of agent functions. They end up in crash dumps, debug output and configuration exports.
  • Redact before you log or archive. Scrub prompts, tool inputs and tool outputs for known secret patterns before they reach CloudWatch Logs, S3 archives or third-party tracing.
  • Treat model invocation logging as sensitive data. Bedrock model invocation logging is off by default. When you enable it, it records input and output data for invocations in the account to CloudWatch Logs or S3, so restrict access to that destination and encrypt it with a KMS key you control.
python
import json
import re

import boto3

secrets = boto3.client("secretsmanager")

SECRET_PATTERNS = re.compile(
    r"(AKIA[0-9A-Z]{16}"            # AWS access key IDs
    r"|ghp_[A-Za-z0-9]{36}"         # GitHub classic personal access tokens
    r"|xox[abprs]-[A-Za-z0-9-]+"    # Slack tokens
    r"|-----BEGIN [A-Z ]*PRIVATE KEY-----)"
)


def redact(text: str) -> str:
    return SECRET_PATTERNS.sub("[REDACTED]", text)


def open_pull_request(tenant: str, repo: str, branch: str, title: str, github) -> dict:
    """Tool handler: the credential is read here and never returned to the model."""
    secret = secrets.get_secret_value(SecretId=f"tenants/{tenant}/github-app")
    creds = json.loads(secret["SecretString"])
    pr = github(creds).create_pull(repo=repo, head=branch, title=title)
    return {"ok": True, "prUrl": pr["html_url"]}  # safe fields only


def log_tool_call(logger, name: str, args: dict, result: dict) -> None:
    logger.info(redact(json.dumps({"tool": name, "args": args, "result": result})))

Pattern-based redaction is a safety net, not a guarantee; it only catches formats you know about. The primary control is architectural: secrets stay inside tools and never enter the context. When you rotate a secret, make sure long-running jobs re-read it instead of caching the old value for hours, for example by caching with a short TTL or by re-fetching after an authentication failure.

Troubleshooting

SymptomLikely causeFix
Calls hang, then time outEndpoint security group doesn’t allow 443 from the caller, or a network ACL blocks itAllow inbound TCP 443 from the caller’s security group; check NACLs in both directions
dig returns public IPsPrivate DNS is off, or the VPC lacks DNS hostnames/resolutionEnable both VPC DNS attributes and private DNS on the endpoint, or use endpoint_url
Converse works, Retrieve times outNo bedrock-agent-runtime endpointCreate it with the same subnets, security group and a matching policy
GetPrompt or StartIngestionJob fails from the VPCThese are build-time APIs on bedrock-agentAdd a bedrock-agent endpoint, or run those calls from CI outside the VPC
AccessDeniedException right after a policy editThe endpoint policy doesn’t allow the action or resource, or the change hasn’t applied yetCheck the action list and ARNs; wait a few minutes after updating
Cross-Region inference profile deniedPolicy only allows foundation models in the local RegionAllow the foundation-model ARNs in the Regions the profile routes to
First call after an idle period hangs or resetsInterface endpoints and NAT gateways drop TCP connections idle for more than 350 seconds without telling the clientEnable TCP keep-alive in the SDK (boto3 Config(tcp_keepalive=True)) and lower the OS keep-alive time below 350 seconds; both are needed
Fargate task stuck pulling the imageMissing ecr.api, ecr.dkr or the S3 gateway endpointCreate all three; enable private DNS on ecr.dkr
ping to the endpoint failsInterface endpoints don’t respond to ICMPTest with nc or a real API call instead

Production checklist

  • ☐ Endpoints exist for every Bedrock API family your VPC code calls (runtime, agent-runtime, agent, control plane)
  • ☐ VPC DNS hostnames and resolution enabled; private DNS on for each endpoint
  • ☐ Endpoint security group allows 443 only from planner and agent-host security groups
  • ☐ Custom endpoint policy: model invocation only, organization principals only
  • ☐ Sandboxes have no NAT or internet route and don’t call Bedrock directly
  • ☐ Verification script (DNS, port, no internet) runs after every network change
  • ☐ CloudTrail spot check shows your vpcEndpointId on Converse events
  • ☐ Optional aws:SourceVpce deny tested in non-prod, with a break-glass path
  • ☐ ECR (api, dkr, S3 gateway), Logs, STS and Secrets Manager endpoints exist for runner dependencies
  • ☐ No credentials in prompts, system messages, tool descriptions or plain environment variables; tools fetch secrets at call time
  • ☐ Prompts and tool I/O redacted before logging; invocation logging destination encrypted and access-restricted
  • ☐ TCP keep-alive configured for long-lived Bedrock clients

FAQ

Is there a gateway endpoint for Bedrock, like S3?

No. Bedrock is reached through interface endpoints (PrivateLink). Gateway endpoints exist only for Amazon S3 and DynamoDB.

Does PrivateLink encrypt my prompts better than the public endpoint?

No. TLS protects the traffic either way. What PrivateLink gives you is network isolation: callers need no internet route, and the endpoint policy limits what can pass through.

Do I still need IAM permissions?

Yes. The calling role still needs bedrock:InvokeModel (and the streaming action, if you stream). The endpoint policy can only narrow access, never grant it.

If traffic is private, can I put an API key in the system prompt?

No. Private networking protects the path, not the content. Anything in the context can be echoed back or exfiltrated through prompt injection. Keep credentials inside tools.

Will streaming work over the endpoint?

Yes. ConverseStream goes over the same bedrock-runtime endpoint. Keep long-running tool work outside the streamed request: end the model turn, run the tool, then start a new Converse call with the tool result.

Official documentation

Related on CheatCoders: Bedrock Knowledge Bases for monorepos · Bedrock Prompt Management · Parameter Store config for coding agents

Last updated on October 10, 2026

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

1 Comment

  1. From what I’ve seen, an SEO consultant is kind of like a doctor for your website. They diagnose problems like slow pages, missing meta tags, or weak content, then prescribe fixes based on data instead of guesswork. The good ones also keep an eye on algorithm updates so your rankings don’t randomly tank overnight, which happened to me once before I hired someone.

    What surprised me most is how much of the job is just communication, explaining to clients why rankings don’t improve overnight and managing expectations. A lot of consultants also coordinate with whoever writes your content or builds your site, since SEO touches basically everything. Anyone here actually worked with one long term, was it worth the monthly cost?

    I talked to a few different consultants before settling on one, and the biggest thing I learned is that most of them just repeat generic advice you could find anywhere. The guy I eventually went with actually looked at my site specifics instead of giving me a template checklist. That alone made the fee worth it honestly.

    My advice after going through this is don’t expect overnight miracles, good consultants will actually tell you that upfront. The ones who promise page one in a month are usually the ones you should avoid. Anyone here had a similar experience with consultants overpromising and underdelivering?
    SEO consultant UK [url=https://yourhomeyourwayltd.co.uk/what-an-seo-consultant-actually-does-advice/#SEO-consultant-UK]https://yourhomeyourwayltd.co.uk/what-an-seo-consultant-actually-does-advice/[/url] .

Leave a comment

No account needed. Name and email are optional.