[ E2E ] SSL 중간 인증서 문제
오늘은 E2E 테스트 도메인에 외부 로그인 API 를 사용하면서 발생한 중간 인증서 문제에 대해 정리해보겠습니다.
브라우저로는 정상적으로 접속되는 사이트가 API 호출이나 프로그래밍 언어에서는 SSL 오류가 발생하는 경험, 한 번쯤은 있으실 것입니다. 이런 문제의 원인과 해결 방법을 차근차근 살펴보겠습니다.
SSL 인증서 체인은 어떻게 구성되는가?
SSL 인증서는 단독으로 동작하지 않습니다. **신뢰 체인(Chain of Trust)**이라는 계층적 구조로 작동하는데, 이를 시각화하면 다음과 같습니다.
🏛️ Root CA (루트 인증서)
최상위 인증 기관 (예: DigiCert Global Root CA)
🔗 Intermediate CA (중간 인증서)
중간 인증 기관 (예: Let's Encrypt R3)
🌐 End Entity (웹사이트 인증서)
실제 웹사이트 인증서
각 레벨이 담당하는 역할은 다음과 같습니다:
- 루트 인증서: 운영체제나 브라우저에 미리 설치된 신뢰할 수 있는 최상위 인증서
- 중간 인증서: 루트 CA에서 발급받아 실제 웹사이트 인증서를 서명하는 중간 단계
- 웹사이트 인증서: 특정 도메인에 대한 실제 SSL 인증서
중간 인증서는 왜 필요한가?
직접 연결과 중간 인증서를 통한 연결의 차이를 살펴보겠습니다.
❌ 직접 연결 (위험)
Root CA → Website Certificate
✅ 중간 인증서 사용 (안전)
Root CA → Intermediate CA → Website Certificate
중간 인증서를 사용하는 주요 이유는 다음과 같습니다:
- 보안 강화: 루트 키를 오프라인에 보관하고 중간 키만 온라인에서 사용
- 확장성: 하나의 루트 CA로 여러 중간 CA를 운영할 수 있음
- 유연성: 특정 목적별로 다른 중간 CA 사용 가능
- 리스크 분산: 중간 인증서 탈취 시 해당 중간 CA만 폐기하면 됨
중간 인증서 문제는 언제 발생하는가?
1. 서버 설정 문제
가장 흔한 원인은 서버 설정에서 중간 인증서를 누락하는 경우입니다.
# ❌ 잘못된 설정 (웹사이트 인증서만 설정)
ssl_certificate /path/to/website.crt;
ssl_certificate_key /path/to/website.key;
# ✅ 올바른 설정 (체인 포함)
ssl_certificate /path/to/fullchain.pem; # 웹사이트 + 중간 인증서
ssl_certificate_key /path/to/website.key;
2. 자동화 도구의 함정
Let's Encrypt의 certbot을 사용할 때도 주의해야 합니다.
# certbot이 생성하는 파일들:
# cert.pem ← 웹사이트 인증서만 (사용하면 안됨)
# chain.pem ← 중간 인증서만
# fullchain.pem ← 웹사이트 + 중간 인증서 (이걸 사용해야 함)
3. Let's Encrypt의 특수한 경우
Let's Encrypt는 중간 인증서를 자주 변경합니다:
- 2020년 이전: Let's Encrypt Authority X3
- 2021년 이후: Let's Encrypt R3
- 2024년 이후: Let's Encrypt R10, R11
브라우저는 잘 되는데 프로그래밍 언어에서는 안 되는 이유
이 부분이 많은 개발자들이 혼란스러워하는 지점입니다. 브라우저와 프로그래밍 언어의 동작 방식이 다르기 때문입니다.
🌐 브라우저의 관대한 처리
✅ 브라우저가 제공하는 보완 기능
- AIA (Authority Information Access) 확장 → 중간 인증서 자동 다운로드
- 캐시된 중간 인증서 활용 → 이전에 방문한 사이트의 중간 인증서 재사용
- OCSP Stapling 지원 → 서버에서 인증서 상태 정보 함께 제공
- 사용자 선택권 제공 → "위험을 무시하고 계속" 옵션
⚡ 프로그래밍 언어의 엄격한 처리
❌ RFC 표준을 엄격하게 따름
- 서버에서 제공하는 인증서 체인만 사용
- 자동으로 중간 인증서를 다운로드하지 않음
- 불완전한 체인 → 즉시 오류 발생
- 사용자 선택권 제공 안함
문제 진단은 어떻게 하는가?
1. OpenSSL로 진단하기
# 기본 SSL 연결 테스트
openssl s_client -connect example.com:443 -servername example.com
# 인증서 체인 확인
openssl s_client -connect example.com:443 -showcerts
# 검증 오류까지 확인
openssl s_client -connect example.com:443 -servername example.com -verify_return_error
2. 결과 해석하기
올바른 인증서 체인의 경우:
# ✅ 올바른 인증서 체인
Certificate chain
0 s:CN=example.com
i:C=US, O=Let's Encrypt, CN=R10
1 s:C=US, O=Let's Encrypt, CN=R10
i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
중간 인증서가 누락된 경우:
# ❌ 중간 인증서 누락
Certificate chain
0 s:CN=example.com
i:C=US, O=Let's Encrypt, CN=R10
# 1번 체인이 없음 → 문제!
3. 일반적인 오류 메시지들
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
SSL_ERROR_UNKNOWN_CA_ALERT
CERT_UNTRUSTED
UNABLE_TO_VERIFY_LEAF_SIGNATURE
해결 방법들
1. 서버 설정 수정
Nginx 설정:
server {
listen 443 ssl;
server_name example.com;
# ✅ 올바른 설정
ssl_certificate /path/to/fullchain.pem; # 웹사이트 + 중간 인증서
ssl_certificate_key /path/to/privkey.pem;
# 추가 보안 설정
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}
Apache 설정:
<VirtualHost *:443>
ServerName example.com
# 웹사이트 인증서
SSLCertificateFile /path/to/cert.pem
# 개인 키
SSLCertificateKeyFile /path/to/privkey.pem
# ✅ 중간 인증서 체인
SSLCertificateChainFile /path/to/chain.pem
</VirtualHost>
2. 수동 체인 생성
# 웹사이트 인증서와 중간 인증서 결합
cat website.crt intermediate.crt > fullchain.pem
# Let's Encrypt 중간 인증서 다운로드
wget https://letsencrypt.org/certs/r10.pem
# 체인 생성
cat cert.pem r10.pem > fullchain.pem
3. 프로그래밍 언어별 해결책
Node.js:
const https = require('https');
const fs = require('fs');
// 방법 1: 중간 인증서 수동 추가
const intermediateCert = fs.readFileSync('intermediate.pem');
const options = {
ca: [intermediateCert],
};
// 방법 2: 개발 환경에서만 SSL 검증 비활성화
if (process.env.NODE_ENV === 'development') {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
Python:
import ssl
import urllib.request
# 방법 1: 중간 인증서 추가
context = ssl.create_default_context()
context.load_verify_locations('intermediate.pem')
# 방법 2: 개발 환경에서만 SSL 검증 비활성화
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
예방을 위한 모범 사례
1. 서버 설정 체크리스트
📋 SSL 설정 체크리스트
- ✅ fullchain.pem 사용 (cert.pem 단독 사용 금지)
- ✅ 중간 인증서 순서 확인
- ✅ 인증서 만료일 모니터링
- ✅ 자동 갱신 설정
- ✅ 정기적인 SSL 테스트
2. 자동화 및 모니터링
# 크론잡 설정 예시
0 0 1 * * /usr/bin/certbot renew --quiet && systemctl reload nginx
0 9 * * 1 /usr/local/bin/ssl-check.sh # 매주 월요일 9시 SSL 체크
3. 개발 환경 설정
// 개발 환경에서는 SSL 검증 우회
if (process.env.NODE_ENV === 'development') {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
} else {
// 프로덕션에서는 엄격한 SSL 검증
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
}
마무리
중간 인증서 문제는 SSL 설정에서 가장 흔히 발생하는 문제 중 하나입니다.
브라우저에서는 잘 되는데 API 호출에서만 문제가 발생한다면, 십중팔구 중간 인증서 문제일 가능성이 높습니다.
중요한 것은 예방입니다.
처음 SSL 인증서를 설정할 때부터 올바른 방법을 사용하고, 정기적인 모니터링을 통해 문제를 조기에 발견하는 것이 최선의 해결책입니다.