Skip to content

Split View: [DevOps] Nginx TLS 설정 완전 가이드: PEM/Key 적용과 보안 최적화

|

[DevOps] Nginx TLS 설정 완전 가이드: PEM/Key 적용과 보안 최적화


1. PEM, Key, CRT 파일 형식 이해

1.1 PEM (Privacy Enhanced Mail)

PEM은 Base64로 인코딩된 인증서/키 형식이다. 텍스트 에디터로 열어볼 수 있다.

-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
... (Base64 인코딩된 데이터)
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7o4qne60TB3pO
... (Base64 인코딩된 데이터)
-----END PRIVATE KEY-----

1.2 DER (Distinguished Encoding Rules)

바이너리 형식이다. PEM에서 Base64 인코딩을 제거한 것과 동일하다.

1.3 키 타입

알고리즘키 크기성능보안권장
RSA 20482048 bit느림충분호환성 중시
RSA 40964096 bit매우 느림높음높은 보안 필요 시
ECDSA P-256256 bit빠름충분권장 (Modern)
ECDSA P-384384 bit빠름높음높은 보안 + 성능

ECDSA는 RSA 대비 동일 보안 수준에서 키 크기가 훨씬 작아 TLS 핸드셰이크 성능이 우수하다.

1.4 Let's Encrypt 발급 파일 매핑

/etc/letsencrypt/live/example.com/
  fullchain.pem  = 서버 인증서 + 중간 CA 인증서
  privkey.pem    = 개인키
  chain.pem      = 중간 CA 인증서만
  cert.pem       = 서버 인증서만

Nginx에서 사용하는 파일:
  ssl_certificate     -> fullchain.pem
  ssl_certificate_key -> privkey.pem

2. Nginx 기본 TLS 설정

2.1 최소 설정

