Introduction
Network Load Balancers (NLB) operate at Layer 4 and can terminate TLS connections when configured with TLS listeners. Unlike Application Load Balancers, NLBs require specific configuration for TLS termination, including certificate management, security policies, and backend protocol settings. Misconfiguration leads to connection failures, certificate errors, or health check failures.
The Error You'll See
Certificate not found: ```bash $ aws elbv2 create-listener \ --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/my-nlb/1234567890abcdef \ --protocol TLS \ --port 443 \ --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 \ --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-targets/def456
An error occurred (CertificateNotFound) when calling the CreateListener operation: Certificate 'arn:aws:acm:us-east-1:123456789012:certificate/abc123' not found ```
TLS handshake failure:
``bash
$ openssl s_client -connect my-nlb-1234567890.elb.us-east-1.amazonaws.com:443
CONNECTED(00000003)
write:errno=104
no peer certificate available
Health check failures: ```bash $ aws elbv2 describe-target-health \ --target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-targets/def456
{ "TargetHealthDescriptions": [{ "Target": {"Id": "i-1234567890abcdef0", "Port": 443}, "TargetHealth": { "State": "unhealthy", "Reason": "Target.ResponseCodeMismatch", "Description": "Health checks failed with code: 400" } }] } ```
Why This Happens
- 1.Certificate in wrong region - ACM certificates must be in the same region as the NLB
- 2.Certificate not validated - ACM certificate pending DNS/email validation
- 3.Wrong protocol on target group - Using HTTP health checks with TCP targets
- 4.Backend protocol mismatch - NLB sends TCP but backend expects TLS
- 5.Security policy incompatibility - TLS security policy doesn't support required ciphers
- 6.ALPN configuration - Missing or incorrect ALPN policy for HTTP/2
- 7.Certificate chain incomplete - Missing intermediate certificates
Step 1: Verify ACM Certificate
```bash # List certificates in the region aws acm list-certificates --region us-east-1
# Check certificate status and details aws acm describe-certificate \ --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc123 \ --region us-east-1
# Check certificate validation status aws acm describe-certificate \ --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc123 \ --query 'Certificate.[Status,Type,KeyAlgorithm]' \ --region us-east-1 ```
Certificate must have Status: ISSUED before use with NLB.
Step 2: Create or Import Certificate
Request new certificate: ```bash # Request certificate with DNS validation aws acm request-certificate \ --domain-name api.example.com \ --subject-alternative-names api.example.com \ --validation-method DNS \ --region us-east-1
# Add the CNAME record to your DNS # _acme-challenge.api.example.com -> <validation-value> ```
Import existing certificate:
``bash
# Import certificate with private key and chain
aws acm import-certificate \
--certificate fileb://certificate.pem \
--private-key fileb://private-key.pem \
--certificate-chain fileb://certificate-chain.pem \
--region us-east-1
Step 3: Create NLB with TLS Listener
```bash # Create NLB aws elbv2 create-load-balancer \ --name my-nlb \ --type network \ --subnets subnet-abc123 subnet-def456 \ --region us-east-1
# Create target group for TCP backend aws elbv2 create-target-group \ --name my-targets \ --protocol TCP \ --port 8080 \ --vpc-id vpc-123456 \ --health-check-protocol TCP \ --health-check-port 8080 \ --region us-east-1
# Create TLS listener aws elbv2 create-listener \ --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/my-nlb/1234567890abcdef \ --protocol TLS \ --port 443 \ --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 \ --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \ --alpn-policy HTTP2Preferred \ --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-targets/def456 \ --region us-east-1 ```
Step 4: Configure Security Policy
Choose appropriate TLS security policy:
```bash # List available policies aws elbv2 describe-ssl-policies --region us-east-1
# Recommended policies: # - ELBSecurityPolicy-TLS13-1-2-2021-06 (TLS 1.2+ with modern ciphers) # - ELBSecurityPolicy-TLS13-1-3-2021-06 (TLS 1.3 only) # - ELBSecurityPolicy-FS-1-2-Res-2020-10 (Forward secrecy, TLS 1.2)
# Update listener security policy aws elbv2 modify-listener \ --listener-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/net/my-nlb/1234567890abcdef/abc123def456 \ --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \ --region us-east-1 ```
Step 5: Configure ALPN Policy
ALPN (Application-Layer Protocol Negotiation) is required for HTTP/2:
```bash # Available ALPN policies: # - HTTP2Preferred - Prefer HTTP/2, fallback to HTTP/1.1 # - HTTP1Only - Only HTTP/1.1 # - None - No ALPN negotiation
# Create listener with ALPN aws elbv2 create-listener \ --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/my-nlb/1234567890abcdef \ --protocol TLS \ --port 443 \ --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 \ --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \ --alpn-policy HTTP2Preferred \ --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-targets/def456 ```
Step 6: Configure Backend Protocol
Decide whether NLB terminates TLS or passes through:
Option A: TLS Termination (NLB decrypts, sends plain TCP to backend): ```bash # Target group uses TCP protocol aws elbv2 create-target-group \ --name my-targets \ --protocol TCP \ --port 8080 \ --vpc-id vpc-123456
# Listener uses TLS, forwards to TCP target aws elbv2 create-listener \ --load-balancer-arn <nlb-arn> \ --protocol TLS \ --port 443 \ --certificates CertificateArn=<cert-arn> \ --default-actions Type=forward,TargetGroupArn=<tg-arn> ```
Option B: TLS Passthrough (NLB passes encrypted traffic to backend): ```bash # Target group uses TCP protocol with TLS health checks aws elbv2 create-target-group \ --name my-targets-tls \ --protocol TCP \ --port 443 \ --vpc-id vpc-123456 \ --health-check-protocol HTTPS \ --health-check-path /health
# Listener uses TCP (not TLS) - passes through aws elbv2 create-listener \ --load-balancer-arn <nlb-arn> \ --protocol TCP \ --port 443 \ --default-actions Type=forward,TargetGroupArn=<tg-arn> ```
Step 7: Configure Health Checks
For TLS termination with HTTP backends:
# Update target group health check
aws elbv2 modify-target-group \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-targets/def456 \
--health-check-protocol HTTP \
--health-check-port 8080 \
--health-check-path /health \
--health-check-interval-seconds 30 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 2Step 8: Test TLS Configuration
```bash # Test TLS connection openssl s_client -connect my-nlb-1234567890.elb.us-east-1.amazonaws.com:443 -servername api.example.com
# Check certificate details echo | openssl s_client -connect my-nlb-1234567890.elb.us-east-1.amazonaws.com:443 2>/dev/null | openssl x509 -noout -text
# Test with curl curl -v https://api.example.com/
# Test HTTP/2 support curl -v --http2 https://api.example.com/ ```
Step 9: Terraform Configuration
```hcl # ACM Certificate resource "aws_acm_certificate" "main" { domain_name = "api.example.com" validation_method = "DNS"
lifecycle { create_before_destroy = true } }
# DNS validation record resource "aws_route53_record" "cert_validation" { for_each = { for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } }
allow_overwrite = true name = each.value.name records = [each.value.record] type = each.value.type zone_id = aws_route53_zone.main.zone_id ttl = 60 }
# Wait for validation resource "aws_acm_certificate_validation" "main" { certificate_arn = aws_acm_certificate.main.arn validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] }
# NLB resource "aws_lb" "main" { name = "my-nlb" internal = false load_balancer_type = "network" subnets = aws_subnet.public[*].id }
# Target Group resource "aws_lb_target_group" "main" { name = "my-targets" port = 8080 protocol = "TCP" vpc_id = aws_vpc.main.id target_type = "instance"
health_check { enabled = true healthy_threshold = 2 interval = 30 port = 8080 protocol = "TCP" unhealthy_threshold = 2 } }
# TLS Listener resource "aws_lb_listener" "https" { load_balancer_arn = aws_lb.main.arn port = 443 protocol = "TLS" ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" alpn_policy = ["HTTP2Preferred"]
certificate_arn = aws_acm_certificate_validation.main.certificate_arn
default_action { type = "forward" target_group_arn = aws_lb_target_group.main.arn } } ```
Verify the Fix
```bash # Check listener configuration aws elbv2 describe-listeners \ --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/my-nlb/1234567890abcdef
# Check target health aws elbv2 describe-target-health \ --target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-targets/def456
# Test end-to-end curl -v https://api.example.com/health ```
NLB TLS Termination Checklist
| Check | Command | Expected |
|---|---|---|
| Certificate status | aws acm describe-certificate | ISSUED |
| Certificate region | ARN check | Same as NLB |
| Security policy | aws elbv2 describe-listeners | TLS 1.2+ |
| ALPN policy | aws elbv2 describe-listeners | HTTP2Preferred |
| Target health | aws elbv2 describe-target-health | healthy |
| TLS handshake | openssl s_client | Certificate shown |
Related Issues
- [Fix AWS ALB CreateListener TargetGroupNotFound](/articles/aws-alb-createlistener-targetgroupnotfound)
- [Fix AWS ELB Listener Certificate Issues](/articles/fix-aws-elb-listener-certificate)
- [Fix AWS NLB Connection Timeout](/articles/fix-aws-nlb-connection-timeout)