11분 읽기

OpenSSL 인증서 설정 읽기

Kubernetes 안에서 mTLS를 붙이려다 보면 인증서 설정 파일부터 마주친다. 아래 설정도 그중 하나다. aipub namespace의 Gateway가 서버 인증서를 제시하고, 필요하다면 다른 서비스에 클라이언트 인증서도 제시할 수 있게 하려는 구성이다.

# tls-gateway.conf
[req]
default_bits = 2048
encrypt_key = no
default_md = sha256
distinguished_name = req_distinguished_name
prompt = no

[req_distinguished_name]
CN = aipub-backend-gateway

# tls-gateway.ext
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
basicConstraints = critical, CA:FALSE
subjectAltName = @alt_names

[alt_names]
DNS.1 = aipub-backend-gateway.aipub.svc.cluster.local
DNS.2 = aipub-backend-gateway.aipub.svc
DNS.3 = aipub-backend-gateway

처음에는 CN, SAN, EKU가 모두 비슷해 보인다. 하지만 각 항목이 정하는 내용과 사용되는 시점은 다르다. 이 글에서는 설정을 키 생성 → CSR 생성 → CA 서명 → TLS 검증 순서로 읽어 본다.

두 설정 파일의 역할 나누기

tls-gateway.conf는 주로 openssl reqCSR(Certificate Signing Request) 을 만들 때 읽는 설정이다. 반면 tls-gateway.ext는 CA가 CSR을 실제 X.509 인증서로 서명할 때 넣을 확장 필드를 정의한다.

개인키 ──▶ CSR 생성(req 설정) ──▶ CA 서명(ext 설정) ──▶ 서버/클라이언트 인증서
OpenSSL req 설정으로 CSR을 만들고 ext 설정을 적용해 CA가 인증서를 서명하는 흐름
CSR은 “이 공개키와 이 이름으로 인증서를 발급해 달라”는 요청이고, 확장 필드는 CA가 최종 인증서에 허용하는 성질이다.

[req]로 CSR 만드는 방법 정하기

[req]openssl req의 기본 동작을 정하는 섹션이다.

설정의미이 상황에서의 해석
default_bits = 2048새 RSA 키를 함께 생성할 때 쓸 기본 키 길이이미 만든 개인키를 -key로 넘기면 이 값은 쓰이지 않는다. 키 길이는 조직의 암호 정책에 맞춰 별도로 정한다.
encrypt_key = no새로 만든 개인키 PEM에 암호를 걸지 않음Pod 시작 시 사람이 비밀번호를 입력할 수 없다는 점은 편하지만, 개인키 접근 권한과 Secret 관리가 더 중요해진다.
default_md = sha256요청 서명에 사용할 기본 digestCSR이 개인키 소유자에게서 왔음을 보이는 서명에 쓰인다.
distinguished_name = req_distinguished_namesubject 정보를 읽을 섹션의 이름아래 [req_distinguished_name]을 가리킨다.
prompt = no대화형 입력을 받지 않음CI, 스크립트, cert-manager 같은 자동화 흐름에 어울린다.

encrypt_key = no는 “개인키가 안전하다”는 뜻이 아니다. 개인키를 담은 Kubernetes Secret은 Git에 올리지 않고, 워크로드와 운영자만 읽을 수 있도록 RBAC·마운트 권한·Secret 관리 방식을 함께 설계해야 한다.

CN과 SAN은 무엇이 다를까?

[req_distinguished_name]은 인증서 subject에 넣을 식별 정보를 모으는 섹션이다.

[req_distinguished_name]
CN = aipub-backend-gateway

여기서 CN(Common Name)은 subject의 대표 이름이다. 이 예에서는 “이 인증서는 aipub-backend-gateway 워크로드를 위해 발급됐다”는 사람이 읽기 좋은 표식으로 쓸 수 있다.

다만 HTTPS 클라이언트가 https://aipub-backend-gateway.aipub.svc.cluster.local에 접속했을 때 확인하는 이름은 보통 CN이 아니라 SAN(Subject Alternative Name) 이다. 서버 인증서를 검증하는 라이브러리는 접속에 사용한 호스트 이름과 DNS SAN을 비교한다. CN만 맞춰 두고 SAN을 빼면, 최신 TLS 클라이언트에서 호스트 이름 검증이 실패할 수 있다.

basicConstraints로 CA 여부 정하기

basicConstraints = critical, CA:FALSE

basicConstraints는 인증서가 CA인지 아닌지를 나타낸다.

  • CA:TRUE: 다른 인증서에 서명할 수 있는 CA 인증서
  • CA:FALSE: 서버나 클라이언트가 실제 연결에서 제시할 인증서

Gateway가 다른 서비스의 인증서를 발급할 일이 없다면 CA:FALSE가 맞다. CA 개인키와 Gateway 개인키는 역할이 완전히 다르다. Gateway Pod에는 자기 인증서와 개인키만 배포하고, CA 개인키는 접근이 엄격히 제한된 인증서 발급 시스템에 보관해야 한다.

앞의 critical은 검증 구현체가 이 확장을 이해하지 못하면 인증서를 거부하라는 표시다. 인증서의 역할을 판단하는 필수 제약에 붙인다.

KU로 키의 사용 범위 정하기

keyUsage = critical, digitalSignature, keyEncipherment

keyUsage는 이 인증서의 공개키에 허용하는 암호학적 용도를 좁힌다.

  • digitalSignature: TLS 핸드셰이크에서 개인키 소유를 증명하는 서명에 사용한다.
  • keyEncipherment: 전통적인 RSA 키 교환 프로필에서 키를 암호화하는 용도다.

