14분 읽기

OpenSSL로 TLS 인증서 만들기

TLS 설정을 보다 보면 server.key, server.crt, ca.crt, tls.crt, fullchain.pem처럼 비슷한 파일이 한꺼번에 나온다. 처음에는 “.crt가 인증서고 .key가 키겠지” 정도로 외우게 되지만, 서버 간 통신에서 어느 파일을 어디에 넣어야 하는지까지 이해하려면 발급 과정을 한 번 따라가 보는 편이 빠르다.

이 글은 RSA 키와 OpenSSL 3.x를 기준으로 다음 흐름을 정리한다.

개인키 생성 → CSR 생성 → CA가 서명 → 서버 인증서 발급 → TLS 연결에서 검증

먼저 가장 중요한 구분부터 잡자. 인증서는 공개 정보이고, 개인키는 절대 공개하면 안 되는 Secret이다.

인증서 파일 이름부터 구분하기

확장자만 보고 파일 내용을 단정하면 헷갈린다. OpenSSL은 PEM, DER, PKCS#12 같은 형식을 읽고 쓴다. 이 중 PEM은 Base64 텍스트에 -----BEGIN ...----------END ...----- 줄을 붙인 표현이다. 하나의 PEM 파일에는 여러 인증서도 들어갈 수 있다. OpenSSL 파일 형식 문서를 보면 PEM·DER·P12의 차이를 확인할 수 있다.

자주 보는 이름보통 담긴 내용민감성어디에 두나
server.key, tls.key서버 RSA 개인키O서버의 Kubernetes Secret 또는 Secret Manager
server.csr공개키, 서버 이름(SAN), 요청자 서명이 담긴 인증서 발급 요청XCA에 전달
server.crt, cert.pem, tls.crt서버 공개키와 SAN, 유효기간 등을 CA가 서명한 서버 인증서XTLS 서버
ca.crt신뢰할 루트 CA 또는 중간 CA 인증서X클라이언트와 서버의 신뢰할 CA 목록
fullchain.pem서버 인증서 + 중간 CA 인증서XTLS 서버
.p12, .pfx개인키와 인증서 등을 비밀번호로 묶은 PKCS#12 컨테이너개인키 포함 시 OJava keystore, 운영 도구 등

tls.crttls.key는 Kubernetes kubernetes.io/tls Secret에서 흔히 쓰는 key 이름이다. tls라는 별도 인증서 형식이 있는 것은 아니다. 마찬가지로 server.crt가 서버 인증서 하나인지, 중간 CA까지 이어 붙인 체인인지는 확장자가 아니라 파일 내용을 봐야 안다.

# PEM 인증서의 소유자, 발급자, SAN, 유효기간 확인
openssl x509 -in server.crt -noout -text

# 개인키인지 확인
openssl pkey -in server.key -noout -text

인증서는 공개키와 서버 정보를 담는다

서버 인증서에는 대략 아래 정보가 들어간다.

Subject: api.internal.example
Subject Alternative Name: DNS:api.internal.example, DNS:api
Public Key: 서버 공개키
Validity: 이 날짜부터 이 날짜까지
Issuer: 이 인증서를 발급한 CA
Signature: CA 개인키로 만든 서명

클라이언트는 server.crt만 보고 곧바로 믿지 않는다. 자신이 신뢰하는 ca.crt의 공개키로 인증서의 CA 서명을 검증한다. 이어서 인증서 체인이 Root CA까지 이어지는지, 유효기간이 맞는지, 연결한 호스트 이름이 SAN에 들어 있는지 확인한다.

CN에 도메인을 넣는 예전 방식만으로는 부족하다. 현재 클라이언트는 서버 이름을 인증서의 subjectAltName과 비교해야 하며, IP 주소로 연결한다면 iPAddress SAN이 필요하다. RFC 6125에 이 검증 규칙이 정리되어 있다.

CA 개인키와 서버 개인키에서 CSR과 서버 인증서가 생성되고, TLS 연결에서 서버는 개인키와 fullchain을, 클라이언트는 CA 인증서를 사용하는 흐름
개인키는 서버가 인증서의 실제 소유자임을 증명할 때 쓰고, 인증서는 공개키와 서버 이름을 전달한다. HTTP 데이터는 핸드셰이크 뒤 만들어진 대칭 세션 키로 보호된다.

내부 CA와 서버 인증서 만들기

아래 예시는 개발·사내망에서 쓸 수 있는 작은 내부 CA를 만드는 흐름이다. 공개 인터넷에서 브라우저가 신뢰하는 인증서가 필요하다면 Let’s Encrypt 같은 공인 CA에 CSR을 제출한다. 어느 경우든 개인키는 발급 기관에 보내지 않는다.