server {
    listen 443 ssl;
    server_name example.com;

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

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

2.2 HTTP에서 HTTPS로 리다이렉트

server {
    listen 80;
    server_name example.com www.example.com;

    # ACME 챌린지용 (Let's Encrypt 갱신)
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # 나머지 모든 요청을 HTTPS로 리다이렉트
    location / {
        return 301 https://$host$request_uri;
    }
}

2.3 완전한 프로덕션 설정 예제

# /etc/nginx/conf.d/ssl-params.conf
# 공통 SSL 파라미터 (모든 서버 블록에서 include)

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:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

# DH 파라미터
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

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

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

# SSL 세션 캐싱
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# 보안 헤더
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$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;

    include /etc/nginx/conf.d/ssl-params.conf;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # 정적 파일 캐싱
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

3. TLS 버전 설정

3.1 TLS 버전별 보안 상태

버전상태권장
SSL 2.0폐기 (1996년)절대 사용 금지
SSL 3.0폐기 (POODLE 취약점)절대 사용 금지
TLS 1.0폐기 (2021년)비활성화
TLS 1.1폐기 (2021년)비활성화
TLS 1.2현재 표준사용
TLS 1.3최신 표준사용 (권장)

3.2 TLS 1.2/1.3만 허용

# TLS 1.2와 1.3만 허용 (권장)
ssl_protocols TLSv1.2 TLSv1.3;

3.3 TLS 1.3 전용 (Modern)

# TLS 1.3만 허용 (최신 클라이언트만 지원)
ssl_protocols TLSv1.3;

TLS 1.3의 장점:

  • 핸드셰이크 라운드 트립 감소 (1-RTT, 0-RTT)
  • 취약한 레거시 암호화 스위트 제거
  • 전방 보안(Forward Secrecy) 기본 적용
  • 핸드셰이크 암호화 (서버 인증서 포함)

4. 암호화 스위트 설정

4.1 Mozilla SSL Configuration Generator 프로파일

Mozilla는 세 가지 구성 프로파일을 제공한다.

Modern (TLS 1.3 전용):

ssl_protocols TLSv1.3;
# TLS 1.3은 별도 cipher 설정 불필요 (프로토콜에 내장)
ssl_prefer_server_ciphers off;

Intermediate (TLS 1.2 + 1.3, 권장):

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:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

Old (레거시 호환, 비권장):

ssl_protocols TLSv1 TLSv1.1 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:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA;
ssl_prefer_server_ciphers on;

4.2 암호화 스위트 이름 해석

ECDHE-RSA-AES256-GCM-SHA384 의 의미:

ECDHE  = 키 교환 알고리즘 (Elliptic Curve Diffie-Hellman Ephemeral)
         → Forward Secrecy 제공
RSA    = 인증 알고리즘 (서버 인증서 서명)
AES256 = 대칭 암호화 (256비트 AES)
GCM    = 암호화 모드 (Galois/Counter Mode, AEAD)
SHA384 = 해시 함수 (무결성 검증)

4.3 ssl_prefer_server_ciphers

# TLS 1.2: on으로 설정하면 서버가 선호하는 cipher를 우선 사용
# TLS 1.3: 클라이언트가 결정 (이 설정 무시됨)
# Mozilla Intermediate에서는 off 권장
ssl_prefer_server_ciphers off;

5. DH 파라미터

Diffie-Hellman 키 교환에 사용되는 파라미터다. DHE 암호화 스위트를 사용할 때 필요하다.

# DH 파라미터 생성 (2048비트 이상 권장)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

# 더 강력한 4096비트 (생성 시간이 오래 걸림)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

참고: TLS 1.3에서는 DHE 대신 ECDHE만 사용하므로 DH 파라미터가 필요 없다. TLS 1.2의 DHE 암호화 스위트를 제거하면 DH 파라미터도 불필요하다.


6. HSTS (HTTP Strict Transport Security)

6.1 HSTS란

HSTS는 브라우저에게 해당 도메인에 항상 HTTPS로 접속하도록 지시하는 HTTP 헤더다.

# max-age: HSTS 유지 기간 (초 단위, 63072000 = 2년)
# includeSubDomains: 모든 서브도메인에도 적용
# preload: 브라우저 Preload List 등록 의사 표시
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

6.2 HSTS Preload List

HSTS Preload List에 등록하면 브라우저가 첫 접속부터 HTTPS를 강제한다.

등록 조건:

  • 유효한 TLS 인증서
  • 포트 80에서 443으로 리다이렉트
  • 모든 서브도메인에 HTTPS 적용
  • HSTS 헤더에 max-age 최소 31536000(1년), includeSubDomains, preload 포함

등록 사이트: hstspreload.org

6.3 주의사항

  • HSTS를 설정하면 되돌리기 어려움 (max-age 동안 브라우저가 HTTP 접속 차단)
  • 처음에는 작은 max-age (예: 300초)로 시작해서 점진적으로 늘리는 것을 권장
  • 모든 서브도메인이 HTTPS를 지원하는지 확인 후 includeSubDomains 추가

7. OCSP Stapling

7.1 OCSP Stapling이란

OCSP(Online Certificate Status Protocol)는 인증서가 폐기(revoke)되었는지 확인하는 프로토콜이다. OCSP Stapling은 서버가 CA에서 OCSP 응답을 미리 받아서 TLS 핸드셰이크 시 클라이언트에게 전달하는 기술이다.

OCSP Stapling 없이:
Client → Server (TLS 핸드셰이크)
Client → CA OCSP Server (인증서 유효성 확인) ← 추가 지연

OCSP Stapling 있을 때:
Server → CA OCSP Server (주기적으로 OCSP 응답 갱신)
Client → Server (TLS 핸드셰이크 + OCSP 응답 포함) ← 빠름

7.2 설정

ssl_stapling on;
ssl_stapling_verify on;

# 중간 CA 인증서 (chain.pem)
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

# OCSP 응답 서버의 DNS 해석용 리졸버
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

7.3 OCSP Stapling 확인

# OCSP Stapling 동작 확인
openssl s_client -connect example.com:443 -servername example.com -status < /dev/null 2>/dev/null | grep -A 20 "OCSP Response"

# 정상 응답 예시:
# OCSP Response Status: successful (0x0)
# OCSP Response Data:
#     OCSP Response Status: successful (0x0)
#     Cert Status: good

8. SSL 세션 캐싱

8.1 세션 캐시

TLS 핸드셰이크는 비용이 크다. 세션 캐시를 사용하면 재연결 시 전체 핸드셰이크를 생략할 수 있다.

# 공유 메모리 캐시 (여러 worker 프로세스 간 공유)
# 10MB 캐시 = 약 40,000 세션 저장
ssl_session_cache shared:SSL:10m;

# 세션 유효 기간
ssl_session_timeout 1d;

# 세션 티켓 비활성화 (Forward Secrecy 보장을 위해)
ssl_session_tickets off;

8.2 왜 Session Tickets를 비활성화하는가

Session Tickets는 서버 메모리 대신 암호화된 티켓을 클라이언트에 저장한다. 문제점:

  • 티켓 암호화 키가 모든 세션의 보안을 좌우
  • 키 로테이션이 제대로 되지 않으면 Forward Secrecy가 깨짐
  • TLS 1.3에서는 0-RTT 재연결로 대체 가능

9. HTTP/2 설정

9.1 HTTP/2 활성화

server {
    listen 443 ssl http2;
    server_name example.com;

    # ... SSL 설정 ...
}

HTTP/2의 장점:

  • 멀티플렉싱: 하나의 TCP 연결에서 여러 요청을 동시 처리
  • 헤더 압축: HPACK으로 HTTP 헤더 압축
  • 서버 푸시: 클라이언트가 요청하기 전에 리소스 전송
  • 우선순위: 리소스별 전송 우선순위 설정

9.2 HTTP/3 (QUIC) 준비

# Nginx 1.25.0+ (또는 nginx-quic)
server {
    listen 443 ssl;
    listen 443 quic reuseport;
    http2 on;
    http3 on;

    server_name example.com;

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

    # QUIC 광고 헤더
    add_header Alt-Svc 'h3=":443"; ma=86400' always;
}

10. mTLS (Mutual TLS) 설정

10.1 mTLS란

일반 TLS는 서버만 인증서를 제시하지만, mTLS는 클라이언트도 인증서를 제시하여 양방향 인증을 수행한다.

일반 TLS:
Client ──── 서버 인증서 확인 ───> Server

Mutual TLS:
Client <─── 서버 인증서 확인 ───> Server
Client ──── 클라이언트 인증서 ──> Server (서버가 클라이언트 인증서 확인)

사용 사례:

  • 마이크로서비스 간 통신 (서비스 메시)
  • API 인증 (API Gateway)
  • IoT 디바이스 인증
  • 내부 관리 도구 접근 제어

10.2 CA 인증서로 클라이언트 인증서 발급

# 1. CA 개인키 생성
openssl genrsa -out ca.key 4096

# 2. CA 인증서 생성 (자체 서명)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/C=KR/ST=Seoul/O=MyOrg/CN=Internal CA"

# 3. 클라이언트 개인키 생성
openssl genrsa -out client.key 2048

# 4. 클라이언트 CSR 생성
openssl req -new -key client.key -out client.csr \
  -subj "/C=KR/ST=Seoul/O=MyOrg/CN=service-a"

# 5. CA로 클라이언트 인증서 서명
openssl x509 -req -days 365 -in client.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt

# 6. PFX/PKCS12 번들 생성 (브라우저 임포트용)
openssl pkcs12 -export -out client.pfx \
  -inkey client.key -in client.crt -certfile ca.crt

10.3 Nginx mTLS 설정

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/server.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/server.privkey.pem;

    # 클라이언트 인증서 검증
    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;        # on: 필수, optional: 선택적
    ssl_verify_depth 2;          # 인증서 체인 검증 깊이

    # CRL (Certificate Revocation List) 설정
    # ssl_crl /etc/nginx/ssl/ca.crl;

    # 클라이언트 인증서 정보를 백엔드로 전달
    location / {
        proxy_pass http://backend;
        proxy_set_header X-SSL-Client-Cert $ssl_client_cert;
        proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
        proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
        proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
    }
}

