🌍 네트워크

SSL 종료

SSL Termination

로드밸런서에서 SSL 복호화. 백엔드 부하 감소.

📖 상세 설명

SSL 종료(SSL Termination, 또는 TLS Termination)는 로드밸런서나 리버스 프록시에서 암호화된 HTTPS 연결을 복호화하고, 백엔드 서버와는 평문 HTTP로 통신하는 아키텍처 패턴입니다. 클라이언트 → 로드밸런서 구간만 암호화되고, 로드밸런서 → 백엔드 서버 구간은 암호화되지 않습니다.

SSL/TLS 암호화와 복호화는 CPU 집약적인 작업입니다. 특히 TLS 핸드셰이크 과정에서 RSA 2048비트 키 교환은 단일 연결당 수 밀리초의 CPU 시간을 소모합니다. SSL 종료를 적용하면 이 부하가 전용 로드밸런서로 집중되어, 백엔드 서버는 비즈니스 로직에만 집중할 수 있습니다. AWS ALB에서는 초당 수만 개의 TLS 연결을 처리할 수 있습니다.

인증서 관리도 대폭 간소화됩니다. SSL 종료 없이는 모든 백엔드 서버에 인증서를 설치하고 갱신해야 하지만, SSL 종료를 사용하면 로드밸런서 한 곳에서만 인증서를 관리하면 됩니다. Let's Encrypt 자동 갱신이나 AWS Certificate Manager 같은 서비스와 결합하면 인증서 만료로 인한 장애 위험을 크게 줄일 수 있습니다.

보안 관점에서 SSL 종료는 트레이드오프가 있습니다. 내부 네트워크 구간이 평문으로 노출되므로, 신뢰할 수 있는 프라이빗 네트워크(VPC) 내에서만 사용해야 합니다. 금융, 의료 등 규제가 엄격한 환경에서는 end-to-end 암호화를 요구하는 경우가 있어, SSL Passthrough나 Re-encryption을 고려해야 할 수 있습니다.

💻 코드 예제

# Nginx SSL 종료 설정
# /etc/nginx/conf.d/ssl-termination.conf

upstream backend_servers {
    server 10.0.1.10:8080;  # 백엔드는 HTTP (8080)
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    # SSL 인증서 (Let's Encrypt 또는 상용 인증서)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # 최신 TLS 보안 설정
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # OCSP Stapling (인증서 유효성 검증 가속화)
    ssl_stapling on;
    ssl_stapling_verify on;

    # 백엔드로 HTTP 평문 전달
    location / {
        proxy_pass http://backend_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # 원본 클라이언트 정보 전달
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;  # https
    }
}

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
# HAProxy SSL 종료 설정
# /etc/haproxy/haproxy.cfg

global
    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
    ssl-default-bind-options ssl-min-ver TLSv1.2
    tune.ssl.default-dh-param 2048
    maxconn 50000

defaults
    mode http
    timeout connect 5s
    timeout client 30s
    timeout server 30s
    option httplog

# HTTPS 프론트엔드 (SSL 종료 지점)
frontend https_front
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem

    # HSTS 헤더 추가
    http-response set-header Strict-Transport-Security "max-age=31536000"

    # 원본 정보 헤더 추가
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Real-IP %[src]

    default_backend http_back

# HTTP 리다이렉트
frontend http_front
    bind *:80
    redirect scheme https code 301

# 백엔드 (평문 HTTP)
backend http_back
    balance roundrobin
    option httpchk GET /health

    server web1 10.0.1.10:8080 check
    server web2 10.0.1.11:8080 check
    server web3 10.0.1.12:8080 check
# AWS ALB SSL 종료 - Terraform 설정
# main.tf

# ACM 인증서 (자동 갱신)
resource "aws_acm_certificate" "main" {
  domain_name       = "example.com"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

# Application Load Balancer
resource "aws_lb" "main" {
  name               = "main-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = var.public_subnets
}

# HTTPS 리스너 (SSL 종료)
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.main.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

# HTTP → HTTPS 리다이렉트
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# 타겟 그룹 (백엔드는 HTTP)
resource "aws_lb_target_group" "main" {
  name     = "main-tg"
  port     = 8080      # 백엔드는 평문 HTTP
  protocol = "HTTP"    # SSL 없음
  vpc_id   = var.vpc_id

  health_check {
    path                = "/health"
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }
}

🗣️ 실무에서 이렇게 말하세요

💬 아키텍처 설계 회의에서
"SSL 종료를 ALB에서 처리하면 백엔드 EC2의 CPU 부하를 20-30% 줄일 수 있습니다. 인증서 관리도 ACM에서 자동 갱신되니까 운영 부담이 크게 줄어요. 다만 ALB와 백엔드 간 통신은 평문이니까 VPC 내부에서만 트래픽이 흐르도록 보안 그룹 설정을 확실히 해야 합니다."
💬 보안 검토 회의에서
"규제 요건상 end-to-end 암호화가 필요하다면 SSL Passthrough나 Re-encryption을 써야 합니다. Passthrough는 로드밸런서가 암호화를 건드리지 않고 그대로 전달하는 방식이고, Re-encryption은 로드밸런서에서 복호화 후 다시 암호화해서 백엔드로 보내는 방식입니다. 성능과 보안 사이의 트레이드오프를 고려해서 선택하시면 됩니다."
💬 장애 대응 상황에서
"백엔드에서 X-Forwarded-Proto 헤더를 확인해보세요. SSL 종료 후 프록시가 이 헤더를 설정해줘야 앱에서 HTTPS 요청인지 알 수 있습니다. 이 헤더가 없으면 앱이 HTTP로 인식해서 무한 리다이렉트 루프가 발생할 수 있어요."

⚠️ 흔한 실수 & 주의사항

내부 네트워크 보안 무시

SSL 종료 후 백엔드 구간이 평문이므로, 반드시 VPC/프라이빗 네트워크 내에서만 통신해야 합니다. 퍼블릭 네트워크에서 평문 통신은 중간자 공격(MITM) 위험이 있습니다.

X-Forwarded 헤더 미설정

SSL 종료 후 X-Forwarded-Proto, X-Forwarded-For 헤더를 설정하지 않으면 백엔드에서 원본 클라이언트 IP와 프로토콜을 알 수 없습니다. 로깅, 보안 정책, HTTPS 리다이렉트 로직이 오작동합니다.

인증서 만료 모니터링 부재

SSL 종료 지점의 인증서가 만료되면 전체 서비스가 다운됩니다. ACM 자동 갱신을 사용하거나, certbot 자동화와 함께 만료 30일 전 알림을 설정하세요.

올바른 SSL 종료 구성

TLS 1.2 이상만 허용, HSTS 헤더 설정, 강력한 암호화 스위트 사용. AWS라면 ELBSecurityPolicy-TLS13-1-2-2021-06 이상의 정책을 적용하고, CloudWatch로 SSL 핸드셰이크 오류를 모니터링하세요.

🔗 관련 용어

📚 더 배우기