gRPC 보안

gRPC 보안 (Securing gRPC)

Cilium을 사용해 gRPC 인지(gRPC-aware) 보안 정책을 시행하는 방법을 소개하는 문서예요. 단일 노드 Cilium 환경을 구성하고 gRPC 서비스 접근을 L7 정책으로 제한하는 과정을 단계별로 보여줘요. 대략 15~30분 정도 걸려요.

출처: Securing gRPC

본문

이 문서는 Cilium을 사용해 gRPC 인지 보안 정책을 시행하는 방법을 소개해요. 여러분의 머신에서 단일 노드 Cilium 환경을 구동하는 상세한 walk-through이며, 15~30분 정도 걸리도록 설계됐어요.

Cilium 설치 (Setup Cilium)

아직 Cilium을 설치하지 않았다면, Cilium Quick Installation 가이드에 따라 Kubernetes 클러스터를 빠르게 부트스트랩하고 Cilium을 설치하는 방법을 따라 해 보세요. 고민된다면 minikube 방식을 고르면 5분 안에 준비를 마칠 수 있어요.

이 데모에서는 kube-dns가 올바르게 동작하는 것이 중요해요. kube-dns의 상태를 확인하려면 다음 명령을 실행하세요.

$ kubectl get deployment kube-dns -n kube-system
NAME       DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kube-dns   1         1         1            1           13h

최소 하나의 파드가 available 상태여야 해요.

데모 애플리케이션 배포 (Deploy the Demo Application)

이제 Cilium이 배포됐고 kube-dns가 올바르게 동작하므로 데모 gRPC 애플리케이션을 배포할 수 있어요. 첫 번째 Cilium + HTTP 인지 보안 정책 데모가 Star Wars 테마였기 때문에, gRPC도 같은 테마를 사용하기로 했어요. HTTP 인지 Cilium Star Wars 데모가 은하 제국이 HTTP 인지 보안 정책으로 Death Star를 반란군 연합으로부터 보호하는 방법을 보여줬다면, 이 gRPC 데모는 gRPC 인지 보안 정책의 부재로 인해 Leia, Chewbacca, Lando, C-3PO, R2-D2가 제국군에게 점령당한 Cloud City에서 탈출할 수 있었던 방법을 보여줘요.

gRPC는 Google이 널리 퍼뜨린 protobuf 직렬화/역직렬화 라이브러리 위에 구축된 고성능 RPC 프레임워크예요. gRPC 바인딩은 많은 프로그래밍 언어로 제공되며, protobuf 파싱의 효율성과 HTTP 2를 전송 계층으로 활용하는 이점 덕분에 처음부터 새 마이크로서비스를 구축하는 사람들에게 인기 있는 RPC 프레임워크예요.

영화의 세부 내용을 모르는 분을 위해 설명하면, Leia와 다른 반란군들은 스톰트루퍼를 피해 Millennium Falcon이 주차된 우주항구 플랫폼에 도달하려 도망치고 있어요. 그런데 플랫폼으로 가는 문이 닫혀 있고 접근 코드가 바뀌어 있어요. 하지만 R2-D2는 공용 터미널을 통해 Cloud City 컴퓨터 시스템에 접근해 이 보안을 무력화하고, 문을 열어 반란군들이 제때 Millennium Falcon에 도착해 탈출할 수 있게 해 줘요.

이 예제에서 Cloud City의 내부 컴퓨터 시스템은 gRPC 기반 마이크로서비스 집합으로 구축돼 있다고 가정해요. gRPC에서 각 서비스는 언어 독립적인 프로토콜 버퍼 정의를 사용해 정의돼요. Cloud City 안의 문을 관리하는 시스템을 위한 정의는 다음과 같아요.

package cloudcity;

// The door manager service definition.
service DoorManager {

  // Get human readable name of door.
  rpc GetName(DoorRequest) returns (DoorNameReply) {}

  // Find the location of this door.
  rpc GetLocation (DoorRequest) returns (DoorLocationReply) {}

  // Find out whether door is open or closed
  rpc GetStatus(DoorRequest) returns (DoorStatusReply) {}

  // Request maintenance on the door
  rpc RequestMaintenance(DoorMaintRequest) returns (DoorActionReply) {}

  // Set Access Code to Open / Lock the door
  rpc SetAccessCode(DoorAccessCodeRequest) returns (DoorActionReply) {}

}

