이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • TLS termination을 Nginx에서 한다는 것은 Spring 운영에서 무엇을 의미하는가?
  • 인증서 만료, chain 오류, reload 실패를 어떻게 사전에 확인하는가?
  • HTTPS 종료 후 Spring redirect, cookie, client 정보가 틀어지지 않게 하려면 무엇이 필요한가?

개요

TLS termination은 client와 Nginx 사이의 HTTPS 연결을 Nginx가 끝내는 구조다.

Nginx는 인증서를 제시하고 암호화된 요청을 복호화한 뒤 내부 Spring upstream으로 전달한다.

Spring은 내부 HTTP로 요청을 받을 수 있지만, 원래 요청이 HTTPS였다는 사실은 header로 전달받아야 한다.

인증서와 TLS 설정은 보안 기능이면서 동시에 배포 장애 원인이 될 수 있다.

요청 흐름

client HTTPS
  -> Nginx 443 TLS termination
    -> proxy header
      -> Spring HTTP upstream

TLS가 Nginx에서 끝나도 사용자 입장에서는 HTTPS 서비스다.

그러나 Spring 입장에서는 Nginx가 전달한 HTTP 요청으로 보일 수 있다.

이 차이를 처리하지 않으면 redirect가 HTTP로 내려가거나 secure cookie 판단이 틀릴 수 있다.

기본 설정 예시

server {
    listen 80;
    server_name api.example.com;
    return 308 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    server_name api.example.com;
 
    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
 
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

fullchain.pem은 서버 인증서와 중간 인증서 chain을 포함한다.

chain이 빠지면 일부 client에서만 TLS 오류가 날 수 있다.

private key 파일은 권한을 제한해야 하며 repository에 포함되면 안 된다.

인증서 갱신

인증서는 만료일이 있다.

자동 갱신을 쓰더라도 갱신 성공과 Nginx reload 성공은 별개다.

갱신 파일이 만들어져도 Nginx가 reload되지 않으면 이전 인증서를 계속 제공할 수 있다.

운영 문서에는 갱신 방식, 만료 알림, reload 명령, 실패 시 담당자가 있어야 한다.

nginx -t
systemctl reload nginx
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null

nginx -t는 인증서 파일 경로와 설정 문법을 확인한다.

실제 제공 중인 인증서는 외부에서 openssl s_client나 모니터링으로 확인해야 한다.

Spring과 Forwarded Header

TLS termination 뒤의 Spring은 원래 scheme을 알아야 한다.

X-Forwarded-Proto: https가 전달되지 않으면 Spring은 자신이 HTTP 요청을 받았다고 판단할 수 있다.

그 결과 login redirect, OAuth callback URL, secure cookie, absolute URL 생성이 틀어질 수 있다.

Spring Boot에서는 front proxy 뒤에서 forwarded header를 처리하는 설정을 명확히 둔다.

Nginx header만 추가하고 Spring이 무시하면 문제는 해결되지 않는다.

HSTS 주의

HSTS는 browser가 해당 domain을 HTTPS로만 접속하게 만드는 강한 정책이다.

설정하면 보안에는 도움이 되지만, TLS 설정이 깨졌을 때 사용자가 HTTP로 우회할 수 없다.

처음에는 짧은 max-age로 검증하고, subdomain 적용 여부를 신중히 정해야 한다.

테스트 domain과 운영 domain의 HSTS 정책을 섞으면 복구가 어려워질 수 있다.

운영 체크리스트

  • 인증서 경로가 fullchain.pem과 private key를 올바르게 가리키는가?
  • 인증서 만료 알림이 있고 실제 외부 endpoint 기준으로 확인하는가?
  • 갱신 후 nginx -t와 reload가 자동 또는 문서화되어 있는가?
  • private key 파일 권한과 접근자가 제한되어 있는가?
  • HTTP에서 HTTPS로 redirect하는 정책이 명확한가?
  • X-Forwarded-Proto, Host, client IP header가 Spring까지 전달되는가?
  • HSTS 적용 범위와 rollback 어려움을 이해하고 있는가?

장애 신호

  • browser에서 인증서 만료 또는 신뢰할 수 없는 인증서 오류가 난다.
  • 일부 Android, Java client에서만 chain 오류가 난다.
  • 인증서 갱신 로그는 성공인데 실제 endpoint는 이전 인증서를 제공한다.
  • HTTPS 접속 후 Spring redirect가 HTTP URL로 내려온다.
  • secure cookie가 설정되지 않아 login session이 유지되지 않는다.
  • Nginx reload가 실패했지만 이전 worker가 계속 떠 있어 변경이 반영되지 않는다.

안전한 완화 조치

TLS 장애에서는 먼저 실제 외부 endpoint가 제공하는 인증서를 확인한다.

파일만 보면 Nginx가 그 파일을 reload했는지 알 수 없다.

인증서 파일 경로나 chain 문제라면 nginx -t로 확인한 뒤 reload한다.

private key 노출이 의심되면 인증서를 교체하고 노출 경로를 확인한다.

HSTS가 이미 강하게 배포된 domain에서는 HTTP 우회로를 기대하면 안 된다.

Spring redirect 문제는 Nginx header와 Spring forwarded header 설정을 같이 확인한다.

인프라 담당자와 공유할 자료

증상: 일부 client TLS handshake 실패
domain: api.example.com
외부 확인: issuer chain incomplete
Nginx 설정: ssl_certificate cert.pem 사용
필요 조치: fullchain.pem 적용 후 nginx -t, reload
Spring 영향: 없음, 요청이 Spring까지 도달하지 않음

TLS 문제는 client 종류, domain, 실제 제공 인증서, Nginx 설정 파일 경로를 같이 전달해야 한다.

실전 팁

  • 인증서 만료는 파일 기준이 아니라 외부 endpoint 기준으로 감시한다.
  • 갱신 자동화에는 reload 성공 여부 확인까지 포함한다.
  • TLS 종료 후에도 Spring에는 원래 scheme을 전달한다.
  • HSTS는 작은 max-age로 시작해 검증 후 확대한다.
  • private key는 백업, 채팅, repository에 복사하지 않는다.

위험 신호!

  • 인증서 파일만 갱신하고 Nginx reload를 확인하지 않는다.
  • cert.pemfullchain.pem의 차이를 모른다.
  • Spring redirect가 HTTP로 내려오는데 TLS 설정만 계속 바꾼다.
  • HSTS를 넓게 켜고 복구 절차를 준비하지 않는다.
  • private key 권한을 넓게 열어 둔다.

확인 질문

확인 질문

  • TLS termination 후 Spring에 X-Forwarded-Proto가 필요한 이유는 무엇인가?
    • Spring이 원래 요청이 HTTPS였다는 사실을 알아야 redirect, secure cookie, URL 생성이 맞게 동작하기 때문이다.
  • 인증서 갱신 성공만으로 충분하지 않은 이유는 무엇인가?
    • 새 인증서 파일이 생겨도 Nginx가 reload되지 않으면 실제 endpoint는 이전 인증서를 계속 제공할 수 있기 때문이다.
  • fullchain.pem을 써야 하는 이유는 무엇인가?
    • 일부 client가 서버 인증서만으로는 신뢰 chain을 완성하지 못하므로 중간 인증서까지 함께 제공해야 하기 때문이다.

참고 문서