인증서를 갱신했다는 알림은 성공으로 끝났는데, 사용자는 여전히 만료된 인증서 경고를 봅니다. 포트 443은 열려 있고 nginx도 떠 있지만 새 인증서를 읽지 못한 상태였습니다.
HTTPS 장애는 인증서, 포트, TLS 협상, 웹서버 reload가 함께 맞아야 해결됩니다.
HTTPS와 TLS — curl 인증서 에러와 443 장애 디버깅
HTTPS 장애는 항상 급하게 옵니다. 서비스가 갑자기 503이 뜨거나, API 연동이 안 된다는 연락이 옵니다. 원인이 인증서 만료인지, 포트가 막힌 건지, TLS 버전 불일치인지 — 5분 안에 판단할 수 있어야 합니다. curl과 openssl로 충분합니다.
TLS 동작 원리를 깊게 알 필요 없습니다. "어떤 에러가 왜 나는지", "어떤 명령으로 확인하는지"만 알면 대부분의 HTTPS 장애를 해결할 수 있습니다.
- 1HTTP vs HTTPS — TLS가 추가하는 것
- 2TLS 핸드셰이크 — 연결이 맺어지는 과정
- 3인증서 구조 — CA, 체인, 만료일 확인
- 4openssl s_client — 서버 인증서 직접 점검
- 5실무 에러 5종 — 만료·자체서명·핸드셰이크 실패·포트 차단·인증서 체인 끊김
HTTP vs HTTPS — TLS가 하는 일
로그인 API를 만들고 배포했는데 보안 담당자가 "HTTPS 안 쓰면 안 된다"고 합니다. 인증서를 발급받으려니 Let's Encrypt, CSR, PEM 파일 등 낯선 용어가 쏟아집니다. 또 curl https://api.internal이 "certificate verify failed"로 막히는데 왜 안 되는지 알 수 없습니다. TLS가 어떻게 동작하는지 이해하지 못하면 인증서 문제가 생길 때마다 그냥 -k 옵션으로 검증을 끄게 됩니다.