설정을 작게 유지하기 위해 이 설정을 나타내는 두 개의 파드만 띄울게요.

  • cc-door-mgr: app=cc-door-mgr 라벨을 가진 gRPC 도어 매니저 서비스 하나를 실행하는 단일 파드.
  • terminal-87: Cloud City 곳곳에 흩어진 공용 네트워크 접근 터미널 중 하나. 반란군이 필사적으로 탈출을 시도하는 동안 R2-D2가 terminal-87에 접속해요. 이 터미널은 app=public-terminal 라벨을 가진 도어 관리 서비스와 통신하기 위해 gRPC 클라이언트 코드를 사용해요.

cc-door-app.yaml 파일에는 도어 매니저 서비스용 Kubernetes Deployment, terminal-87을 나타내는 Kubernetes Pod, 도어 매니저 서비스용 Kubernetes Service가 들어 있어요. 이 예제 앱을 배포하려면 다음을 실행하세요.

$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.20.2/examples/kubernetes-grpc/cc-door-app.yaml
deployment "cc-door-mgr" created
service "cc-door-server" created
pod "terminal-87" created

Kubernetes는 파드와 서비스를 백그라운드에서 배포해요. kubectl get svc,pods를 실행하면 작업 진행 상황을 알 수 있어요. 각 파드는 Running 상태에 도달할 때까지 여러 상태를 거치며, 그 시점에 설정이 준비된 거예요.

$ kubectl get pods,svc
NAME                                 READY     STATUS    RESTARTS   AGE
po/cc-door-mgr-3590146619-cv4jn      1/1       Running   0          1m
po/terminal-87                       1/1       Running   0          1m

NAME                 CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
svc/cc-door-server   10.0.0.72    <none>        50051/TCP   1m
svc/kubernetes       10.0.0.1     <none>        443/TCP     6m

gRPC 클라이언트와 서버 간 접근 테스트 (Test Access Between gRPC Client and Server)

먼저 공용 터미널이 도어 서비스의 클라이언트로 제대로 동작하는지 확인해 볼게요. terminal-87 컨테이너에 존재하는 도어 서비스용 Python gRPC 클라이언트를 실행해 테스트할 수 있어요. 호출할 gRPC 메서드 이름과 매개변수(이 경우 door-id)를 지정해 cc_door_client를 호출할게요.

$ kubectl exec terminal-87 -- python3 /cloudcity/cc_door_client.py GetName 1
Door name is: Spaceport Door #1

$ kubectl exec terminal-87 -- python3 /cloudcity/cc_door_client.py GetLocation 1
Door location is lat = 10.222200393676758 long = 68.87879943847656

이 정보를 공용 터미널에 노출하는 것은 Cloud City에 처음 온 여행객들이 서로 다른 문을 식별하고 위치를 찾는 데 도움이 되므로 유용해 보여요. 하지만 도어 서비스가 SetAccessCode를 포함한 여러 다른 메서드도 노출한다는 점을 기억하세요. 도어 매니저 서비스에 대한 접근이 기존의 IP·포트 기반 방화벽으로만 보호된다면, 서비스의 TCP 포트(이 예제에서는 50051)가 GetName, GetLocation 같은 합법적인 호출을 허용하기 위해 완전히 열려 있어야 하므로, SetAccessCode 같은 더 민감한 호출도 함께 노출되게 돼요. 전통적인 방화벽의 거친(course) 세분성과 gRPC 호출의 세밀한(fine-grained) 특성 사이의 이런 불일치가 R2-D2가 보안을 무력화하고 반란군의 탈출을 돕는 데 악용한 지점이에요. 이를 확인하려면 다음을 실행하세요.

$ kubectl exec terminal-87 -- python3 /cloudcity/cc_door_client.py SetAccessCode 1 999
Successfully set AccessCode to 999

Cilium으로 gRPC 서비스에 대한 접근 보안 (Securing Access to a gRPC Service with Cilium)

Cloud City의 합법적인 소유자들이 제국으로부터 도시를 되찾은 후, 어떻게 Cilium을 사용해 이 핵심 보안 구멍을 막고 SetAccessCode와 GetStatus 요청은 차단하면서 GetName, GetLocation, RequestMaintenance는 계속 허용할 수 있을까요?

gRPC는 HTTP 위에 구축되므로, gRPC 호출이 HTTP URL로 매핑되는 방식을 이해한 다음 Cilium HTTP 인지 필터를 적용해 공용 터미널이 도어 서비스에서 제공하는 전체 gRPC 메서드 중 일부만 호출하도록 허용하면 쉽게 달성할 수 있어요.

