LabHub
开始
学习 学习路径 课程

Envoy 内部结构

两个授权服务器,两台 Envoy

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

HTTP·gRPC 두 방식의 인가 서버를 직접 파이썬으로 쓰고 각각을 묻는 Envoy 를 띄워, 헤더가 어디로 흐르는지와 인가 서버가 죽었을 때 무엇이 일어나는지를 잰다.

왜 중요한가

인가를 서비스마다 코드로 넣으면 서비스 수만큼 서로 다른 인가가 생긴다. 판단을 한곳에 모으면 이번에는 그 한곳이 모든 요청의 경로 위에 놓인다 — 그 서버가 죽었을 때 막을지 흘릴지를 정하지 않으면, 장애 때 가용성과 보안 가운데 하나를 모르는 사이에 잃는다. Istio 의 CUSTOM 인가가 뒤에서 쓰는 것도 이 필터라, 여기서 본 헤더 규칙과 실패 동작이 메시에서도 그대로 나타난다.

단계

  1. /root/envd-authz/authz_http.py127.0.0.1:9191 에서 듣는 인가 서버를 쓰고 백그라운드로 띄우세요. Authorization: Bearer alice-token 이면 사용자 alice, bob-token 이면 bob 으로 보고 200 과 응답 헤더 x-auth-user: <사용자> 를, 토큰이 없거나 모르는 값이면 403 과 응답 헤더 x-deny-reason: missing-or-unknown-token 을 돌려줍니다. 요청마다 /root/envd-authz/authz-http.log 에 JSON 한 줄(path·user·tenant(x-tenant 헤더 값)·allowed)을 덧붙입니다.
  2. 업스트림 python3 /opt/lab/envoy/echo.py 8091(받은 요청을 JSON 으로 돌려줌)를 띄우고, /root/envd-authz/envoy-http.yaml 에 관리 포트 9981, 리스너 127.0.0.1:10081 의 설정을 써서 띄우세요 (--base-id 11). HTTP 필터 사슬 맨 앞에 envoy.filters.http.ext_authz 를 두고 stat_prefix: http_authz, http_service 로 클러스터 authz_http(127.0.0.1:9191)에 묻게 합니다. 인가 서버가 준 x-auth-user 는 업스트림으로, x-deny-reason 은 거절 응답과 함께 클라이언트로 넘기고, failure_mode_allow 는 false 입니다.
  3. 리스너 10081 로 세 요청을 보내고 결과를 /root/envd-authz/03-http.txt 에 네 줄로 적으세요 — 토큰 없이 보낸 요청의 상태 코드 no_token=, 그 응답의 x-deny-reasondeny_reason=, bob-token위조한 헤더 x-auth-user: mallory 를 함께 보냈을 때 업스트림이 받은 x-auth-userspoof_upstream=, 그리고 alice-tokenx-tenant: acme 를 붙인 요청이 인가 서버 로그에 남긴 tenanthttp_saw_tenant=(값이 없으면 none).
  4. /root/envd-authz/authz_grpc.pyenvoy.service.auth.v3.Authorization/Check 를 구현해 127.0.0.1:9192 에서 띄우세요(/opt/xds/bin/python3 로 실행합니다 — grpcio 와 Envoy 의 protobuf 가 든 가상 환경입니다). 판단은 HTTP 판과 같되, 경로가 /public 으로 시작하면 토큰 없이 허용하고 이때 사용자는 anonymous 입니다. 허용이면 OkHttpResponse 에 헤더 x-auth-user 를, 거절이면 DeniedHttpResponse 에 상태 403·헤더 x-deny-reason: grpc-missing-or-unknown-token·본문을 담습니다. 요청마다 /root/envd-authz/authz-grpc.log 에 같은 모양의 JSON 한 줄을 남깁니다.
  5. /root/envd-authz/envoy-grpc.yaml 에 관리 포트 9982, 리스너 127.0.0.1:10082 인 두 번째 Envoy 를 써서 띄우세요(--base-id 12). ext_authz 는 stat_prefix: grpc_authz, grpc_serviceenvoy_grpc 로 클러스터 authz_grpc(127.0.0.1:9192, HTTP/2)를 부르고, failure_mode_allow: truefailure_mode_allow_header_add: true 를 켭니다. 첫 Envoy 는 그대로 둡니다.
  6. /root/envd-authz/envoy-grpc.yaml 에 경로가 정확히 /healthz 인 라우트를 맨 앞에 더해, 본문 ok 로 200 을 직접 돌려주고(direct_response) 그 라우트에서만 ext_authz 를 끄세요(typed_per_filter_configExtAuthzPerRoutedisabled: true). 그 Envoy 를 다시 띄운 뒤 토큰 없이 /healthz 를 세 번 부르세요.
  7. 두 인가 서버를 모두 멈춘 채로 첫 Envoy 에는 alice-token 을 붙인 요청을, 두 번째 Envoy 에는 토큰 없는 /orders 요청을 보내고 /root/envd-authz/07-failure.txthttp_on_error=(첫 Envoy 의 상태 코드), grpc_on_error=(두 번째의 상태 코드), grpc_failure_header=(업스트림이 받은 x-envoy-auth-failure-mode-allowed 값) 세 줄을 적으세요. 적은 뒤 두 인가 서버를 다시 띄웁니다.
  8. /root/envd-authz/08-report.md 에 여섯 줄 — fail_closed_code=·fail_open_code=(7단계의 두 코드), spoofed_header_reached_upstream=(위조한 mallory 가 업스트림에 닿았으면 yes), http_authz_saw_tenant=·grpc_authz_saw_tenant=(각 인가 서버 로그에 x-tenant 값이 찍혔으면 yes), healthz_asked_authz=(gRPC 인가 서버 로그에 /healthz 가 있으면 yes) — 을 적고, 그 아래 배운 것을 네 줄 이상 적으세요. gRPC 쪽 값을 확인하려면 두 번째 Envoy 로 alice-tokenx-tenant: acme 를 붙인 요청을 한 번 보내 두세요.

