13분 읽기

mTLS 인증서의 역할 나누기

mTLS를 적용할 때 인증서 설정에 serverAuth, clientAuth를 추가하라는 말을 자주 듣는다. 두 값을 무조건 넣는 것이 아니라, 서비스가 TLS 서버인지 클라이언트인지에 맞춰 골라야 한다.

예를 들어 Gateway는 외부 요청을 받을 때 TLS 서버이고, 내부 gRPC 서비스를 호출할 때는 TLS 클라이언트다. 반면 배치 Worker는 Gateway를 호출하기만 하고 TLS 서버를 열지 않을 수 있다. 두 서비스에 똑같은 인증서를 주는 대신, 필요한 역할만 허용한 서비스별 인증서를 발급하는 편이 mTLS의 신원 분리와 폐기 범위를 지키기 쉽다.

인증서의 기본 구성 요소가 낯설다면 먼저 OpenSSL 인증서 설정 읽기: req, CN, SAN, KU, EKU는 무엇일까를 읽고 오면 좋다. Kubernetes에서 Service DNS와 인증서를 배포하는 방법은 Kubernetes 같은 namespace에서 mTLS 적용하기에서 이어서 다룬다.

EKU는 인증서를 어디에 쓸지 정한다

EKU(Extended Key Usage)는 X.509 v3 확장 필드다. 이 인증서의 공개키를 어떤 애플리케이션 목적에 사용할 수 있는지 나타낸다. OpenSSL 설정에서는 extendedKeyUsage로 쓴다.

extendedKeyUsage = serverAuth, clientAuth

TLS에서 주로 보는 값은 두 가지다.

EKU 값TLS에서의 역할인증서를 제시하는 순간
serverAuthTLS 서버 인증서버가 핸드셰이크 중 서버 인증서를 보낼 때
clientAuthTLS 클라이언트 인증mTLS에서 호출자가 클라이언트 인증서를 보낼 때

OpenSSL은 serverAuthclientAuth를 웹에 한정하지 않고 모든 종류의 TLS 서버·클라이언트에 사용하는 것으로 설명한다. 즉 HTTP뿐 아니라 gRPC, 메시지 브로커, 내부 제어 API에도 같은 구분이 적용된다. OpenSSL의 X.509 v3 확장 문서에서 지원하는 EKU 값과 설정 문법을 확인할 수 있다.

KU와 EKU는 무엇이 다를까?

keyUsage(KU)와 EKU는 이름이 비슷해 같이 외우기 쉽지만, 답하는 질문이 다르다.

확장제한하는 대상예시질문으로 바꾸면
keyUsage키로 수행할 암호 연산digitalSignature, keyEncipherment, keyCertSign이 키로 서명하거나 인증서에 서명해도 되는가?
extendedKeyUsage인증서를 제시할 애플리케이션 목적serverAuth, clientAuth, codeSigning이 인증서를 TLS 서버·클라이언트로 써도 되는가?

실제 서비스가 사용하는 TLS 인증서에서는 아래 설정을 자주 볼 수 있다.

keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
basicConstraints = critical, CA:FALSE

digitalSignature는 TLS 연결 과정에서 개인키 소유를 증명하는 서명과 관련이 있다. 반면 keyCertSign은 CA가 다른 인증서에 서명할 때 필요한 용도다. 그래서 Gateway나 Worker가 쓰는 인증서에는 넣지 않고, CA 인증서에만 둔다.

basicConstraints = CA:FALSE도 빠지면 안 된다. EKU가 TLS 역할을 정한다고 해서 그 인증서가 CA가 되는 것은 아니며, 다른 인증서를 발급할 수 없어야 한다. EKU와 KU의 표준 의미는 RFC 5280의 X.509 인증서 프로필에 정의돼 있다.

서비스 역할에 맞춰 EKU 고르기

가상의 platform namespace에 다음과 같은 7개 서비스가 있다고 해 보자.