각 gRPC 메서드는 /cloudcity.DoorManager/<method-name> 형태의 URL에 대한 HTTP POST 호출로 매핑돼요.

결과적으로 다음 CiliumNetworkPolicy 규칙은 app=public-terminal 라벨의 파드 접근을, app=cc-door-mgr 라벨로 식별되는 도어 서비스에서 GetName, GetLocation, RequestMaintenance만 호출하도록 제한해요.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: L7 policy to allow public terminals to call GetName, GetLocation, and RequestMaintenance, but not GetState, or SetAccessCode on the Door Manager Service
  endpointSelector:
    matchLabels:
      app: cc-door-mgr
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: public-terminal
    toPorts:
    - ports:
      - port: "50051"
        protocol: TCP
      rules:
        http:
        - method: "POST"
          path: "/cloudcity.DoorManager/GetName"
        - method: "POST"
          path: "/cloudcity.DoorManager/GetLocation"
        - method: "POST"
          path: "/cloudcity.DoorManager/RequestMaintenance"

CiliumNetworkPolicy는 허용된 요청을 정의하는 규칙 목록을 포함해요. 즉, 어떤 규칙과도 일치하지 않는 요청(예: SetAccessCode)은 유효하지 않은 것으로 거부돼요. 위 규칙은 cc-door-mgr 파드(endpointSelector 섹션의 app: cc-door-mgr로 표시)로의 인바운드(즉, "ingress") 연결에 적용돼요. 규칙은 fromEndpoints 섹션에 표시된 대로 app: public-terminal 라벨을 가진 파드의 연결에 적용돼요. 규칙은 TCP 50051로 향하는 gRPC 연결을 명시적으로 일치시키고, 허용된 URL을 구체적으로 화이트리스트에 등록해요.

기본 창에서 kubectl을 사용해 이 gRPC 인지 네트워크 보안 정책을 적용하세요.

$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.20.2/examples/kubernetes-grpc/cc-door-ingress-security.yaml

이 보안 정책이 적용된 후에는 GetLocation 같은 무해한 호출이 여전히 의도대로 동작해요.

$ kubectl exec terminal-87 -- python3 /cloudcity/cc_door_client.py GetLocation 1
Door location is lat = 10.222200393676758 long = 68.87879943847656

하지만 다시 SetAccessCode를 호출하려 하면 거부돼요.

$ kubectl exec terminal-87 -- python3 /cloudcity/cc_door_client.py SetAccessCode 1 999

Traceback (most recent call last):
  File "/cloudcity/cc_door_client.py", line 71, in <module>
    run()
  File "/cloudcity/cc_door_client.py", line 52, in run
    response = stub.SetAccessCode(cloudcity_pb2.DoorAccessCodeRequest(
  File "/usr/local/lib/python3.8/dist-packages/grpc/_channel.py", line 826, in __call__
    return _end_unary_response_blocking(state, call, False, None)
  File "/usr/local/lib/python3.8/dist-packages/grpc/_channel.py", line 729, in _end_unary_response_blocking
    raise _InactiveRpcError(state)
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
    status = StatusCode.PERMISSION_DENIED
    details = "Access denied"
          debug_error_string = "{"created":"@1748342370.953859451","description":"Error received from peer ipv4:10.96.86.105:50051","file":"src/core/lib/surface/call.cc","file_line":1055,"grpc_message":"Access denied\r\n","grpc_status":7}"

이제 Cilium 네트워크 정책 덕분에 이 호출이 차단돼요. 그리고 전통적인 방화벽이 네트워크 장애와 구분할 수 없을 정도로 패킷을 그냥 버리는 것과 달리, Cilium은 API 계층에서 동작하므로 gRPC 상태 코드 7 PERMISSION_DENIED로 명시적으로 응답해 해당 요청이 보안상 이유로 의도적으로 거부됐다는 것을 알려줄 수 있어요.

제국 IT 직원들이 탈출 시도 전에 Cloud City 내부 네트워크에 Cilium을 배포할 시간이 없었던 게 정말 다행이에요. 그랬다면 Leia와 다른 반란군의 운명이 아주 달라졌을 테니까요!

정리 (Clean-Up)

이제 Cilium을 설치하고, 데모 앱을 배포하고, L7 gRPC 인지 네트워크 보안 정책을 테스트했어요. 정리하려면 다음을 실행하세요.

$ kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/1.20.2/examples/kubernetes-grpc/cc-door-app.yaml
$ kubectl delete cnp rule1

이후 1단계부터 튜토리얼을 다시 실행할 수 있어요.

더 알아보기 (Learn more)