<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Token Exchange on jyukki's Blog</title><link>https://jyukki.com/tags/token-exchange/</link><description>Recent content in Token Exchange on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Mon, 13 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/token-exchange/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Token Exchange와 Downscoped Token, 서비스 간 권한을 좁혀 넘기는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-token-exchange-downscoped-token-playbook/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-token-exchange-downscoped-token-playbook/</guid><description>사용자 토큰이나 넓은 서비스 계정 토큰을 그대로 전달하지 않고, 대상 서비스와 작업 범위에 맞는 짧은 수명의 downscoped token으로 바꾸는 실무 기준을 정리합니다.</description><content:encoded><![CDATA[<p>서비스가 작을 때는 인증 토큰 전달이 단순해 보입니다. API gateway가 사용자 access token을 받고, 내부 서비스가 그 토큰을 검증한 뒤 필요한 데이터를 조회합니다. 문제는 서비스가 늘어나면서 시작됩니다. 주문 서비스가 결제 서비스와 쿠폰 서비스와 파일 서비스를 호출하고, 관리자 화면이 사용자 토큰으로 export worker를 트리거하고, batch가 하나의 넓은 service token으로 여러 시스템을 건드립니다. 이때 원본 토큰을 그대로 넘기면 &ldquo;누가 무엇을 위해 호출했는가&quot;가 흐려지고, 토큰 하나가 탈취됐을 때 피해 반경도 커집니다.</p>
<p>Token Exchange와 downscoped token의 목적은 토큰을 더 많이 만드는 것이 아닙니다. <strong>원래 가진 권한을 대상 서비스, 작업, 시간, 사용자 맥락에 맞게 좁혀서 넘기는 것</strong>입니다. 이 글은 <a href="/learning/deep-dive/deep-dive-oauth2-oidc/">OAuth2/OIDC 실무</a>, <a href="/learning/deep-dive/deep-dive-spring-security-oauth2-jwt-practical/">Spring Security OAuth2/JWT Practical</a>, <a href="/learning/deep-dive/deep-dive-secret-management/">시크릿 관리</a>, <a href="/learning/deep-dive/deep-dive-api-key-lifecycle-rotation-revocation-playbook/">API Key Lifecycle</a>과 이어서 보면 좋습니다. 핵심은 인증 성공이 아니라, downstream 호출의 권한 계약을 작게 만드는 것입니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>사용자 토큰 전달, 서비스 계정 토큰, Token Exchange의 차이를 구분할 수 있습니다.</li>
<li><code>subject</code>, <code>actor</code>, <code>audience</code>, <code>scope</code>, <code>resource</code>, <code>ttl</code>을 어떤 기준으로 설계할지 감을 잡을 수 있습니다.</li>
<li>downscoped token을 도입할 때 생기는 latency, cache, revocation, 장애 fallback 문제를 미리 볼 수 있습니다.</li>
<li>서비스 간 권한 전달을 &ldquo;토큰 문자열&quot;이 아니라 감사 가능한 운영 계약으로 문서화할 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-downstream에-원본-토큰을-그대로-넘기면-confused-deputy가-생긴다">1) downstream에 원본 토큰을 그대로 넘기면 confused deputy가 생긴다</h3>
<p>사용자 access token을 내부 서비스 전체에 그대로 전달하는 구조는 구현이 쉽습니다. 하지만 토큰의 <code>aud</code>가 gateway나 public API를 가리키고 있는데 결제 서비스가 그것을 받아들이면, audience 검증이 약해집니다. 또한 원본 토큰에 <code>profile:read</code>, <code>order:write</code>, <code>admin:export</code> 같은 넓은 scope가 섞여 있으면 downstream은 자기에게 필요한 권한보다 더 많은 정보를 보게 됩니다.</p>
<p>서비스 계정 토큰도 비슷합니다. <code>batch-service</code>라는 하나의 토큰이 주문 수정, 결제 조회, 파일 export, 관리자 알림까지 모두 할 수 있다면 운영자는 장애 때 &ldquo;이 호출이 어떤 업무 권한으로 실행됐는가&quot;를 분리하기 어렵습니다. 이 문제는 <a href="/learning/deep-dive/deep-dive-permission-drift-access-review-playbook/">Permission Drift와 Access Review</a>의 권한 누적 문제와 같습니다. 권한은 처음에는 작아도 예외가 쌓이면 넓어집니다.</p>
<p>Token Exchange는 이 지점에 들어갑니다. 원본 사용자 토큰이나 service credential을 받아, 대상 서비스가 이해할 수 있는 좁은 토큰으로 바꿉니다. 예를 들어 BFF가 <code>GET /me/orders</code> 요청을 처리하며 주문 서비스에 호출할 때, 원본 토큰을 그대로 넘기는 대신 <code>aud=orders-api</code>, <code>scope=orders.read</code>, <code>sub=user-123</code>, <code>act=bff-service</code>, <code>ttl=10m</code> 토큰을 발급받아 전달합니다.</p>
<h3 id="2-subject와-actor를-분리해야-감사가-된다">2) subject와 actor를 분리해야 감사가 된다</h3>
<p>downscoped token에서 가장 자주 빠지는 개념이 <code>actor</code>입니다. <code>subject</code>는 권한의 대상이 되는 사용자나 계정이고, <code>actor</code>는 실제 호출을 수행하는 서비스입니다. 사용자가 웹에서 버튼을 눌러 export를 요청했다면 <code>sub=user-123</code>, <code>act=admin-bff</code>가 될 수 있습니다. 야간 batch가 시스템 정책에 따라 정산을 조회한다면 <code>sub</code> 없이 <code>act=billing-reconciler</code>만 둘 수도 있습니다.</p>
<p>이 둘을 섞으면 감사가 흐려집니다. 모든 호출이 사용자 <code>sub</code>만 남으면 어떤 서비스가 호출했는지 모릅니다. 반대로 모든 호출이 서비스 계정으로만 보이면 사용자의 위임인지 시스템 자동화인지 구분하지 못합니다. 실무 claim은 아래 정도를 기본값으로 둘 수 있습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">claims</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">iss</span>: <span style="color:#f1fa8c">&#34;https://auth.internal.example&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">sub</span>: <span style="color:#f1fa8c">&#34;user-123&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">act</span>: <span style="color:#f1fa8c">&#34;order-bff&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">aud</span>: <span style="color:#f1fa8c">&#34;payment-api&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">scope</span>: [<span style="color:#f1fa8c">&#34;payment.method.read&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">resource</span>: <span style="color:#f1fa8c">&#34;customer:123&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">exp</span>: <span style="color:#f1fa8c">&#34;now+10m&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">policy_version</span>: <span style="color:#f1fa8c">&#34;token-exchange-v3&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">exchange_id</span>: <span style="color:#f1fa8c">&#34;txe_20260713_...&#34;</span>
</span></span></code></pre></div><p>여기서 <code>exchange_id</code>는 운영에 중요합니다. downstream 로그, token exchange server 로그, gateway 로그를 하나로 묶어야 나중에 &ldquo;누가 어떤 원본 권한으로 어떤 좁은 토큰을 받았는가&quot;를 추적할 수 있습니다.</p>
<h3 id="3-audience는-하나로-좁히고-scope는-작업-단위로-나눈다">3) audience는 하나로 좁히고 scope는 작업 단위로 나눈다</h3>
<p>downscoped token의 기본 규칙은 간단합니다. 토큰 하나는 audience 하나를 향해야 합니다. <code>aud=[&quot;orders-api&quot;,&quot;payment-api&quot;,&quot;file-api&quot;]</code>처럼 여러 대상을 한 토큰에 넣으면 다시 넓은 토큰이 됩니다. scope도 서비스가 실제로 요구하는 작업 단위로 줄여야 합니다.</p>
<p>권장 시작 기준:</p>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>출발 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>audience</td>
          <td>토큰 1개당 1개 service 또는 1개 resource server</td>
      </tr>
      <tr>
          <td>scope 개수</td>
          <td>일반 요청 1~3개, 5개 초과면 정책 재검토</td>
      </tr>
      <tr>
          <td>TTL</td>
          <td>기본 5<del>15분, 고위험 write/export/delete는 1</del>5분</td>
      </tr>
      <tr>
          <td>refresh token</td>
          <td>내부 downscoped token에는 기본 발급 금지</td>
      </tr>
      <tr>
          <td>claims</td>
          <td>email, phone, raw role list 같은 개인정보와 고카디널리티 값 최소화</td>
      </tr>
      <tr>
          <td>key rotation</td>
          <td><code>kid</code> 기반, 신/구 키 동시 검증 기간은 token max TTL 이상</td>
      </tr>
  </tbody>
