매년 1~2회, 아무 예고도 없이 인증서 교체 작업이 찾아옵니다. 보안팀이 새 인증서 파일을 메일로 보내왔는데, 첨부된 파일은 .pfx 형식입니다. Nginx는 PEM 형식만 받고, Tomcat은 JKS를 쓰는데, 도대체 어떻게 변환하고 어디에 넣어야 할까요?
형식 변환에서 한 번 막히고, 체인 순서를 잘못 넣어서 한 번 더 막히고, 결국 서비스 중단까지 이어지는 경우가 생각보다 많습니다. 이 모듈에서 형식별 변환 명령어부터 서버별 교체 절차까지 단계별로 정리합니다.
- 1PEM, JKS, PFX(PKCS12) 형식의 차이와 각 서버의 사용 형식을 구분할 수 있다
- 2openssl과 keytool 명령어로 인증서 형식을 상호 변환할 수 있다
- 3Nginx와 Tomcat에 인증서를 서비스 영향 최소화 방식으로 교체할 수 있다
- 4openssl s_client로 인증서 체인 오류를 현장에서 즉시 진단할 수 있다
- 5만료일 모니터링 스크립트를 작성해 cron으로 자동 알림을 구성할 수 있다
openssl versionkeytool -versionmkdir -p /tmp/cert-work && cd /tmp/cert-workgrep -r 'ssl_certificate' /etc/nginx/sites-enabled/ 2>/dev/null | head -5grep -r 'keystoreFile' /opt/tomcat/conf/server.xml 2>/dev/null | head -3인증서 형식 이해
PEM, JKS, PFX: 형식이 다른 이유
인증서 형식이 여러 가지로 나뉜 것은 운영 체제와 플랫폼의 역사적 차이에서 비롯됩니다. Nginx와 Apache가 주로 쓰는 Linux 환경은 텍스트 기반 PEM을, Java/Tomcat은 바이너리 키스토어인 JKS를, Windows IIS는 PKCS12(PFX) 형식을 표준으로 채택했기 때문입니다. 보안팀이 전달한 파일이 어떤 형식인지, 적용할 서버가 어떤 형식을 요구하는지를 파악하지 못하면 올바른 인증서를 받았어도 교체 작업이 막히고 서비스 중단으로 이어질 수 있습니다.
확대
보안팀이 인증서 갱신 파일을 .pfx로 보내왔습니다. Nginx 서버에 적용해야 하는데 Nginx는 PEM만 받고, Tomcat용으로는 JKS가 필요합니다. 변환 명령어를 찾아 실행했더니 또 비밀번호를 묻습니다. 어떤 비밀번호인지 모르고, 변환에 성공해도 Nginx에 적용하고 보니 체인 오류가 납니다. 형식 차이를 모른 채 시작하면 이 순간마다 막힙니다.
인증서 형식이 여러 개인 것은 역사적 이유와 플랫폼 차이 때문입니다. 형식을 모르면 올바른 파일을 받아도 어디에 넣어야 할지 막막합니다.
PEM (Privacy Enhanced Mail)
가장 범용적인 형식입니다. Base64로 인코딩된 텍스트 파일이며 -----BEGIN CERTIFICATE-----로 시작합니다. 인증서(cert.pem), 개인키(key.pem), 체인 포함 인증서(fullchain.pem)를 별도 파일로 분리해 관리합니다. 리눅스 기반 서버(Nginx, Apache, HAProxy)에서 표준으로 사용합니다.
# PEM 파일 헤더로 내용 식별
head -1 cert.pem
# -----BEGIN CERTIFICATE----- ← 인증서
# -----BEGIN PRIVATE KEY----- ← 개인키 (PKCS8 형식)
# -----BEGIN RSA PRIVATE KEY----- ← RSA 개인키 (PKCS1 형식)
# -----BEGIN CERTIFICATE REQUEST----- ← CSR (인증서 서명 요청)
JKS (Java KeyStore)
Java 전용 바이너리 형식입니다. 인증서와 개인키, 체인을 하나의 파일에 묶어 비밀번호로 보호합니다. Tomcat의 전통적인 keystore 형식이며 keytool 명령어로만 다룰 수 있습니다.
PFX / PKCS12
Windows IIS의 표준 형식이지만, Java/Tomcat도 지원합니다. JKS처럼 인증서와 개인키를 하나로 묶되 PKCS12 표준을 따릅니다. 보안팀이 Windows에서 발급한 인증서를 전달할 때 자주 이 형식으로 옵니다. Java 9부터 JDK의 기본 keystore 형식이 PKCS12로 변경됐습니다.
| 형식 | 구분 | 사용 서버 | 다루는 도구 |
|---|---|---|---|
| PEM | 텍스트 | Nginx, Apache, HAProxy | openssl |
| JKS | 바이너리 | Tomcat (Java 8 이하 기본) | keytool |
| PFX/PKCS12 | 바이너리 | IIS, Tomcat (Java 9+) | openssl, keytool |
인증서 구성요소: 체인을 이해해야 교체 실수가 없다
인증서 하나가 아닌 체인 구조라는 것을 이해하지 못하면 교체 후 체인 오류가 반드시 납니다.
인증서 체인 구조입니다.
- 루트 CA (Root CA)
- 중간 CA (Intermediate CA) — 예: DigiCert TLS RSA SHA256 2020 CA1
- 도메인 인증서 (End-Entity) — 예:
*.yourdomain.com
- 도메인 인증서 (End-Entity) — 예:
- 중간 CA (Intermediate CA) — 예: DigiCert TLS RSA SHA256 2020 CA1
브라우저 검증 순서: 도메인 인증서 → 중간 CA → 루트 CA. 중간 CA가 체인에 없으면 브라우저는 루트 CA까지 연결 고리를 찾지 못해 unable to verify the first certificate 오류를 냅니다.
파일별 포함 내용:
| 파일명 | 포함 내용 | Nginx 옵션 |
|---|---|---|
cert.pem | 도메인 인증서만 | 단독 사용 불가 |
chain.pem | 중간 CA만 | — |
fullchain.pem | 도메인 인증서 + 중간 CA | ssl_certificate |
privkey.pem | 개인키 | ssl_certificate_key |
Nginx에는 반드시 fullchain.pem을 씁니다. cert.pem만 넣으면 일부 클라이언트에서 체인 오류가 납니다.
브라우저가 HTTPS 인증서를 신뢰하기까지 — 검증 5단계
HTTPS로 접속하면 주소창에 자물쇠가 뜨거나, 반대로 "이 연결은 비공개가 아닙니다" 경고 페이지가 뜹니다. 이 둘을 가르는 것은, 브라우저가 서버에게서 받은 인증서를 두고 아래 5단계를 모두 통과했는지 여부입니다. 인증서를 교체했는데 경고가 뜬다면 이 5단계 중 어디서 걸렸는지를 좁히는 것이 진단의 출발점입니다. 인증서 검증은 단순히 "인증서가 있으니 믿는다"가 아니라 아래 순서의 결과입니다.
[브라우저] https://yourdomain.com 접속
│
① 서버가 인증서 체인 제시 (핸드셰이크에서 리프 + 중간 CA 전송)
│
② 서명 체인 검증 (리프 ← 중간 CA ← 루트 CA)
│ → 각 인증서를 상위 CA의 공개키로 서명 확인
│
③ 신뢰 앵커 도달 확인 (최상단 루트 CA가 OS·브라우저 신뢰 저장소에 있나)
│
④ 유효기간·도메인(SAN) 확인 (notBefore~notAfter, SAN에 접속 도메인 포함)
│
⑤ 폐기 여부 확인 (CRL/OCSP — 만료 전 폐기되지 않았나)
▼
[신뢰] 자물쇠 표시 + 암호화 통신 (한 단계라도 실패하면 경고 페이지)
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 체인 제시 | 서버가 TLS 핸드셰이크에서 리프(도메인) 인증서와 중간 CA를 함께 보낸다. fullchain.pem이면 둘 다, cert.pem만이면 리프만 전송 | 중간 CA 누락 → 브라우저가 리프의 발급자를 못 찾음 → unable to verify the first certificate · NET::ERR_CERT_AUTHORITY_INVALID |
| ② 서명 검증 | 리프는 중간 CA의 개인키로, 중간 CA는 루트의 개인키로 서명돼 있다. 각 인증서를 상위 공개키로 검증해 위조가 아님을 확인 | 체인 순서가 뒤바뀌거나 엉뚱한 중간 CA를 붙이면 서명 검증 실패 |
| ③ 신뢰 앵커 | 체인 최상단의 루트 CA가 OS·브라우저의 신뢰 저장소(trust store)에 있어야 한다. 여기 담긴 루트가 곧 "믿기로 약속한 발급자" | 사설·자체서명 CA는 신뢰 저장소에 없음 → self-signed certificate · unable to get local issuer certificate |
| ④ 유효기간·도메인 | notBefore~notAfter 안이어야 하고, 접속한 도메인이 인증서의 SAN(Subject Alternative Name)에 포함돼야 한다. 요즘 브라우저는 CN이 아니라 SAN을 본다 | 만료 → NET::ERR_CERT_DATE_INVALID / 도메인 불일치 → NET::ERR_CERT_COMMON_NAME_INVALID |
| ⑤ 폐기 확인 | 유효기간이 남아도 개인키 유출 등으로 만료 전 폐기됐을 수 있다. 브라우저는 CRL(폐기 목록)이나 OCSP로 상태를 확인 | 폐기된 인증서 → NET::ERR_CERT_REVOKED / OCSP 응답 지연은 stapling으로 완화(이 모듈 뒤 심화 참고) |
즉 "브라우저가 신뢰함"은 다섯 가지가 모두 참이라는 뜻입니다 — 체인이 리프부터 루트까지 끊김 없이 이어지고(①②), 그 루트를 브라우저가 이미 신뢰하며(③), 기간·도메인이 맞고(④), 폐기되지 않았다(⑤). 경고가 뜰 때는 openssl s_client -connect yourdomain.com:443 -showcerts의 Verify return code로 어느 단계에서 끊겼는지 좁힙니다 — code 21(unable to verify the first certificate)이면 ①의 중간 CA 누락, code 19(self signed certificate in certificate chain)면 ③의 신뢰 앵커, code 10(certificate has expired)이면 ④의 만료입니다.
인증서 형식 변환
인증서 작업 전 항상 파일 내용을 먼저 확인합니다. Subject는 이 인증서가 발급된 도메인, Issuer는 발급한 CA, Not After는 만료일입니다. 교체 전 올바른 도메인의 인증서인지 반드시 확인합니다.
openssl x509 -in cert.pem -noout -text | grep -E 'Subject:|Issuer:|Not Before|Not After'- 먼저 Subject: CN= 값으로 도메인 일치 여부 확인, 그 다음 Not After 날짜로 만료일 확인 — CN이 서비스 도메인과 다르면 브라우저에서 인증서 오류, Not After가 오늘 이전이면 즉시 교체 필요
- 만료일 기준: 30일 이상=정상, 14일 이하=자동 갱신 미동작 상태로 즉시 수동 갱신 필요, 와일드카드(*.yourdomain.com)이면 서브도메인 전체에 적용 가능 — 단, 2단계 이상 서브도메인(a.b.yourdomain.com)은 와일드카드 적용 불가
- CN이 맞고 만료일도 충분한데 브라우저에서 인증서 오류가 나면 → Issuer(중간 CA) 누락 — openssl s_client -connect yourdomain.com:443 에서 Certificate chain 개수가 1개이면 fullchain.pem이 아닌 단독 cert.pem 사용 중
# 원격 서버 인증서 만료일 확인 (스크립팅에 자주 씀)
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -dates
# 만료까지 남은 일수 계산
EXPIRY=$(echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo "만료일: $EXPIRY"
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
echo "남은 기간: ${DAYS}일"
-nokeys 옵션으로 인증서만 추출합니다. 개인키는 별도로 추출합니다. -legacy 옵션은 OpenSSL 3.x에서 구버전 PFX 파일을 처리할 때 필요합니다. 명령 실행 시 PFX 비밀번호(Import Password)를 입력하라는 프롬프트가 나옵니다.
openssl pkcs12 -in cert.pfx -nokeys -out cert.pem -legacy- Enter Import Password 프롬프트에서 PFX 비밀번호 입력 후, 먼저 ls -lh cert.pem 으로 파일 생성 확인, 그 다음 head -1 cert.pem 으로 헤더 확인 — "-----BEGIN CERTIFICATE-----" 가 있어야 정상 PEM 변환
- 인증서 개수 기준: grep -c "BEGIN CERTIFICATE" cert.pem 결과 1=도메인 인증서만(중간CA 누락으로 브라우저 경고 가능), 2=도메인+중간CA(정상), 3+=전체 체인 포함 — Nginx는 fullchain.pem(2개 이상)을 사용해야 브라우저 신뢰
- PFX 비밀번호 입력 후 cert.pem 이 비어있거나 "-----BEGIN CERTIFICATE-----" 가 없으면 → 잘못된 비밀번호 입력 또는 PFX 파일 자체 손상 — file cert.pfx 로 파일 형식 확인 후 보안팀에 재발급 요청
# 개인키 별도 추출 (-nocerts)
openssl pkcs12 -in cert.pfx -nocerts -nodes -out privkey.pem -legacy
# -nodes: 개인키를 다시 암호화하지 않음 (Nginx가 패스프레이즈 없이 읽을 수 있게)
# fullchain.pem이 없을 때: 체인 파일을 수동으로 합치기
cat cert.pem chain.pem > fullchain.pem
# PEM → PFX 변환 (JKS 없는 환경에서 Tomcat에 줄 때)
openssl pkcs12 -export \
-in fullchain.pem \
-inkey privkey.pem \
-out cert.pfx \
-name tomcat
# Export Password 입력 → Tomcat server.xml에 이 비밀번호 사용
# PEM → JKS 변환 (구형 Tomcat 8 이하)
# 1단계: PEM을 PFX로 먼저 변환 (위 명령)
# 2단계: PFX를 JKS로 변환
keytool -importkeystore \
-srckeystore cert.pfx \
-srcstoretype PKCS12 \
-destkeystore keystore.jks \
-deststoretype JKS
Nginx 인증서 교체 (무중단)
Nginx 인증서 교체 원칙: reload면 무중단
인증서 교체가 두렵게 느껴지는 이유는 "서비스를 내려야 하는 게 아닐까"라는 걱정 때문입니다. 자정에 점검 시간을 잡고 교체하다 실수로 설정이 깨지면 복구하는 동안 서비스 다운이 길어집니다. Nginx는 restart 없이도 인증서를 교체할 수 있어서, 이 두려움이 사라집니다.
Nginx는 systemctl reload nginx 명령으로 기존 연결을 끊지 않고 설정과 인증서를 다시 읽습니다. 재시작(restart)은 불필요합니다. 다만 새 인증서 파일을 제자리에 복사하기 전에 nginx -t로 설정 검증을 먼저 해야 합니다. 인증서 파일 경로가 바뀌면 설정 파일도 수정해야 합니다.
확대
인증서 파일을 교체한 뒤 설정 문법을 검증하고 reload합니다. reload는 마스터 프로세스가 새 설정을 읽고 워커 프로세스를 graceful하게 교체합니다. 기존 연결은 유지되고 새 연결부터 새 인증서를 사용합니다.
sudo nginx -t && sudo systemctl reload nginx# Nginx 인증서 교체 전체 절차
# 1. 기존 인증서 백업
BACKUP_DATE=$(date +%Y%m%d)
sudo cp /etc/nginx/ssl/cert.pem /etc/nginx/ssl/cert.pem.bak_${BACKUP_DATE}
sudo cp /etc/nginx/ssl/privkey.pem /etc/nginx/ssl/privkey.pem.bak_${BACKUP_DATE}
# 2. 새 인증서 복사
sudo cp /tmp/cert-work/fullchain.pem /etc/nginx/ssl/cert.pem
sudo cp /tmp/cert-work/privkey.pem /etc/nginx/ssl/privkey.pem
# 3. 파일 권한 설정
sudo chmod 644 /etc/nginx/ssl/cert.pem
sudo chmod 600 /etc/nginx/ssl/privkey.pem # 개인키는 root만 읽기
sudo chown root:root /etc/nginx/ssl/privkey.pem
# 4. 설정 검증 후 reload
sudo nginx -t && sudo systemctl reload nginx
# 5. 교체 결과 즉시 확인
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -dates
- nginx -t 두 줄 통과 먼저 확인, 그 다음 systemctl reload nginx 후 openssl s_client -connect yourdomain.com:443 | openssl x509 -noout -enddate 로 새 만료일 확인 — reload 없이는 기존 인증서 그대로 동작
- 만료일 기준: 교체 후 openssl 출력의 notAfter 날짜가 새 인증서의 만료일과 일치해야 정상 — 이전 만료일과 동일하면 nginx가 새 파일을 읽지 않은 것(reload 미실행 또는 경로 불일치)
- openssl에서 새 만료일이 확인됐는데 curl -I 에서 500/502가 나오면 → 개인키(privkey.pem)와 인증서(fullchain.pem) 페어가 맞지 않는 것 — openssl x509 -noout -modulus -in fullchain.pem | md5sum 과 openssl rsa -noout -modulus -in privkey.pem | md5sum 을 비교해 해시값 일치 확인
Tomcat 인증서 교체 (재시작 필요)
Tomcat 인증서 교체: 재시작 피할 수 없다
연 1~2회 찾아오는 인증서 교체 작업에서 Tomcat은 Nginx보다 까다롭습니다. keystore를 교체해도 Tomcat이 이미 메모리에 올려둔 인증서를 그냥 씁니다. 결국 재시작이 필요하고, 재시작 = 다운타임입니다. 이것을 모르고 교체했다가 "왜 아직도 옛날 인증서가 보이냐"는 문의를 받는 경우가 생깁니다.
Tomcat은 시작 시 keystore 파일을 한 번만 읽습니다. Nginx와 달리 reload 개념이 없어 인증서를 교체하면 재시작이 필요합니다. 따라서 교체 시간 = 다운타임입니다. 점검 시간대를 확보하거나, L4/L7 앞에 여러 Tomcat이 있다면 Rolling 교체로 무중단을 구성할 수 있습니다.
# Tomcat JKS keystore 교체 절차
# 1. 기존 keystore 백업
BACKUP_DATE=$(date +%Y%m%d)
sudo cp /opt/tomcat/conf/keystore.jks \
/opt/tomcat/conf/keystore.jks.bak_${BACKUP_DATE}
# 2. 새 keystore 복사
sudo cp /tmp/cert-work/keystore.jks /opt/tomcat/conf/keystore.jks
sudo chown tomcat:tomcat /opt/tomcat/conf/keystore.jks
sudo chmod 640 /opt/tomcat/conf/keystore.jks
# 3. server.xml keystore 비밀번호 확인 (필요 시 수정)
grep 'keystorePass\|keystoreFile' /opt/tomcat/conf/server.xml
# 4. keystore 내용 검증
keytool -list -v -keystore /opt/tomcat/conf/keystore.jks -storepass 비밀번호 \
| grep -E 'Alias|Valid from|Owner'
# 5. Tomcat 재시작
sudo systemctl stop tomcat
sudo systemctl start tomcat
# 6. 시작 로그에서 SSL 초기화 오류 확인
sudo tail -50 /opt/tomcat/logs/catalina.out | grep -i 'ssl\|cert\|keystore'
<!-- server.xml SSL 커넥터 예시 (PFX 직접 사용 — Java 9+) -->
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/cert.pfx"
certificateKeystoreType="PKCS12"
certificateKeystorePassword="${KEYSTORE_PASSWORD}"
type="RSA" />
</SSLHostConfig>
</Connector>
비밀번호를 ${KEYSTORE_PASSWORD} 형태의 환경변수 참조로 설정하면 server.xml에 평문이 남지 않습니다. Tomcat 9.0.x 이상에서 지원합니다.
인증서 체인 오류 진단
-showcerts로 서버가 전송하는 전체 인증서 체인을 출력합니다. 정상이라면 도메인 인증서(depth=0)와 중간 CA(depth=1)가 보여야 합니다. Verify return code: 0 (ok)가 마지막에 나오면 체인 검증 성공입니다.
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null | grep -E 'subject|issuer|Verify'- 출력 마지막 줄의 "Verify return code:" 먼저 확인, 그 다음 depth=0/1 subject로 인증서 체인 구성 확인 — Verify return code가 0이어야만 나머지 내용을 신뢰할 수 있음
- 체인 구성 기준: depth=0(도메인 인증서)+depth=1(중간CA) 두 단계가 있어야 정상 — Certificate chain에 1개만 있으면 fullchain.pem이 아닌 단독 cert.pem을 사용 중으로 일부 브라우저/클라이언트에서 검증 실패
- Verify return code: 21(unable to verify)이고 depth=1이 없으면 → 중간 CA 누락 — cat cert.pem chain.pem > fullchain.pem 으로 체인 파일 합성 후 Nginx ssl_certificate 경로를 fullchain.pem으로 수정
443포트로 HTTPS 요청을 보냈는데 서버가 암호화되지 않은 HTTP 응답을 반환할 때 발생합니다. Nginx 설정에서 listen 443 ssl;의 ssl 키워드가 빠졌거나, ssl_certificate 경로가 잘못된 경우가 가장 흔한 원인입니다.
# 1. 443포트에서 SSL로 리스닝 중인지 확인
sudo ss -tlnp | grep ':443'
# 출력이 없으면 Nginx가 443을 열지 않음
# 2. Nginx 설정에서 ssl 키워드 확인
sudo nginx -T | grep -B2 -A5 'listen 443'
# listen 443; ← 잘못됨 (ssl 없음)
# listen 443 ssl; ← 올바름
# 3. 인증서 파일 경로 존재 여부 확인
sudo nginx -T | grep 'ssl_certificate'
# 경로가 올바른지 실제 파일 존재 확인
ls -lh /경로/fullchain.pem /경로/privkey.pem
# 4. 설정 수정 후 검증 및 reload
sudo nginx -t && sudo systemctl reload nginx
Nginx ssl_certificate에 fullchain.pem 대신 cert.pem만 지정했을 때 발생합니다. 도메인 인증서만 있고 중간 CA가 빠져 브라우저가 루트 CA까지 체인을 추적하지 못합니다. 내부 테스트에서는 통과했지만 외부 클라이언트나 API 연동 서버에서만 오류가 날 수 있습니다.
# 현재 Nginx가 사용 중인 인증서 파일 확인
sudo nginx -T | grep 'ssl_certificate '
# ssl_certificate /etc/nginx/ssl/cert.pem; ← fullchain이 아닌 cert.pem이면 문제
# fullchain.pem이 있는지 확인
ls -lh /etc/nginx/ssl/fullchain.pem
# cert.pem에 체인이 포함됐는지 확인 (인증서 개수 확인)
grep -c 'BEGIN CERTIFICATE' /etc/nginx/ssl/cert.pem
# 1이면 도메인 인증서만 있음
# 2 이상이면 체인 포함
# fullchain.pem이 없으면 직접 만들기
cat /etc/nginx/ssl/cert.pem /etc/nginx/ssl/chain.pem > /etc/nginx/ssl/fullchain.pem
# server.xml 수정 후 reload
# ssl_certificate /etc/nginx/ssl/fullchain.pem; ← 수정
sudo nginx -t && sudo systemctl reload nginx
# 수정 후 체인 재확인
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null | grep 'Verify return code'
심화 — 체인이 이어져도 '폐기 여부'는 별개 문제다
심화: 폐기(revocation)와 OCSP stapling — 체인 다음의 신뢰
체인이 루트까지 이어지면 '이 인증서가 진짜인가'는 증명됩니다. 하지만 '이 인증서가 아직 유효한가(개인키 유출 등으로 만료 전에 폐기되지 않았나)'는 별개의 질문입니다. 브라우저가 폐기된 인증서를 걸러내려면 폐기 상태를 확인해야 하고, 그 방식이 CRL(폐기 목록)과 OCSP(온라인 상태 질의)입니다.
- 평범한 OCSP의 문제: 클라이언트가 핸드셰이크마다 CA의 OCSP 응답 서버에 직접 질의하면 세 가지가 나빠집니다 — (1) 접속 지연이 늘고, (2) 사용자가 어떤 사이트에 가는지 CA에 노출되며(프라이버시), (3) CA의 OCSP 서버가 느리거나 죽으면 접속에 영향을 줍니다(soft-fail면 검사 스킵, hard-fail면 접속 실패).
- OCSP stapling의 동작: 서버가 미리 CA로부터 서명된 OCSP 응답을 받아 캐시했다가, TLS 핸드셰이크에 그 응답을 '스테이플(첨부)'해 클라이언트에 함께 건넵니다. 클라이언트는 CA에 따로 안 물어봐도 되니 빠르고 프라이버시가 보호되며, CA의 OCSP 부하도 줄어듭니다. Nginx에서는
ssl_stapling on,ssl_stapling_verify on, 루트까지 포함한ssl_trusted_certificate, 그리고resolver지시어가 함께 있어야 합니다. - 핵심 한계 — 조용한 실패: stapling은 best-effort입니다. 서버가 CA의 OCSP 응답 서버에 못 닿으면 에러를 내지 않고 그냥 스테이플 없이 응답합니다. 즉
ssl_stapling on을 켜 놓고도 '안 켜진 것처럼' 동작하는 상태가 흔합니다 — 켰다고 믿지 말고 실제 스테이플 여부를 확인해야 합니다. 반대로 인증서에 OCSP Must-Staple 확장을 박으면, 스테이플이 없을 때 클라이언트가 hard-fail 하므로 stapling이 깨지는 순간 전면 장애가 됩니다(양날의 검).
정리하면 체인은 '누가 보증하는가', 폐기 확인은 '지금도 유효한가'를 다루는 서로 다른 축이고, OCSP stapling은 그 폐기 확인을 빠르고 안전하게 만드는 서버 측 최적화입니다.
상황: 보안팀이 SSL Labs A+ 요건으로 OCSP stapling 활성화를 요청했습니다. ssl_stapling on과 ssl_stapling_verify on을 넣고 reload했는데도 점검 결과는 여전히 stapling 미동작이고, error_log에는 눈에 띄는 에러가 없습니다.
원인: stapling은 best-effort라 서버가 CA의 OCSP 응답을 받아오지 못하면 에러 대신 그냥 스테이플 없이 응답합니다. 흔한 세 원인은 (1) 서버에서 CA의 OCSP responder로 나가는 아웃바운드가 방화벽에 막힘, (2) resolver 지시어가 없어 OCSP responder 도메인을 해석하지 못함, (3) ssl_trusted_certificate에 루트까지 포함한 체인이 없어 OCSP 응답 검증(verify)이 실패하는 것입니다.
진단: openssl s_client -connect yourdomain.com:443 -status의 출력에서 OCSP Response Status: successful과 스테이플된 응답 본문이 오는지 봅니다(no response sent이면 미동작). Nginx error_log에서 OCSP 관련 warn을, 서버에서 curl로 CA의 OCSP responder에 직접 나갈 수 있는지, resolver가 설정돼 있는지 확인합니다.
해결: resolver(사내 DNS 또는 공용 DNS) 지시어를 추가하고, 아웃바운드 방화벽에서 OCSP responder로의 접속을 허용하며, ssl_trusted_certificate에 중간+루트를 포함한 체인을 지정합니다. 첫 스테이플은 Nginx가 백그라운드로 OCSP 응답을 받아온 뒤부터 채워지므로, reload 직후가 아니라 한 번의 요청이 지난 뒤 -status로 successful을 재확인합니다. Must-Staple 인증서라면 stapling이 확실히 동작함을 검증한 뒤에만 배포합니다(Let's Encrypt 인증서 발급과 Nginx SSL 설정의 TLS 하드닝과 함께 점검).
연 1~2회 인증서 교체 작업 절차서 예시
#!/bin/bash
# 인증서 교체 작업 체크리스트 스크립트
# 실행 전 DOMAIN, CERT_DIR, NEW_CERT_DIR 변수 수정
DOMAIN="yourdomain.com"
CERT_DIR="/etc/nginx/ssl"
NEW_CERT_DIR="/tmp/cert-work"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
echo "=== 1. 작업 전 현재 인증서 상태 확인 ==="
echo | openssl s_client -connect ${DOMAIN}:443 2>/dev/null \
| openssl x509 -noout -dates
echo ""
echo "=== 2. 새 인증서 정보 확인 ==="
openssl x509 -in ${NEW_CERT_DIR}/fullchain.pem -noout -dates
openssl x509 -in ${NEW_CERT_DIR}/fullchain.pem -noout -subject
echo ""
echo "=== 3. 기존 인증서 백업 ==="
cp ${CERT_DIR}/fullchain.pem ${CERT_DIR}/fullchain.pem.bak_${BACKUP_DATE}
cp ${CERT_DIR}/privkey.pem ${CERT_DIR}/privkey.pem.bak_${BACKUP_DATE}
echo "백업 완료: ${CERT_DIR}/*.bak_${BACKUP_DATE}"
echo ""
echo "=== 4. 새 인증서 적용 ==="
cp ${NEW_CERT_DIR}/fullchain.pem ${CERT_DIR}/fullchain.pem
cp ${NEW_CERT_DIR}/privkey.pem ${CERT_DIR}/privkey.pem
chmod 644 ${CERT_DIR}/fullchain.pem
chmod 600 ${CERT_DIR}/privkey.pem
nginx -t && systemctl reload nginx
echo "Nginx reload 완료"
echo ""
echo "=== 5. 교체 후 검증 ==="
sleep 2
echo | openssl s_client -connect ${DOMAIN}:443 2>/dev/null \
| openssl x509 -noout -dates
openssl s_client -connect ${DOMAIN}:443 -showcerts 2>/dev/null \
| grep 'Verify return code'
만료일 자동 알림 스크립트 (cron으로 매주 월요일 실행)
#!/bin/bash
# /usr/local/bin/check-ssl-expiry.sh
DOMAINS=("yourdomain.com" "api.yourdomain.com" "admin.yourdomain.com")
ALERT_DAYS=30
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
MAIL_TO="ops@yourcompany.com"
for DOMAIN in "${DOMAINS[@]}"; do
EXPIRY=$(echo | openssl s_client -connect ${DOMAIN}:443 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$EXPIRY" ]; then
MESSAGE="경고: ${DOMAIN} 인증서 확인 실패 (연결 불가)"
else
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
if [ "$DAYS" -lt "$ALERT_DAYS" ]; then
MESSAGE="인증서 만료 임박: ${DOMAIN} — ${DAYS}일 후 만료 (${EXPIRY})"
fi
fi
if [ -n "$MESSAGE" ]; then
echo "$MESSAGE" | mail -s "[SSL 경고] ${DOMAIN}" "$MAIL_TO"
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"${MESSAGE}\"}" "$SLACK_WEBHOOK"
fi
done
# crontab 등록 (매주 월요일 오전 9시)
# 0 9 * * 1 /usr/local/bin/check-ssl-expiry.sh
인증서 교체 작업 기록 예시:
[인증서 교체 기록]
날짜: 2026-05-30
담당자: 홍길동
대상: yourdomain.com (Nginx + Tomcat)
이전 만료일: 2026-06-01
새 만료일: 2027-06-01
발급기관: DigiCert
교체 절차: PFX → PEM 변환 → Nginx fullchain 적용 → Tomcat JKS 갱신
다운타임: Tomcat 재시작 2분 (00:30~00:32)
검증: openssl s_client 체인 확인 완료
다음 교체 예정: 2027-05-01 (만료 30일 전)
명령어·단축키 빠른 참조
이 모듈에서 다룬 인증서 변환·검증·교체 명령을 모았습니다. openssl은 PEM 계열, keytool은 JKS/PKCS12를 다룹니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
openssl x509 -in cert.pem -noout -text | 인증서 상세(CN·Issuer) | openssl x509 -in cert.pem -noout -text | grep -E 'Subject:|Not After' |
openssl x509 -in cert.pem -noout -dates | 만료일 확인 | -enddate 로 notAfter만 출력 |
openssl s_client -connect host:443 | 원격 인증서 조회 | openssl s_client -connect host:443 -showcerts (전체 체인) |
openssl s_client … -status | OCSP stapling 확인 | 출력에 OCSP Response Status: successful |
openssl pkcs12 -in cert.pfx -nokeys … | PFX→PEM(인증서) | 개인키는 openssl pkcs12 -nocerts -nodes |
openssl pkcs12 -export … | PEM→PFX(Tomcat용) | -in fullchain.pem -inkey privkey.pem -out cert.pfx |
keytool -list -v -keystore <ks> | keystore 내용 확인 | keytool -list -v -keystore keystore.jks |
keytool -importkeystore … | PFX→JKS 변환 | -srcstoretype PKCS12 -deststoretype JKS |
openssl … -modulus | md5sum | 인증서·개인키 페어 일치 | x509와 rsa의 modulus 해시 비교 |
grep -c 'BEGIN CERTIFICATE' cert.pem | 체인 포함 개수 확인 | 1=단독 cert, 2+=fullchain |
cat cert.pem chain.pem > fullchain.pem | 체인 파일 합성 | 중간 CA 누락 오류 해결 |
nginx -t && systemctl reload nginx | 무중단 교체 반영 | Nginx는 reload, Tomcat은 재시작 필요 |
ss -tlnp | grep ':443' | 443 SSL 리스닝 확인 | 응답 없으면 listen 443 ssl 누락 |
관련 모듈로 더 깊이:
- Let's Encrypt 인증서 발급과 Nginx SSL 설정 — 변환한 인증서를 Nginx에 실제로 적용하고 HTTPS를 구성하는 법
- WAR 배포부터 server.xml 튜닝, 장애 대응까지 — JKS 인증서 갱신이 들어가는 Tomcat 운영의 전체 맥락
- A레코드/CNAME 운영과 /etc/hosts 활용 실무 — 인증서가 보증하는 도메인·SAN과 DNS 레코드의 관계
다음 모듈에서는 인프라 장애 상황에서의 포스트모템 작성과 장애 원인 분석 프레임워크를 다룹니다.