웹 서버·WAS 학습 노트 목차

TLS 종단과 HTTP/2·3 — 앞단에서 무엇을 끝내나

요청이 암호화된 채로 들어오면 누군가는 그것을 풀어야 라우팅도 압축도 할 수 있다. 그 "누군가" 를 어디에 둘지가 인프라 설계의 갈림길이다.


1. TLS 종단(termination)이란

클라이언트 ══암호화══> [ 여기서 푼다 ] ──평문──> 애플리케이션
                          ↑
                     TLS 종단 지점

이 지점이 어디냐에 따라 구조가 달라진다.

  • ① LB 에서 종단 ALB · CLB 가 인증서를 들고 푼다

    • 뒤는 평문 HTTP. 관리가 가장 단순하다
    • 내부망도 신뢰 못 하는 요건이면 부적합
  • 웹 서버(nginx)에서 nginx 가 인증서를 들고 푼다

    • 가장 흔한 구성. 캐시·압축·라우팅을 여기서 다 한다
  • ③ 애플리케이션까지 끝까지 암호화. 종단 간 암호화

    • LB 는 L4 로 그냥 흘려보낸다(TCP 패스스루)
    • LB 가 경로·헤더를 못 보므로 L7 기능을 못 쓴다
  • ④ 다시 암호화(re-encrypt) 앞에서 풀고, 뒤로 보낼 때 다시 암호화

    • L7 기능도 쓰고 내부 구간도 암호화한다
    • 비용이 두 배. 규제 요건에서 흔하다

판단 기준 — "내부 구간을 신뢰할 수 있는가" 와 "L7 기능이 필요한가" 둘의 조합이다. 둘 다 필요하면 ④, 내부를 신뢰하면 ①·②.

2. nginx 에서의 기본형

