Tomcat & nginx 운영 · SSL 종료와 인증서 · 이론
인증서 장애의 90%는 체인과 만료다
한 줄 요약
인증서 장애의 대부분은 암호학이 아니라 두 가지에서 나온다 — 중간 인증서를 안 붙인 체인과, 아무도 안 보고 있던 만료일.
왜 이게 문제인가
이 두 가지가 위험한 이유는 개발자 PC 에서는 재현되지 않는다는 점이다. 브라우저는 중간 인증서를 캐시하거나 알아서 내려받아 채워 주기 때문에, 체인이 빠져 있어도 화면은 멀쩡히 뜬다. 그런데 서버끼리 붙는 연동(자바 클라이언트, 배치)은 그런 보정을 하지 않아서 그대로 실패한다. "내 브라우저에서는 되는데요" 가 여기서 나온다.
만료는 더 단순하고 더 아프다. 유효기간이 지나는 순간 전면 장애이고, 새벽에 시작되며, 조치는 파일 교체 하나인데 그 파일을 발급받는 데 며칠이 걸린다. 그래서 대응이 아니라 감시의 문제다 — 남은 날짜를 세는 스크립트 하나와 알림 하나면 끝나는 일을, 매년 어딘가에서 겪는다.
SI 현장에서 인증서를 만나는 순간
인증서는 보통 이런 식으로 온다. 고객사 보안팀이 .pfx 파일 하나와 비밀번호를
메일로 보낸다. 또는 server.crt, server.key, chain.crt 세 파일이 온다.
가끔은 .cer 하나만 온다. 그리고 "오픈 전까지 적용 부탁드립니다"라고 쓰여 있다.
이때 필요한 지식은 암호학이 아니라 파일이 무엇을 담고 있는지 확인하는 법이다.
# 인증서 내용 보기 (주체, 발급자, 유효기간, SAN)openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName# 키와 인증서가 짝인지 확인 (두 해시가 같아야 함)openssl x509 -in server.crt -noout -modulus | openssl md5openssl rsa -in server.key -noout -modulus | openssl md5# pfx 를 crt/key 로 분해openssl pkcs12 -in cert.pfx -clcerts -nokeys -out server.crtopenssl pkcs12 -in cert.pfx -nocerts -nodes -out server.key키와 인증서가 짝이 아니면 nginx 는 기동조차 안 되면서key values mismatch 라는 짧은 메시지만 남긴다. 이 명령 두 줄을 알면 30초에 끝난다.
체인 — 개발자 PC 에서는 되는데 서버끼리는 안 되는 이유
인증서는 보통 3단이다.
Root CA (브라우저·OS 에 이미 들어 있음) └─ Intermediate CA (중간 인증서) └─ 서버 인증서 (우리 것)서버는 자기 인증서 + 중간 인증서를 함께 보내야 한다. Root 는 상대가 갖고 있다.
중간 인증서를 빼먹으면 어떻게 되는가?
- 브라우저: 대부분 잘 된다. 브라우저가 예전에 다른 사이트에서 받은
- 서버 간 통신(연동 시스템의 HTTP 클라이언트, 자바
HttpsURLConnection, curl):
중간 인증서를 캐시하고 있거나, AIA 확장으로 알아서 받아 오기 때문이다.
실패한다. 캐시도 없고 AIA 를 안 따라가는 구현이 많다.
그래서 증상이 이렇게 나타난다.
"개발자 PC 브라우저에서는 잘 되는데, 연동 상대 시스템에서만 SSL 오류가 납니다."
이건 체인 누락의 전형적인 모습이다. 확인은 한 줄이면 된다.
echo | openssl s_client -connect api.example.com:443 \ -servername api.example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'1 이 나오면 체인이 없는 것이다. 정상이면 2 이상이 나온다.
nginx 는 ssl_certificate 에 서버 인증서 → 중간 인증서 순서로 이어 붙인 파일을 준다.
순서가 반대면 안 된다.
cat server.crt intermediate.crt > fullchain.pemSAN 이 없으면 요즘 브라우저는 거부한다
옛날에는 CN(Common Name)에 도메인을 적었다. 지금은 SAN(Subject Alternative Name)
확장이 없으면 최신 브라우저가 인증서를 거부한다. CN 은 참고용이 됐다.
자체 서명 인증서를 만들 때 SAN 을 빼먹어서 "왜 안 되지"를 반복하는 일이 흔하다.
개발/검증계용 사설 CA 를 만들 땐 반드시 SAN 을 넣는다.
subjectAltName = DNS:labhub.local, DNS:*.labhub.local, IP:127.0.0.1만료 — 가장 흔하고 가장 어이없는 장애
실제 사례를 하나 말하면, 대규모 SSO 전환 프로젝트에서 **SAML 서명 인증서 만료로
47분간 전면 로그인 장애**가 난 적이 있다. 인증서 만료는
- 예고 없이 오지 않는다 (만료일은 처음부터 알고 있었다)
- 그런데 담당자가 바뀌고 알림이 스팸함으로 가면 아무도 모른다
그래서 만료 감시는 사람이 아니라 스크립트가 해야 한다.
openssl x509 -in server.crt -noout -enddate# notAfter=Nov 12 09:00:00 2026 GMT# 임계일 이내면 실패로 종료 (checkend 는 초 단위)openssl x509 -in server.crt -noout -checkend $((30*86400)) \ || echo "30일 이내 만료"-checkend 는 거의 안 알려져 있는데, 감시 스크립트를 만들 때 정확히 이 용도다.
서버 인증서뿐 아니라 **연동 상대의 인증서, 클라이언트 인증서, SAML 서명 인증서,
코드 서명 인증서**까지 목록으로 관리해야 한다. 목록이 없으면 반드시 하나를 놓친다.
nginx TLS 설정의 실무 기본값
server { listen 8443 ssl; server_name labhub.local; ssl_certificate /etc/nginx/tls/fullchain.pem; ssl_certificate_key /etc/nginx/tls/server.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;}ssl_protocols TLSv1.2 TLSv1.3— TLS 1.0/1.1 은 이미 폐기됐고ssl_session_cache shared:SSL:10m— 10MB 면 대략 4만 세션이다.ssl_prefer_server_ciphers off— TLS 1.3 에서는 클라이언트 선호를
국내 금융·공공 보안 점검에서도 지적 대상이다. 다만 **오래된 연동 상대가
1.0 만 말하는 경우**가 실제로 있어서, 그럴 땐 해당 상대용 포트를 따로 열고
기한을 못 박는 식으로 처리한다. 전체를 낮추면 안 된다.
세션 재사용이 되면 핸드셰이크 비용이 크게 준다.
존중하는 편이 낫다는 것이 현재 권고다.
HTTP → HTTPS 리다이렉트
server { listen 8088; server_name labhub.local; return 301 https://$host:8443$request_uri;}rewrite 대신 return 301 을 쓰는 것이 권장된다. 더 빠르고 의도가 명확하다.
그리고 리다이렉트를 걸었다면 HSTS 도 함께 고려한다. 다만 HSTS 는
한 번 브라우저에 박히면 되돌리기 어려우니, 내부 시스템에는 max-age 를
짧게 시작해 늘려 가는 것이 안전하다.