Skip to content

필사 모드: SSL/TLS 인증서 완벽 가이드: 발급부터 자동 갱신, 트러블슈팅까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

HTTPS는 이제 선택이 아닌 필수입니다. 브라우저는 HTTP 사이트에 "안전하지 않음" 경고를 표시하고, 검색 엔진은 HTTPS를 랭킹 시그널로 활용합니다. 하지만 SSL/TLS 인증서를 제대로 이해하고 운영하는 것은 생각보다 복잡합니다. 인증서 종류 선택, 올바른 설정, 자동 갱신, 체인 구성 등 놓치기 쉬운 부분이 많습니다.

이 글에서는 SSL/TLS의 동작 원리부터 실전 운영까지 한 번에 정리합니다.

1. SSL/TLS 동작 원리

대칭 암호화 vs 비대칭 암호화

SSL/TLS는 두 가지 암호화 방식을 함께 사용합니다.

| 구분 | 대칭 암호화 | 비대칭 암호화 |

| -------- | ------------------------- | ------------------------------- |

| 키 | 하나의 키로 암호화/복호화 | 공개키(암호화) + 개인키(복호화) |

| 속도 | 빠름 | 느림 (100~1000배) |

| 알고리즘 | AES-256, ChaCha20 | RSA, ECDSA, Ed25519 |

| 용도 | 실제 데이터 전송 | 키 교환, 인증서 서명 |

| 키 길이 | 128/256 bit | 2048/4096 bit (RSA) |

TLS 1.3 Handshake 과정

TLS 1.3에서는 handshake가 1-RTT로 줄었습니다.

Client Server

| |

|--- ClientHello (supported ciphers, key share) ->|

| |

|<-- ServerHello (chosen cipher, key share, |

| EncryptedExtensions, Certificate, |

| CertificateVerify, Finished) ---------------|

| |

|--- Finished (encrypted) ---------------------->|

| |

|<========= Application Data (encrypted) =======>|

**TLS 1.3의 주요 개선점:**

1. **1-RTT Handshake**: TLS 1.2의 2-RTT에서 1-RTT로 단축

2. **0-RTT Resumption**: 재접속 시 추가 지연 없이 데이터 전송 가능

3. **강화된 보안**: 취약한 암호 스위트 제거 (RC4, 3DES, CBC 모드 등)

4. **Forward Secrecy 필수**: 모든 키 교환에 ECDHE 또는 DHE 사용

인증서 검증 흐름

[브라우저] -> 서버 인증서 수신

-> 인증서 유효기간 확인

-> 도메인명 일치 확인

-> 인증서 체인 검증 (서버 인증서 → 중간 CA → 루트 CA)

-> OCSP/CRL로 폐기 여부 확인

-> 검증 성공 → 자물쇠 아이콘 표시

2. 인증서 종류

검증 레벨별 분류

| 종류 | 검증 수준 | 발급 시간 | 비용 | 용도 |

| -------------------------------- | -------------------- | --------- | --------- | -------------------------- |

| **DV** (Domain Validation) | 도메인 소유권만 확인 | 수 분 | 무료~저가 | 개인 블로그, 소규모 서비스 |

| **OV** (Organization Validation) | 조직 실재 확인 | 1~3일 | 중간 | 기업 웹사이트 |

| **EV** (Extended Validation) | 법적 실체 확인 | 1~2주 | 고가 | 금융, 전자상거래 |

적용 범위별 분류

| 종류 | 적용 범위 | 예시 | 비용 |

| ------------------ | ----------------------------- | ---------------------------- | ------------------ |

| **단일 도메인** | 하나의 FQDN | `www.example.com` | 가장 저렴 |

| **와일드카드** | 같은 레벨의 모든 서브도메인 | `*.example.com` | 중간 |

| **SAN/멀티도메인** | 여러 도메인을 하나의 인증서에 | `example.com`, `example.org` | 도메인당 추가 비용 |

**와일드카드 인증서 주의사항:**

*.example.com이 커버하는 범위

www.example.com # O 커버됨

api.example.com # O 커버됨

example.com # X 커버 안 됨 (SAN에 별도 추가 필요)

