The MCP server was already inside the VPC. hrr-mcp.internal.example.com presented a certificate from our own CA, the security group only allowed the Lattice ENIs, and nothing about that host was meant to be reachable from the internet.
AgentCore Gateway still refused the handshake. By default it trusts public certificate authorities only. A private CA looks the same as a cert it has never heard of. The documented fix, until October 2, 2026, was an internal Application Load Balancer with a public ACM certificate in front of the server, and routing_domain set to that ALB's DNS name.
That worked. It also meant a load balancer, a listener, a target group, and a public name for a server that was never supposed to have one. The ALB existed so the gateway would trust a certificate. It did not exist because I needed to balance anything.
On October 2, AWS shipped the other option. You register the private CA on the gateway target. The gateway fetches the PEM from S3 or Secrets Manager and uses it as the trust anchor for that target. No ALB in the middle.
What October 2 actually changed
This is outbound TLS from AgentCore Gateway to a target on a VPC Lattice private endpoint. Lambda invoke is a different path, and so is inbound auth for the client talking to the gateway.
You don't paste the certificate into the API request. You store a PEM and pass a reference. On create or update, the gateway reads that object with the gateway execution role, checks it, encrypts it with the gateway's own KMS key, and keeps it as the trust anchor for outbound connections to that one target.
The PEM has to be a CA certificate. Basic constraints must say CA:TRUE. A leaf certificate gets rejected. The file, chain included, has to be 16 KB or smaller. If it lives in S3, the object has to be in the same Region as the gateway. A chain is allowed. The gateway tracks the earliest notAfter in the file, so an intermediate that expires first is the one that matters.
Which targets can use it
Private CA trust only applies when the target already has a privateEndpoint, managed Lattice or self-managed. These are the types AWS lists.
| Target |
Private CA on the target |
MCP server (mcp.mcpServer) |
Yes |
OpenAPI (mcp.openApiSchema) |
Yes. One domain per target. A spec with several server hosts is a support case, not a second config block. |
HTTP proxy (http.passthrough) |
Yes |
| Lambda |
No. The gateway invokes the function. There is no customer TLS handshake to trust. |
| Smithy |
No privateEndpoint on Smithy targets today. |
| API Gateway as a native REST target |
No private endpoint on that target type. Export the API as OpenAPI and attach that schema instead. |
If the tool is a Lambda in a private subnet, you do not need this feature. If the tool is an MCP server or an HTTP API you run yourself, and that process presents your CA, this is the feature.
Register the CA on the target
certificateConfigurations is an array with exactly one entry. That entry sets exactly one source, s3 or secretsManager. Two sources, or an empty array with both keys, fails immediately. The gateway checks the URI, the secret ARN, and the account id before it accepts the request. Everything else waits.
This is the MCP target I would create, with the CA in S3. Names are examples.
{
"name": "hrr-private-mcp",
"privateEndpoint": {
"managedVpcResource": {
"vpcIdentifier": "vpc-0abc123def456",
"subnetIds": ["subnet-0abc123", "subnet-0def456"],
"endpointIpAddressType": "IPV4",
"securityGroupIds": ["sg-0abc123def"]
}
},
"targetConfiguration": {
"mcp": {
"mcpServer": {
"endpoint": "https://hrr-mcp.internal.example.com/mcp"
}
}
},
"certificateConfigurations": [
{
"s3": {
"uri": "s3://hrr-agentcore-ca/private-ca.pem",
"bucketOwnerAccountId": "111122223333"
}
}
]
}
bucketOwnerAccountId is optional. When you set it, S3 checks that the bucket owner is that account. I set it. A wrong bucket in another account should fail the read, not silently trust whatever object happens to be at that key.
Secrets Manager is the same shape, with a string secret, not a binary one. OpenAPI targets use it the same way. The schema's server URL still has to be the private host.
"certificateConfigurations": [
{
"secretsManager": {
"secretArn": "arn:aws:secretsmanager:us-west-2:111122223333:secret:hrr/agentcore/private-ca-AbCdEf"
}
}
]
To drop the private CA later, call UpdateGatewayTarget and leave certificateConfigurations out. The target goes back to public CAs only. A failed update doesn't swap the cert. If validation of the new PEM fails, the target stays on the certificate it already trusted.
Terraform still documents the ALB
I checked aws_bedrockagentcore_gateway_target on the current HashiCorp AWS provider docs before writing this. private_endpoint is there. certificate_configurations is not. The registry page still tells you to put an internal ALB in front and set routing_domain to the ALB DNS name when the server uses a private certificate.
So this is what Terraform can express today. It is the workaround, not the October 2 path.
resource "aws_bedrockagentcore_gateway_target" "hrr_mcp" {
gateway_identifier = aws_bedrockagentcore_gateway.hrr.gateway_id
name = "hrr-private-mcp-via-alb"
target_configuration {
mcp {
mcp_server {
endpoint = "https://hrr-mcp.example.com/mcp"
}
}
}
private_endpoint {
managed_vpc_resource {
vpc_identifier = aws_vpc.hrr.id
subnet_ids = aws_subnet.hrr[*].id
endpoint_ip_address_type = "IPV4"
routing_domain = aws_lb.hrr_mcp.dns_name
}
}
}
The endpoint host has to match the public ACM certificate on that ALB. routing_domain is where Lattice actually sends the traffic.
Until the provider grows an argument for certificateConfigurations, I would create the private-endpoint target in Terraform and register the CA with aws bedrock-agentcore-control update-gateway-target, or whatever the CLI name is in the version you have. Check it. Don't invent a Terraform block the provider will reject. If a later provider release adds the argument, move the PEM reference into the resource and delete the extra CLI step. Two writers for the same field will fight.
The execution role has to read the PEM
The gateway assumes its execution role to fetch the certificate. Trust bedrock-agentcore.amazonaws.com. Then grant the read on the one object, not the bucket.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::hrr-agentcore-ca/private-ca.pem"
}
]
}
Secrets Manager is secretsmanager:GetSecretValue on that secret ARN. If the object or the secret uses a customer managed KMS key, the same role needs kms:Decrypt on that key. Miss this and the API response still looks fine. The target fails a minute later, when the async check can't read the PEM.
A privateEndpoint target also can't use NO_AUTH as the gateway's inbound authorizer unless you attach an interceptor Lambda. That rule was already true before private CAs. The CA doesn't relax it.
READY is not instant
Most certificate checks are asynchronous. Create or update returns, then the target workflow reads the PEM.
Success is status READY. A bad create lands on FAILED. A bad update lands on UPDATE_UNSUCCESSFUL, and the previous certificate keeps serving. statusReasons is where the boring failures show up: no private endpoint, unsupported target type, missing S3 object or secret, expired cert, not a CA, file over 16 KB.
The synchronous rejects are narrower. Each entry must set exactly one of s3 or secretsManager, and the URI, ARN, and account id have to look valid. Those come back on the request. Everything else is a status you have to poll.
At call time the server certificate has to chain to the CA you registered, sit inside its validity window, and carry a subject alternative name that matches the target host. A mismatch is a client-side configuration error from the gateway, not a service fault. I would log the target host and the SAN next to each other before I touched IAM again.
Revocation does not mean what you think
The gateway doesn't check CRLs, and it doesn't do OCSP. If your CA revokes a server certificate, the gateway keeps trusting it until that certificate expires, as long as it still chains to the CA you configured and the hostname matches.
To stop trusting it, update the target. Pass a new PEM that no longer includes that CA, or omit certificateConfigurations and go back to public CAs. There's no revoke API on the gateway.
Even after a good update, an open connection can keep the old trust material. The gateway reuses TLS connections and only checks the certificate on the handshake. That connection can live up to 900 seconds. A rotation is immediate for new connections. In-flight ones can lag a quarter of an hour. If you are rotating because a key leaked, 15 minutes of old handshakes is part of the plan, not a surprise you want at 2 a.m.
Alarm before the cert expires
Once a private CA is configured, the gateway emits EarliestCertificateDaysToExpiry. It is the whole number of days until the earliest notAfter in the PEM. A negative value means it already expired. The metric only exists for targets that have a private certificate configured.
When that certificate expires, the next handshake fails and the target is unreachable. Tool calls die. I would alarm when the value drops under 14, on the target that holds hrr-private-mcp, and send it to the SNS topic you already use for gateway failures. Fourteen days is enough time to publish a new PEM and call UpdateGatewayTarget. It's not enough time if the only person who can issue the cert is on leave, so pick the number for your CA.
Inbound auth is a separate problem. A 401 from the gateway, a bad audience, a missing scope: that is the client proving who it is. I wrote that up in MCP Authentication Explained: Fix 401 Errors in a Remote MCP Server. Private CA trust never fixes a 401, and a valid token never fixes a handshake to hrr-mcp.internal.example.com.
Hope you enjoyed this one. If you have already deleted the ALB, or the provider grew a certificate_configurations block and I should update the Terraform section, find me on X at https://x.com/harundotdev.
On this post
Comments
A reply stays under the note it answers.
No comments yet.
If you have a note on I Stopped Putting an ALB in Front of My Private MCP Just for TLS, sign in and leave it.