infra
Platform

모듈 맵

[Network] HTTPS/TLS 작동 원리와 curl SSL 에러 장애 디버깅

0 / 35 완료

펼치기
0 / 35 완료0%

Networking · 16 / 35

[Network] HTTPS/TLS 작동 원리와 curl SSL 에러 장애 디버깅

SSL/TLS 인증서 구조부터 CERTIFICATE_VERIFY_FAILED 에러 해결까지 — 개발자가 실무에서 만나는 HTTPS 문제 완전 정복

🚨INCIDENT ALERT
HIGH

인증서를 갱신했다는 알림은 성공으로 끝났는데, 사용자는 여전히 만료된 인증서 경고를 봅니다. 포트 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 vs HTTPS — TLS가 하는 일

HTTP는 평문(Plain Text)으로 데이터를 전송합니다. HTTPS는 HTTP 위에 TLS(Transport Layer Security) 레이어를 추가합니다.

HTTP:   클라이언트 ──────────────────────── 서버
         "password=1234" 그대로 전송

HTTPS:  클라이언트 ──[TLS 암호화]──────── 서버
         "x9@#$%^&*" 암호화된 데이터 전송
         중간에서 가로채도 내용 모름

TLS가 보장하는 것 3가지: 각 속성이 실제로 막는 위협을 함께 이해해야 합니다.

속성의미
기밀성(Confidentiality)중간에서 읽을 수 없음
무결성(Integrity)전송 중 변조 감지
인증(Authentication)서버가 진짜 그 서버인지 확인

1HTTP vs HTTPS 응답 헤더 비교

실습 디렉토리를 만든 뒤 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가지 조건:

  1. 신뢰할 수 있는 CA(Certificate Authority)가 서명했는가?
  2. 인증서가 만료되지 않았는가?
  3. 인증서의 도메인이 접속하는 도메인과 일치하는가?

2curl -v로 TLS 핸드셰이크 과정 확인

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단계 진단 플로우

3443 포트 연결 가능 여부 확인

포트 차단인지 인증서 문제인지 구분하려면 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으로 응답 없이 차단 중
4인증서 만료일 확인

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 이 출력되면 인증서 체인 검증 성공
5도메인-인증서 일치 및 체인 완전성 확인

인증서의 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)'

지식 확인

퀴즈 — 5문제

Q1

curl로 HTTPS 요청 시 'SSL certificate problem: certificate has expired' 에러가 났다. 서버 인증서 만료일을 터미널에서 즉시 확인하는 명령은?

Q2

내부 개발 서버가 자체 서명(Self-Signed) 인증서를 쓰고 있다. curl로 API 테스트 시 인증서 검증 에러를 임시로 우회하는 방법은?

Q3

nginx에서 Let's Encrypt 인증서를 갱신(certbot renew)했는데 브라우저에서 여전히 만료된 인증서로 표시된다. 원인은?

Q4

curl -v로 HTTPS 요청을 확인했더니 'SSL: HANDSHAKE_FAILURE'가 나타났다. 이 에러의 의미는?

Q5

443 포트로 HTTPS 요청이 타임아웃된다. HTTP(80)는 정상이다. 가장 먼저 확인해야 할 것은?

0 / 5 답변

🧪 실습으로 확인하기

포트는 열렸다는데 왜 안 되지? — ss/netstat/telnet으로 TCP 진단

초급

"포트 8080 열었는데요?"와 "왜 안 돼요?" 사이의 간극을 메우는 실습. ss로 바인딩 상태를 확인하고, telnet/nc으로 원격 연결을 테스트하고, iptables 방화벽을 진단하고, 바인딩 주소(0.0.0.0 vs 127.0.0.1)까지 수정하는 4단계 TCP 포트 진단 플로우를 완성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

networking중급 · 40
[Network] netstat과 ss 명령어로 커넥션 상태(ESTABLISHED 등) 분석
Networking 트랙 계속
docker입문 · 30
[Docker] 백엔드 개발자에게 Docker와 컨테이너 가상화가 필수인 이유
Docker 트랙 시작점