HTTP는 평문(Plain Text)으로 데이터를 전송합니다. HTTPS는 HTTP 위에 TLS(Transport Layer Security) 레이어를 추가합니다.
HTTP: 클라이언트 ──────────────────────── 서버
"password=1234" 그대로 전송
HTTPS: 클라이언트 ──[TLS 암호화]──────── 서버
"x9@#$%^&*" 암호화된 데이터 전송
중간에서 가로채도 내용 모름
TLS가 보장하는 것 3가지: 각 속성이 실제로 막는 위협을 함께 이해해야 합니다.
| 속성 | 의미 |
|---|---|
| 기밀성(Confidentiality) | 중간에서 읽을 수 없음 |
| 무결성(Integrity) | 전송 중 변조 감지 |
| 인증(Authentication) | 서버가 진짜 그 서버인지 확인 |
실습 디렉토리를 만든 뒤 HTTP와 HTTPS 응답 헤더를 각각 확인합니다. HTTP는 HTTPS로 리다이렉트하고 HTTPS는 보안 헤더를 포함합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/networking/part3/exam_16 && cd /tmp/networking/part3/exam_16
# HTTP와 HTTPS 응답 헤더 비교
curl -I http://example.com # Location: https://... (리다이렉트)
curl -I https://example.com # 200 OK + 보안 헤더들
curl -I http://example.com- HTTP 응답에 Location: https://... 리다이렉트 헤더가 포함되는지 확인
- HTTPS 응답에 Strict-Transport-Security 등 보안 헤더가 있는지 확인
- 응답 코드: HTTP는 301/302, HTTPS는 200 OK 예상
TLS 핸드셰이크 — 연결 맺는 과정
HTTPS 연결은 데이터를 바로 주고받기 전에 **핸드셰이크(Handshake)**라는 준비 과정을 거칩니다. 처음 만나는 두 사람이 명함을 교환하고 신분을 확인한 뒤 대화를 시작하는 것과 같습니다. 이 과정에서 세 가지 일이 일어납니다: 어떤 암호화 방식을 쓸지 협상하고, 서버가 인증서로 자신의 신원을 증명하며, 암호화에 쓸 키를 안전하게 교환합니다. 이 준비 단계가 완료된 뒤에야 실제 데이터가 암호화되어 전송됩니다.
CA(Certificate Authority, 인증 기관)는 이 과정에서 핵심 역할을 합니다. 서버가 "나는 진짜 example.com입니다"라고 주장할 때, 클라이언트가 그 주장을 믿을 수 있어야 합니다. CA는 이를 보증하는 공증인입니다. Let's Encrypt, DigiCert, GlobalSign 같은 CA가 서버 신원을 검증하고 인증서에 디지털 서명을 합니다. 브라우저는 시스템에 내장된 신뢰 CA 목록(Root CA Store)에 있는 CA가 서명한 인증서만 신뢰합니다. 이것이 CA 신뢰 체계입니다.
아래는 TLS 핸드셰이크 전체 흐름입니다:
클라이언트 서버
│ │
│── ClientHello (지원 TLS 버전) ──►│
│ │
│◄─ ServerHello (선택된 버전) ─────│
│◄─ Certificate (서버 인증서) ─────│
│ │
│ [인증서 검증: CA 체인 확인, │
│ 유효기간 확인, 도메인 일치 확인] │
│ │
│── 암호화 키 교환 ────────────────►│
│ │
│◄──────── 암호화 통신 시작 ────────│
인증서 검증 3가지 조건:
- 신뢰할 수 있는 CA(Certificate Authority)가 서명했는가?
- 인증서가 만료되지 않았는가?
- 인증서의 도메인이 접속하는 도메인과 일치하는가?
curl verbose 모드로 TLS 핸드셰이크 세부 과정을 확인합니다. 어떤 TLS 버전이 협상됐는지, 인증서 발급자(issuer)와 만료일이 무엇인지 볼 수 있습니다.
# curl -v로 핸드셰이크 과정 확인
curl -v https://example.com 2>&1 | grep -E "TLS|SSL|certificate|subject|expire"
curl -v https://example.com 2>&1 | grep -E 'TLS|SSL|certificate|subject|expire'- SSL connection using TLSv1.3 (또는 TLSv1.2) 출력 확인
- Server certificate: subject / issuer / start date / expire date 필드 확인
- SSL certificate verify ok 메시지가 보이면 인증서 체인 정상
openssl s_client — 인증서 직접 점검
HTTPS 접속이 안 되는데 원인이 인증서 만료인지, 체인 문제인지, 도메인 불일치인지 알 수 없습니다. 브라우저는 "안전하지 않은 연결"이라는 오류만 보여줄 뿐입니다. openssl s_client로 직접 TLS 핸드셰이크를 해보면 인증서의 모든 정보를 즉시 확인할 수 있습니다.
실습: HTTPS 장애 5단계 진단 플로우
포트 차단인지 인증서 문제인지 구분하려면 TCP 연결 자체부터 확인합니다. nc가 성공해야 TLS 진단으로 넘어갈 수 있습니다.
# 1단계: 포트부터 확인 (가장 먼저)
nc -zv example.com 443
# Connection to example.com 443 port [tcp/https] succeeded! → 포트 열림
# nc: Connection refused → 443 차단
nc -zv example.com 443- succeeded 메시지: TCP 연결 정상 → TLS 진단으로 진행
- Connection refused: 443 포트가 차단됨 → 방화벽/Security Group 확인
- Connection timed out: 방화벽이 DROP으로 응답 없이 차단 중
openssl s_client로 서버와 TLS 핸드셰이크를 맺고 인증서의 유효기간을 파싱합니다. notAfter 필드가 현재 날짜보다 과거면 만료된 인증서입니다.
# 만료일만 빠르게 확인
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -dates
# 출력:
# notBefore=Jan 1 00:00:00 2024 GMT
# notAfter=Dec 31 23:59:59 2024 GMT ← 이 날짜가 만료일
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -dates- notBefore, notAfter 날짜 출력 확인
- notAfter 날짜가 현재보다 과거이면 인증서 만료 — certbot renew 후 nginx reload 필요
- verify return:1 이 출력되면 인증서 체인 검증 성공
인증서의 subject/SAN(Subject Alternative Name)이 접속 도메인과 일치하는지, 그리고 중간 CA 체인이 완전한지 확인합니다.
# 4단계: 도메인-인증서 일치 확인
openssl s_client -connect example.com:443 </dev/null 2>/dev/null \
| openssl x509 -noout -subject -subjectAltName
# 5단계: 체인 완전성 확인
openssl s_client -connect example.com:443 -showcerts </dev/null 2>&1 \
| grep "Certificate chain"
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -subject -subjectAltName- subject에 CN=example.com 또는 CN=*.example.com 확인
- DNS:example.com, DNS:www.example.com 등 SAN 목록 확인
- Certificate chain에 depth 0(서버), depth 1(중간 CA), depth 2(루트 CA) 표시
- 체인이 끊기면 'unable to get local issuer certificate' 에러 발생
인증서 만료
이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.
# 증상
curl: (60) SSL certificate problem: certificate has expired
More details here: https://curl.haxx.se/docs/sslcerts.html
# 원인: 서버 TLS 인증서 유효기간 초과
# 진단: 실제 만료일 확인
openssl s_client -connect api.example.com:443 </dev/null 2>/dev/null \
| openssl x509 -noout -dates
# notAfter=Apr 10 00:00:00 2026 GMT → 이미 지난 날짜
# 해결 (Let's Encrypt 사용 시)
sudo certbot renew --force-renewal
sudo systemctl reload nginx # ← 이 단계를 빠뜨리면 안 됨
# 자동 갱신 cron 확인 (보통 certbot이 설정)
cat /etc/cron.d/certbot
# 또는
systemctl list-timers | grep certbot
# 갱신 후 적용 확인
openssl s_client -connect api.example.com:443 </dev/null 2>/dev/null \
| openssl x509 -noout -dates
자체 서명 인증서
# 증상: 내부 서버, 개발 환경에서 자주 발생
curl: (60) SSL certificate problem: self signed certificate
# 원인: 공인 CA가 아닌 자체 서명 인증서 사용
# 임시 우회 (개발/테스트만)
curl -k https://dev.internal.company.com/api
# 올바른 해결: 자체 CA 인증서를 신뢰 목록에 추가
# curl의 경우
curl --cacert /etc/ssl/internal-ca.crt https://dev.internal.company.com/api
# 시스템 전체 신뢰 (Ubuntu)
sudo cp internal-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
TLS 핸드셰이크 실패
# 증상
curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to example.com:443
# 원인: TLS 버전 또는 암호화 스위트 불일치
# 진단: 버전별 테스트
openssl s_client -connect example.com:443 -tls1_2 </dev/null 2>&1 | grep "Protocol"
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>&1 | grep "Protocol"
# 서버가 지원하는 TLS 버전 확인
nmap --script ssl-enum-ciphers -p 443 example.com
# nginx에서 TLS 버전 설정 (최신 권고)
# /etc/nginx/nginx.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
실무에서 HTTPS 문제를 만나는 순간
Let's Encrypt 자동 갱신 설정: certbot renew를 cron으로 등록하면 인증서 만료를 예방할 수 있습니다.
이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.
# certbot 설치 및 nginx 인증서 발급
sudo certbot --nginx -d example.com -d www.example.com
# 갱신 테스트 (실제 갱신 안 함)
sudo certbot renew --dry-run
# 갱신 후 자동 nginx 리로드 (crontab 또는 deploy-hook)
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx
# 만료 30일 전 알림 모니터링 스크립트
EXPIRY=$(openssl s_client -connect $DOMAIN:443 </dev/null 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
echo "$DOMAIN: $DAYS_LEFT days remaining"
내부 서비스 mTLS (Mutual TLS): 클라이언트도 인증서를 제시하는 양방향 TLS 설정입니다.
# 마이크로서비스 간 통신에서 클라이언트 인증서로 양방향 인증
curl --cert client.crt --key client.key \
--cacert ca.crt \
https://internal-service.company.com/api
AWS/클라우드 환경: ACM을 쓰면 인증서 발급과 갱신이 자동화됩니다.
# ACM(AWS Certificate Manager)로 인증서 관리 시
# 갱신은 자동이지만 ALB/CloudFront 연결 상태 확인
aws acm describe-certificate --certificate-arn arn:aws:... \
| jq '.Certificate.NotAfter'
# Security Group에서 443 인바운드 확인
aws ec2 describe-security-groups --group-ids sg-xxx \
| jq '.SecurityGroups[].IpPermissions[] | select(.FromPort==443)'