10.4 ssl_verify_client 옵션

# on: 클라이언트 인증서 필수 (없으면 400 에러)
ssl_verify_client on;

# optional: 인증서 선택적 (없어도 접속 가능, 인증서가 있으면 검증)
ssl_verify_client optional;

# optional_no_ca: 인증서 검증 없이 전달만 (백엔드에서 검증)
ssl_verify_client optional_no_ca;

10.5 mTLS 테스트

# 클라이언트 인증서로 요청
curl --cert client.crt --key client.key \
  --cacert ca.crt \
  https://api.example.com/data

# 인증서 없이 요청 (ssl_verify_client on이면 실패)
curl --cacert ca.crt https://api.example.com/data
# 400 Bad Request - No required SSL certificate was sent

11. 자체 서명 인증서 생성 (개발용)

11.1 단일 도메인

# 개인키 + 자체 서명 인증서 한 번에 생성
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -days 365 \
  -subj "/C=KR/ST=Seoul/O=Dev/CN=localhost"

11.2 SAN (Subject Alternative Name) 포함

여러 도메인이나 IP를 포함하는 인증서:

# SAN 설정 파일 생성
cat > san.cnf << 'SANEOF'
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C = KR
ST = Seoul
O = Dev
CN = localhost

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = myapp.local
DNS.3 = *.myapp.local
IP.1 = 127.0.0.1
IP.2 = 192.168.1.100
SANEOF

