배포한 서비스에 처음으로 도메인을 연결하고 나면 브라우저가 "이 연결은 안전하지 않습니다"라는 경고를 띄웁니다. 사용자 입장에서 HTTP 사이트는 이미 신뢰할 수 없는 곳입니다. 게다가 구글 검색 순위도 HTTPS를 우선합니다. 팀장은 "오늘 안에 HTTPS로 전환해줘"라고 말하는데, 어디서부터 시작해야 할지 막막합니다.
Let's Encrypt를 쓰면 무료로, 그것도 10분 안에 해결할 수 있습니다. 이 모듈에서 처음부터 끝까지 같이 해봅니다.
- 1SSL/TLS 인증서가 무엇인지, CA와 인증서 체인이 왜 필요한지 설명할 수 있다
- 2Certbot으로 Let's Encrypt 인증서를 발급하고 Nginx에 자동 적용할 수 있다
- 3HTTP → HTTPS 리다이렉트와 HSTS를 Nginx에 설정할 수 있다
- 4인증서 자동 갱신을 cron 또는 systemd timer로 구성할 수 있다
- 5openssl 명령어로 인증서 만료일을 확인하고 현장에서 즉시 점검할 수 있다
dig +short yourdomain.comsudo ss -tlnp | grep -E ':80|:443'sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginxsudo dnf install -y certbot python3-certbot-nginxcertbot --versionSSL/TLS 기초 개념
인증서와 CA: 브라우저가 신뢰하는 구조
HTTPS의 자물쇠 아이콘은 단순히 "암호화됐다"는 표시가 아닙니다. "이 서버가 주장하는 도메인의 진짜 소유자임을 제3자가 서명했다"는 의미입니다. 이 서명 체계가 없다면 공격자가 example.com을 사칭하는 서버를 만들어도 브라우저는 알아채지 못합니다. CA(인증기관)와 인증서 체인은 브라우저가 신뢰할 서버와 그렇지 않은 서버를 구분하는 공개키 기반 신뢰 인프라의 핵심입니다.