</table>
<p>scope 이름은 너무 기술적으로 쪼개도 어렵고, 너무 넓어도 위험합니다. <code>db.read</code>는 너무 넓고, <code>select_order_table_by_id</code>는 너무 구현 종속적입니다. <code>orders.read</code>, <code>orders.cancel</code>, <code>files.export.create</code>, <code>payment.method.read</code>처럼 업무 행위와 resource server가 함께 보이는 이름이 운영에서 읽기 쉽습니다.</p>
<h3 id="4-revocation은-ttl만으로-해결하지-않는다">4) revocation은 TTL만으로 해결하지 않는다</h3>
<p>짧은 TTL은 강력한 방어선입니다. 하지만 권한 회수가 1분 안에 필요하거나, 관리자 권한 변경처럼 즉시 차단해야 하는 경로라면 TTL만 믿으면 부족합니다. 예를 들어 사용자가 퇴사 처리됐는데 10분짜리 downscoped token이 이미 발급되어 있다면, 그 10분 동안 호출이 가능할 수 있습니다.</p>
<p>회수 요구에 따라 검증 방식을 나눕니다.</p>
<table>
  <thead>
      <tr>
          <th>권한 위험도</th>
          <th>검증 방식</th>
          <th>기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>낮음</td>
          <td>JWT 로컬 검증</td>
          <td>TTL 10~15분, scope read-only</td>
      </tr>
      <tr>
          <td>중간</td>
          <td>JWT + policy_version claim</td>
          <td>정책 변경 시 새 버전만 허용</td>
      </tr>
      <tr>
          <td>높음</td>
          <td>introspection 또는 denylist 확인</td>
          <td>export, delete, admin, payment write</td>
      </tr>
      <tr>
          <td>매우 높음</td>
          <td>요청별 PDP 확인</td>
          <td>계정 폐쇄, 권한 상승, 대량 데이터 이동</td>
      </tr>
  </tbody>