1. CA 개인키와 CA 인증서 만들기

openssl genpkey \
  -algorithm RSA \
  -pkeyopt rsa_keygen_bits:4096 \
  -out ca.key

openssl req -x509 -new \
  -key ca.key \
  -sha256 \
  -days 3650 \
  -out ca.crt \
  -subj "/CN=Joon Internal Root CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

openssl genpkey는 개인키 또는 키 쌍을 생성한다. -algorithm RSArsa_keygen_bits로 RSA 키 크기를 정한다. OpenSSL genpkey 문서를 참고할 수 있다.

이 예시에서 ca.crt는 자기 자신이 서명한 Root CA 인증서다. CA:TRUEkeyCertSign은 이 인증서가 다른 인증서를 발급할 수 있음을 나타낸다. 반면 ca.key는 실제로 다른 인증서에 서명할 수 있는 키이므로, 애플리케이션 서버에 넣거나 Git에 올리면 안 된다.

2. 서버 개인키 만들기

openssl genpkey \
  -algorithm RSA \
  -pkeyopt rsa_keygen_bits:3072 \
  -out server.key

server.keyapi.internal.example 서버가 보관할 개인키다. Kubernetes에서 실행한다면 일반 ConfigMap이 아니라 Kubernetes Secret에 넣고, 애플리케이션 또는 Ingress에 필요한 범위로만 마운트한다.

3. CSR 만들기

openssl req -new \
  -key server.key \
  -out server.csr \
  -subj "/CN=api.internal.example" \
  -addext "subjectAltName=DNS:api.internal.example,DNS:api"

CSR(Certificate Signing Request)은 다음을 담은 요청서다.

  • 서버 공개키
  • 인증서에 넣고 싶은 서버 이름(SAN)
  • 이 요청이 해당 개인키 소유자에게서 왔음을 보이는 서명

CA는 CSR을 받아 공개키와 SAN을 확인하고, 정책에 맞으면 자신의 개인키로 인증서에 서명한다. openssl req는 PKCS#10 CSR을 생성·처리하며, -x509를 사용하면 스스로 서명한 인증서도 만들 수 있다. OpenSSL req 문서

4. 서버 인증서에 들어갈 확장 정보 준비하기

server-ext.cnf 파일을 만든다.

[v3_server]
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:api.internal.example,DNS:api
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
  • CA:FALSE: 이 인증서는 다른 인증서를 발급하지 못한다.
  • serverAuth: 서버 인증용 인증서임을 나타낸다.
  • subjectAltName: 클라이언트가 실제로 접속할 DNS 이름 또는 IP 주소다.

CSR에 SAN을 넣었다고 해서 서명 도구가 자동으로 인증서에 복사한다고 기대하면 안 된다. OpenSSL의 x509 명령은 확장을 기본적으로 복사하지 않으므로, 발급 과정에서 허용할 확장을 명시하는 편이 안전하다. OpenSSL x509 문서

5. CA로 CSR에 서명해 server.crt 만들기

openssl x509 -req \
  -in server.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out server.crt \
  -days 90 \
  -sha256 \
  -extfile server-ext.cnf \
  -extensions v3_server

이 명령이 실제 발급 단계다.

  1. CSR에서 서버 공개키와 인증서 소유자 정보를 읽는다.
  2. server-ext.cnf에서 SAN과 서버 용도를 넣는다.
  3. ca.crt의 subject를 새 인증서의 issuer로 기록한다.
  4. ca.key로 인증서 전체에 서명한다.
  5. 결과로 server.crt를 만든다.

-CAcreateserial은 CA의 다음 인증서 일련번호를 관리할 ca.srl 파일을 만든다. 실서비스에서 다수의 인증서를 발급할 때는 이 간단한 명령 조합 대신 cert-manager, Vault PKI, step-ca 같은 PKI 도구로 발급·갱신·폐기를 관리하는 편이 안전하다.

server.crtfullchain.pem 구분하기

공인 CA는 보통 Root CA 아래에 Intermediate CA(중간 CA)를 두고 서버 인증서를 발급한다.

Root CA (클라이언트 trust store에 있음)
  └─ Intermediate CA
       └─ server.crt (api.internal.example)

TLS 서버는 보통 서버 인증서와 중간 CA 인증서를 함께 보낸다. 이를 fullchain.pem이라고 부른다.

cat server.crt intermediate-ca.crt > fullchain.pem

