Nginx 인증서 교체와 무중단 리로드
이 글의 순서 4개 절
인증서 교체는 파일 두 개를 바꾸는 일이다. 사고는 그 앞뒤에서 난다. 검증 없이 리로드했다가 워커가 옛 설정으로 계속 돌기도 하고, 리로드가 실패했는데 성공한 줄 알고 퇴근하기도 하고, 애초에 다른 서버 블록의 인증서를 바꾸기도 한다. 절차를 고정해 두면 이 셋은 다 없어진다.
reload와 restart
둘을 섞어 쓰면 무중단이 깨진다.
reload (-s reload) |
restart |
|
|---|---|---|
| 마스터 프로세스 | 유지 | 종료 후 재시작 |
| 기존 커넥션 | 워커가 다 처리하고 종료 | 끊김 |
| 리스닝 소켓 | 유지 | 닫혔다 다시 열림 |
| 설정 오류 시 | 옛 설정으로 계속 동작 | 서비스 다운 |
reload를 하면 마스터가 새 설정을 읽고 새 워커를 띄운 뒤, 옛 워커에게 “받은 요청만 마저 처리하고
죽어라”(graceful shutdown)를 보낸다. 처리 중이던 요청은 끊기지 않는다. 인증서 교체에 필요한 건 언제나
reload이고, restart를 쓸 이유가 없다.
바이너리를 업그레이드하는 게 아니라면 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
두 해시가 같아야 한다. 키가 맞지 않으면 아래 nginx -t에서도 걸리지만, 그때는 이미 파일을 바꾼 뒤다.
배포 전에 여기서 잡는 게 싸다.
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는 시작하거나
리로드할 때 인증서를 메모리로 읽고 그걸로 끝이다. 파일을 바꿔도 리로드하지 않으면 아무 효과가 없다. “파일은
새건데 서빙은 옛것”이 되는 원인이 이것이다. SSL 인증서 만료일 확인하는 법에서
말한 파일과 서빙의 불일치가 정확히 여기서 생긴다.
자동화
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 --run-deploy-hooks # 기본 dry-run은 deploy hook을 건너뛴다
--run-deploy-hooks가 없는 옛 certbot이라면 hook 스크립트를 직접 한 번 실행해 본다.
손으로 교체하는 환경이 남아 있다면 위 순서(기록 → 검증 → 원자적 교체 → nginx -t && reload → 지문 대조)를
스크립트 하나로 묶어 두기만 해도 사고가 크게 준다.