infra
Platform

모듈 맵

[Infra Ops] Let's Encrypt 인증서 발급과 Nginx SSL 설정

0 / 52 완료

펼치기
0 / 52 완료0%

Infra-ops · 14 / 52

[Infra Ops] Let's Encrypt 인증서 발급과 Nginx SSL 설정

Let's Encrypt와 Certbot으로 무료 SSL 인증서를 발급하고 Nginx에 적용하는 실무 과정 전체를 다룹니다. 인증서 자동 갱신과 HSTS 설정까지 프로덕션 수준으로 구성합니다.

🚨INCIDENT ALERT
HIGH

배포한 서비스에 처음으로 도메인을 연결하고 나면 브라우저가 "이 연결은 안전하지 않습니다"라는 경고를 띄웁니다. 사용자 입장에서 HTTP 사이트는 이미 신뢰할 수 없는 곳입니다. 게다가 구글 검색 순위도 HTTPS를 우선합니다. 팀장은 "오늘 안에 HTTPS로 전환해줘"라고 말하는데, 어디서부터 시작해야 할지 막막합니다.

Let's Encrypt를 쓰면 무료로, 그것도 10분 안에 해결할 수 있습니다. 이 모듈에서 처음부터 끝까지 같이 해봅니다.

이번 챕터에서 배울 것
  • 1SSL/TLS 인증서가 무엇인지, CA와 인증서 체인이 왜 필요한지 설명할 수 있다
  • 2Certbot으로 Let's Encrypt 인증서를 발급하고 Nginx에 자동 적용할 수 있다
  • 3HTTP → HTTPS 리다이렉트와 HSTS를 Nginx에 설정할 수 있다
  • 4인증서 자동 갱신을 cron 또는 systemd timer로 구성할 수 있다
  • 5openssl 명령어로 인증서 만료일을 확인하고 현장에서 즉시 점검할 수 있다
실습 환경 준비
도메인이 서버 IP로 연결됐는지 확인 (A 레코드)
dig +short yourdomain.com
80/443 포트 방화벽 오픈 여부 확인
sudo ss -tlnp | grep -E ':80|:443'
Certbot 설치 (Ubuntu/Debian)
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
Certbot 설치 (RHEL/CentOS 9+)
sudo dnf install -y certbot python3-certbot-nginx
설치 확인
certbot --version

SSL/TLS 기초 개념

💡개념

인증서와 CA: 브라우저가 신뢰하는 구조

HTTPS의 자물쇠 아이콘은 단순히 "암호화됐다"는 표시가 아닙니다. "이 서버가 주장하는 도메인의 진짜 소유자임을 제3자가 서명했다"는 의미입니다. 이 서명 체계가 없다면 공격자가 example.com을 사칭하는 서버를 만들어도 브라우저는 알아채지 못합니다. CA(인증기관)와 인증서 체인은 브라우저가 신뢰할 서버와 그렇지 않은 서버를 구분하는 공개키 기반 신뢰 인프라의 핵심입니다.

SSL/TLS 인증서 체인 구조

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 같은 설정이 왜 필요한지 납득이 됩니다.

TLS 설정 레이어 — cipher suite, 프로토콜 버전, 인증서 체인 구조

핸드셰이크는 대략 이렇게 동작합니다:

  1. 클라이언트 → 서버: "이런 암호화 방식을 지원해요" (ClientHello)
  2. 서버 → 클라이언트: "이 방식을 쓰자, 내 인증서야" (ServerHello + Certificate)
  3. 클라이언트: 인증서를 CA 체인으로 검증
  4. 클라이언트 ↔ 서버: 세션 키 교환 (대칭키 협상)
  5. 이후부터는 협상한 대칭키로 암호화 통신

CPU 비용이 있습니다. 핸드셰이크는 비대칭 암호 연산을 포함하기 때문에 일반 HTTP보다 비쌉니다. 그래서 Nginx에 세션 캐시를 설정해 재연결 시 핸드셰이크를 생략합니다.

Nginx
# 이 설정이 왜 있는지 이제 납득이 됩니다
ssl_session_cache shared:SSL:10m;   # 워커 공유 캐시 10MB
ssl_session_timeout 10m;            # 캐시 유지 시간

Certbot으로 인증서 발급

1Certbot Nginx 플러그인으로 인증서 발급 및 자동 설정

--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 접속 시 자물쇠 아이콘 표시
2Certbot이 수정한 Nginx 설정 확인 및 이해

--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 플러그인이 자동으로 해주는 것도 좋지만, 실제로 무슨 설정이 들어가는지 알아야 합니다. 직접 관리하는 서버에서는 설정의 각 줄이 무엇을 의미하는지 설명할 수 있어야 합니다.

Nginx
# /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으로 시작해 문제 없으면 늘리는 게 안전합니다.

3설정 적용 및 Nginx 재로드

-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 서버 장애나 네트워크 문제로 갱신이 실패할 경우를 대비합니다. 한 번 실패해도 몇 시간 후 재시도 기회가 있습니다.

인증서 점검 명령어

4인증서 만료일 및 상태 점검

현재 서버에 발급된 모든 인증서의 상태, 만료일, 경로를 한 번에 확인합니다. 온콜 대응 중 "인증서가 만료됐나요?"라는 질문을 받으면 이 명령 하나로 대답할 수 있습니다.

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 압축 설정을 다룹니다.

지식 확인

퀴즈 — 4문제

Q1

Let's Encrypt 인증서의 유효 기간은 90일입니다. Certbot 자동 갱신이 인증서를 실제로 갱신하는 시점은?

Q2

Nginx에서 HTTP(80포트) 요청을 HTTPS(443포트)로 강제 리다이렉트할 때 올바른 설정은?

Q3

SSL 설정에서 ssl_session_cache shared:SSL:10m; 의 역할은?

Q4

openssl s_client 명령어로 인증서 만료일을 확인하는 올바른 방법은?

0 / 4 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

30📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

infra-ops중급 · 70
[Infra Ops] WAR 배포부터 server.xml 튜닝, 장애 대응까지
인프라 서비스 운영 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점