참고

HTTP 인가 서버를 띄운다

/root/envd-authz/authz_http.py127.0.0.1:9191 에서 듣는 인가 서버를 쓰고 백그라운드로 띄우세요. Authorization: Bearer alice-token 이면 사용자 alice, bob-token 이면 bob 으로 보고 200 과 응답 헤더 x-auth-user: <사용자> 를, 토큰이 없거나 모르는 값이면 403 과 응답 헤더 x-deny-reason: missing-or-unknown-token 을 돌려줍니다. 요청마다 /root/envd-authz/authz-http.log 에 JSON 한 줄(path·user·tenant(x-tenant 헤더 값)·allowed)을 덧붙입니다.

ext_authz 의 HTTP 방식에서 인가 서버는 평범한 웹 서버입니다. Envoy 가 원래 요청의 메서드·경로로 다시 요청을 보내고, 2xx 면 허용, 그 밖이면 거절로 읽습니다. 그래서 GET 만이 아니라 모든 메서드에 같은 판단을 해야 합니다. 표준 라이브러리 http.server 로 충분하고, 띄울 때는 setsid --fork nohup python3 … > 로그 2>&1 </dev/null 로 셸에서 떼어 내세요. 채점기는 이 서버에 직접 토큰 셋(alice·bob·없음)을 보내 봅니다.

Envoy 가 요청마다 인가 서버에 묻게 한다

업스트림 python3 /opt/lab/envoy/echo.py 8091(받은 요청을 JSON 으로 돌려줌)를 띄우고, /root/envd-authz/envoy-http.yaml 에 관리 포트 9981, 리스너 127.0.0.1:10081 의 설정을 써서 띄우세요 (--base-id 11). HTTP 필터 사슬 맨 앞에 envoy.filters.http.ext_authz 를 두고 stat_prefix: http_authz, http_service 로 클러스터 authz_http(127.0.0.1:9191)에 묻게 합니다. 인가 서버가 준 x-auth-user 는 업스트림으로, x-deny-reason 은 거절 응답과 함께 클라이언트로 넘기고, failure_mode_allow 는 false 입니다.