sub.api.example.com # X 커버 안 됨 (2단계 서브도메인)

3. Let's Encrypt + Certbot 실전

Certbot 설치

Ubuntu/Debian

sudo apt update

sudo apt install -y certbot python3-certbot-nginx

CentOS/Rocky Linux

sudo dnf install -y epel-release

sudo dnf install -y certbot python3-certbot-nginx

macOS (개발용)

brew install certbot

Docker

docker run -it --rm \

-v /etc/letsencrypt:/etc/letsencrypt \

-v /var/lib/letsencrypt:/var/lib/letsencrypt \

certbot/certbot certonly --help

인증서 발급 방식

Webroot 방식 (서비스 중단 없음)

Nginx가 실행 중인 상태에서 발급

sudo certbot certonly --webroot \

-w /var/www/html \

-d example.com \

-d www.example.com \

--email admin@example.com \

--agree-tos \

--no-eff-email

Nginx 설정에 .well-known 경로 추가 필요

location /.well-known/acme-challenge/ {

root /var/www/html;

}

Standalone 방식 (80번 포트 필요)

80번 포트를 사용하는 서비스를 잠시 중단해야 함

sudo systemctl stop nginx

sudo certbot certonly --standalone \

-d example.com \

-d www.example.com

sudo systemctl start nginx

DNS 방식 (와일드카드 인증서 발급 시 필수)

DNS TXT 레코드로 검증

sudo certbot certonly --manual \

--preferred-challenges dns \

-d "*.example.com" \

-d example.com

자동화를 위한 DNS 플러그인 사용 (Cloudflare 예시)

sudo pip install certbot-dns-cloudflare

API 토큰 파일 생성

cat > /etc/letsencrypt/cloudflare.ini << 'EOF'

dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN

EOF

chmod 600 /etc/letsencrypt/cloudflare.ini

Cloudflare DNS로 자동 발급

sudo certbot certonly \

--dns-cloudflare \

--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \

-d "*.example.com" \

-d example.com

발급된 인증서 파일 구조

/etc/letsencrypt/live/example.com/

├── cert.pem # 서버 인증서

├── chain.pem # 중간 CA 인증서

├── fullchain.pem # cert.pem + chain.pem (Nginx에서 사용)

├── privkey.pem # 개인키

└── README

4. Nginx SSL 설정 (A+ 등급)

기본 SSL 설정

/etc/nginx/sites-available/example.com

server {

listen 80;

server_name example.com www.example.com;

HTTP -> HTTPS 리다이렉트

return 301 https://$server_name$request_uri;

}

server {

listen 443 ssl http2;

server_name example.com www.example.com;

인증서 설정

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

SSL 프로토콜 (TLS 1.2, 1.3만 허용)

ssl_protocols TLSv1.2 TLSv1.3;

암호 스위트 (서버 우선)

ssl_prefer_server_ciphers on;

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';

ECDH 커브

ssl_ecdh_curve X25519:prime256v1:secp384r1;

SSL 세션 캐시

ssl_session_cache shared:SSL:10m;

ssl_session_timeout 1d;

ssl_session_tickets off;

OCSP Stapling

ssl_stapling on;

ssl_stapling_verify on;

ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

resolver 8.8.8.8 8.8.4.4 valid=300s;

resolver_timeout 5s;

DH 파라미터 (2048비트 이상)

ssl_dhparam /etc/nginx/dhparam.pem;

보안 헤더

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

add_header X-Content-Type-Options "nosniff" always;

add_header X-Frame-Options "DENY" always;

add_header X-XSS-Protection "1; mode=block" always;

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;

root /var/www/example.com;

index index.html;

}

DH 파라미터 생성

2048비트 DH 파라미터 생성 (약 1~2분 소요)

sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048

4096비트 (더 안전하지만 5~10분 소요)

sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096

SSL Labs 테스트

설정 검증

sudo nginx -t

설정 적용

sudo systemctl reload nginx

SSL Labs에서 A+ 등급 확인

https://www.ssllabs.com/ssltest/analyze.html?d=example.com

CLI로 확인

curl -sS https://api.ssllabs.com/api/v3/analyze?host=example.com | jq '.endpoints[0].grade'