# 자체 서명 인증서 생성
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -days 365 \
  -config san.cnf \
  -extensions v3_req

11.3 ECDSA 자체 서명 인증서

# ECDSA P-256 키 생성
openssl ecparam -genkey -name prime256v1 -out ecdsa.key

# 자체 서명 인증서
openssl req -x509 -new -key ecdsa.key \
  -out ecdsa.crt \
  -days 365 \
  -subj "/C=KR/ST=Seoul/O=Dev/CN=localhost"

12. 인증서 변환

12.1 PEM에서 DER로

# 인증서 변환
openssl x509 -in cert.pem -outform DER -out cert.der

# 개인키 변환
openssl rsa -in key.pem -outform DER -out key.der

12.2 DER에서 PEM으로

openssl x509 -in cert.der -inform DER -outform PEM -out cert.pem

12.3 PFX/PKCS12에서 PEM으로

# PFX에서 인증서 추출
openssl pkcs12 -in certificate.pfx -clcerts -nokeys -out cert.pem

# PFX에서 개인키 추출
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out key.pem

# PFX에서 CA 체인 추출
openssl pkcs12 -in certificate.pfx -cacerts -nokeys -out chain.pem

12.4 PEM에서 PFX/PKCS12로

openssl pkcs12 -export \
  -out certificate.pfx \
  -inkey key.pem \
  -in cert.pem \
  -certfile chain.pem

13. 인증서 검증 명령어

13.1 인증서 내용 확인

# 인증서 전체 정보
openssl x509 -in cert.pem -text -noout

# 발급자 정보
openssl x509 -in cert.pem -issuer -noout

# 주체 정보
openssl x509 -in cert.pem -subject -noout

# 만료일
openssl x509 -in cert.pem -enddate -noout

# SAN (Subject Alternative Names)
openssl x509 -in cert.pem -noout -ext subjectAltName

# 시리얼 번호
openssl x509 -in cert.pem -serial -noout

13.2 원격 서버 인증서 확인

# 서버 인증서 조회
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  openssl x509 -text -noout

# 인증서 체인 확인
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null 2>/dev/null

# TLS 버전 및 cipher 확인
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  grep -E "Protocol|Cipher"

13.3 인증서-키 매칭 확인

# 인증서와 키의 modulus 비교 (같아야 매칭)
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in key.pem | openssl md5

# 두 값이 동일하면 인증서와 키가 매칭

14. 보안 점검 도구

14.1 SSL Labs

온라인으로 TLS 설정을 점검하는 서비스다.

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

등급 기준:
A+  = 최상 (HSTS + 강력한 설정)
A   = 우수
B   = 양호 (개선 권장)
C   = 미흡 (보안 위험)
F   = 위험 (즉시 조치 필요)

