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. 1.Certificate in wrong region - ACM certificates must be in the same region as the NLB
  2. 2.Certificate not validated - ACM certificate pending DNS/email validation
  3. 3.Wrong protocol on target group - Using HTTP health checks with TCP targets
  4. 4.Backend protocol mismatch - NLB sends TCP but backend expects TLS
  5. 5.Security policy incompatibility - TLS security policy doesn't support required ciphers
  6. 6.ALPN configuration - Missing or incorrect ALPN policy for HTTP/2
  7. 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:

bash
# 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 2

Step 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

CheckCommandExpected
Certificate statusaws acm describe-certificateISSUED
Certificate regionARN checkSame as NLB
Security policyaws elbv2 describe-listenersTLS 1.2+
ALPN policyaws elbv2 describe-listenersHTTP2Preferred
Target healthaws elbv2 describe-target-healthhealthy
TLS handshakeopenssl s_clientCertificate shown
  • [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)