5. AWS ACM 인증서 관리

ACM 인증서 발급

AWS CLI로 인증서 요청

aws acm request-certificate \

--domain-name example.com \

--subject-alternative-names "*.example.com" \

--validation-method DNS \

--region ap-northeast-2

검증 상태 확인

aws acm describe-certificate \

--certificate-arn arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123 \

--query 'Certificate.DomainValidationOptions'

Terraform으로 ACM + Route53 자동화

ACM 인증서 리소스

resource "aws_acm_certificate" "main" {

domain_name = "example.com"

subject_alternative_names = ["*.example.com"]

validation_method = "DNS"

lifecycle {

create_before_destroy = true

}

tags = {

Environment = "production"

ManagedBy = "terraform"

}

}

Route53 DNS 검증 레코드 자동 생성

resource "aws_route53_record" "cert_validation" {

for_each = {

for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {

name = dvo.resource_record_name

type = dvo.resource_record_type

record = dvo.resource_record_value

}

}

allow_overwrite = true

name = each.value.name

records = [each.value.record]

ttl = 60

type = each.value.type

zone_id = data.aws_route53_zone.main.zone_id

}

인증서 검증 완료 대기

resource "aws_acm_certificate_validation" "main" {

certificate_arn = aws_acm_certificate.main.arn

validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]

}

ALB에 인증서 연결

resource "aws_lb_listener" "https" {

load_balancer_arn = aws_lb.main.arn

port = 443

protocol = "HTTPS"

ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"

certificate_arn = aws_acm_certificate_validation.main.certificate_arn

default_action {

type = "forward"

target_group_arn = aws_lb_target_group.main.arn

}

}

ACM vs Let's Encrypt 비교

| 항목 | AWS ACM | Let's Encrypt |

| --------------- | --------------------------- | --------------------- |

| 비용 | 무료 (AWS 서비스 연동 시) | 무료 |

| 유효기간 | 13개월 (자동 갱신) | 90일 (자동 갱신 필요) |

| 와일드카드 | 지원 | 지원 (DNS 검증 필수) |

| 사용 범위 | AWS ALB, CloudFront, API GW | 어디서든 사용 가능 |

| 인증서 내보내기 | 불가 (Private CA 제외) | 가능 |

| 관리 부담 | 거의 없음 | Certbot 관리 필요 |

6. 인증서 자동 갱신

Cron을 이용한 갱신

Certbot 갱신 테스트

sudo certbot renew --dry-run

Cron 작업 등록

sudo crontab -e

매일 새벽 3시에 갱신 확인 (30일 이내 만료 시 자동 갱신)

0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

Systemd Timer를 이용한 갱신 (권장)

/etc/systemd/system/certbot-renewal.service

cat > /etc/systemd/system/certbot-renewal.service << 'EOF'

[Unit]

Description=Certbot Renewal

After=network-online.target

[Service]

Type=oneshot

ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

ExecStartPost=/bin/bash -c 'echo "Certbot renewal completed at $(date)" >> /var/log/certbot-renewal.log'

EOF

/etc/systemd/system/certbot-renewal.timer

cat > /etc/systemd/system/certbot-renewal.timer << 'EOF'

[Unit]

Description=Run certbot renewal twice daily

[Timer]

OnCalendar=*-*-* 00,12:00:00

RandomizedDelaySec=3600

Persistent=true

[Install]

WantedBy=timers.target

EOF

활성화

sudo systemctl daemon-reload

sudo systemctl enable --now certbot-renewal.timer

타이머 상태 확인

sudo systemctl list-timers | grep certbot

갱신 모니터링 스크립트

#!/bin/bash

/usr/local/bin/check-ssl-expiry.sh

DOMAINS=("example.com" "api.example.com" "admin.example.com")

ALERT_DAYS=14

SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

for domain in "${DOMAINS[@]}"; do

expiry_date=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null \

| openssl x509 -noout -enddate 2>/dev/null \

| cut -d= -f2)

if [ -z "$expiry_date" ]; then

echo "ERROR: Cannot connect to $domain"

continue

fi

expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry_date" +%s)