서비스통신 역할권장 EKU
internal-gatewayTLS 서버, 다른 gRPC 서비스의 클라이언트serverAuth, clientAuth
domain-apigRPC 서버, 다른 내부 서비스의 클라이언트serverAuth, clientAuth
event-notifiergRPC 서버, API 호출 클라이언트serverAuth, clientAuth
media-servicegRPC 서버, 필요 시 내부 호출 클라이언트serverAuth, clientAuth
project-controllerGateway 호출 클라이언트 전용clientAuth
batch-worker내부 API 호출 클라이언트 전용clientAuth
report-batch내부 API 호출 클라이언트 전용clientAuth
Gateway는 serverAuth와 clientAuth를 모두 가진 인증서를 쓰고, 호출만 하는 Worker는 clientAuth만 가진 인증서를 쓰는 mTLS 역할 분리 구조
한 서비스가 TLS 서버와 클라이언트를 모두 맡으면 두 EKU가 필요하다. 호출만 하는 서비스에는 서버 역할을 주지 않는다.

서버와 클라이언트 역할을 모두 맡는다면

internal-gateway가 8443 포트로 요청을 받고, 다른 서비스로 gRPC 호출도 보낸다면 인증서에는 두 역할이 모두 필요하다.

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

[alt_names]
DNS.1 = internal-gateway.platform.svc.cluster.local
DNS.2 = internal-gateway.platform.svc
DNS.3 = internal-gateway

서버 역할에서는 호출자가 URL에 사용한 호스트 이름과 DNS.* SAN을 비교한다. 클라이언트 역할에서는 Gateway가 같은 인증서를 호출자 인증서로 검증한다. SAN의 DNS 이름은 서버 주소를 확인하는 기준이고, 클라이언트 인증서로 어떤 호출자를 허용할지는 서버 또는 프록시가 별도로 정한다.

다른 서비스를 호출하기만 한다면

batch-worker가 TLS 서버를 열지 않고 내부 API를 호출만 한다면 서버 인증 용도는 빼는 편이 낫다.

# batch-worker.ext
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
basicConstraints = critical, CA:FALSE
subjectAltName = @alt_names

[alt_names]
DNS.1 = batch-worker.platform.svc.cluster.local
DNS.2 = batch-worker.platform.svc
DNS.3 = batch-worker

클라이언트 인증서의 SAN은 서버 주소를 확인하는 용도로 쓰이지 않는다. 그래도 인증서를 받을 서비스의 대표 DNS 이름을 SAN에 넣고, Gateway가 그 값을 신원 확인 정책에 사용하면 운영 규칙을 일정하게 유지할 수 있다. serverAuth를 불필요하게 주지 않으면 개인키가 유출됐을 때 그 서비스 이름의 TLS 서버로 가장할 수 있는 범위를 줄일 수 있다.

EKU가 실제로 검사되는지 확인하기

EKU가 있는 인증서는 인증서 검증 과정에서 목적에 맞는지 확인할 수 있다. 가령 서버가 client certificate를 받는 상황에서는 clientAuth가 있는 인증서를 기대하고, TLS 클라이언트는 서버 인증서에 serverAuth가 있는지 확인할 수 있다.

다만 “모든 TLS 구현체가 언제나 모든 확장을 똑같이 강제한다”고 단정하면 안 된다. OpenSSL도 특정 구현체가 확장 값을 인식하거나 준수한다는 보장은 없다고 명시한다. 라이브러리, 프록시, Java trust manager의 검증 설정을 실제로 확인해야 한다.

따라서 운영 기준은 다음처럼 잡는 편이 안전하다.

  1. 발급 정책에서 EKU를 역할별로 제한한다.
  2. TLS 라이브러리나 프록시가 서버·클라이언트 목적 검증을 수행하는지 통합 테스트로 확인한다.
  3. 인증서 체인 검증과 별개로, 애플리케이션 또는 프록시에서 호출자 identity를 인가 정책에 연결한다.

EKU는 1번의 역할 제한을 제공한다. “유효한 내부 CA가 발급했다”는 사실만으로 모든 서비스에 접근시키지 않는 일은 3번의 책임이다.

인증서 하나를 여러 서비스가 함께 쓰면?

*.platform.svc.cluster.local 같은 와일드카드 SAN은 한 레이블을 대체하는 FQDN에는 매칭할 수 있다. 예를 들어 internal-gateway.platform.svc.cluster.local에는 맞을 수 있다. 하지만 internal-gateway.platform.svcinternal-gateway 같은 축약 표기는 별도 SAN 없이는 매칭하지 않는다.