14.2 testssl.sh

커맨드라인에서 TLS 설정을 점검하는 오픈소스 도구다.

# 설치
git clone --depth 1 https://github.com/drwetter/testssl.sh.git

# 전체 점검
./testssl.sh/testssl.sh example.com

# 특정 항목만 점검
./testssl.sh/testssl.sh --protocols example.com
./testssl.sh/testssl.sh --ciphers example.com
./testssl.sh/testssl.sh --headers example.com
./testssl.sh/testssl.sh --vulnerabilities example.com

14.3 Nginx 설정 검증

# Nginx 설정 문법 검사
sudo nginx -t

# Nginx 설정 전체 출력
sudo nginx -T

# Gixy (Nginx 보안 분석 도구)
pip install gixy
gixy /etc/nginx/nginx.conf

15. Let's Encrypt 자동 갱신 연동

15.1 Certbot + Nginx Reload Hook

# certbot 갱신 후 Nginx reload
sudo certbot renew --deploy-hook "systemctl reload nginx"

# 또는 renewal 설정 파일에 직접 추가
# /etc/letsencrypt/renewal/example.com.conf
# [renewalparams]
# ...
# renew_hook = systemctl reload nginx

15.2 Docker Compose에서 자동 갱신

version: '3.8'
services:
  nginx:
    image: nginx:latest
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    restart: unless-stopped
    command: '/bin/sh -c ''while :; do sleep 6h & wait; nginx -s reload; done & nginx -g "daemon off;"'''

  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait; done'"

16. 설정 체크리스트

프로덕션 배포 전 확인해야 할 항목:

항목명령어 / 확인 방법기준
TLS 버전ssl_protocolsTLSv1.2 TLSv1.3만
인증서 체인fullchain.pem 사용중간 CA 포함
HSTS헤더 확인max-age 1년 이상
OCSP Staplingopenssl s_client -statusCert Status: good
HTTP 리다이렉트curl -I http://domain301 to HTTPS
SSL Labs 등급ssllabs.comA+ 목표
개인키 권한ls -la privkey.pem600 (root only)
자동 갱신certbot renew --dry-run성공
DH 파라미터2048비트 이상생성 완료
Session TicketsoffForward Secrecy

17. 결론

Nginx TLS 설정은 단순히 인증서를 적용하는 것을 넘어서 종합적인 보안 최적화가 필요하다. 핵심 정리:

  • fullchain.pem + privkey.pem을 사용하여 인증서 체인을 완성
  • TLS 1.2/1.3만 허용하고 레거시 프로토콜 비활성화
  • Mozilla Intermediate 프로파일의 암호화 스위트 권장
  • HSTS로 HTTPS 접속 강제, OCSP Stapling으로 성능 향상
  • SSL 세션 캐싱으로 핸드셰이크 오버헤드 감소
  • HTTP/2로 웹 성능 최적화
  • mTLS로 마이크로서비스 간 양방향 인증 구현
  • SSL Labs에서 A+ 등급을 목표로 설정 최적화
  • Let's Encrypt 인증서 자동 갱신을 반드시 설정

[DevOps] Nginx TLS Configuration Complete Guide: PEM/Key Setup and Security Optimization


1. Understanding PEM, Key, and CRT File Formats

1.1 PEM (Privacy Enhanced Mail)

PEM is a Base64-encoded certificate/key format that can be opened with a text editor.

-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
... (Base64 encoded data)
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7o4qne60TB3pO
... (Base64 encoded data)
-----END PRIVATE KEY-----

1.2 DER (Distinguished Encoding Rules)

Binary format. Equivalent to PEM with Base64 encoding removed.

1.3 Key Types

AlgorithmKey SizePerformanceSecurityRecommendation
RSA 20482048 bitSlowSufficientCompatibility-focused
RSA 40964096 bitVery slowHighHigh security needed
ECDSA P-256256 bitFastSufficientRecommended (Modern)
ECDSA P-384384 bitFastHighHigh security + performance

ECDSA has much smaller key sizes than RSA at equivalent security levels, resulting in superior TLS handshake performance.