current_epoch=$(date +%s)

days_left=$(( (expiry_epoch - current_epoch) / 86400 ))

echo "$domain: $days_left days until expiry ($expiry_date)"

if [ "$days_left" -lt "$ALERT_DAYS" ]; then

curl -s -X POST "$SLACK_WEBHOOK" \

-H 'Content-type: application/json' \

-d "{\"text\":\"SSL 인증서 만료 경고: $domain - ${days_left}일 남음\"}"

fi

done

실행 권한 부여 및 cron 등록

chmod +x /usr/local/bin/check-ssl-expiry.sh

echo "0 9 * * * /usr/local/bin/check-ssl-expiry.sh" | sudo crontab -

7. 인증서 디버깅 명령어 모음

인증서 정보 확인

openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -text -noout

만료일만 확인

openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -enddate

원격 서버 인증서 확인

echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \

| openssl x509 -noout -text

인증서 체인 확인

echo | openssl s_client -showcerts -servername example.com -connect example.com:443 2>/dev/null

인증서와 개인키 매칭 확인

openssl x509 -noout -modulus -in cert.pem | openssl md5

openssl rsa -noout -modulus -in privkey.pem | openssl md5

두 값이 같으면 매칭됨

인증서 SAN 확인

openssl x509 -in cert.pem -noout -ext subjectAltName

PEM → PFX(PKCS12) 변환

openssl pkcs12 -export -out cert.pfx \

-inkey privkey.pem -in cert.pem -certfile chain.pem

PFX → PEM 변환

openssl pkcs12 -in cert.pfx -out cert.pem -nodes

8. 트러블슈팅

문제 1: 인증서 만료 (ERR_CERT_DATE_INVALID)

현재 만료일 확인

echo | openssl s_client -connect example.com:443 2>/dev/null \

| openssl x509 -noout -dates

즉시 갱신

sudo certbot renew --force-renewal --cert-name example.com

sudo systemctl reload nginx

문제 2: 인증서 체인 불완전 (ERR_CERT_AUTHORITY_INVALID)

체인 검증

openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \

/etc/letsencrypt/live/example.com/fullchain.pem

Nginx에서 fullchain.pem을 사용하고 있는지 확인

잘못된 설정:

ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;

올바른 설정:

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;

문제 3: Mixed Content 경고

HTML에서 http:// 리소스 찾기

grep -rn "http://" /var/www/html/ --include="*.html" --include="*.js" --include="*.css"

CSP 헤더로 자동 업그레이드

Nginx 설정에 추가:

add_header Content-Security-Policy "upgrade-insecure-requests;" always;

문제 4: SSL Handshake 실패

지원하는 프로토콜 확인

nmap --script ssl-enum-ciphers -p 443 example.com

특정 TLS 버전으로 연결 테스트

openssl s_client -connect example.com:443 -tls1_2

openssl s_client -connect example.com:443 -tls1_3

클라이언트가 지원하지 않는 암호 스위트 문제

→ ssl_ciphers 설정에 호환성 있는 스위트 추가

문제 5: HSTS 관련 이슈

HSTS 헤더가 설정되어 있으면 HTTP로 돌아갈 수 없음

테스트 시에는 max-age를 짧게 설정

add_header Strict-Transport-Security "max-age=300" always;

프로덕션에서는 충분히 길게

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

문제 6: Let's Encrypt Rate Limit

Rate Limit 확인

- 동일 도메인: 주당 5회 발급 (갱신 제외)

- 중복 인증서: 주당 5회

- 계정당: 3시간에 300개 요청

테스트 시에는 staging 서버 사용

sudo certbot certonly --staging \

--webroot -w /var/www/html \

-d test.example.com

staging 인증서 삭제 후 본 발급

sudo certbot delete --cert-name test.example.com

9. 고급 설정

mTLS (상호 TLS 인증)

