LabHub
开始
学习 学习路径 课程

会话与令牌 — 从浏览器到服务网格

Envoy jwt_authn 与两种身份

在 LabHub 中继续学习

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

목표

Envoy 의 jwt_authn 필터로 로컬 JWKS 의 RS256 토큰을 검증해 틀린 요청을 상류 앞에서 끊고, 검증된 클레임만 헤더로 넘긴 뒤, mTLS 로 증명되는 워크로드 신원과 JWT 로 증명되는 사용자 신원이 서로 다른 것임을 실제 요청으로 확인한다.

왜 중요한가

메시를 들이면 토큰 검증을 서비스마다 반복하지 않고 모든 서비스 앞에 선 프록시 한 곳의 설정으로 옮길 수 있다. 그 대신 상류는 프록시가 넘겨 준 헤더를 신원으로 믿게 되므로, 검증을 건너뛰는 경로에서 흉내 낸 헤더가 새어 들어오면 곧바로 신원 위조가 된다. 또 서비스 사이에 mTLS 가 걸려 있다고 사용자 인증이 끝난 것이 아니다. 인증서가 말해 주는 것은 어느 워크로드가 연결했는지뿐이고, 누구를 대신한 요청인지는 토큰만 말해 준다. 이 실습은 두 신원을 한 요청 안에서 나란히 보게 한다.