</table>
<p>모든 호출에 introspection을 붙이면 지연과 장애 의존성이 커집니다. 반대로 모든 호출을 로컬 JWT 검증으로만 처리하면 회수성이 약합니다. 그래서 위험도별로 나눠야 합니다. <a href="/learning/deep-dive/deep-dive-api-key-lifecycle-rotation-revocation-playbook/">API Key Lifecycle</a>에서 키 폐기와 사용량 관측을 같이 보듯, token exchange도 발급과 회수를 한 쌍으로 봐야 합니다.</p>
<h3 id="5-token-exchange-server는-hot-path-의존성이-된다">5) token exchange server는 hot path 의존성이 된다</h3>
<p>Token Exchange를 도입하면 인증 서버가 더 자주 호출됩니다. BFF가 사용자 요청마다 downstream별 토큰을 새로 받으면 latency와 가용성 문제가 생깁니다. 따라서 발급 정책과 cache 정책을 함께 설계해야 합니다.</p>
<p>권장 지표:</p>
<ul>
<li><code>token_exchange_latency_p95</code>: 50ms 이하를 1차 목표로 둔다.</li>
<li><code>token_exchange_error_rate</code>: 0.1% 초과 시 degraded mode 검토.</li>
<li><code>downscoped_token_cache_hit_ratio</code>: read-only 고빈도 호출은 80% 이상 목표.</li>
<li><code>audience_mismatch_denied</code>: 0이 정상, 배포 직후 증가하면 설정 오류 후보.</li>
<li><code>scope_denied_rate</code>: 기능 오류인지 공격 신호인지 owner가 봐야 한다.</li>
</ul>
<p>캐시는 조심해야 합니다. cache key에는 <code>subject</code>, <code>actor</code>, <code>audience</code>, <code>scope set</code>, <code>resource</code>, <code>policy_version</code>이 들어가야 합니다. scope 순서가 다르다고 다른 토큰을 만들 필요는 없으므로 정렬된 scope set으로 canonicalization합니다. 단, 고위험 작업 토큰은 cache를 끄거나 TTL의 20~30%만 cache하는 편이 안전합니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-원본-토큰-전달-경로부터-inventory로-만든다">1) 원본 토큰 전달 경로부터 inventory로 만든다</h3>
<p>처음부터 모든 서비스를 바꾸려 하지 말고, 현재 토큰이 어디로 흐르는지 표로 만듭니다.</p>
<table>
  <thead>
      <tr>
          <th>호출자</th>
          <th>대상</th>
          <th>현재 방식</th>
          <th>위험</th>
          <th>전환 후보</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>web-bff</td>
          <td>orders-api</td>
          <td>user access token 전달</td>
          <td>audience 불일치</td>
          <td><code>aud=orders-api</code>, <code>orders.read</code></td>
      </tr>
      <tr>
          <td>admin-bff</td>
          <td>export-worker</td>
          <td>admin token 전달</td>
          <td>대량 데이터 export</td>
          <td><code>files.export.create</code>, TTL 3분</td>
      </tr>
      <tr>
          <td>billing-job</td>
          <td>payment-api</td>
          <td>broad service token</td>
          <td>write 권한 과다</td>
          <td><code>payment.settlement.read</code></td>
      </tr>
      <tr>
          <td>support-tool</td>
          <td>user-api</td>
          <td>shared API key</td>
          <td>사용자별 감사 약함</td>
          <td>actor + reason claim 추가</td>
      </tr>
  </tbody>