server {
    listen 443 ssl;
    http2 on;                                  # 1.25.1+ 지시자 형태
    server_name shop.example.com;

    ssl_certificate     /etc/ssl/fullchain.pem;   # 인증서 + 중간 CA
    ssl_certificate_key /etc/ssl/privkey.pem;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;             # TLS 1.3 에서는 의미가 없다
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

server {
    listen 80;
    server_name shop.example.com;
    return 301 https://$host$request_uri;
}

fullchain 이 아니면 일부 클라이언트만 실패한다

서버는 '자기 인증서 + 중간 CA' 를 함께 보내야 한다

리프 인증서만 보내면

  • 중간 CA 를 이미 캐시한 브라우저 → 정상 동작
  • 처음 오는 클라이언트 · 서버 간 호출(curl · SDK) → 검증 실패

"브라우저는 되는데 서버에서 호출하면 인증서 오류" 의 전형이다 확인: openssl s_client -connect host:443 -showcerts

  • 체인이 몇 개 나오는지 본다

세션 재사용이 지연을 줄인다

  • 전체 핸드셰이크 — 비대칭 연산 + 왕복 2회 (TLS 1.2) 또는 1회 (TLS 1.3)

  • 세션 재개 — 이전 세션 정보를 재사용 → 왕복과 연산을 줄인다

  • ssl_session_cache — 서버가 세션을 기억한다 (서버 쪽 상태) ssl_session_tickets 세션 정보를 암호화해 클라이언트에게 맡긴다 (서버 무상태)

티켓은 편하지만, 티켓 암호화 키를 주기적으로 교체하지 않으면 전방향 비밀성(forward secrecy)이 약해진다

  • 키 회전을 자동화하지 못할 환경이면 tickets off + cache 조합이 안전하다

3. HTTP/2 — 무엇이 실제로 달라지나

  • HTTP/1.1 — 연결 하나에 요청을 한 줄로 보낸다

    • 앞 응답이 느리면 뒤가 막힌다 (HTTP 레벨 HoL)
    • 브라우저가 연결을 6개씩 여는 것으로 우회했다
  • HTTP/2 — 하나의 연결에 스트림을 다중화한다

    • 요청·응답이 섞여 오갈 수 있다
    • 헤더를 HPACK 으로 압축한다 (반복되는 쿠키·UA 가 커진다)
    • 연결 하나로 충분해진다

남는 문제 — TCP 위에서 돌기 때문에 패킷 하나가 유실되면 그 뒤 모든 스트림이 재전송을 기다린다. HTTP 레벨 HoL 은 없앴지만 TCP 레벨 HoL 이 남았다. 그래서 손실률이 높은 모바일 환경에서는 HTTP/2 의 이득이 줄어든다.

설정에서 실제로 걸리는 것

http2 on;                       # nginx 1.25.1+ 의 형태
# listen 443 ssl http2;         # 그 이전의 형태 — 아직 이 표기의 문서가 많다
  • HTTP/2 는 사실상 TLS 위에서만 쓴다 (브라우저가 평문 h2 를 지원하지 않는다)
  • 뒤로 프록시하는 구간은 별개다 — nginx→업스트림은 기본이 HTTP/1.1 이다
    • (nginx 1.31 부터 proxy 모듈이 HTTP/2 백엔드를 지원한다)
  • 도메인 샤딩(정적 자원을 여러 도메인으로 쪼개기)은 HTTP/2 에서 오히려 손해다
    • 연결이 늘어나 다중화 이점이 사라진다 — HTTP/1.1 시대의 최적화는 되돌린다

4. HTTP/3 — TCP 를 버렸다

HTTP/3 = HTTP over QUIC, QUIC 는 UDP 위에서 돈다

왜 UDP 인가

  • TCP 는 커널·중간 장비에 깊이 박혀 있어 바꾸기가 사실상 불가능하다
  • 유저 공간에서 구현할 수 있는 UDP 위에 새로 쌓았다

얻은 것

  • 스트림별 독립 — 한 스트림의 손실이 다른 스트림을 막지 않는다 (TCP HoL 해소)
  • 연결 수립과 TLS 를 합쳐 왕복이 줄었다 (1-RTT, 재개 시 0-RTT)
  • 연결 ID 기반이라 IP 가 바뀌어도 연결이 유지된다 (연결 이주)
    • 와이파이 ↔ LTE 전환에서 끊기지 않는다

nginx 에서

server {
    listen 443 ssl;
    listen 443 quic reuseport;      # QUIC/HTTP3
    http2 on;
    http3 on;

    add_header Alt-Svc 'h3=":443"; ma=86400';   # "나 h3 도 된다" 를 알린다
}

운영에서 확인할 것

  • 빌드에 QUIC 지원이 포함돼 있어야 한다 (nginx -V 로 확인)
  • UDP 443 이 방화벽·보안그룹에서 열려 있어야 한다 ← 가장 흔한 누락
  • Alt-Svc 로 알려야 브라우저가 다음 접속부터 h3 를 쓴다
    • (첫 요청은 TCP 로 온다 — h3 만으로는 시작할 수 없다)
  • LB·CDN 이 UDP 를 통과시키는지 확인한다

0-RTT 의 대가 — 재개 시 첫 요청을 왕복 없이 보낼 수 있지만, 그 데이터는 재전송 공격에 취약하다. 조회(GET)처럼 멱등한 요청에만 허용하고, 결제 같은 상태 변경에는 쓰지 않는다.

5. 인증서 운영

  • 만료가 곧 장애다 — 그리고 예고 없이 오지 않는다

  • 자동 갱신을 건다 (ACME/Let's Encrypt 등)

    • nginx 는 최근 ACME 모듈이 추가돼 외부 도구 없이 갱신할 수 있는 경로가 생겼다
  • 갱신 후 반드시 reload 한다 — 파일만 바뀌고 프로세스가 옛 인증서를 들고 있는

    • 상태가 실제로 자주 발생한다
  • 만료 D-30 알림을 모니터링에 넣는다

    • 확인: openssl x509 -enddate -noout -in fullchain.pem
  • 와일드카드(*.example.com)는 한 단계만 커버한다

    • a.example.com ✓ / a.b.example.com ✗

보안 헤더는 앞단에서 붙인다

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options    "nosniff" always;
add_header Referrer-Policy           "strict-origin-when-cross-origin" always;

always 를 붙이지 않으면 4xx·5xx 응답에는 헤더가 안 붙는다

그리고 nginx 의 add_header 는 상속이 특이하다 — 하위 블록에서 add_header 를 '하나라도' 쓰면 상위의 add_header 가 전부 사라진다

  • 공통 헤더는 별도 파일로 만들어 각 블록에서 include 하는 편이 안전하다

HSTS 는 되돌리기 어렵다 — max-age 동안 브라우저가 http 접속을 거부한다

  • 짧은 값으로 시작해 검증 후 늘린다. preload 는 특히 신중하게

한눈에 정리

  • TLS 종단 — LB / 웹서버 / 종단간 / 재암호화 — 내부 신뢰와 L7 필요 여부로 고른다
  • 인증서fullchain 을 보낸다. 리프만 보내면 서버 간 호출에서만 깨진다
  • 세션 재개 — cache(서버 상태) vs tickets(무상태). 티켓은 키 회전이 전제
  • HTTP/2 — 다중화 + HPACK. TCP 레벨 HoL 은 남는다. 도메인 샤딩은 되돌린다
  • HTTP/3 — QUIC(UDP). 스트림 독립 · 연결 이주 · 0-RTT
    • UDP 443 개방과 Alt-Svc 광고가 실무 체크포인트
  • 0-RTT — 재전송 공격 위험 — 멱등 요청에만
  • 인증서 운영 — 자동 갱신 + 갱신 후 reload + 만료 알림
  • add_header — always 를 붙인다. 하위에서 쓰면 상위 것이 통째로 사라진다

출처 — nginx docs: ngx_http_ssl_module · ngx_http_v2_module · ngx_http_v3_module · "Support for QUIC and HTTP/3" · RFC 9113(HTTP/2) · RFC 9114(HTTP/3) · RFC 9000(QUIC) · RFC 8446(TLS 1.3)

설정 읽기 — server·location·upstreamWAS 커넥터 튜닝 — 스레드는 몇 개가 맞나