순서는 서버 인증서 먼저, 중간 CA 인증서 다음이다. Root CA는 클라이언트가 이미 신뢰할 CA 목록에 가지고 있으므로 보통 fullchain.pem에 넣지 않는다. PEM 파일은 인증서를 여러 개 담을 수 있지만 DER 파일은 하나의 객체만 담을 수 있다. OpenSSL 인증서 체인 처리 문서

TLS 연결에서 각 파일은 어떻게 쓰일까?

일반 TLS에서는 클라이언트가 서버를 확인한다

API 서버
  server.key          개인키
  fullchain.pem       서버 인증서 + 중간 CA 인증서

호출하는 클라이언트 서버
  ca.crt              신뢰할 Root CA 인증서

TLS 1.3에서 서버는 인증서 체인을 보내고, server.key로 핸드셰이크 전체에 대한 CertificateVerify 서명을 만든다. 클라이언트는 ca.crt를 기준으로 인증서 체인과 SAN을 검증한 뒤, 서버가 실제로 개인키를 갖고 있음을 확인한다.

인증서의 공개키가 HTTP 요청과 응답을 매번 직접 암호화하는 것은 아니다. TLS 1.3은 핸드셰이크로 공유 키 자료를 만들고, 그 뒤의 트래픽은 연결별 대칭 키로 보호한다. TLS 1.3(RFC 8446)

mTLS에서는 서버도 클라이언트를 확인한다

mTLS에서는 클라이언트도 자신의 인증서와 개인키를 낸다.

클라이언트 서버
  client.key
  client.crt

API 서버
  client-ca.crt       클라이언트 인증서를 발급한 CA를 신뢰

서버는 client-ca.crt를 기준으로 클라이언트 인증서 체인을 검증한다. 클라이언트도 client.key로 핸드셰이크에 서명하므로, 단순히 인증서 파일만 복사해서는 통과할 수 없다. 서비스 간 호출자를 네트워크 위치가 아니라 인증서 기반 신원으로 제한하고 싶을 때 mTLS를 사용한다.

만든 인증서 확인하기

인증서 체인과 호스트 이름을 확인한다.

openssl verify \
  -CAfile ca.crt \
  -verify_hostname api.internal.example \
  server.crt

간단한 TLS 서버와 클라이언트도 띄워 볼 수 있다.

# 터미널 1
openssl s_server \
  -accept 8443 \
  -cert server.crt \
  -key server.key
# 터미널 2
openssl s_client \
  -connect localhost:8443 \
  -servername api.internal.example \
  -CAfile ca.crt \
  -verify_return_error

s_client-servername은 SNI와 서버 이름 검증 맥락을 재현할 때 중요하다. OpenSSL은 verify, s_client, s_server 명령으로 인증서 검증과 TLS 연결 테스트를 제공한다. OpenSSL verify, OpenSSL s_client, OpenSSL s_server

자체 서명 인증서는 언제 쓸까?

개발 환경에서 빠르게 서버 인증서 하나를 만들 때는 아래처럼 스스로 서명한 인증서(self-signed certificate)를 만들 수 있다.

openssl req -x509 -new \
  -key server.key \
  -sha256 \
  -days 30 \
  -out server.crt \
  -subj "/CN=api.internal.example" \
  -addext "subjectAltName=DNS:api.internal.example,DNS:api" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "extendedKeyUsage=serverAuth"

이 인증서는 스스로 서명했기 때문에, 클라이언트가 그 인증서를 직접 신뢰하도록 설정하지 않으면 연결에 실패한다. 테스트에는 편하지만, 여러 서버·클라이언트가 있는 환경에서는 내부 CA를 만들어 ca.crt 하나만 신뢰하도록 하는 편이 관리하기 쉽다.

핵심 정리

  • server.key는 서버만 보관하는 개인키다. Kubernetes에서는 Secret으로 관리한다.
  • server.csr은 공개키와 SAN을 CA에 전달하는 발급 요청서다. 개인키는 들어 있지 않다.
  • server.crt 또는 tls.crt는 CA가 공개키·서버 이름·유효기간에 서명한 인증서다.
  • fullchain.pem은 서버 인증서와 중간 CA 인증서를 묶은 파일이다.
  • 클라이언트는 ca.crt로 인증서 체인과 SAN을 검증한다.
  • mTLS에서는 클라이언트도 client.keyclient.crt를 내고, 서버는 client-ca.crt로 이를 검증한다.

파일 이름을 외우기보다 “개인키는 누가 보관하는가”, “누가 어떤 CA를 신뢰하는가”, “인증서의 SAN이 실제 연결 주소와 맞는가” 세 가지를 기준으로 보면 TLS 설정이 훨씬 덜 헷갈린다.

참고 자료