HTTPS가 단순히 "암호화된 HTTP"라고 생각하면 절반만 맞습니다. 암호화만으로는 내가 통신하는 상대가 진짜 그 서버인지 알 수 없습니다. 그래서 인증서가 필요합니다. 인증서는 "이 서버가 정말 example.com이다"라는 제3자의 서명입니다.
이 제3자를 **CA(Certificate Authority, 인증기관)**라고 부릅니다. 브라우저와 OS에는 신뢰할 수 있는 CA 목록이 내장되어 있습니다. 서버 인증서가 신뢰할 수 있는 CA의 서명을 받았다면 브라우저는 초록 자물쇠를 표시합니다.
브라우저 내장 루트 CA
└── 중간 CA (Let's Encrypt R11 등)
└── 서버 인증서 (your-domain.com)
Let's Encrypt는 ISRG(Internet Security Research Group)이 운영하는 무료 CA입니다. 2016년 이후 주요 브라우저와 OS 모두에서 신뢰하며, 자동화 도구(Certbot)로 발급과 갱신을 처리할 수 있어 사실상 표준이 됐습니다.
인증서 파일 두 가지:
| 파일 | 내용 | Nginx 설정 |
|---|---|---|
fullchain.pem | 서버 인증서 + 중간 CA 체인 | ssl_certificate |
privkey.pem | 서버 개인키 (절대 외부 노출 금지) | ssl_certificate_key |
TLS 핸드셰이크: 첫 연결에서 무슨 일이 벌어지나
HTTP는 연결하면 바로 데이터를 주고받습니다. HTTPS는 데이터 교환 전에 TLS 핸드셰이크가 먼저 일어납니다. 이 과정을 이해해야 ssl_session_cache 같은 설정이 왜 필요한지 납득이 됩니다.

핸드셰이크는 대략 이렇게 동작합니다:
- 클라이언트 → 서버: "이런 암호화 방식을 지원해요" (ClientHello)
- 서버 → 클라이언트: "이 방식을 쓰자, 내 인증서야" (ServerHello + Certificate)
- 클라이언트: 인증서를 CA 체인으로 검증
- 클라이언트 ↔ 서버: 세션 키 교환 (대칭키 협상)
- 이후부터는 협상한 대칭키로 암호화 통신
CPU 비용이 있습니다. 핸드셰이크는 비대칭 암호 연산을 포함하기 때문에 일반 HTTP보다 비쌉니다. 그래서 Nginx에 세션 캐시를 설정해 재연결 시 핸드셰이크를 생략합니다.
# 이 설정이 왜 있는지 이제 납득이 됩니다
ssl_session_cache shared:SSL:10m; # 워커 공유 캐시 10MB
ssl_session_timeout 10m; # 캐시 유지 시간
Certbot으로 인증서 발급
--nginx 플러그인은 인증서 발급 후 Nginx 설정 파일을 자동으로 수정합니다. 이메일 주소 입력(만료 알림용), 약관 동의, HTTP→HTTPS 자동 리다이렉트 여부를 순서대로 물어봅니다. 도메인이 서버 IP로 제대로 연결되어 있어야 합니다. Certbot이 80포트로 도전(challenge)을 받아 도메인 소유권을 확인하기 때문입니다.
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com- Successfully received certificate 메시지 확인
- 인증서 저장 경로: /etc/letsencrypt/live/yourdomain.com/fullchain.pem
- 개인키 저장 경로: /etc/letsencrypt/live/yourdomain.com/privkey.pem
- 만료일: Certificate is valid until ... 출력 확인
- 브라우저에서 https://yourdomain.com 접속 시 자물쇠 아이콘 표시
--nginx 플러그인이 자동으로 수정한 설정 파일을 확인합니다. ssl_certificate, ssl_certificate_key가 추가됐고, 80포트 블록에 리다이렉트가 붙었는지 살펴봅니다. 자동 설정만 믿지 말고 실제 내용을 이해해야 이후에 문제가 생겼을 때 대응할 수 있습니다.
sudo cat /etc/nginx/sites-enabled/yourdomain.com- listen 443 ssl; 블록 존재 여부 확인
- ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; 경로 확인
- ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; 경로 확인
- listen 80; 블록에 return 301 https://\$host\$request_uri; 리다이렉트 존재 여부
- sudo nginx -t 로 설정 문법 오류 없음 확인
Nginx SSL 수동 설정 (플러그인 없이 직접 작성)
Nginx SSL 설정 전체 구조 이해하기
Certbot 플러그인이 자동으로 해주는 것도 좋지만, 실제로 무슨 설정이 들어가는지 알아야 합니다. 직접 관리하는 서버에서는 설정의 각 줄이 무엇을 의미하는지 설명할 수 있어야 합니다.
# /etc/nginx/sites-available/yourdomain.com
# 1. HTTP → HTTPS 리다이렉트
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
# 301 영구 리다이렉트 — SEO와 브라우저 캐시 모두 HTTPS로 기억
return 301 https://$host$request_uri;
}
# 2. HTTPS 메인 블록
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name yourdomain.com www.yourdomain.com;
# 인증서 파일 경로
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# 권장 TLS 버전 (1.2, 1.3만 허용 — 1.0, 1.1은 취약)
ssl_protocols TLSv1.2 TLSv1.3;
# 강력한 암호화 스위트만 허용
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off; # TLS 1.3에서는 off 권장
# 세션 캐시 — 재연결 시 핸드셰이크 생략
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS — 브라우저가 이 도메인은 항상 HTTPS로 접근하도록 기억
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 기타 보안 헤더
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
root /var/www/yourdomain.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
HSTS 주의사항: max-age=31536000(1년)으로 설정하면 브라우저가 1년간 이 도메인을 HTTPS로만 접근합니다. HTTPS를 다시 HTTP로 내릴 일이 없다고 확신할 때만 설정하세요. 처음에는 max-age=300으로 시작해 문제 없으면 늘리는 게 안전합니다.
-t 플래그로 설정 문법을 먼저 검증합니다. 오류가 없으면 reload로 무중단 적용합니다. restart가 아닌 reload를 쓰는 이유는, restart는 서비스를 잠깐 멈추지만 reload는 기존 연결을 끊지 않고 설정만 다시 읽기 때문입니다.
sudo nginx -t && sudo systemctl reload nginx- nginx -t 출력: configuration file /etc/nginx/nginx.conf syntax is ok
- nginx -t 출력: configuration file /etc/nginx/nginx.conf test is successful
- curl -I http://yourdomain.com 에서 301 응답과 Location: https:// 헤더 확인
- curl -I https://yourdomain.com 에서 200 응답 확인
- 응답 헤더에 Strict-Transport-Security 존재 여부
인증서 자동 갱신
자동 갱신 설정: cron vs systemd timer
Let's Encrypt 인증서는 90일마다 만료됩니다. 수동으로 갱신하다 깜빡하면 사이트가 "인증서 만료" 경고로 접근 불가가 됩니다. 자동 갱신은 선택이 아닌 필수입니다.
Certbot 설치 시 systemd timer가 자동으로 등록됩니다. Ubuntu 기준으로 확인해봅니다.
# systemd timer 등록 여부 확인
sudo systemctl status certbot.timer
# certbot.timer - Run certbot twice daily
# Active: active (waiting)
# Trigger: Wed 2025-01-15 12:00:00 UTC
# 직접 갱신 테스트 (실제로 갱신하지 않고 시뮬레이션만)
sudo certbot renew --dry-run
systemd timer가 없는 환경(CentOS 7, 일부 구버전)이나 직접 통제하고 싶을 때는 cron을 씁니다.
# crontab 편집
sudo crontab -e
# 매일 새벽 2시 30분, 오후 2시 30분 두 번 실행
# certbot은 만료 30일 이내일 때만 실제 갱신 수행
30 2,14 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
# --quiet: 성공 시 출력 없음 (메일 스팸 방지)
# --post-hook: 갱신이 실제로 일어났을 때만 nginx reload
왜 하루 두 번인가? Let's Encrypt 서버 장애나 네트워크 문제로 갱신이 실패할 경우를 대비합니다. 한 번 실패해도 몇 시간 후 재시도 기회가 있습니다.
인증서 점검 명령어
현재 서버에 발급된 모든 인증서의 상태, 만료일, 경로를 한 번에 확인합니다. 온콜 대응 중 "인증서가 만료됐나요?"라는 질문을 받으면 이 명령 하나로 대답할 수 있습니다.
sudo certbot certificates- Certificate Name: yourdomain.com 확인
- Expiry Date: 날짜 (VALID: N days) 형식으로 남은 일수 표시
- Certificate Path, Private Key Path 경로 확인
- 만료 30일 이내이면 VALID 대신 경고 메시지 표시됨
# 원격 서버 인증서를 외부에서 점검하는 방법 (스크립팅에 자주 씀)
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -dates
# notBefore=Jan 10 00:00:00 2025 GMT
# notAfter=Apr 10 23:59:59 2025 GMT
# 만료까지 며칠 남았는지 숫자로 출력
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -enddate \
| awk -F= '{print $2}' \
| xargs -I{} date -d "{}" +%s \
| xargs -I{} sh -c 'echo $(( ({} - $(date +%s)) / 86400 )) days left'
# 로컬 파일로 인증서 만료일 확인
sudo openssl x509 -noout -dates \
-in /etc/letsencrypt/live/yourdomain.com/fullchain.pem
Certbot이 standalone 모드나 webroot 검증 중에 80포트를 직접 사용하려다 Nginx가 이미 80포트를 점유해 충돌이 납니다. --nginx 플러그인은 Nginx를 멈추지 않고 .well-known/acme-challenge/ 경로를 직접 Nginx를 통해 처리하기 때문에 이 오류가 나지 않습니다.
standalone 모드를 굳이 써야 한다면:
# 1. Nginx 임시 중단
sudo systemctl stop nginx
# 2. 인증서 발급 (standalone 모드)
sudo certbot certonly --standalone -d yourdomain.com
# 3. Nginx 재시작
sudo systemctl start nginx
또는 처음부터 --nginx 플러그인을 사용합니다.
# 권장 방법: nginx 플러그인 사용 (서비스 중단 없음)
sudo certbot --nginx -d yourdomain.com
이 오류는 443포트로 접속했는데 서버가 HTTP 응답(암호화 안 된 텍스트)을 보낼 때 발생합니다. 브라우저는 TLS 핸드셰이크 응답을 기대하는데 일반 HTTP 데이터가 오면 이 오류를 냅니다.
원인을 순서대로 확인합니다:
# 1. Nginx가 443에서 ssl로 리스닝 중인지 확인
sudo ss -tlnp | grep ':443'
# LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",...)) ← 있어야 함
# 2. Nginx 설정에 ssl이 빠진 경우
# 잘못된 예시:
# listen 443; ← ssl 키워드가 없음
# 올바른 예시:
# listen 443 ssl;
# 3. 설정 다시 확인
sudo nginx -T | grep -A 20 'listen 443'
# 4. 수정 후 reload
sudo nginx -t && sudo systemctl reload nginx
Certbot 자동 설정 후 수동으로 설정 파일을 편집하다가 ssl 키워드를 실수로 지우는 경우가 많습니다.
--post-hook "systemctl reload nginx"를 쓰지 않고 갱신 후 Nginx가 새 인증서를 읽지 못하는 경우, 그리고 UFW/firewalld가 80포트를 막아 ACME 도전(challenge)이 실패하는 경우입니다.
# 갱신 로그 확인
sudo tail -50 /var/log/letsencrypt/letsencrypt.log
# 방화벽 80포트 열려있는지 확인
sudo ufw status | grep '80\|443'
# 또는
sudo firewall-cmd --list-all | grep ports
# 80포트 열기 (UFW)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# 방화벽 확인 후 dry-run 재시도
sudo certbot renew --dry-run
# renew hook 설정 확인
cat /etc/letsencrypt/renewal/yourdomain.com.conf
# 여기에 post_hook = systemctl reload nginx 가 있어야 함
실무에서 HTTPS 전환 요청이 들어오면 대부분 Certbot으로 10분 만에 처리합니다. 하지만 그 이후가 진짜입니다.
프로덕션에서 자주 생기는 추가 작업:
# 1. 인증서 만료 모니터링 스크립트 (cron으로 매주 실행)
#!/bin/bash
DOMAIN="yourdomain.com"
DAYS_LEFT=$(echo | openssl s_client -connect ${DOMAIN}:443 2>/dev/null \
| openssl x509 -noout -enddate \
| awk -F= '{print $2}' \
| xargs -I{} date -d "{}" +%s \
| xargs -I{} sh -c 'echo $(( ({} - $(date +%s)) / 86400 ))')
if [ "$DAYS_LEFT" -lt 14 ]; then
echo "경고: ${DOMAIN} 인증서 만료 ${DAYS_LEFT}일 남음" | mail -s "SSL 만료 경고" ops@company.com
fi
# 2. 여러 도메인 한 인증서로 묶기 (SAN 인증서)
sudo certbot --nginx \
-d yourdomain.com \
-d www.yourdomain.com \
-d api.yourdomain.com \
-d admin.yourdomain.com
# 3. 와일드카드 인증서 발급 (DNS 도전 방식 필요)
sudo certbot certonly --manual \
--preferred-challenges=dns \
-d "*.yourdomain.com" \
-d yourdomain.com
# → DNS TXT 레코드 추가를 수동으로 해야 함 (자동화하려면 DNS 플러그인 필요)
온콜 체크리스트 — "사이트 SSL 오류 발생" 신고 받았을 때:
# 1. 만료 여부 즉시 확인
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -dates
# 2. Nginx SSL 설정 유효성
sudo nginx -t
# 3. certbot 갱신 로그
sudo tail -30 /var/log/letsencrypt/letsencrypt.log
# 4. 수동 갱신 (만료가 원인인 경우)
sudo certbot renew --force-renewal
sudo systemctl reload nginx
인증서 자동 갱신은 설정 후 방치하는 것이 아닙니다. 갱신 성공 여부를 모니터링하고, 만료 2주 전 알림을 받는 구조를 만들어두는 것이 SRE 관점에서의 완성입니다.
다음 모듈에서는 Nginx 성능 튜닝과 worker_processes, keepalive, gzip 압축 설정을 다룹니다.