Back to blog
AWSBest PracticesCloud SecurityNetworkingServerless

API Gateway Uses Outdated TLS Policy: Enforcing TLS 1.2 on Custom Domains

Learn why API Gateway custom domains accepting TLS 1.0 are a security risk and how to enforce TLS 1.2 with CLI, Terraform, and policy-as-code gates.

TL;DR

This check flags API Gateway custom domains that still accept TLS 1.0, an obsolete protocol with known weaknesses. Switch the domain's security policy to TLS_1_2 to enforce modern encryption on incoming connections.

When you put a custom domain in front of an API Gateway endpoint, AWS terminates TLS at the edge using a security policy you choose. That policy decides which TLS versions and cipher suites clients are allowed to negotiate. If the domain is set to the older policy, it will happily accept TLS 1.0 handshakes, which means a client from 2010 can connect to your modern API over a protocol that the rest of the industry retired years ago.

The apigw_tlspolicy check looks at every custom domain name in your API Gateway configuration and reports any that permit TLS 1.0.


What this check detects

API Gateway custom domain names have a securityPolicy attribute with two possible values:

  • TLS_1_0 — accepts TLS 1.0, 1.1, and 1.2 connections.
  • TLS_1_2 — accepts only TLS 1.2 (and TLS 1.3 where supported).

Despite the name, TLS_1_0 is not a "TLS 1.0 only" setting. It is a floor. It allows the weakest protocol AWS still supports on that domain. This check fires when a custom domain has its security policy set to TLS_1_0, leaving the door open to legacy clients negotiating downward.

Note: This setting only applies to custom domain names. The default execute-api endpoints (like abc123.execute-api.us-east-1.amazonaws.com) are managed by AWS and already enforce TLS 1.2 as a minimum, so they are not affected by this check.


Why it matters

TLS 1.0 was published in 1999 and formally deprecated by the IETF in 2021 (RFC 8996). The PCI Security Standards Council told merchants to stop using it back in 2018. It carries real, well-documented weaknesses:

  • BEAST — exploits TLS 1.0's CBC cipher implementation to decrypt portions of an encrypted session.
  • POODLE-style downgrade attacks — an active attacker on the network path can force a client and server to negotiate the weakest mutually supported protocol.
  • Weak cipher suites — TLS 1.0 supports ciphers and key exchanges that no longer meet modern standards.

The practical risk: if an attacker can position themselves between a client and your API (public Wi-Fi, a compromised router, a hostile ISP), the presence of TLS 1.0 gives them a downgrade target. Even if 99% of your real traffic uses TLS 1.2, leaving 1.0 enabled means a man-in-the-middle can try to coerce a session down to the weaker protocol.

There is also a compliance angle. PCI DSS, HIPAA security guidance, FedRAMP, and most internal security baselines now require TLS 1.2 or higher. An API endpoint accepting TLS 1.0 is a frequent finding in audits, and it tends to block compliance certification until fixed.

Warning: Before flipping to TLS 1.2, confirm nothing legitimate still depends on TLS 1.0 or 1.1. Old mobile SDKs, embedded IoT devices, and ancient Java 6/7 clients may not support TLS 1.2 out of the box. Check your access logs for the negotiated protocol before you make the change in production.


How to fix it

The fix is to set the custom domain's security policy to TLS_1_2. There is no separate "TLS 1.3" policy value on API Gateway. Selecting TLS_1_2 enforces 1.2 as the minimum and AWS handles newer protocols transparently.

Option 1: AWS CLI

First, check the current policy for a domain:

aws apigateway get-domain-name \
  --domain-name api.example.com \
  --query 'securityPolicy' \
  --output text

If it returns TLS_1_0, update it with a patch operation:

aws apigateway update-domain-name \
  --domain-name api.example.com \
  --patch-operations op=replace,path=/securityPolicy,value=TLS_1_2

For an HTTP API or WebSocket API custom domain managed under API Gateway v2, use the v2 command instead:

aws apigatewayv2 update-domain-name \
  --domain-name api.example.com \
  --domain-name-configurations SecurityPolicy=TLS_1_2

Warning: Updating the security policy on a REST API custom domain triggers a CloudFront distribution update behind the scenes and can take up to 40 minutes to fully propagate. The domain stays available during this time, but plan changes outside peak hours.

Option 2: AWS Console

  1. Open the API Gateway console.
  2. Select Custom domain names from the left navigation.
  3. Choose the affected domain.
  4. Click Edit on the domain details.
  5. Under Minimum TLS version, select TLS 1.2.
  6. Save the change.