여기서 자주 생기는 오해가 있다. 인증서가 RSA 키를 쓴다고 해서 HTTP 요청 본문을 RSA로 계속 암호화하는 것은 아니다. TLS 1.3은 일반적으로 ECDHE로 연결별 대칭 키를 만들고, 인증서 개인키는 핸드셰이크 서명에 쓴다. keyEncipherment는 레거시 호환이 필요한 RSA 기반 TLS 프로필을 고려해 함께 넣는 경우가 많다.

EKU로 서버와 클라이언트 역할 정하기

extendedKeyUsage = serverAuth, clientAuth

EKU는 KU보다 한 단계 더 구체적으로 인증서의 애플리케이션 용도를 선언한다.

  • serverAuth: TLS 서버가 제시하는 인증서
  • clientAuth: mTLS에서 호출자가 제시하는 인증서

따라서 위 설정은 한 장의 인증서를 서버와 클라이언트 양쪽 용도로 사용할 수 있게 한다. Gateway가 다른 내부 서비스에도 mTLS로 호출을 보낸다면 실용적인 선택일 수 있다.

반대로 Gateway가 요청을 받기만 하고, Worker가 Gateway를 호출하는 구조라면 서버 인증서는 serverAuth, Worker 인증서는 clientAuth로 나누는 편이 권한을 더 작게 유지한다. 인증서가 유출됐을 때 다른 역할로 재사용될 여지를 줄일 수 있기 때문이다. 이 글의 다음 편에서는 이 분리된 구성을 Kubernetes YAML로 만든다.

SAN에 실제 Service 주소 적기

subjectAltName = @alt_names

[alt_names]
DNS.1 = aipub-backend-gateway.aipub.svc.cluster.local
DNS.2 = aipub-backend-gateway.aipub.svc
DNS.3 = aipub-backend-gateway

subjectAltName = @alt_names는 SAN 값을 바로 쓰지 않고 [alt_names] 섹션에서 읽겠다는 뜻이다. 같은 타입의 DNS 이름을 여러 개 넣을 때 DNS.1, DNS.2처럼 번호를 붙인다.

각 이름은 Kubernetes Service DNS의 서로 다른 표기다.

SAN호출 위치와 의미
aipub-backend-gateway.aipub.svc.cluster.local클러스터 도메인까지 적은 정규 FQDN. namespace 밖에서 호출할 때도 명확하다.
aipub-backend-gateway.aipub.svc클러스터 도메인을 생략한 Service 이름.
aipub-backend-gateway같은 aipub namespace 안에서 DNS search suffix를 이용하는 짧은 이름.

중요한 점은 DNS가 이름을 해석해 준다고 해서 TLS 검증 이름까지 자동으로 바뀌지는 않는다는 것이다. 예를 들어 애플리케이션이 https://aipub-backend-gateway.aipub로 접속한다면, DNS는 해석되더라도 인증서에는 그 정확한 문자열이 없다. 이 표기를 실제로 쓴다면 DNS.4 = aipub-backend-gateway.aipub도 SAN에 추가해야 한다.

가장 단순한 운영 규칙은 하나다. 애플리케이션의 호출 URL을 정하고, 그 URL의 host를 SAN에 정확히 넣는다. namespace 안에서도 FQDN을 쓰면 설정과 장애 로그를 읽을 때 더 명확하다.

설정 파일로 인증서 만들기

개인키가 아직 없다면 먼저 생성한다.

openssl genpkey \
  -algorithm RSA \
  -pkeyopt rsa_keygen_bits:2048 \
  -out tls-gateway.key

그다음 req 설정으로 CSR을 만든다.

openssl req -new \
  -config tls-gateway.conf \
  -key tls-gateway.key \
  -out tls-gateway.csr

CA는 CSR과 확장 설정을 받아 실제 연결에 사용할 인증서를 만든다.

openssl x509 -req \
  -in tls-gateway.csr \
  -CA aipub-ca.crt \
  -CAkey aipub-ca.key \
  -CAcreateserial \
  -out tls-gateway.crt \
  -days 90 \
  -sha256 \
  -extfile tls-gateway.ext

여기서 -extfile이 바로 tls-gateway.ext를 적용하는 지점이다. CSR에 요청한 SAN을 CA가 무조건 그대로 복사하는 것이 아니라, 발급 측이 허용한 확장 값을 최종 인증서에 넣는다고 생각하면 된다.

발급 뒤에는 내용을 반드시 확인한다.

openssl req -in tls-gateway.csr -noout -text

openssl x509 -in tls-gateway.crt -noout -text \
  -ext subjectAltName \
  -ext keyUsage \
  -ext extendedKeyUsage \
  -ext basicConstraints

특히 SAN, issuer, 유효기간, CA:FALSE를 확인한다. openssl 명령으로 수동 발급을 경험해 보는 것은 좋지만, 실제 Kubernetes 운영에서는 발급·갱신·폐기와 Secret 반영까지 자동화해야 한다. 다음 글에서 cert-managerCertificate 리소스로 같은 namespace 안의 Worker와 Gateway에 mTLS를 적용해 본다.

핵심 정리

  • [req]는 CSR 생성 과정의 기본값이고, .ext는 최종 X.509 인증서의 확장을 정한다.
  • CN은 subject의 대표 이름이지만, TLS 서버 이름 검증의 기준은 SAN이다.
  • basicConstraints = CA:FALSE는 Gateway 인증서가 CA가 아니라는 선언이다.
  • KU는 암호 연산의 범위, EKU는 서버 인증·클라이언트 인증 같은 TLS 역할을 제한한다.
  • Kubernetes에서는 실제 호출 URL의 host와 SAN을 맞추는 것이 핵심이다.

참고 자료