더 큰 문제는 mTLS에서 서비스의 신원을 구분하기 어렵다는 점이다. 여러 서비스가 같은 와일드카드 인증서와 개인키를 공유하면 Gateway는 어떤 서비스가 호출했는지 알아낼 수 없다. 개인키가 유출되면 모든 서비스의 인증서를 함께 바꿔야 하고, 한 서비스의 인증서만 폐기하거나 서비스별 감사 로그를 남기기도 어렵다.

공유 와일드카드 인증서
  └─ "유효한 인증서를 가진 누군가"까지만 알 수 있음

서비스별 인증서
  └─ "batch-worker가 호출했다"처럼 인가·감사·폐기 단위를 나눌 수 있음

서버 인증서의 SAN은 실제 접속 주소를 검증하는 데 필요하다. 클라이언트 인증서의 SAN은 서버 주소를 확인하는 용도는 아니지만, 서비스의 신원을 나타내는 값으로 사용할 수 있다. 이때 SAN의 첫 번째 값처럼 순서에 의존하기보다, DNS:batch-worker.platform.svc.cluster.local처럼 정책에서 정한 대표 값을 명시적으로 비교하는 편이 안전하다. URI SAN을 서비스 신원 형식으로 정할 수도 있다.

Java에서 EKU와 SAN 확인하기

Java의 X509Certificate는 EKU를 OID 문자열 목록으로, SAN을 type/value 쌍의 컬렉션으로 돌려준다. TLS 클라이언트 인증서의 EKU와 DNS SAN을 애플리케이션 정책에서 한 번 더 확인하는 예시는 다음과 같다.

import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;

final class ClientCertificatePolicy {
    private static final String CLIENT_AUTH_OID = "1.3.6.1.5.5.7.3.2";
    private static final int DNS_NAME = 2;

    static void requireBatchWorker(X509Certificate certificate) throws Exception {
        List<String> eku = certificate.getExtendedKeyUsage();
        if (eku == null || !eku.contains(CLIENT_AUTH_OID)) {
            throw new SecurityException("clientAuth EKU가 없는 클라이언트 인증서입니다.");
        }

        Collection<List<?>> sans = certificate.getSubjectAlternativeNames();
        boolean matched = sans != null && sans.stream()
                .filter(san -> san.size() >= 2 && DNS_NAME == (Integer) san.get(0))
                .map(san -> (String) san.get(1))
                .anyMatch("batch-worker.platform.svc.cluster.local"::equals);

        if (!matched) {
            throw new SecurityException("허용되지 않은 서비스 identity입니다.");
        }
    }
}

이 코드는 TLS 연결 과정 자체를 대신하지 않는다. 먼저 서버가 클라이언트 인증서를 요구하고, 신뢰하는 CA가 발급했는지 확인해야 한다. 위 코드는 그다음에 “유효한 인증서를 가진 서비스 중 누구를 허용할지” 검사하는 예시다.

getExtendedKeyUsage()getSubjectAlternativeNames()의 반환 형식과 GeneralName type 값은 Java X509Certificate API 문서에서 확인할 수 있다.

발급한 인증서 확인하기

설정 파일을 고쳤다고 끝내지 말고, 결과 인증서에 확장이 실제로 들어갔는지 확인한다.

openssl x509 -in tls.crt -noout -text | \
  grep -A1 "Extended Key Usage"

예상 결과는 역할에 따라 다르다.

# Gateway
X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication

# Worker
X509v3 Extended Key Usage:
    TLS Web Client Authentication

SAN과 CA 여부도 함께 확인할 수 있다.

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

핵심 정리

  • KU는 키의 암호 연산을, EKU는 인증서의 TLS 역할을 제한한다.
  • TLS 서버와 클라이언트를 겸하는 서비스에는 serverAuth, clientAuth가 모두 필요할 수 있다.
  • 호출만 하는 서비스에는 clientAuth만 발급해 역할을 줄인다.
  • EKU가 실제로 검사되는지는 사용하는 TLS 프로그램에서 테스트하고, SAN 같은 값으로 호출자의 신원과 권한을 별도로 확인한다.
  • mTLS의 감사·폐기·최소 권한을 지키려면 와일드카드 인증서를 공유하지 말고 서비스마다 인증서를 발급한다.

참고 자료