</table>
<p>우선순위는 <code>대량 데이터</code>, <code>결제/정산</code>, <code>권한 변경</code>, <code>삭제</code>, <code>외부 전송</code> 순서로 잡습니다. read-only 내부 조회보다 되돌리기 어려운 작업부터 좁히는 편이 효과가 큽니다.</p>
<h3 id="2-정책을-코드보다-먼저-쓴다">2) 정책을 코드보다 먼저 쓴다</h3>
<p>Token Exchange 정책은 코드 안에 흩어지면 안 됩니다. 최소한 아래처럼 선언형 표나 설정으로 관리합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">token_exchange_policies</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ff79c6">name</span>: order_bff_to_payment_method_read
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">actor</span>: order-bff
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">source_subject</span>: user
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">audience</span>: payment-api
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">scopes</span>: [<span style="color:#f1fa8c">&#34;payment.method.read&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">resource_pattern</span>: <span style="color:#f1fa8c">&#34;customer:{customer_id}&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">ttl_seconds</span>: <span style="color:#bd93f9">600</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">cache_seconds</span>: <span style="color:#bd93f9">120</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">require_reason</span>: <span style="color:#ff79c6">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">revocation_mode</span>: jwt_with_policy_version
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ff79c6">name</span>: admin_export_create
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">actor</span>: admin-bff
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">source_subject</span>: admin_user
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">audience</span>: export-worker
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">scopes</span>: [<span style="color:#f1fa8c">&#34;files.export.create&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">ttl_seconds</span>: <span style="color:#bd93f9">180</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">cache_seconds</span>: <span style="color:#bd93f9">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">require_reason</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">revocation_mode</span>: introspection
</span></span></code></pre></div><p>이 설정은 보안팀만 보는 문서가 아니라 코드 리뷰 대상이어야 합니다. 새 scope가 추가되면 API 변경처럼 리뷰합니다. 특히 <code>*.write</code>, <code>*.delete</code>, <code>*.export</code>, <code>*.admin</code>은 owner 승인을 요구하는 식으로 정책을 둡니다.</p>
<h3 id="3-downstream은-audience와-scope를-반드시-검증한다">3) downstream은 audience와 scope를 반드시 검증한다</h3>
<p>Token Exchange를 붙여도 downstream이 <code>aud</code>를 검증하지 않으면 효과가 약합니다. 대상 서비스는 최소 아래를 확인해야 합니다.</p>
<ol>
<li>issuer가 신뢰된 내부 발급자인가</li>
<li>signature와 <code>kid</code>가 유효한가</li>
<li><code>exp</code>, <code>nbf</code>, clock skew 허용 범위가 맞는가</li>
<li><code>aud</code>가 자기 서비스인가</li>
<li>필요한 scope가 있는가</li>
<li>resource claim이 요청 path/body와 일치하는가</li>
<li>고위험 작업이면 policy version 또는 introspection 결과가 유효한가</li>
</ol>
<p>clock skew 허용은 보통 30~60초에서 시작합니다. 5분 이상으로 넓히면 만료 토큰을 너무 오래 받아들이게 됩니다. 이 부분은 <a href="/learning/deep-dive/deep-dive-clock-skew-time-semantics-playbook/">시계 skew와 시간 의미</a>와도 연결됩니다.</p>
<h3 id="4-장애-fallback은-원본-토큰으로-우회가-아니다">4) 장애 fallback은 &ldquo;원본 토큰으로 우회&quot;가 아니다</h3>
<p>token exchange server가 장애라고 해서 원본 사용자 토큰이나 broad service token으로 우회하면, 장애 순간에 보안 경계가 무너집니다. fallback은 위험도별로 정해야 합니다.</p>
<ul>
<li>read-only, 낮은 위험: 짧은 시간 동안 기존 cached token 사용. 최대 cache TTL은 원래 TTL 이하.</li>
<li>중간 위험: degraded read만 허용하고 write/export는 차단.</li>
<li>높은 위험: fail closed. 관리자에게 명확한 오류와 incident id 제공.</li>
<li>batch: 새 token 발급이 안 되면 새 작업 시작 금지, 이미 발급된 토큰의 TTL 안에서만 진행.</li>
</ul>
<p>권장 기준은 <code>token_exchange_error_rate</code>가 5분 동안 1%를 넘으면 degraded mode를 켜고, 5%를 넘으면 고위험 scope 발급을 전면 중지하는 것입니다. 이 숫자는 서비스 특성에 맞게 조정하되, &ldquo;장애니까 넓은 토큰으로 임시 우회&quot;는 break-glass 승인과 감사 로그 없이는 금지하는 편이 안전합니다.</p>
<h3 id="5-로그에는-토큰-원문이-아니라-exchange-evidence를-남긴다">5) 로그에는 토큰 원문이 아니라 exchange evidence를 남긴다</h3>
<p>토큰 원문은 로그에 남기면 안 됩니다. 대신 아래 필드를 남깁니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;exchange_id&#34;</span>: <span style="color:#f1fa8c">&#34;txe_20260713_abc&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;actor&#34;</span>: <span style="color:#f1fa8c">&#34;order-bff&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;subject_type&#34;</span>: <span style="color:#f1fa8c">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;audience&#34;</span>: <span style="color:#f1fa8c">&#34;payment-api&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;scopes&#34;</span>: [<span style="color:#f1fa8c">&#34;payment.method.read&#34;</span>],
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;resource&#34;</span>: <span style="color:#f1fa8c">&#34;customer:123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;policy_version&#34;</span>: <span style="color:#f1fa8c">&#34;token-exchange-v3&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;ttl_seconds&#34;</span>: <span style="color:#bd93f9">600</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;result&#34;</span>: <span style="color:#f1fa8c">&#34;issued&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>downstream 로그에도 같은 <code>exchange_id</code>를 남기면 추적이 쉬워집니다. <a href="/learning/deep-dive/deep-dive-structured-logging/">구조화 로깅</a>과 <a href="/learning/deep-dive/deep-dive-tamper-evident-audit-log-playbook/">Tamper-evident Audit Log</a>의 원칙처럼, 원문 secret이 아니라 검증 가능한 식별자와 정책 버전을 남기는 것이 핵심입니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, token exchange는 latency를 추가합니다. 모든 내부 호출마다 새 토큰을 발급받으면 평균 지연보다 p95/p99가 먼저 나빠질 수 있습니다. read-only 고빈도 호출은 cache를 쓰되, cache key와 TTL을 엄격히 잡아야 합니다.</p>
<p>둘째, 정책이 너무 잘게 쪼개지면 운영자가 이해하지 못합니다. scope가 200개를 넘어가고 owner가 없으면 최소 권한이 아니라 정책 부채가 됩니다. 서비스별 핵심 scope 5~20개 안에서 시작하고, 예외는 review queue로 보내는 편이 현실적입니다.</p>
<p>셋째, JWT self-contained 토큰은 빠르지만 회수가 어렵습니다. 짧은 TTL, policy version, denylist, introspection을 위험도별로 섞어야 합니다. &ldquo;JWT니까 stateless&quot;라는 말로 회수 요구를 무시하면 보안 사고 때 대응이 늦습니다.</p>
<p>넷째, mTLS와 token exchange를 혼동하면 안 됩니다. mTLS는 호출 서비스가 누구인지와 채널 보안을 확인합니다. downscoped token은 그 서비스가 어떤 사용자 맥락과 어떤 업무 목적을 가지고 호출하는지 확인합니다. 둘 중 하나만으로는 설명력이 부족할 수 있습니다.</p>
<p>다섯째, 내부 시스템이라고 개인정보 claim을 과하게 넣으면 토큰 자체가 민감 데이터 묶음이 됩니다. email, phone, raw role list, 조직 전체 권한 배열은 가능한 한 빼고, 필요한 경우 내부 ID와 policy lookup으로 대체합니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<ul>
<li><input disabled="" type="checkbox"> 사용자 access token을 downstream에 그대로 전달하는 API가 목록화되어 있다.</li>
<li><input disabled="" type="checkbox"> audience가 자기 서비스가 아닌 토큰을 resource server가 거절한다.</li>
<li><input disabled="" type="checkbox"> downscoped token TTL, scope 수, cache TTL, revocation mode가 위험도별로 다르다.</li>
<li><input disabled="" type="checkbox"> 고위험 scope에는 reason, ticket, actor, exchange_id가 남는다.</li>
<li><input disabled="" type="checkbox"> token exchange server 장애 시 원본 토큰으로 우회하지 않는 fallback 정책이 있다.</li>
<li><input disabled="" type="checkbox"> 로그에는 token 원문이 아니라 exchange evidence와 policy version만 남는다.</li>
<li><input disabled="" type="checkbox"> <code>scope_denied_rate</code>, <code>audience_mismatch_denied</code>, <code>exchange_latency_p95</code>, <code>cache_hit_ratio</code> 대시보드가 있다.</li>
</ul>
<p>연습은 하나면 충분합니다. 현재 서비스에서 &ldquo;관리자가 사용자 데이터를 CSV로 export한다&quot;는 흐름을 고르고, 원본 사용자 토큰이 어디까지 이동하는지 그려 보세요. 그다음 <code>actor</code>, <code>subject</code>, <code>audience</code>, <code>scope</code>, <code>resource</code>, <code>ttl</code>, <code>revocation_mode</code>, <code>fallback</code>을 한 줄씩 채웁니다. <code>audience</code>가 2개 이상이거나 TTL이 15분을 넘거나 export scope가 cache된다면, 왜 그래야 하는지 owner가 설명할 수 있어야 합니다.</p>
<p>오늘의 결론은 단순합니다. 서비스 간 인증은 &ldquo;토큰이 유효한가&quot;에서 끝나지 않습니다. 안전한 백엔드 권한 모델은 <strong>누가, 누구를 대신해, 어떤 대상에게, 어떤 작업을, 얼마 동안 허용받았는가</strong>를 좁고 검증 가능한 형태로 남기는 데서 시작합니다.</p>
]]></content:encoded></item></channel></rss>