1.4 Let's Encrypt File Mapping

/etc/letsencrypt/live/example.com/
  fullchain.pem  = Server cert + Intermediate CA cert
  privkey.pem    = Private key
  chain.pem      = Intermediate CA cert only
  cert.pem       = Server cert only

Files used by Nginx:
  ssl_certificate     -> fullchain.pem
  ssl_certificate_key -> privkey.pem

2. Basic Nginx TLS Configuration

2.1 Minimal Setup

server {
    listen 443 ssl;
    server_name example.com;

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

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

2.2 HTTP to HTTPS Redirect

server {
    listen 80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

2.3 Complete Production Configuration

# /etc/nginx/conf.d/ssl-params.conf
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:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

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

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;

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$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;

    include /etc/nginx/conf.d/ssl-params.conf;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

3. TLS Version Configuration

3.1 TLS Version Security Status

VersionStatusRecommendation
SSL 2.0Deprecated (1996)Never use
SSL 3.0Deprecated (POODLE)Never use
TLS 1.0Deprecated (2021)Disable
TLS 1.1Deprecated (2021)Disable
TLS 1.2Current standardUse
TLS 1.3Latest standardUse (recommended)

3.2 Allow Only TLS 1.2/1.3

ssl_protocols TLSv1.2 TLSv1.3;

3.3 TLS 1.3 Only (Modern)

ssl_protocols TLSv1.3;

TLS 1.3 advantages:

  • Reduced handshake round trips (1-RTT, 0-RTT)
  • Removed vulnerable legacy cipher suites
  • Forward Secrecy enabled by default
  • Encrypted handshake (including server certificate)

4. Cipher Suite Configuration

4.1 Mozilla SSL Configuration Generator Profiles

Mozilla provides three configuration profiles.

Modern (TLS 1.3 only):

ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;

Intermediate (TLS 1.2 + 1.3, recommended):

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:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

4.2 Cipher Suite Name Breakdown

ECDHE-RSA-AES256-GCM-SHA384 meaning:

ECDHE  = Key exchange (Elliptic Curve Diffie-Hellman Ephemeral)
         -> Provides Forward Secrecy
RSA    = Authentication algorithm (server certificate signing)
AES256 = Symmetric encryption (256-bit AES)
GCM    = Encryption mode (Galois/Counter Mode, AEAD)
SHA384 = Hash function (integrity verification)

5. DH Parameters

Parameters used for Diffie-Hellman key exchange. Required when using DHE cipher suites.

# Generate DH parameters (2048-bit minimum recommended)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

# Stronger 4096-bit (takes longer to generate)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

Note: TLS 1.3 uses only ECDHE instead of DHE, so DH parameters are not needed. Removing DHE cipher suites from TLS 1.2 also eliminates the need.


6. HSTS (HTTP Strict Transport Security)

6.1 What Is HSTS

HSTS is an HTTP header that instructs browsers to always connect to the domain using HTTPS.

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

6.2 HSTS Preload List

Registering in the HSTS Preload List forces HTTPS from the very first browser visit.

Requirements:

  • Valid TLS certificate
  • Redirect from port 80 to 443
  • HTTPS applied to all subdomains
  • HSTS header with min max-age of 31536000 (1 year), includeSubDomains, preload

Registration site: hstspreload.org

6.3 Caution

  • HSTS is hard to undo (browser blocks HTTP for max-age duration)
  • Start with a small max-age (e.g., 300 seconds) and gradually increase
  • Verify all subdomains support HTTPS before adding includeSubDomains

7. OCSP Stapling

7.1 What Is OCSP Stapling

OCSP (Online Certificate Status Protocol) checks whether a certificate has been revoked. OCSP Stapling has the server pre-fetch the OCSP response from the CA and deliver it to the client during the TLS handshake.

Without OCSP Stapling:
Client -> Server (TLS handshake)
Client -> CA OCSP Server (certificate validity check) <- additional delay

With OCSP Stapling:
Server -> CA OCSP Server (periodically refreshes OCSP response)
Client -> Server (TLS handshake + OCSP response included) <- faster

7.2 Configuration

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;

7.3 Verify OCSP Stapling

openssl s_client -connect example.com:443 -servername example.com -status < /dev/null 2>/dev/null | grep -A 20 "OCSP Response"

8. SSL Session Caching

# Shared memory cache (shared across worker processes)
# 10MB cache = approximately 40,000 sessions
ssl_session_cache shared:SSL:10m;

ssl_session_timeout 1d;

# Disable session tickets (to ensure Forward Secrecy)
ssl_session_tickets off;

Why disable Session Tickets: The ticket encryption key controls the security of all sessions. If key rotation is not properly managed, Forward Secrecy breaks. TLS 1.3 offers 0-RTT reconnection as an alternative.


9. HTTP/2 Configuration

9.1 Enable HTTP/2

server {
    listen 443 ssl http2;
    server_name example.com;
    # ... SSL config ...
}

HTTP/2 advantages:

  • Multiplexing: Handle multiple requests over a single TCP connection
  • Header compression: HPACK compresses HTTP headers
  • Server push: Send resources before client requests them
  • Priority: Set per-resource transmission priority

9.2 HTTP/3 (QUIC) Preparation

# Nginx 1.25.0+ (or nginx-quic)
server {
    listen 443 ssl;
    listen 443 quic reuseport;
    http2 on;
    http3 on;

    server_name example.com;

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

    add_header Alt-Svc 'h3=":443"; ma=86400' always;
}

10. mTLS (Mutual TLS) Configuration

10.1 What Is mTLS

Regular TLS has only the server present a certificate, but mTLS requires the client to also present a certificate for mutual authentication.

Regular TLS:
Client ---- verify server cert ----> Server

Mutual TLS:
Client <--- verify server cert ----> Server
Client ---- client certificate ----> Server (server verifies client cert)

Use cases:

  • Inter-microservice communication (service mesh)
  • API authentication (API Gateway)
  • IoT device authentication
  • Internal admin tool access control

10.2 Issuing Client Certificates with CA

# 1. Generate CA private key
openssl genrsa -out ca.key 4096

# 2. Generate CA certificate (self-signed)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/C=US/ST=California/O=MyOrg/CN=Internal CA"

# 3. Generate client private key
openssl genrsa -out client.key 2048

# 4. Generate client CSR
openssl req -new -key client.key -out client.csr \
  -subj "/C=US/ST=California/O=MyOrg/CN=service-a"

# 5. Sign client certificate with CA
openssl x509 -req -days 365 -in client.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt

# 6. Create PFX/PKCS12 bundle (for browser import)
openssl pkcs12 -export -out client.pfx \
  -inkey client.key -in client.crt -certfile ca.crt

10.3 Nginx mTLS Configuration

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/server.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/server.privkey.pem;

    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    location / {
        proxy_pass http://backend;
        proxy_set_header X-SSL-Client-Cert $ssl_client_cert;
        proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
        proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
        proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
    }
}

10.4 ssl_verify_client Options

# on: Client certificate required (400 error without one)
ssl_verify_client on;

# optional: Certificate optional (access allowed without, verified if present)
ssl_verify_client optional;

# optional_no_ca: No verification, just forward (backend verifies)
ssl_verify_client optional_no_ca;

10.5 mTLS Testing

# Request with client certificate
curl --cert client.crt --key client.key \
  --cacert ca.crt \
  https://api.example.com/data

# Request without certificate (fails if ssl_verify_client on)
curl --cacert ca.crt https://api.example.com/data
# 400 Bad Request - No required SSL certificate was sent

11. Self-Signed Certificates (Development)

11.1 Single Domain

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -days 365 \
  -subj "/C=US/ST=California/O=Dev/CN=localhost"

11.2 With SAN (Subject Alternative Name)

cat > san.cnf << 'SANEOF'
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C = US
ST = California
O = Dev
CN = localhost

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = myapp.local
DNS.3 = *.myapp.local
IP.1 = 127.0.0.1
IP.2 = 192.168.1.100
SANEOF

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -days 365 \
  -config san.cnf \
  -extensions v3_req

12. Certificate Conversion

12.1 PEM to DER

openssl x509 -in cert.pem -outform DER -out cert.der
openssl rsa -in key.pem -outform DER -out key.der

12.2 DER to PEM

openssl x509 -in cert.der -inform DER -outform PEM -out cert.pem

12.3 PFX/PKCS12 to PEM

openssl pkcs12 -in certificate.pfx -clcerts -nokeys -out cert.pem
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out key.pem
openssl pkcs12 -in certificate.pfx -cacerts -nokeys -out chain.pem

12.4 PEM to PFX/PKCS12

openssl pkcs12 -export \
  -out certificate.pfx \
  -inkey key.pem \
  -in cert.pem \
  -certfile chain.pem

13. Certificate Verification Commands

13.1 Check Certificate Contents

openssl x509 -in cert.pem -text -noout
openssl x509 -in cert.pem -issuer -noout
openssl x509 -in cert.pem -subject -noout
openssl x509 -in cert.pem -enddate -noout
openssl x509 -in cert.pem -noout -ext subjectAltName

13.2 Check Remote Server Certificate

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  openssl x509 -text -noout

# Check certificate chain
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null 2>/dev/null

# Check TLS version and cipher
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  grep -E "Protocol|Cipher"

13.3 Verify Certificate-Key Matching

# Compare modulus of certificate and key (must match)
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in key.pem | openssl md5

14. Security Audit Tools

14.1 SSL Labs

Online TLS configuration audit service.

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

Grade criteria:
A+  = Excellent (HSTS + strong configuration)
A   = Good
B   = Fair (improvements recommended)
C   = Poor (security risk)
F   = Dangerous (immediate action required)

14.2 testssl.sh

Open-source command-line TLS audit tool.

git clone --depth 1 https://github.com/drwetter/testssl.sh.git

./testssl.sh/testssl.sh example.com

./testssl.sh/testssl.sh --protocols example.com
./testssl.sh/testssl.sh --ciphers example.com
./testssl.sh/testssl.sh --headers example.com
./testssl.sh/testssl.sh --vulnerabilities example.com

14.3 Nginx Configuration Validation

sudo nginx -t
sudo nginx -T

# Gixy (Nginx security analyzer)
pip install gixy
gixy /etc/nginx/nginx.conf

15. Let's Encrypt Auto-Renewal Integration

15.1 Certbot + Nginx Reload Hook

sudo certbot renew --deploy-hook "systemctl reload nginx"

15.2 Auto-Renewal in Docker Compose

version: '3.8'
services:
  nginx:
    image: nginx:latest
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    restart: unless-stopped
    command: '/bin/sh -c ''while :; do sleep 6h & wait; nginx -s reload; done & nginx -g "daemon off;"'''

  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait; done'"

16. Configuration Checklist

Items to verify before production deployment:

ItemCommand / CheckStandard
TLS versionssl_protocolsTLSv1.2 TLSv1.3 only
Certificate chainUse fullchain.pemIncludes intermediate CA
HSTSCheck headermax-age 1 year+
OCSP Staplingopenssl s_client -statusCert Status: good
HTTP redirectcurl -I http://domain301 to HTTPS
SSL Labs gradessllabs.comTarget A+
Private key permissionsls -la privkey.pem600 (root only)
Auto renewalcertbot renew --dry-runSuccess
DH parameters2048-bit minimumGenerated
Session TicketsoffForward Secrecy

17. Conclusion

Nginx TLS configuration goes beyond simply applying certificates -- it requires comprehensive security optimization. Key takeaways:

  • Use fullchain.pem + privkey.pem to complete the certificate chain
  • Allow only TLS 1.2/1.3 and disable legacy protocols
  • Use Mozilla Intermediate profile cipher suites
  • Enforce HTTPS with HSTS, improve performance with OCSP Stapling
  • Reduce handshake overhead with SSL session caching
  • Optimize web performance with HTTP/2
  • Implement mutual authentication between microservices with mTLS
  • Target A+ grade on SSL Labs
  • Always configure Let's Encrypt certificate auto-renewal