server {

listen 443 ssl http2;

server_name api.example.com;

ssl_certificate /etc/nginx/ssl/server.crt;

ssl_certificate_key /etc/nginx/ssl/server.key;

클라이언트 인증서 요구

ssl_client_certificate /etc/nginx/ssl/ca.crt;

ssl_verify_client on;

ssl_verify_depth 2;

location / {

클라이언트 인증서 정보를 백엔드로 전달

proxy_set_header X-Client-Cert-DN $ssl_client_s_dn;

proxy_set_header X-Client-Cert-Serial $ssl_client_serial;

proxy_pass http://backend;

}

}

클라이언트 인증서 생성

CA 키 및 인증서 생성

openssl genrsa -out ca.key 4096

openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \

-subj "/C=KR/ST=Seoul/O=MyOrg/CN=Internal CA"

클라이언트 키 및 CSR 생성

openssl genrsa -out client.key 2048

openssl req -new -key client.key -out client.csr \

-subj "/C=KR/ST=Seoul/O=MyOrg/CN=client1"

클라이언트 인증서 발급

openssl x509 -req -days 365 \

-in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \

-out client.crt

테스트

curl --cert client.crt --key client.key https://api.example.com/

Certificate Transparency (CT) 로그 확인

CT 로그에서 도메인 인증서 검색

https://crt.sh/?q=example.com

CLI로 확인

curl -s "https://crt.sh/?q=example.com&output=json" | jq '.[0:5] | .[] | {id, name_value, not_after}'

10. 운영 체크리스트

초기 설정 체크리스트

- [ ] TLS 1.2 이상만 허용 (`ssl_protocols TLSv1.2 TLSv1.3`)

- [ ] 강력한 암호 스위트만 사용 (ECDHE + AES-GCM/ChaCha20)

- [ ] HSTS 헤더 설정 (`Strict-Transport-Security`)

- [ ] HTTP → HTTPS 리다이렉트 (301)

- [ ] OCSP Stapling 활성화

- [ ] DH 파라미터 2048비트 이상 생성

- [ ] SSL Labs에서 A+ 등급 확인

- [ ] Mixed Content 없는지 확인

자동 갱신 체크리스트

- [ ] Certbot 자동 갱신 설정 (cron 또는 systemd timer)

- [ ] 갱신 테스트 (`certbot renew --dry-run`)

- [ ] 갱신 후 Nginx reload hook 설정

- [ ] 만료 모니터링 스크립트 배포

- [ ] Slack/Email 알림 설정 (만료 14일 전)

정기 점검 체크리스트

- [ ] 매월 인증서 만료일 확인

- [ ] 분기별 SSL Labs 재점검

- [ ] 새 보안 취약점 패치 확인 (OpenSSL, Nginx)

- [ ] CT 로그에서 비정상 인증서 발급 모니터링

- [ ] 보안 헤더 변경사항 점검

인시던트 대응 체크리스트

- [ ] 인증서 만료 시 즉시 갱신 절차 문서화

- [ ] 인증서 폐기(revoke) 절차 숙지

- [ ] 긴급 인증서 재발급 절차 준비

- [ ] 롤백 계획 수립 (이전 인증서 백업)

- [ ] 인시던트 발생 시 알림 채널 지정

마무리

SSL/TLS 인증서 관리는 한 번 설정하면 끝이 아닙니다. Let's Encrypt의 90일 유효기간 정책은 자동화를 강제하는 좋은 관행입니다. 이 글에서 다룬 내용을 바탕으로 안전하고 자동화된 인증서 관리 체계를 구축하시기 바랍니다.

핵심 정리:

1. **TLS 1.3을 기본으로** 사용하되, 호환성을 위해 TLS 1.2도 허용

2. **Let's Encrypt + Certbot**으로 무료 인증서 자동 발급 및 갱신

3. **Nginx 설정**은 SSL Labs A+ 등급을 목표로 최적화

4. **자동 갱신**은 systemd timer로 설정하고 모니터링 필수

5. **트러블슈팅**은 `openssl s_client` 명령어로 시작

현재 단락 (1/314)

HTTPS는 이제 선택이 아닌 필수입니다. 브라우저는 HTTP 사이트에 "안전하지 않음" 경고를 표시하고, 검색 엔진은 HTTPS를 랭킹 시그널로 활용합니다. 하지만 SSL/TLS...

작성 글자: 0원문 글자: 12,895작성 단락: 0/314