필터 순서가 곧 처리 순서입니다. ext_authz 가 router 보다 앞에 있어야 거절된 요청이 업스트림에 닿지 않습니다. 인가 서버의 응답 헤더를 어디로 넘길지는 authorization_response 의 두 목록이 정합니다 — allowed_upstream_headers 는 허용일 때 업스트림 요청에, allowed_client_headers 는 거절일 때 클라이언트 응답에 붙습니다. 이 파드에는 Envoy 를 여럿 띄우므로 --base-id 를 서로 다르게 주고, 끌 때는 curl -X POST localhost:9981/quitquitquit 을 쓰세요.

무엇이 인가 서버로, 무엇이 업스트림으로 갔나

리스너 10081 로 세 요청을 보내고 결과를 /root/envd-authz/03-http.txt 에 네 줄로 적으세요 — 토큰 없이 보낸 요청의 상태 코드 no_token=, 그 응답의 x-deny-reasondeny_reason=, bob-token위조한 헤더 x-auth-user: mallory 를 함께 보냈을 때 업스트림이 받은 x-auth-userspoof_upstream=, 그리고 alice-tokenx-tenant: acme 를 붙인 요청이 인가 서버 로그에 남긴 tenanthttp_saw_tenant=(값이 없으면 none).

업스트림이 무엇을 받았는지는 echo 가 돌려주는 JSON 의 headers 에 있습니다. 인가 서버가 무엇을 받았는지는 여러분이 남긴 로그에 있습니다. HTTP 방식의 인가 요청에는 Host·Method·Path·Content-Length·Authorization 만 기본으로 실리고, 다른 헤더는 allowed_headers 에 적어야 넘어갑니다. 반대 방향(인가 서버 → 업스트림)에서 allowed_upstream_headers 는 같은 이름의 헤더를 덮어씁니다 — 클라이언트가 신원 헤더를 위조해도 업스트림에는 인가 서버가 정한 값이 닿는다는 뜻입니다.

gRPC 인가 서버를 띄운다

/root/envd-authz/authz_grpc.pyenvoy.service.auth.v3.Authorization/Check 를 구현해 127.0.0.1:9192 에서 띄우세요(/opt/xds/bin/python3 로 실행합니다 — grpcio 와 Envoy 의 protobuf 가 든 가상 환경입니다). 판단은 HTTP 판과 같되, 경로가 /public 으로 시작하면 토큰 없이 허용하고 이때 사용자는 anonymous 입니다. 허용이면 OkHttpResponse 에 헤더 x-auth-user 를, 거절이면 DeniedHttpResponse 에 상태 403·헤더 x-deny-reason: grpc-missing-or-unknown-token·본문을 담습니다. 요청마다 /root/envd-authz/authz-grpc.log 에 같은 모양의 JSON 한 줄을 남깁니다.

gRPC 방식은 Envoy 가 요청을 다시 보내지 않고 요청의 속성을 CheckRequest 로 넘깁니다. 헤더는 request.attributes.request.http.headers(소문자 이름의 맵), 경로는 .path 에 있습니다. 판정은 CheckResponse.status.code 가 정합니다(google.rpc.code_pb2.OK 면 허용). 모듈 경로는 envoy.service.auth.v3.external_auth_pb2 · _pb2_grpc 입니다. 채점기는 이 서버에 직접 Check 를 세 번 보냅니다.

두 번째 Envoy 는 gRPC 로 묻고, 인가 서버가 없으면 흘려보낸다

/root/envd-authz/envoy-grpc.yaml 에 관리 포트 9982, 리스너 127.0.0.1:10082 인 두 번째 Envoy 를 써서 띄우세요(--base-id 12). ext_authz 는 stat_prefix: grpc_authz, grpc_serviceenvoy_grpc 로 클러스터 authz_grpc(127.0.0.1:9192, HTTP/2)를 부르고, failure_mode_allow: truefailure_mode_allow_header_add: true 를 켭니다. 첫 Envoy 는 그대로 둡니다.