Option 3: Terraform

For a REST API domain:

resource "aws_api_gateway_domain_name" "api" {
  domain_name              = "api.example.com"
  regional_certificate_arn = aws_acm_certificate.api.arn
  security_policy          = "TLS_1_2"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

For an HTTP or WebSocket API domain (API Gateway v2):

resource "aws_apigatewayv2_domain_name" "api" {
  domain_name = "api.example.com"

  domain_name_configuration {
    certificate_arn = aws_acm_certificate.api.arn
    endpoint_type   = "REGIONAL"
    security_policy = "TLS_1_2"
  }
}

Option 4: CloudFormation

{
  "Type": "AWS::ApiGateway::DomainName",
  "Properties": {
    "DomainName": "api.example.com",
    "RegionalCertificateArn": { "Ref": "ApiCertificate" },
    "SecurityPolicy": "TLS_1_2",
    "EndpointConfiguration": { "Types": ["REGIONAL"] }
  }
}

Tip: If you manage dozens of domains, script the audit and remediation together. List every domain with its policy, then patch only the offenders:

for domain in $(aws apigateway get-domain-names \
  --query 'items[?securityPolicy==`TLS_1_0`].domainName' \
  --output text); do
  echo "Updating $domain to TLS_1_2"
  aws apigateway update-domain-name \
    --domain-name "$domain" \
    --patch-operations op=replace,path=/securityPolicy,value=TLS_1_2
done

How to verify the fix

After the change propagates, confirm the policy and test the handshake. Re-run the CLI query to check the stored value:

aws apigateway get-domain-name \
  --domain-name api.example.com \
  --query 'securityPolicy' \
  --output text
# Expect: TLS_1_2

Then prove that a TLS 1.0 handshake is actually rejected at the wire:

# This should now FAIL with a handshake error
openssl s_client -connect api.example.com:443 -tls1

# This should succeed
openssl s_client -connect api.example.com:443 -tls1_2

If the -tls1 attempt still completes, the change has not finished propagating yet. Wait and retry.


How to prevent it from happening again

Fixing one domain is easy. Keeping every domain compliant as your account grows is the real work. Push enforcement left so the weak policy never lands in production.

Block it in IaC review with policy-as-code

If you use Terraform, a Checkov or OPA/Conftest rule can fail the pipeline when someone sets TLS_1_0 or omits the policy. A simple Conftest policy against a Terraform plan:

package main

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_api_gateway_domain_name"
  resource.change.after.security_policy != "TLS_1_2"
  msg := sprintf("API Gateway domain '%s' must use TLS_1_2", [resource.address])
}

Wire that into your CI pipeline so the merge is blocked before any apply runs:

terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json

Set a sane default in your modules

If you wrap API Gateway domains in a shared Terraform module, hardcode security_policy = "TLS_1_2" as the default and do not expose it as an overridable variable. The weak option simply stops being reachable.

Catch drift at runtime

IaC gates do not help for resources created by hand or by an older pipeline. Continuous monitoring with Lensix runs the apigw_tlspolicy check across your accounts and surfaces any domain that drifts back to TLS 1.0, including ones provisioned outside your Terraform state.

Tip: AWS Config ships a managed rule, api-gw-ssl-enabled, plus you can author a custom rule keyed on securityPolicy. Pair detective controls in Config with the preventive IaC gate above so nothing slips through either layer.


Best practices

  • Default to TLS 1.2 everywhere. Not just API Gateway. Apply the same minimum to CloudFront distributions, Application Load Balancers, and ElastiCache in-transit settings.
  • Audit access logs before tightening. Enable access logging and inspect the negotiated TLS version field so you know exactly which clients would break before you cut off TLS 1.0.
  • Give legacy clients a deadline, not an exception. If a partner genuinely needs TLS 1.0, treat it as a tracked, time-boxed risk with an owner and an end date, not a permanent carve-out.
  • Standardize through modules. Centralize domain creation so the security policy is set once and inherited everywhere, rather than copy-pasted per service.
  • Re-check after every migration. Account migrations, region expansions, and disaster-recovery rebuilds are common moments where old defaults sneak back in. Make the TLS policy check part of your go-live checklist.

TLS 1.0 on an API endpoint is a small misconfiguration with an outsized footprint. It rarely breaks anything visibly, so it lingers, which is exactly why it shows up in audits and breach post-mortems. Flip the policy to TLS 1.2, gate it in your pipeline, and monitor for drift, and this one stays fixed.