Nginx 인증서 교체와 무중단 리로드
인증서 교체 자체는 파일 두 개를 바꾸는 일이다. 사고는 교체가 아니라 그 앞뒤에서 난다. 검증 없이 리로드했다가 워커가 옛 설정으로 계속 돌거나, 리로드가 실패했는데 성공한 줄 알고 퇴근하거나, 애초에 다른 서버 블록의 인증서를 바꾸거나.
절차를 고정해두면 이 세 가지가 전부 사라진다.
reload 와 restart 는 다르다
먼저 개념부터. 이 둘을 섞어 쓰면 무중단이 아니게 된다.
reload (-s reload) |
restart |
|
|---|---|---|
| 마스터 프로세스 | 유지 | 종료 후 재시작 |
| 기존 커넥션 | 워커가 다 처리하고 종료 | 끊김 |
| 리스닝 소켓 | 유지 | 닫혔다 다시 열림 |
| 설정 오류 시 | 옛 설정으로 계속 동작 | 서비스 다운 |
reload 는 마스터가 새 설정을 읽고, 새 워커를 띄운 뒤, 옛 워커에게 “받은 요청만
마저 처리하고 죽어라”(graceful shutdown)를 보낸다. 처리 중이던 요청은 끊기지
않는다. 인증서 교체에 필요한 건 언제나 reload 다. restart 를 쓸 이유가 없다.
single 바이너리 업그레이드가 아닌 한, systemctl restart nginx 를 습관적으로
치는 건 무중단을 스스로 포기하는 것이다.
교체 절차
0. 현재 상태 기록
교체 전에 지금 서빙 중인 인증서의 지문을 남긴다. 교체 후 비교 기준이 된다.
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256 -enddate
1. 새 인증서 검증 — 배포 전에
파일을 서버에 올리기 전, 그 파일이 맞는 파일인지부터.
# 만료일·도메인 확인
openssl x509 -in fullchain.pem -noout -subject -enddate
openssl x509 -in fullchain.pem -noout -ext subjectAltName
# 인증서와 개인키가 짝이 맞는지 — 공개키 지문 비교
openssl x509 -in fullchain.pem -noout -pubkey | openssl sha256
openssl pkey -in privkey.pem -pubout | openssl sha256
두 해시가 같아야 한다. 키 불일치는 리로드 시점에야 터지는데, 그때는 이미 설정 검증을 통과한 뒤라 당황하게 된다. 배포 전에 잡는 게 싸다.
fullchain 인지도 확인한다. leaf 만 있는 파일이면 교체 순간부터 체인 오류가 시작된다.
grep -c "BEGIN CERTIFICATE" fullchain.pem # 2 이상이어야 정상 (leaf + 중간)
2. 파일 교체 — 원자적으로
쓰다 만 파일을 nginx 가 읽는 상황을 피하려면 같은 파일시스템에서 mv 로 바꾼다.
mv 는 rename 이라 원자적이다. cp 는 아니다.
# 백업
cp -a /etc/nginx/ssl/example.com.pem /etc/nginx/ssl/example.com.pem.bak
cp -a /etc/nginx/ssl/example.com.key /etc/nginx/ssl/example.com.key.bak
# 새 파일을 같은 디렉토리에 올린 뒤 rename
mv /etc/nginx/ssl/fullchain.pem.new /etc/nginx/ssl/example.com.pem
mv /etc/nginx/ssl/privkey.pem.new /etc/nginx/ssl/example.com.key
chmod 600 /etc/nginx/ssl/example.com.key
백업을 남기는 이유는 롤백이 mv 두 번 + reload 한 번으로 끝나게 하기 위해서다.
3. 설정 검증 — 반드시 리로드 전에
nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
nginx -t 는 문법만 보는 게 아니라 인증서 파일을 실제로 열어 개인키와 대조한다.
키 불일치, 파일 없음, 권한 문제가 여기서 걸린다. 이게 통과하지 않으면 리로드하지
않는다 — 한 줄로 묶어두면 실수 자체가 불가능해진다.
nginx -t && nginx -s reload
4. 리로드 후 확인 — 파일 말고 서빙을 본다
리로드가 성공했다는 로그만 믿지 말고, 실제로 서빙되는 인증서를 다시 찍는다.
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256 -enddate
0번에서 기록한 지문과 달라졌는지, notAfter 가 새 날짜인지 확인한다.
지문이 그대로면 리로드가 안 됐거나, 다른 서버 블록을 고친 것이다.
리로드가 조용히 실패하는 경우들
워커가 안 죽는다 — reload 신호는 보냈는데 옛 워커가 shutting down 상태로
계속 남아 있는 경우. 원인은 대부분 안 끊기는 커넥션이다. WebSocket, 스트리밍,
long polling 이 물려 있으면 그 워커는 커넥션이 끝날 때까지 산다. 옛 워커가 살아
있는 동안 그 워커로 들어간 요청은 옛 인증서를 받는다.
ps -ef | grep "nginx: worker" | grep "shutting down"
오래 남으면 worker_shutdown_timeout (기본 무제한)을 설정해 상한을 준다.
worker_shutdown_timeout 30s;
sudo 없이 reload — 권한 부족으로 신호가 안 가는데 셸에는 에러가 애매하게
나오는 경우가 있다. systemctl reload nginx 를 쓰면 권한과 로깅이 일관된다.
교체한 파일과 설정의 파일이 다르다 — ssl_certificate 가 가리키는 경로와
실제 교체한 경로가 다른 고전적 실수. 서버 블록이 수십 개면 흔하다. 어느 파일을
읽는지는 설정에서 직접 뽑는 게 확실하다.
nginx -T 2>/dev/null | grep -E "server_name|ssl_certificate " | grep -B1 ssl_certificate
symlink 갱신인데 리로드를 안 함 — certbot 처럼 symlink(live/)를 새 파일로
돌리는 구조는 파일이 바뀌어도 nginx 가 모른다. nginx 는 시작·리로드 시점에
인증서를 메모리로 읽고 끝이다. 파일 교체는 리로드 없이는 아무 효과가 없다.
“파일은 새건데 서빙은 옛것”의 원인이 이것이고, 1편에서 말한 파일·서빙 불일치가 정확히 이 지점에서 생긴다.
자동화한다면
certbot 은 --deploy-hook 이 갱신 성공 시에만 실행된다. 여기 리로드를 걸어두면
갱신-리로드 누락이 구조적으로 사라진다.
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
nginx -t && systemctl reload nginx
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
certbot renew --dry-run # hook 실행까지 포함해 리허설
수동 교체가 남아 있는 환경이면 위 절차(기록 → 검증 → 원자적 교체 → nginx -t && reload → 지문 대조)를 스크립트 하나로 묶어두는 것만으로 사고 확률이 크게 준다.
정리
- 인증서 교체는 항상
reload.restart는 커넥션을 끊고, 얻는 것이 없다 - 배포 전: 만료일·SAN·키 짝 맞춤·fullchain 여부를 파일 단계에서 검증
nginx -t && nginx -s reload를 한 몸으로 — 검증 없는 리로드를 불가능하게- 교체 후 판정은 파일이 아니라 서빙 지문으로. 교체 전 지문과 달라야 성공
- 옛 워커가 살아 있는 동안은 옛 인증서가 나갈 수 있다 —
worker_shutdown_timeout으로 상한