gRPC 는 HTTP/2 위에서만 돕니다. 클러스터에 HTTP/2 를 켜지 않으면 Envoy 가 HTTP/1.1 로 붙다가 실패하고, 그 실패는 '인가 서버 오류' 로 세어집니다 — 이 Envoy 는 실패하면 흘려보내도록 설정했으므로 모든 요청이 통과해 버려 설정이 틀린 것을 알아채기 어렵습니다. 클러스터의 typed_extension_protocol_optionsHttpProtocolOptionsexplicit_http_config.http2_protocol_options 를 넣으세요.

건강 검사 경로는 인가 서버에 묻지 않는다

/root/envd-authz/envoy-grpc.yaml 에 경로가 정확히 /healthz 인 라우트를 맨 앞에 더해, 본문 ok 로 200 을 직접 돌려주고(direct_response) 그 라우트에서만 ext_authz 를 끄세요(typed_per_filter_configExtAuthzPerRoutedisabled: true). 그 Envoy 를 다시 띄운 뒤 토큰 없이 /healthz 를 세 번 부르세요.

로드밸런서의 건강 검사는 토큰을 모릅니다. 인가를 모든 경로에 걸면 건강 검사가 403 을 받고, 로드밸런서는 멀쩡한 프록시를 빼 버립니다. 필터를 끄는 것은 필터 설정이 아니라 라우트 쪽의 몫입니다 — 같은 필터가 경로마다 다르게 동작하게 하는 장치가 typed_per_filter_config 입니다. 끈 경로는 인가 서버에 아예 가지 않으므로 인가 서버 로그에 /healthz 가 남지 않아야 합니다.

인가 서버가 죽으면 — 막는 쪽과 흘리는 쪽

두 인가 서버를 모두 멈춘 채로 첫 Envoy 에는 alice-token 을 붙인 요청을, 두 번째 Envoy 에는 토큰 없는 /orders 요청을 보내고 /root/envd-authz/07-failure.txthttp_on_error=(첫 Envoy 의 상태 코드), grpc_on_error=(두 번째의 상태 코드), grpc_failure_header=(업스트림이 받은 x-envoy-auth-failure-mode-allowed 값) 세 줄을 적으세요. 적은 뒤 두 인가 서버를 다시 띄웁니다.

인가 서버가 응답하지 않으면 Envoy 는 설정에 따라 두 길 중 하나를 고릅니다. 막는 쪽(fail closed)은 status_on_error(기본 403)를 돌려주고, 흘리는 쪽(fail open)은 요청을 그대로 보내면서 원하면 표시 헤더를 붙입니다. 어느 쪽이든 통계의 ext_authz.<stat_prefix>.error 가 오르고, 흘렸다면 failure_mode_allowed 도 오릅니다. 서버를 멈출 때 pkill -f authz_http.py 처럼 쓰면 그 문자열이 든 셸도 함께 죽을 수 있으니 pkill -f '[a]uthz_http.py' 처럼 첫 글자를 대괄호로 감싸세요.

어느 쪽으로 실패할지 정한 근거를 남긴다

/root/envd-authz/08-report.md 에 여섯 줄 — fail_closed_code=·fail_open_code=(7단계의 두 코드), spoofed_header_reached_upstream=(위조한 mallory 가 업스트림에 닿았으면 yes), http_authz_saw_tenant=·grpc_authz_saw_tenant=(각 인가 서버 로그에 x-tenant 값이 찍혔으면 yes), healthz_asked_authz=(gRPC 인가 서버 로그에 /healthz 가 있으면 yes) — 을 적고, 그 아래 배운 것을 네 줄 이상 적으세요. gRPC 쪽 값을 확인하려면 두 번째 Envoy 로 alice-tokenx-tenant: acme 를 붙인 요청을 한 번 보내 두세요.

값은 기억이 아니라 증거 파일과 두 인가 서버의 로그에서 옮기세요. 설명 줄에는 '인가 서버가 죽었을 때 막는 쪽을 고르면 무엇을 잃고, 흘리는 쪽을 고르면 무엇을 잃는가' 를 자기 말로 적어 두세요. 채점기는 같은 로그와 파일을 읽어 대조합니다.