도메인 컷오버를 준비하면서 인증서를 새로 발급하다 보니, 예전에 대충 잡아두고 그대로 굴러온 TLS 설정이 다시 눈에 들어왔다. SSL Labs에 걸어보면 A는 나오는데 A+에서 막히거나, 인증서만 붙여놓고 세션 재사용이나 OCSP는 손도 안 댄 서버가 현장에 꽤 많다. 이 참에 최신 기준으로 한 번 정리해두면 다음에 서버를 새로 올릴 때 그대로 복사해서 쓸 수 있다.
결론부터 말하자면, A+는 어렵지 않다. ssl_protocols를 TLS 1.2와 1.3으로 못 박고, OCSP stapling과 세션 캐시를 켜고, HSTS 헤더에 6개월 이상 max-age를 주면 된다. SSL Labs Rating Guide 기준으로 A+는 좋은 설정에 경고가 없고 HSTS max-age가 최소 6개월인 서버에 부여된다.
핸드셰이크와 이 글에서 손대는 것들
TLS 핸드셰이크는 클라이언트와 서버가 암호화 방식을 합의하고 서버 인증서를 검증하는 과정이다. 이 과정에서 왕복이 몇 번 오가는데, 여기에 인증서 폐기 여부 확인까지 얹히면 첫 연결이 느려진다. 이 글에서 손대는 네 가지는 다음과 같다.
- 프로토콜 버전: 낡은 TLS 1.0/1.1을 끄고 1.2와 1.3만 남긴다.
- OCSP stapling: 인증서 폐기 확인 응답을 서버가 미리 받아 핸드셰이크에 붙여준다.
- 세션 재사용:
ssl_session_cache로 한 번 맺은 세션을 재활용해 왕복을 줄인다. - HSTS: 브라우저가 이후 접속을 항상 HTTPS로 강제하게 한다.
아래에서 버전별 기본값 함정부터 확인하고, 설정 블록을 통째로 제시한 뒤 항목별로 차례로 살펴본다.
ssl_protocols는 반드시 명시한다
nginx 버전에 따라 프로토콜 기본값이 다르다. 여기서 오독이 많아 정확히 짚어둔다. 공식 CHANGES 기준으로 nginx 1.23.4(2023-03-28)에서 TLSv1.3이 기본 활성화됐다. 하지만 TLSv1과 TLSv1.1이 기본 비활성화된 것은 그보다 한참 뒤인 1.27.3(2024-11-26)부터다.
즉 1.23.4부터 1.27.2 사이 버전에서는 TLS 1.0/1.1이 여전히 기본으로 켜져 있다. "최신 nginx니까 명시 안 해도 구버전 프로토콜은 꺼져 있겠지"라고 넘기면 안 된다. SSL Labs는 서버가 TLS 1.0 또는 1.1을 지원하면 등급을 B로 강등한다. 버전을 따지기 전에 그냥 명시적으로 못 박는 편을 권한다.
ssl_protocols TLSv1.2 TLSv1.3;
현재 실행 중인 nginx 버전은 이렇게 확인한다.
$ nginx -v nginx version: nginx/1.27.3
참고로 TLS 1.3이 아예 지원되지 않으면 SSL Labs는 경고와 함께 등급 상한을 A-로 제한한다. 그래서 1.2만 켜는 것도 A+에는 부족하다. 1.2와 1.3을 함께 켜는 것이 기본이다.
전체 설정 블록
먼저 완성된 server 블록을 통째로 보여준다. 경로는 Let's Encrypt 발급 기준이고, 도메인만 바꿔 쓰면 된다.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 1. 프로토콜
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
# 2. 세션 재사용
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# 3. OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# 4. HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}
이 아래로는 네 덩어리를 하나씩 뜯어본다.
사이퍼는 Mozilla intermediate를 따른다
사이퍼 목록은 직접 손으로 조합하기보다 Mozilla SSL Configuration Generator의 intermediate 프로파일을 쓰는 편이 낫다. 위 ssl_ciphers가 그 목록이고, 전부 ECDHE 기반이라 순방향 비밀성(forward secrecy)을 만족한다.
TLS 1.3의 사이퍼는 nginx가 별도로 지정하지 않고 OpenSSL 기본값을 쓴다. ssl_ciphers는 TLS 1.2 이하에만 적용된다. 그래서 TLS 1.3 사이퍼를 여기서 건드리려 애쓸 필요는 없다.
ssl_prefer_server_ciphers는 off로 둔다. 최신 클라이언트는 자신에게 유리한 사이퍼를 알아서 고르고, 서버가 순서를 강제하면 오히려 최적이 아닌 조합이 선택될 수 있다. Mozilla intermediate도 off를 권장한다.
ssl_session_cache로 재접속 왕복을 줄인다
세션 재사용은 이미 한 번 핸드셰이크를 끝낸 클라이언트가 다시 붙을 때, 키 교환을 처음부터 다시 하지 않고 이전 세션을 되살리는 방법이다. 첫 연결은 어쩔 수 없지만 재접속의 지연을 줄여준다.
ssl_session_cache shared:SSL:10m은 모든 워커 프로세스가 공유하는 10MB 캐시를 만든다. nginx 문서 기준으로 1MB에 약 4000개 세션이 들어가니 10MB면 약 4만 세션이다. ssl_session_timeout 1d는 캐시된 세션의 유효 시간이다.
ssl_session_tickets off는 의도적으로 껐다. 세션 티켓은 편리하지만, nginx가 기본 생성하는 티켓 키는 재시작 전까지 회전되지 않는다. 키를 주기적으로 교체하는 운영 체계를 갖추지 않았다면, 티켓을 끄고 세션 캐시만 쓰는 쪽이 순방향 비밀성 측면에서 안전하다.
OCSP stapling으로 폐기 확인을 서버가 대신한다
OCSP(Online Certificate Status Protocol)는 인증서가 폐기됐는지 CA에 물어보는 방식이다. 이걸 브라우저가 직접 하면 접속할 때마다 CA 서버로 왕복이 하나 더 생긴다. OCSP stapling은 서버가 그 응답을 미리 받아 캐시해두고 핸드셰이크에 붙여(staple) 보내는 것이다. 그러면 클라이언트가 CA에 따로 물을 필요가 없어 첫 연결이 빨라진다.
ssl_stapling on으로 켜고, ssl_stapling_verify on으로 받아온 OCSP 응답의 서명을 검증한다. 검증에는 발급 CA의 인증서 체인이 필요해서 ssl_trusted_certificate에 chain.pem을 지정한다.
여기서 한 번 걸리는 지점이 resolver다. nginx는 OCSP 응답자의 호스트명을 해석하려고 DNS를 쓰는데, resolver가 없으면 stapling이 조용히 동작하지 않는다. 에러가 나기보다 그냥 안 붙는 식이라 놓치기 쉽다. 공개 DNS든 내부 DNS든 하나는 반드시 넣는다.
한 가지 덧붙이면, 일부 CA가 OCSP를 단계적으로 폐지하는 방향으로 가고 있다. 발급 기관이 OCSP 응답을 더 제공하지 않으면 stapling 설정은 무해하게 무시된다. 지금 시점에서는 켜두는 것이 손해가 아니지만, CA 공지를 한 번 확인해두면 좋다.
HSTS는 A+의 요건이다
HSTS(HTTP Strict Transport Security)는 브라우저에게 "앞으로 이 도메인은 항상 HTTPS로만 접속하라"고 지시하는 응답 헤더다. 한 번 받은 브라우저는 지정된 기간 동안 평문 HTTP 요청 자체를 하지 않는다.
SSL Labs Rating Guide 기준으로 HSTS가 없거나 max-age가 짧으면 A+를 받을 수 없다. HSTS가 있으면 A+ 후보가 되고, 없으면 잘 잡아도 A에서 멈춘다. A+의 조건이 6개월 이상 max-age다.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
max-age=63072000은 2년이라 6개월 기준을 넉넉히 넘는다. always를 빼먹으면 4xx/5xx 응답에는 헤더가 안 붙으니 반드시 넣는다. preload는 브라우저 프리로드 목록 등재를 노릴 때 붙이는데, 한 번 올라가면 되돌리기 번거로우니 도메인 정책이 확정된 뒤에 붙이는 편이 낫다.
적용과 검증
설정을 고쳤으면 문법 검사부터 한다.
$ sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful $ sudo systemctl reload nginx
OCSP stapling이 실제로 붙는지는 openssl로 확인한다. -status 옵션이 OCSP 응답을 요청한다.
$ echo | openssl s_client -connect example.com:443 -status 2>/dev/null | grep -A1 "OCSP Response Status" OCSP Response Status: successful (0x0)
successful (0x0)가 나오면 stapling이 동작하는 것이다. reload 직후에는 nginx가 아직 OCSP 응답을 캐시하지 못해 비어 있을 수 있으니, 몇 초 뒤 한 번 더 확인한다. 처음 몇 번은 안 붙다가 이후에 붙는 것이 정상이다.
협상된 프로토콜은 이렇게 본다.
$ echo | openssl s_client -connect example.com:443 -tls1_3 2>/dev/null | grep "Protocol"
Protocol : TLSv1.3
마지막으로 브라우저에서 SSL Labs(ssllabs.com/ssltest)에 도메인을 걸어 등급을 확인한다. 명령줄에서 빠르게 훑고 싶으면 testssl.sh도 쓸 만하다. 위 네 가지가 모두 반영됐다면 A+가 나온다.
정리
A+의 핵심은 네 줄이다. ssl_protocols로 TLS 1.2와 1.3만 남기고, OCSP stapling을 resolver와 함께 켜고, ssl_session_cache로 재접속을 줄이고, HSTS에 6개월 이상 max-age를 준다. 버전별 기본값을 믿지 말고 프로토콜은 직접 명시하는 것, 그리고 resolver와 always를 빼먹지 않는 것이 실무에서 자주 놓치는 지점이다.