x.509 인증서 실습 - OpenSSL 완벽 가이드

2024년 12월 17일

dev-common

# OpenSSL# x.509# 인증서# SSL# TLS# 보안# 암호화

개요

이 글에서는 OpenSSL을 사용하여 실제로 인증서를 생성하고 관리하는 방법을 다룹니다.

사전 지식: x.509 인증서와 PKI - 구조와 원리를 먼저 읽으시면 좋습니다.


OpenSSL 설치 확인

설치 여부 확인

openssl version
# OpenSSL 3.0.11 19 Sep 2023 (Library: OpenSSL 3.0.11 19 Sep 2023)

설치

# macOS
brew install openssl

# Ubuntu/Debian
sudo apt-get install openssl

# CentOS/RHEL
sudo yum install openssl

Private Key 생성

RSA 방식

# 2048-bit RSA 개인키 생성
openssl genrsa -out server.key 2048

# 4096-bit (더 안전, 성능 느림)
openssl genrsa -out server.key 4096

# 비밀번호로 보호된 개인키
openssl genrsa -aes256 -out server.key 2048
# Enter pass phrase: ****

ECDSA 방식 (타원곡선)

# secp256r1 (NIST P-256) 곡선 사용
openssl ecparam -genkey -name prime256v1 -out server.key

# 사용 가능한 곡선 목록 확인
openssl ecparam -list_curves

RSA vs ECDSA:

구분 RSA 2048-bit ECDSA P-256
보안 수준 112-bit 128-bit
키 크기 2048-bit 256-bit
서명 속도 느림 빠름
검증 속도 빠름 느림
호환성 매우 높음 중간

개인키 확인

# RSA 개인키 정보 출력
openssl rsa -in server.key -text -noout

# ECDSA 개인키 정보 출력
openssl ec -in server.key -text -noout

CSR (Certificate Signing Request) 생성

CSR은 CA에게 인증서 발급을 요청할 때 사용합니다.

대화형 생성

openssl req -new -key server.key -out server.csr

# 입력 항목:
# Country Name (2 letter code) [XX]:KR
# State or Province Name (full name) []:Seoul
# Locality Name (eg, city) []:Gangnam
# Organization Name (eg, company) []:Example Corp
# Organizational Unit Name (eg, section) []:IT Department
# Common Name (eg, YOUR name) []:example.com
# Email Address []:admin@example.com

설정 파일로 생성

csr.conf 파일 생성:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C = KR
ST = Seoul
L = Gangnam
O = Example Corp
OU = IT Department
CN = example.com
emailAddress = admin@example.com

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = api.example.com

CSR 생성:

openssl req -new -key server.key -out server.csr -config csr.conf

CSR 확인

openssl req -in server.csr -text -noout

주요 확인 사항:

- Subject: CN, O, C 등이 올바른지
- Public Key: 알고리즘과 키 크기
- Subject Alternative Name: 다중 도메인 확인

Self-Signed 인증서 생성

개발/테스트 환경에서 사용하는 자체 서명 인증서입니다.

기본 방법

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

기존 개인키로 생성

# 이미 있는 개인키 사용
openssl req -x509 -new -key server.key \
  -out selfsigned.crt -days 365 \
  -subj "/C=KR/ST=Seoul/O=Example/CN=localhost"

SAN 포함 (다중 도메인)

selfsigned.conf:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = v3_ca

[dn]
CN = localhost

[v3_ca]
subjectAltName = @alt_names
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[alt_names]
DNS.1 = localhost
DNS.2 = *.localhost
DNS.3 = 127.0.0.1
IP.1 = 127.0.0.1
IP.2 = ::1

생성:

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -days 365 \
  -config selfsigned.conf

Root CA 만들기

실제 Certificate Chain을 구성해봅니다.

1. Root CA 개인키 생성

# 4096-bit (Root CA는 더 강력한 키 사용)
openssl genrsa -aes256 -out rootCA.key 4096
# Enter pass phrase: **** (안전하게 보관!)

