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), 요청자 서명이 담긴 인증서 발급 요청 | X | CA에 전달 |
server.crt, cert.pem, tls.crt | 서버 공개키와 SAN, 유효기간 등을 CA가 서명한 서버 인증서 | X | TLS 서버 |
ca.crt | 신뢰할 루트 CA 또는 중간 CA 인증서 | X | 클라이언트와 서버의 신뢰할 CA 목록 |
fullchain.pem | 서버 인증서 + 중간 CA 인증서 | X | TLS 서버 |
.p12, .pfx | 개인키와 인증서 등을 비밀번호로 묶은 PKCS#12 컨테이너 | 개인키 포함 시 O | Java keystore, 운영 도구 등 |
tls.crt와 tls.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와 서버 인증서 만들기
아래 예시는 개발·사내망에서 쓸 수 있는 작은 내부 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 RSA와 rsa_keygen_bits로 RSA 키 크기를 정한다. OpenSSL genpkey 문서를 참고할 수 있다.
이 예시에서 ca.crt는 자기 자신이 서명한 Root CA 인증서다. CA:TRUE와 keyCertSign은 이 인증서가 다른 인증서를 발급할 수 있음을 나타낸다. 반면 ca.key는 실제로 다른 인증서에 서명할 수 있는 키이므로, 애플리케이션 서버에 넣거나 Git에 올리면 안 된다.
2. 서버 개인키 만들기
openssl genpkey \
-algorithm RSA \
-pkeyopt rsa_keygen_bits:3072 \
-out server.key
server.key는 api.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
이 명령이 실제 발급 단계다.
- CSR에서 서버 공개키와 인증서 소유자 정보를 읽는다.
server-ext.cnf에서 SAN과 서버 용도를 넣는다.ca.crt의 subject를 새 인증서의 issuer로 기록한다.ca.key로 인증서 전체에 서명한다.- 결과로
server.crt를 만든다.
-CAcreateserial은 CA의 다음 인증서 일련번호를 관리할 ca.srl 파일을 만든다. 실서비스에서 다수의 인증서를 발급할 때는 이 간단한 명령 조합 대신 cert-manager, Vault PKI, step-ca 같은 PKI 도구로 발급·갱신·폐기를 관리하는 편이 안전하다.
server.crt와 fullchain.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.key와client.crt를 내고, 서버는client-ca.crt로 이를 검증한다.
파일 이름을 외우기보다 “개인키는 누가 보관하는가”, “누가 어떤 CA를 신뢰하는가”, “인증서의 SAN이 실제 연결 주소와 맞는가” 세 가지를 기준으로 보면 TLS 설정이 훨씬 덜 헷갈린다.