단계

  1. /root/st/mesh/private.pem 에 RSA 2048 개인 키를, 그 공개 성분만 담은 JWK Set 을 /root/st/mesh/jwks.json 에(kid mesh-k1, RS256) 두고, /root/st/mesh/mint.py 로 RS256 토큰을 찍는다(iss https://issuer.mesh.lab, aud mesh-api).
  2. /root/st/mesh/envoy.yaml 에 관리 19901, 리스너 18000, 클러스터 app(18081), jwt_authn(provider mesh) 다음 router 를 두고 envoy --mode validate 로 검사한다.
  3. /root/st/mesh/upstream.py 에코 서버를 18081 에, Envoy 를 띄우고 alice 토큰으로 /api/orders 를 요청해 /root/st/mesh/allow.txtcode=200, path=/api/orders 를 적는다.
  4. 토큰 없음·위조·만료·다른 청중을 보내 /root/st/mesh/deny.txt 에 401·401·401·403 을 적고, 거절된 요청이 상류 로그에 남지 않게 한다.
  5. forward_payload_header: x-jwt-payload 를 더해 상류가 받은 클레임을 /root/st/mesh/claims.txt 에 적는다.
  6. /public 을 검증 예외로 열고 그 경로에서 x-jwt-payload 를 지운 뒤 /root/st/mesh/public.txt 에 200·401·absent 를 적는다.
  7. /root/st/mesh/pki/ 에 CA·서버·클라이언트 인증서를 만들고 18000 에 mTLS 체인을 더한 뒤 /root/st/mesh/e2e.sh 로 두 신원을 시험해 /root/st/mesh/e2e.out 에 일곱 줄을 남긴다.

참고

서명 키와 JWKS 만들기

/root/st/mesh/private.pem 에 RSA 2048 개인 키를 만들고, 그 공개 성분만 담은 JWK Set 을 /root/st/mesh/jwks.json 에 둔다(키 하나: kty RSA, kid mesh-k1, alg RS256, use sig, n, e). /root/st/mesh/mint.pypython3 /root/st/mesh/mint.py <sub> 로 RS256 토큰 한 줄을 출력한다 — 헤더 kidmesh-k1, 클레임은 iss https://issuer.mesh.lab, aud mesh-api, sub, iat, exp(기본 5분 뒤). --aud, --ttl(초, 음수면 이미 만료), --key(다른 개인 키) 옵션을 받게 한다.

JWT 는 base64url(헤더).base64url(페이로드).base64url(서명) 세 조각이고, 서명 대상은 앞 두 조각을 점으로 이은 문자열입니다. RS256 은 cryptography 의 key.sign(데이터, padding.PKCS1v15(), hashes.SHA256()) 입니다. base64url 은 끝의 = 를 떼어 냅니다. JWK 의 n·e 는 공개 키 숫자(public_numbers())를 빅엔디언 바이트로 바꿔 같은 방식으로 인코딩합니다. JWKS 는 누구에게나 공개되는 파일이므로 개인 성분(d·p·q)이 들어가면 안 됩니다.

jwt_authn 설정을 검증하기

/root/st/mesh/envoy.yaml 을 만든다 — 관리 127.0.0.1:19901, 리스너 127.0.0.1:18000, 클러스터 app127.0.0.1:18081. HTTP 필터는 envoy.filters.http.jwt_authn 다음에 envoy.filters.http.router 순서다. provider 이름은 mesh, issuer https://issuer.mesh.lab, audiences [mesh-api], local_jwks.filename /root/st/mesh/jwks.json, 규칙은 prefix /provider_name: mesh 를 요구한다. envoy --mode validate -c /root/st/mesh/envoy.yaml 이 통과해야 한다.

필터 사슬은 순서대로 돕니다. 검증 필터가 router 뒤에 있으면 요청은 이미 상류로 나간 뒤라 아무 의미가 없습니다. rulesrequires 가 비어 있으면 그 경로는 검증하지 않습니다 — 토큰이 있으면 검사하는 것이 아니라 아예 보지 않는 것입니다. allow_missing_or_failed 는 틀린 토큰도 통과시키므로 이 단계에서는 쓰지 않습니다. --mode validate 는 포트를 열지 않고 설정만 읽어 보므로, 띄우기 전에 필드 이름 실수를 잡는 데 씁니다.

유효한 토큰은 상류까지 간다

/root/st/mesh/upstream.py127.0.0.1:18081 에 띄운다 — 어떤 GET 경로든 200 과 {"path": 요청 경로, "headers": {소문자 헤더 이름: 값}} JSON 을 돌려주고, 같은 JSON 을 /root/st/mesh/upstream.log 에 한 줄씩 덧붙인다. Envoy 를 /root/st/mesh/envoy.yaml 로 띄우고, mint.py alice 토큰으로 http://127.0.0.1:18000/api/orders 를 요청해 /root/st/mesh/allow.txtcode=200path=/api/orders 두 줄을 적는다.

상류는 인증을 하지 않습니다. 앞에 선 Envoy 가 검증했다고 믿고, 받은 것을 그대로 보여 주기만 합니다 — 그래서 상류가 무엇을 받았는지가 곧 실습의 증거가 됩니다. 서버와 Envoy 는 셸에서 떼어 백그라운드로 띄우고(setsid --fork nohup ...), Envoy 는 관리 포트의 /ready 가 LIVE 가 될 때까지 기다립니다. 토큰은 Authorization: Bearer <토큰> 헤더로 보냅니다.

토큰이 없거나 위조되면 상류 앞에서 끊는다

같은 /api/orders 로 네 가지를 보내 /root/st/mesh/deny.txtmissing=(토큰 없음) · forged=(다른 RSA 키로 서명하고 kid 는 그대로 둔 토큰) · expired=(--ttl -600) · wrong_aud=(--aud billing-api) 네 줄로 받은 코드를 적는다. 앞의 셋은 401, 마지막은 403 이어야 하고, 거절된 요청은 /root/st/mesh/upstream.log 에 한 건도 남지 않아야 한다.

공격자는 kid 도 클레임도 마음대로 적을 수 있습니다. 가질 수 없는 것은 발급자의 개인 키 하나뿐이고, 그래서 서명 검증이 방어의 전부입니다. 위조 토큰은 openssl genpkey 로 키를 하나 더 만들어 mint.py --key 로 찍으면 됩니다. 401 과 403 이 갈리는 지점을 보세요 — 서명·만료가 틀리면 '누구인지 모른다', 서명은 맞는데 청중이 다르면 '누구인지는 알지만 여기 올 토큰이 아니다' 입니다. 채점기는 요청마다 표식 헤더를 붙여 보내고 상류 로그에 그 표식이 찍혔는지까지 봅니다.

검증된 클레임을 하류에 넘긴다

provider meshforward_payload_header: x-jwt-payload 를 더하고(forward 는 기본값 false 그대로) Envoy 를 다시 띄운다. mint.py alice 토큰으로 요청해 상류가 받은 헤더를 풀어 /root/st/mesh/claims.txtsub=alice, aud=mesh-api, iss=https://issuer.mesh.lab, authorization_forwarded=no 네 줄을 적는다.

상류가 토큰을 다시 검증하지 않아도 되게, Envoy 는 검증에 성공한 페이로드를 헤더로 실어 줍니다. 값은 패딩 없는 base64url 로 인코딩한 JSON 입니다. forward 가 false 이면 검증 뒤 원래 토큰은 요청에서 지워집니다 — 상류가 받은 토큰을 다른 곳에 다시 쓸 수 없게 하는 기본값입니다. 클라이언트가 같은 이름의 헤더를 흉내 내 보내면 어떻게 되는지도 한 번 보내 보세요. 채점기는 그것도 확인합니다. Envoy 는 정적 설정을 다시 읽지 않으니 고쳤으면 내리고 다시 띄웁니다.

공개 경로를 열고 흉내 낸 신원은 지운다

jwt_authn 규칙에 prefix /publicrequires 없이 / 규칙보다 앞에 두고, 라우트에도 prefix /public 을 따로 두어 request_headers_to_removex-jwt-payload 를 지운다. Envoy 를 다시 띄운 뒤 /root/st/mesh/public.txtpublic=(토큰 없이 /public/status 의 코드) · api=(토큰 없이 /api/orders 의 코드) · spoofed_payload=(가짜 x-jwt-payload 를 붙여 /public/status 로 보냈을 때 상류가 그 헤더를 받았으면 present, 아니면 absent) 세 줄을 적는다. 기대값은 200 · 401 · absent 다.

규칙은 처음 맞는 것이 이깁니다. / 가 먼저 오면 모든 경로가 거기에 걸려 공개 경로가 생기지 않습니다. 검증하는 경로에서는 Envoy 가 검증된 값으로 x-jwt-payload 를 덮어쓰지만, 검증하지 않는 경로에서는 아무도 그 헤더를 건드리지 않습니다. 상류가 그 헤더를 신원으로 믿는다면, 공개 경로가 곧 신원 위조 통로가 됩니다. 라우트의 request_headers_to_remove 가 그 구멍을 막습니다.

워크로드 신원과 사용자 신원을 나눠 본다

/root/st/mesh/pki/ 에 CA(ca.pem), 서버 인증서(server.pem·server.key, SAN IP:127.0.0.1URI:spiffe://mesh.lab/ns/shop/sa/orders), 클라이언트 인증서(client.pem·client.key, SAN URI:spiffe://mesh.lab/ns/shop/sa/frontend)를 만든다. 18000 리스너에 tls_inspectortransport_protocol: tls 필터 체인을 더한다 — require_client_certificate: true, trusted_caca.pem, 그 체인의 HCM 은 forward_client_cert_details: SANITIZE_SETset_current_client_cert_detailsuri: true. 다시 띄운 뒤 /root/st/mesh/e2e.sh 로 시험해 /root/st/mesh/e2e.outvalid=200, missing=401, forged=401, workload=spiffe://mesh.lab/ns/shop/sa/frontend(mTLS 로 alice 토큰을 보냈을 때 상류가 받은 XFCC 의 URI), user=alice(같은 요청의 x-jwt-payload 의 sub), mtls_without_jwt=401(인증서만 있고 토큰 없음), without_client_cert=rejected(토큰만 있고 인증서 없음, 핸드셰이크 실패) 일곱 줄을 남긴다.

mTLS 는 어느 워크로드가 연결을 맺었는지를, JWT 는 어느 사람을 대신한 요청인지를 증명합니다. 인증서만 있고 토큰이 없으면 401 이 나와야 하는 이유가 여기 있습니다 — 프런트엔드 파드라는 사실이 앨리스라는 뜻은 아닙니다. TLS 와 평문을 한 포트에서 받으려면 리스너 필터 tls_inspector 가 연결의 첫 바이트를 보고 체인을 고르게 합니다. XFCC 의 기본 동작은 SANITIZE 라 평문 연결로 흉내 낸 XFCC 는 지워집니다. SANITIZE_SET 은 mTLS 일 때 받은 것을 버리고 자기가 검증한 인증서 정보로 새로 채웁니다. 서버 인증서에 IP:127.0.0.1 SAN 이 없으면 curl 이 서버를 믿지 않습니다. openssl x509 -req ... -extfile 로 SAN 을 넣습니다.