2. Root CA 인증서 생성

rootCA.conf:

[req]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = v3_ca

[dn]
C = KR
O = Example Root CA
CN = Example Root CA

[v3_ca]
basicConstraints = critical, CA:TRUE
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
subjectKeyIdentifier = hash

생성 (10년 유효):

openssl req -x509 -new -key rootCA.key \
  -out rootCA.crt -days 3650 \
  -config rootCA.conf
# Enter pass phrase for rootCA.key: ****

3. Root CA 확인

openssl x509 -in rootCA.crt -text -noout

확인 사항:

- Issuer == Subject (자체 서명)
- X509v3 Basic Constraints: CA:TRUE
- X509v3 Key Usage: Certificate Sign, CRL Sign
- Validity: 10년

Intermediate CA 만들기

1. Intermediate CA 개인키 생성

openssl genrsa -aes256 -out intermediateCA.key 4096

2. Intermediate CA CSR 생성

intermediateCA.conf:

[req]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_intermediate_ca

[dn]
C = KR
O = Example Intermediate CA
CN = Example Intermediate CA

[v3_intermediate_ca]
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

생성:

openssl req -new -key intermediateCA.key \
  -out intermediateCA.csr \
  -config intermediateCA.conf

3. Root CA로 Intermediate CA 서명

openssl x509 -req -in intermediateCA.csr \
  -CA rootCA.crt -CAkey rootCA.key \
  -CAcreateserial \
  -out intermediateCA.crt \
  -days 1825 \
  -sha256 \
  -extfile intermediateCA.conf \
  -extensions v3_intermediate_ca
# Enter pass phrase for rootCA.key: ****

4. Intermediate CA 확인

openssl x509 -in intermediateCA.crt -text -noout

확인 사항:

- Issuer: CN=Example Root CA (Root CA가 서명)
- Subject: CN=Example Intermediate CA
- X509v3 Basic Constraints: CA:TRUE, pathlen:0
  → pathlen:0 = 이 아래 더 이상 하위 CA 불가

End-entity 인증서 발급

실제 서버가 사용할 인증서를 Intermediate CA로 발급합니다.

1. 서버 개인키 생성

openssl genrsa -out server.key 2048

2. 서버 CSR 생성

server.conf:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C = KR
O = Example Corp
CN = example.com

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = api.example.com

생성:

openssl req -new -key server.key \
  -out server.csr \
  -config server.conf

3. Intermediate CA로 서명

server_cert.conf:

[v3_end]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = api.example.com

서명:

openssl x509 -req -in server.csr \
  -CA intermediateCA.crt -CAkey intermediateCA.key \
  -CAcreateserial \
  -out server.crt \
  -days 365 \
  -sha256 \
  -extfile server_cert.conf \
  -extensions v3_end
# Enter pass phrase for intermediateCA.key: ****

Certificate Chain 구성

Chain 파일 생성

서버에서 사용할 전체 체인 파일을 만듭니다:

# 순서 중요: 서버 인증서 → Intermediate CA → Root CA
cat server.crt intermediateCA.crt rootCA.crt > fullchain.crt

또는 Intermediate만 포함 (일반적):

# Root CA는 클라이언트가 이미 가지고 있다고 가정
cat server.crt intermediateCA.crt > chain.crt

Nginx 설정 예시

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /path/to/chain.crt;       # 전체 체인
    ssl_certificate_key /path/to/server.key;  # 개인키

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
}

Apache 설정 예시

<VirtualHost *:443>
    ServerName example.com

    SSLEngine on
    SSLCertificateFile /path/to/server.crt
    SSLCertificateKeyFile /path/to/server.key
    SSLCertificateChainFile /path/to/intermediateCA.crt
</VirtualHost>

인증서 검증

형식 확인

# PEM 형식 확인
openssl x509 -in server.crt -text -noout

