TLS 와 mTLS: 채널을 잠그고 상대를 확인한다
한 줄 요약
서버 TLS 는 클라이언트가 「부르려던 상대가 맞는가」 를, mTLS 는 서버가 「부르는 상대가 누구인가」 를 CA 서명과 인증서의 이름으로 확인하게 합니다. 인가는 그렇게 확인한 인증서에서만 신원을 읽어야 합니다.
왜 이게 필요했나
평문 gRPC(h2c)는 같은 망의 누구든 엿보고, 고치고, 다른 서비스인 척할 수 있습니다. 예전에는 「클러스터 안이니 괜찮다」 고 여겼지만, 파드 하나가 뚫리면 같은 망의 모든 평문 호출이 그 파드의 손에 들어갑니다. 제로 트러스트는 망의 위치를 믿지 않고 호출마다 상대를 확인하자는 생각이고, 그 확인 수단이 TLS 와 mTLS 입니다.
어떻게 동작하나
TLS 핸드셰이크에서 서버는 인증서 체인을 보냅니다. 클라이언트는 두 가지를 확인합니다. 그 체인이 자기가 믿는 루트 CA 까지 서명으로 이어지는가, 그리고 인증서의 이름이 자기가 부른 이름과 같은가. 이름은 SAN(subjectAltName)에서 봅니다. 요즘 클라이언트는 CN 을 보지 않으므로 CN 에만 이름을 적으면 「No match found for server name」 으로 핸드셰이크가 끊깁니다 (grpcio 1.84.0 의 실측 로그). CA 인증서에는 basicConstraints CA:TRUE 가 있어야 그것이 서명한 인증서를 믿습니다.
grpc.ssl_server_credentials([(키, 인증서)]) 서버 TLS
grpc.ssl_server_credentials(..., root_certificates=CA, require_client_auth=True) mTLS
grpc.ssl_channel_credentials(root_certificates=CA) 클라이언트가 믿을 CA
grpc.ssl_channel_credentials(CA, 클라이언트 키, 클라이언트 인증서) mTLS 클라이언트
("grpc.ssl_target_name_override", 이름) 확인할 이름을 바꾼다
IP 로 붙는데 인증서에는 DNS 이름만 있을 때가 있습니다. 쿠버네티스에서 파드 IP 로 바로 부를 때가 그렇습니다. 이때 할 일은 확인을 끄는 것이 아니라 grpc.ssl_target_name_override 로 확인할 이름을 알려 주는 것입니다. 확인을 끄면 TLS 를 쓴 의미가 사라집니다.
mTLS 는 서버가 클라이언트 인증서를 요구하는 것입니다. require_client_auth 를 빼면 서버는 인증서를 요청만 하고 없어도 받아들여서, mTLS 처럼 보이지만 누구나 붙습니다. 클라이언트 인증서가 없으면 grpcio 1.84.0 은 핸드셰이크를 PEER_DID_NOT_RETURN_A_CERTIFICATE 로 끊고 클라이언트는 UNAVAILABLE 을 받습니다(실측).
gRPC 는 TLS 위에서 ALPN 으로 h2 를 합의해야 합니다. 그래서 TLS 를 끝내는 프록시가 ALPN 을 h2 로 넘기지 않으면 연결은 맺어지는데 호출이 실패하는 이상한 장면이 생깁니다. 그리고 인증서는 언젠가 바뀝니다. grpcio 는 서버를 다시 띄우지 않고 새 인증서를 읽도록 dynamic_ssl_server_credentials 를 제공하고, 서비스 메시는 짧은 수명의 인증서를 계속 새로 발급해 교체를 일상으로 만듭니다. 만료를 사건이 아니라 늘 일어나는 일로 다루는 쪽이 안전합니다.
신원은 SAN 의 URI 에 담는 것이 요즘의 약속입니다. SPIFFE ID 는 spiffe://신뢰 영역/ns/네임스페이스/sa/서비스 계정 모양이고, 서비스 메시가 파드마다 나눠 주는 인증서도 이 모양입니다. 서버에서는 context.auth_context() 의 x509_subject_alternative_name 에 bytes 목록으로 들어옵니다. 인가는 이 값으로 합니다. 메타데이터 머리에 적힌 신원은 클라이언트가 마음대로 적을 수 있으므로 근거가 될 수 없습니다.
현장에서 만나는 모습
가장 흔한 TLS 장애는 만료입니다. 인증서는 조용히 만료되고, 그날 모든 새 연결이 끊깁니다. 그래서 수명이 짧은 인증서를 자동으로 다시 발급하고, 서버는 재시작 없이 새 인증서를 읽게 합니다. 두 번째로 흔한 것은 이름 불일치입니다. 서비스 이름을 바꾸거나 다른 주소로 부르기 시작하면 SAN 에 그 이름이 없어 실패합니다.
서비스 메시는 이 일을 사이드카나 노드 프록시가 대신해 줍니다. 그래도 메시의 인증서 만료, SPIFFE ID 와 인가 정책의 불일치를 읽으려면 한 번은 손으로 해 봐야 합니다. CA 개인 키는 이 모든 신뢰의 뿌리라 파일로 나눠 두지 않습니다. etcd 와 API 서버 사이의 mTLS 실측은 블로그 글 작은 클러스터에서 실제로 도는 gRPC 에 있습니다.
다음 실습에서 할 것
openssl 로 사설 CA 와 서버 인증서를 만들어 TLS 서버를 띄우고, IP 로 붙는 클라이언트가 확인할 이름을 맞추게 합니다. 그다음 SPIFFE ID 를 담은 클라이언트 인증서를 만들어 mTLS 를 걸고, 인증서의 신원으로 쓰기 호출을 허락합니다. 같은 신원을 메타데이터로 흉내 낸 호출이 거절되는 것도 확인합니다.