# 시작/종료 날짜만 확인
openssl x509 -in server.crt -noout -dates
# notBefore=Jan  1 00:00:00 2024 GMT
# notAfter=Jan  1 00:00:00 2025 GMT

# Subject와 Issuer 확인
openssl x509 -in server.crt -noout -subject -issuer

개인키와 인증서 일치 확인

# 개인키의 modulus (MD5 해시)
openssl rsa -noout -modulus -in server.key | openssl md5
# (stdin)= a1b2c3d4...

# 인증서의 modulus
openssl x509 -noout -modulus -in server.crt | openssl md5
# (stdin)= a1b2c3d4...

# 두 값이 같으면 → 개인키와 인증서가 쌍임

CSR과 인증서 일치 확인

# CSR의 modulus
openssl req -noout -modulus -in server.csr | openssl md5

# 인증서의 modulus
openssl x509 -noout -modulus -in server.crt | openssl md5

# 같으면 → CSR로 만든 인증서가 맞음

Certificate Chain 검증

# Intermediate CA가 Root CA로 서명되었는지 확인
openssl verify -CAfile rootCA.crt intermediateCA.crt
# intermediateCA.crt: OK

# 서버 인증서가 전체 체인으로 유효한지 확인
openssl verify -CAfile rootCA.crt -untrusted intermediateCA.crt server.crt
# server.crt: OK

옵션 설명:

  • -CAfile: 신뢰하는 Root CA
  • -untrusted: 중간 인증서 체인 (신뢰는 안 하지만 검증에 사용)

SSL/TLS 연결 테스트

# 특정 서버의 인증서 확인
openssl s_client -connect example.com:443 -showcerts

# SNI (Server Name Indication) 지정
openssl s_client -connect example.com:443 -servername example.com

# 특정 Root CA로 검증
openssl s_client -connect example.com:443 -CAfile rootCA.crt

출력에서 확인 사항:

- Verify return code: 0 (ok) → 검증 성공
- Verify return code: 20 (unable to get local issuer certificate) → Root CA 없음
- Verify return code: 10 (certificate has expired) → 만료됨

인증서 포맷 변환

PEM과 DER

# PEM → DER (바이너리)
openssl x509 -in server.crt -outform DER -out server.der

# DER → PEM (텍스트)
openssl x509 -in server.der -inform DER -out server.crt

포맷 설명:

  • PEM: Base64 인코딩, -----BEGIN CERTIFICATE----- 헤더
  • DER: 바이너리 형식, Java 등에서 사용

PKCS#12 (PFX)

개인키 + 인증서 + 체인을 하나의 파일로:

# PEM → PKCS#12
openssl pkcs12 -export \
  -in server.crt \
  -inkey server.key \
  -certfile intermediateCA.crt \
  -out server.p12 \
  -name "My Server Cert"
# Enter Export Password: ****

# PKCS#12 → PEM
openssl pkcs12 -in server.p12 -out server.pem -nodes
# Enter Import Password: ****

용도: Windows IIS, Java Keystore 등에서 사용


인증서 정보 추출

Subject Alternative Names

openssl x509 -in server.crt -text -noout | grep -A 1 "Subject Alternative Name"
# X509v3 Subject Alternative Name:
#     DNS:example.com, DNS:www.example.com

공개키 추출

# 인증서에서 공개키만 추출
openssl x509 -in server.crt -pubkey -noout > public.key

# 개인키에서 공개키 추출
openssl rsa -in server.key -pubout -out public.key

Fingerprint (지문)

# SHA-256 fingerprint
openssl x509 -in server.crt -noout -fingerprint -sha256
# SHA256 Fingerprint=A1:B2:C3:D4:...

# SHA-1 fingerprint
openssl x509 -in server.crt -noout -fingerprint -sha1

용도: 인증서 고유 식별, Pin 검증 등


인증서 폐기 (CRL)

CRL 생성

crl.conf:

[ca]
default_ca = CA_default

[CA_default]
database = index.txt
crlnumber = crlnumber
default_crl_days = 30
default_md = sha256

필요 파일 생성:

# 폐기 인증서 데이터베이스
touch index.txt

# CRL 일련번호
echo "1000" > crlnumber

인증서 폐기:

# 인증서 폐기 등록
openssl ca -revoke server.crt \
  -keyfile intermediateCA.key \
  -cert intermediateCA.crt \
  -config crl.conf

# CRL 생성
openssl ca -gencrl \
  -keyfile intermediateCA.key \
  -cert intermediateCA.crt \
  -out crl.pem \
  -config crl.conf

CRL 확인

openssl crl -in crl.pem -text -noout

CRL로 검증

openssl verify -CAfile rootCA.crt \
  -untrusted intermediateCA.crt \
  -crl_check -CRLfile crl.pem \
  server.crt

실전 시나리오

시나리오 1: 로컬 개발 환경 HTTPS

# 1. 개인키 생성
openssl genrsa -out local.key 2048

# 2. 자체 서명 인증서 (localhost + 127.0.0.1)
cat > local.conf <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = v3_req

[dn]
CN = localhost

[v3_req]
subjectAltName = @alt_names
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment

[alt_names]
DNS.1 = localhost
DNS.2 = *.localhost
IP.1 = 127.0.0.1
IP.2 = ::1
EOF

openssl req -x509 -new -key local.key \
  -out local.crt -days 365 -config local.conf

# 3. macOS 키체인에 등록 (신뢰 설정)
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain local.crt

시나리오 2: 내부망 Private CA 구축

# 1. Root CA 생성
openssl genrsa -aes256 -out internal-rootCA.key 4096
openssl req -x509 -new -key internal-rootCA.key \
  -out internal-rootCA.crt -days 7300 \
  -subj "/C=KR/O=Internal Root CA/CN=Internal Root CA"

# 2. Intermediate CA 생성
openssl genrsa -aes256 -out internal-intermediateCA.key 4096
openssl req -new -key internal-intermediateCA.key \
  -out internal-intermediateCA.csr \
  -subj "/C=KR/O=Internal Intermediate CA/CN=Internal Intermediate CA"

openssl x509 -req -in internal-intermediateCA.csr \
  -CA internal-rootCA.crt -CAkey internal-rootCA.key \
  -CAcreateserial -out internal-intermediateCA.crt \
  -days 3650 -sha256

# 3. 서버 인증서 발급 (자동화 스크립트)
cat > issue-cert.sh <<'EOF'
#!/bin/bash
DOMAIN=$1
openssl genrsa -out ${DOMAIN}.key 2048
openssl req -new -key ${DOMAIN}.key -out ${DOMAIN}.csr \
  -subj "/C=KR/O=Internal/CN=${DOMAIN}"
openssl x509 -req -in ${DOMAIN}.csr \
  -CA internal-intermediateCA.crt \
  -CAkey internal-intermediateCA.key \
  -CAcreateserial -out ${DOMAIN}.crt \
  -days 365 -sha256
cat ${DOMAIN}.crt internal-intermediateCA.crt > ${DOMAIN}-fullchain.crt
EOF

chmod +x issue-cert.sh
./issue-cert.sh internal.example.com

시나리오 3: 인증서 갱신

# 기존 개인키는 유지하고 인증서만 갱신
openssl req -new -key server.key -out server-renewal.csr

# CA에서 서명 (동일한 과정)
openssl x509 -req -in server-renewal.csr \
  -CA intermediateCA.crt -CAkey intermediateCA.key \
  -CAcreateserial -out server-new.crt \
  -days 365 -sha256

# 무중단 배포: 새 인증서 적용 후 Nginx reload
sudo cp server-new.crt /etc/nginx/certs/server.crt
sudo nginx -s reload

보안 권장사항

개인키 관리

# 개인키 권한 설정 (소유자만 읽기)
chmod 400 server.key
chown root:root server.key

# 비밀번호 보호된 개인키 사용
openssl genrsa -aes256 -out server.key 2048

# 비밀번호 제거 (자동화 필요 시)
openssl rsa -in server.key -out server-nopass.key

강력한 알고리즘 사용

# SHA-256 이상 사용 (SHA-1은 deprecated)
-sha256 또는 -sha384

# RSA는 최소 2048-bit (권장 4096-bit for CA)
-newkey rsa:2048

# ECDSA는 P-256 이상
-newkey ec:<(openssl ecparam -name prime256v1)

인증서 유효 기간

- Root CA: 10-20년
- Intermediate CA: 5-10년
- End-entity: 1년 (Let's Encrypt는 90일)
- 짧을수록 안전 (손상 시 피해 최소화)

트러블슈팅

“unable to get local issuer certificate”

# 원인: Root CA를 찾을 수 없음
# 해결: -CAfile로 Root CA 지정
openssl verify -CAfile rootCA.crt server.crt

“certificate has expired”

# 확인
openssl x509 -in server.crt -noout -dates

# 해결: 인증서 갱신

“certificate signature failure”

# 원인: 서명이 유효하지 않음 (위조 또는 체인 손상)
# 확인
openssl verify -CAfile rootCA.crt -untrusted intermediateCA.crt server.crt

“Hostname mismatch”

# 원인: 인증서 CN/SAN에 호스트명이 없음
# 확인
openssl x509 -in server.crt -text | grep -A 1 "Subject Alternative Name"

# 해결: SAN에 도메인 추가하여 재발급

Private key와 Certificate 불일치

# MD5 해시 비교
openssl rsa -noout -modulus -in server.key | openssl md5
openssl x509 -noout -modulus -in server.crt | openssl md5
# 다르면 → 쌍이 아님, 다시 생성 필요

유용한 One-liner

# 인증서 만료일까지 남은 일수
echo | openssl s_client -connect example.com:443 2>/dev/null | \
  openssl x509 -noout -enddate

# 여러 서버 인증서 만료일 확인
for host in example.com google.com github.com; do
  echo -n "$host: "
  echo | openssl s_client -connect $host:443 2>/dev/null | \
    openssl x509 -noout -dates | grep notAfter
done

# PEM 인증서를 한 줄로 (JWT 등에서 사용)
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' server.crt

# 인증서 체인에서 각 인증서 Subject 출력
openssl crl2pkcs7 -nocrl -certfile fullchain.crt | \
  openssl pkcs7 -print_certs -noout

정리

개인키 생성

# RSA
openssl genrsa -out server.key 2048

# ECDSA
openssl ecparam -genkey -name prime256v1 -out server.key

인증서 발급 흐름

1. Private Key 생성
2. CSR 생성 (개인키로)
3. CA에 CSR 제출
4. CA가 서명하여 Certificate 발급
5. Certificate + Intermediate CA → Chain 파일 구성

검증

# 형식 확인
openssl x509 -in server.crt -text -noout

# 체인 검증
openssl verify -CAfile rootCA.crt -untrusted intermediateCA.crt server.crt

# 서버 연결 테스트
openssl s_client -connect example.com:443

Self-Signed vs CA-Signed

구분 Self-Signed CA-Signed
용도 개발/테스트 프로덕션
신뢰 체인 없음 Root CA까지
브라우저 경고 있음 없음 (신뢰된 CA)
비용 무료 유료 (또는 Let’s Encrypt)

다음 단계

  • Let’s Encrypt: 무료 자동 갱신 인증서 (Certbot 사용)
  • ACME 프로토콜: 인증서 자동화 표준
  • HSM (Hardware Security Module): CA 개인키 안전 보관
  • Certificate Transparency: 인증서 발급 투명성 로그

참고 자료

© 2025, 미나리와 함께 만들었음