<?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>Security on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/security/</link>
    <description>Recent content in Security on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Fri, 06 Mar 2026 09:05:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/security/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Kyvernoで始めるKubernetes Admission Policy実践: 事故を減らすポリシー設計プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-06-kubernetes-admission-policy-kyverno-playbook/</link>
      <pubDate>Fri, 06 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-06-kubernetes-admission-policy-kyverno-playbook/</guid>
      <description>KubernetesでKyvernoを使い、現場で運用可能なAdmission Policyを段階導入するための実装手順とトラブル対応を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="kyvernoで始めるkubernetes-admission-policy実践-事故を減らすポリシー設計プレイブック">Kyvernoで始めるKubernetes Admission Policy実践: 事故を減らすポリシー設計プレイブック</h1>
<p>Kubernetes運用で一番つらい事故は、クラスタが壊れるよりも「本来防げたはずのミスがそのまま本番へ入る」ことです。たとえば、<code>latest</code> タグのイメージが本番に入り再現不能になる、<code>resources</code> 未設定でノードが詰まる、<code>privileged</code> コンテナが混入する。これらは人の注意力だけに依存すると必ず再発します。</p>
<p>そこで有効なのが Admission Policy（入場制御）です。本記事では <strong>Kyverno</strong> を使って、現場で本当に運用できるポリシー群を段階導入する手順をまとめます。単なる「denyの例」ではなく、監査→警告→強制の移行、例外管理、CI連携まで含めて解説します。</p>
<h2 id="1-なぜkyvernoなのか">1. なぜKyvernoなのか</h2>
<p>OPA Gatekeeper も強力ですが、Kyvernoは以下の特徴があり、初期導入が比較的スムーズです。</p>
<ul>
<li>YAML中心で書ける（Rego学習コストを後回しにしやすい）</li>
<li>validate / mutate / generate / verifyImages を一貫して扱える</li>
<li>PolicyReportにより違反可視化がしやすい</li>
<li>Pod SecurityやSupply Chain対策との相性が良い</li>
</ul>
<p>「まずルールを回し始める」目的なら、Kyvernoは現実的な選択肢です。</p>
<h2 id="2-先に決めるべき設計原則">2. 先に決めるべき設計原則</h2>
<p>導入前に、以下だけは先に決めておきます。</p>
<ol>
<li><strong>導入フェーズ</strong>: <code>Audit</code> → <code>Enforce</code> を基本にする</li>
<li><strong>責任分界</strong>: プラットフォームチームが共通ポリシー、各チームがアプリ固有例外</li>
<li><strong>例外の期限</strong>: 永久例外は禁止。期限付きで必ず棚卸し</li>
<li><strong>観測性</strong>: 違反数・対象Namespace・上位違反ルールをダッシュボード化</li>
</ol>
<p>この原則なしにルールだけ増やすと、運用が破綻します。</p>
<h2 id="3-最小導入手順3060分">3. 最小導入手順（30〜60分）</h2>
<h3 id="31-kyvernoのインストール">3.1 Kyvernoのインストール</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>helm repo add kyverno https://kyverno.github.io/kyverno/
</span></span><span style="display:flex;"><span>helm repo update
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>helm upgrade --install kyverno kyverno/kyverno <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -n kyverno --create-namespace <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set admissionController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set backgroundController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set cleanupController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set reportsController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>本番では可用性のため、admission/backgroundは最低2レプリカ推奨です。</p>
<h3 id="32-まずはauditモードで3ルール">3.2 まずはAuditモードで3ルール</h3>
<p>最初に効くルールは、次の3つです。</p>
<ul>
<li>イメージタグに <code>latest</code> を禁止</li>
<li>CPU/Memory requests/limits必須</li>
<li><code>privileged: true</code> を禁止</li>
</ul>
<p>例: <code>latest</code> タグ禁止</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">apiVersion</span>: <span style="color:#ae81ff">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">disallow-latest-tag</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validationFailureAction</span>: <span style="color:#ae81ff">Audit</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">background</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">validate-image-tag</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;latestタグは禁止です。固定タグまたはdigestを使用してください。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">foreach</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">list</span>: <span style="color:#e6db74">&#34;request.object.spec.containers&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">deny</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">conditions</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>                  - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;{{ element.image }}&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">Matches</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;.*:latest$&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="33-レポートで現状把握">3.3 レポートで現状把握</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl get policyreport -A
</span></span><span style="display:flex;"><span>kubectl get clusterpolicy
</span></span><span style="display:flex;"><span>kubectl describe clusterpolicy disallow-latest-tag
</span></span></code></pre></td></tr></table>
</div>
</div><p>導入直後は違反が大量に出るのが普通です。ここで「Kyvernoが厳しすぎる」と判断しないでください。違反は“見えていなかった負債”です。</p>
<h2 id="4-実務で効くルールセット具体例">4. 実務で効くルールセット（具体例）</h2>
<h3 id="41-リソース未設定防止">4.1 リソース未設定防止</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">apiVersion</span>: <span style="color:#ae81ff">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">require-resources</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validationFailureAction</span>: <span style="color:#ae81ff">Audit</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">check-resources</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;全コンテナにrequests/limitsを設定してください。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="42-特権設定防止">4.2 特権設定防止</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">apiVersion</span>: <span style="color:#ae81ff">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">disallow-privileged</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validationFailureAction</span>: <span style="color:#ae81ff">Enforce</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#66d9ef">no</span>-<span style="color:#ae81ff">privileged</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;privilegedコンテナは禁止です。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">=(privileged)</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="43-digest固定の推奨supply-chain">4.3 Digest固定の推奨（Supply Chain）</h3>
<p>本来は digest 固定が理想です。移行期は <code>Audit</code> で始め、違反率が下がってから <code>Enforce</code> に切り替えます。</p>
<h2 id="5-auditからenforceへ移行する基準">5. AuditからEnforceへ移行する基準</h2>
<p>「何となく」で切り替えると炎上します。次の客観指標を使うと安全です。</p>
<ul>
<li>直近14日で対象ポリシー違反率が5%未満</li>
<li>主要Namespace（prod/stg/shared）で違反ゼロ</li>
<li>例外申請フロー（Issue/PRテンプレート）が整備済み</li>
<li>当番がトラブル時に切り戻し手順を理解している</li>
</ul>
<p><code>validationFailureAction</code> を一括で上げるのではなく、ルール単位・Namespace単位で段階化するのがポイントです。</p>
<h2 id="6-例外運用のテンプレート">6. 例外運用のテンプレート</h2>
<p>ポリシー導入で最も壊れるのは「例外管理」です。おすすめは以下。</p>
<ul>
<li>例外は <code>PolicyException</code> で明示</li>
<li>期限（例: 14日）を必須にする</li>
<li>チケット番号をannotationで必須化</li>
<li>期限切れを毎日バッチで通知</li>
</ul>
<p>例外YAMLに <code>expires</code> と <code>owner</code> を必須化すると、放置率が一気に下がります。</p>
<h2 id="7-ciに組み込んで本番前に落とす">7. CIに組み込んで“本番前に落とす”</h2>
<p>クラスタ投入時に拒否されるより、PR段階で検知される方が開発体験は良いです。<code>kyverno-cli</code> をCIで実行します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kyverno apply policies/ -r manifests/ --audit-warn
</span></span></code></pre></td></tr></table>
</div>
</div><p>GitHub Actions例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">name</span>: <span style="color:#ae81ff">Validate manifests with Kyverno</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    kyverno version
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    kyverno apply ./policies -r ./k8s --audit-warn</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで「マージ後に初めて失敗する」パターンを減らせます。</p>
<h2 id="8-よくある障害と対処">8. よくある障害と対処</h2>
<h3 id="症状1-正常なpodまで拒否される">症状1: 正常なPodまで拒否される</h3>
<ul>
<li>match条件が広すぎる可能性</li>
<li><code>exclude</code> で <code>kube-system</code> や監視系を一時除外</li>
<li>まずAuditで影響範囲を確認してからEnforceへ</li>
</ul>
<h3 id="症状2-admission-timeoutでデプロイ遅延">症状2: Admission timeoutでデプロイ遅延</h3>
<ul>
<li>Kyvernoコンポーネントのリソース不足を確認</li>
<li>policy数が多い場合、ルール統合とmatch最適化</li>
<li>API Serverとのネットワーク遅延確認</li>
</ul>
<h3 id="症状3-レポートは出るが改善が進まない">症状3: レポートは出るが改善が進まない</h3>
<ul>
<li>違反上位3ルールに絞ってKPI化</li>
<li>チーム別に違反件数を見える化</li>
<li>例外を期限付きに強制</li>
</ul>
<h2 id="9-運用を続けるためのダッシュボード指標">9. 運用を続けるためのダッシュボード指標</h2>
<p>最低限、以下を可視化してください。</p>
<ul>
<li>ルール別違反件数（7日移動平均）</li>
<li>Namespace別違反件数</li>
<li><code>Audit</code> と <code>Enforce</code> の比率</li>
<li>期限切れ例外の件数</li>
<li>deploy失敗要因のうちpolicy起因の割合</li>
</ul>
<p>これを見ないと、ポリシーは「導入しただけ」で止まります。</p>
<h2 id="10-実践ロードマップ最初の4週間">10. 実践ロードマップ（最初の4週間）</h2>
<ul>
<li><strong>Week 1</strong>: Kyverno導入、3ルールをAuditで開始</li>
<li><strong>Week 2</strong>: 違反上位を改善、例外テンプレート導入</li>
<li><strong>Week 3</strong>: 一部NamespaceでEnforce化</li>
<li><strong>Week 4</strong>: CI連携完了、SLOに違反率を組み込み</li>
</ul>
<p>4週間で「人頼みのレビュー文化」から「仕組みで防ぐ文化」へ移行できます。</p>
<h2 id="まとめ">まとめ</h2>
<p>Kyverno導入の本質は、Kubernetesを縛ることではなく、<strong>再発するミスを設計で減らすこと</strong>です。</p>
<ul>
<li>最初はAuditで可視化</li>
<li>指標を持って段階的にEnforce</li>
<li>例外は期限付きで管理</li>
<li>CIに前倒し検知を組み込む</li>
</ul>
<p>この4点を守れば、ポリシーは“開発を止める壁”ではなく“事故を減らすガードレール”になります。まずは <code>latest</code> 禁止とリソース必須の2ルールから始めて、違反データを見ながら育てていきましょう。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>Kyverno</category>
      <category>Security</category>
      <category>Platform Engineering</category>
      <category>Policy as Code</category>
    </item>
    <item>
      <title>GitHub Actions OIDCで実現する鍵レス本番デプロイ：漏えい事故を減らす実装プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-05-github-actions-oidc-secure-deploy-playbook/</link>
      <pubDate>Thu, 05 Mar 2026 09:08:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-github-actions-oidc-secure-deploy-playbook/</guid>
      <description>長期シークレットを廃止し、GitHub Actions OIDCでAWSへ安全にデプロイするための設計・実装・監査手順を具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions-oidcで実現する鍵レス本番デプロイ漏えい事故を減らす実装プレイブック">GitHub Actions OIDCで実現する鍵レス本番デプロイ：漏えい事故を減らす実装プレイブック</h1>
<p>CI/CD の事故は、ビルドが失敗することより「漏えいしても気づけない鍵」が残り続けることのほうが深刻です。特に <code>AWS_ACCESS_KEY_ID</code> のような長期シークレットを GitHub Secrets に保存し続ける運用は、便利ですがリスクが高いです。</p>
<p>本記事では、GitHub Actions の <strong>OIDC（OpenID Connect）連携</strong>を使って、長期鍵を使わずに AWS へデプロイする実践手順をまとめます。単なる設定紹介ではなく、<strong>最小権限・ブランチ制限・監査ログ設計</strong>まで含めて、明日から本番投入できる形で説明します。</p>
<h2 id="1-まず何が危険なのか長期シークレット運用の限界">1. まず何が危険なのか：長期シークレット運用の限界</h2>
<p>従来構成では、次のような問題が起きます。</p>
<ul>
<li>Secret が漏れても検知が遅い（CIログ、誤コミット、権限の広いメンバー）</li>
<li>ローテーションが後回しになる</li>
<li>1つの鍵で複数環境へアクセスできてしまう</li>
<li>「誰のどの workflow 実行が何をしたか」が追いにくい</li>
</ul>
<p>OIDC 連携では、GitHub が発行する短命トークンを信頼し、AWS 側で一時認証情報を払い出します。つまり、<strong>保管する鍵そのものを減らす</strong>のが最大の価値です。</p>
<h2 id="2-全体アーキテクチャ">2. 全体アーキテクチャ</h2>
<p>基本フローは以下です。</p>
<ol>
<li>GitHub Actions ジョブが OIDC トークンを取得</li>
<li>AWS IAM の OIDC プロバイダとロール信頼ポリシーで検証</li>
<li>条件に一致したジョブだけ <code>AssumeRoleWithWebIdentity</code></li>
<li>一時クレデンシャルで S3/CloudFront/ECR/ECS へデプロイ</li>
</ol>
<p>ポイントは「GitHub 側の workflow 制御」だけでなく、<strong>AWS 側で repo・branch・workflow を強制する</strong>ことです。</p>
<h2 id="3-aws-側の初期設定oidc-provider--iam-role">3. AWS 側の初期設定（OIDC Provider + IAM Role）</h2>
<h3 id="31-oidc-provider-を作成">3.1 OIDC Provider を作成</h3>
<p>CLI 例（すでに存在する場合はスキップ）:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>aws iam create-open-id-connect-provider <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --url https://token.actions.githubusercontent.com <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --client-id-list sts.amazonaws.com <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="32-信頼ポリシーを厳密化する">3.2 信頼ポリシーを厳密化する</h3>
<p>以下のように <code>sub</code> と <code>aud</code> を必ず絞ります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">&#34;Version&#34;</span>: <span style="color:#e6db74">&#34;2012-10-17&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;Statement&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Allow&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Principal&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;Federated&#34;</span>: <span style="color:#e6db74">&#34;arn:aws:iam::&lt;ACCOUNT_ID&gt;:oidc-provider/token.actions.githubusercontent.com&#34;</span>
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Action&#34;</span>: <span style="color:#e6db74">&#34;sts:AssumeRoleWithWebIdentity&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Condition&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;StringEquals&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;token.actions.githubusercontent.com:aud&#34;</span>: <span style="color:#e6db74">&#34;sts.amazonaws.com&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;StringLike&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;token.actions.githubusercontent.com:sub&#34;</span>: <span style="color:#e6db74">&#34;repo:your-org/your-repo:ref:refs/heads/main&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>sub</code> を <code>repo:org/repo:*</code> のように広く取りすぎると、意図しない workflow からも引き受ける可能性があり危険です。</p>
<h3 id="33-デプロイ権限ポリシーを分離する">3.3 デプロイ権限ポリシーを分離する</h3>
<p>「ロール1個に全部盛り」は避けます。</p>
<ul>
<li><code>deploy-web-prod-role</code>: S3同期 + CloudFront invalidation</li>
<li><code>deploy-api-prod-role</code>: ECR push + ECS update</li>
<li><code>read-only-audit-role</code>: CloudWatch Logs / Describe 系のみ</li>
</ul>
<p>環境別（dev/stg/prod）にロールを分離すると、誤デプロイ時の被害半径が大きく減ります。</p>
<h2 id="4-github-actions-workflow-実装">4. GitHub Actions workflow 実装</h2>
<p>最小サンプル:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">name</span>: <span style="color:#ae81ff">deploy-web</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#e6db74">&#34;main&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configure AWS credentials via OIDC</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">aws-actions/configure-aws-credentials@v4</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">role-to-assume</span>: <span style="color:#ae81ff">arn:aws:iam::&lt;ACCOUNT_ID&gt;:role/deploy-web-prod-role</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">aws-region</span>: <span style="color:#ae81ff">ap-northeast-1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          npm ci
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          npm run build</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload to S3</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">aws s3 sync ./dist s3://example-prod-web --delete</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Invalidate CloudFront</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">aws cloudfront create-invalidation --distribution-id E123456 --paths &#34;/*&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>重要なのは <code>permissions.id-token: write</code> を明示する点です。これがないと OIDC トークンを取得できません。</p>
<h2 id="5-事故を防ぐための実務ルール">5. 事故を防ぐための実務ルール</h2>
<h3 id="51-branch-protection-と-environment-protection-を組み合わせる">5.1 branch protection と environment protection を組み合わせる</h3>
<ul>
<li><code>main</code> 直push禁止</li>
<li>必ず PR + 1 approval</li>
<li>production environment には Required reviewers を設定</li>
<li>夜間デプロイを禁止したい場合は手動承認ステップを入れる</li>
</ul>
<h3 id="52-workflow-ファイル改変の監査">5.2 workflow ファイル改変の監査</h3>
<p><code>.github/workflows/*.yml</code> の変更は CODEOWNERS で必ずレビュアー固定にします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>.github/workflows/*  @platform-team
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="53-self-hosted-runner-の扱い">5.3 self-hosted runner の扱い</h3>
<p>OIDC を導入しても、runner 自体が侵害されると意味が薄れます。</p>
<ul>
<li>runner をプロジェクト共有にしない（専用化）</li>
<li>ジョブ後にワークディレクトリをクリーン</li>
<li>egress 制限をかける</li>
<li>runner グループを環境ごとに分離</li>
</ul>
<h2 id="6-トラブルシューティング">6. トラブルシューティング</h2>
<h3 id="症状1-not-authorized-to-perform-stsassumerolewithwebidentity">症状1: <code>Not authorized to perform sts:AssumeRoleWithWebIdentity</code></h3>
<p>確認ポイント:</p>
<ol>
<li><code>aud</code> が <code>sts.amazonaws.com</code> になっているか</li>
<li><code>sub</code> が実際の実行 ref と一致しているか（タグ実行でハマりやすい）</li>
<li><code>permissions.id-token: write</code> が設定されているか</li>
<li>Role ARN の typo がないか</li>
</ol>
<h3 id="症状2-main-以外で偶発的にデプロイされた">症状2: main 以外で偶発的にデプロイされた</h3>
<ul>
<li>workflow の <code>on.push.branches</code> 見直し</li>
<li>IAM 信頼ポリシー <code>sub</code> を main 固定へ</li>
<li>環境保護ルール（Required reviewers）追加</li>
</ul>
<h3 id="症状3-デプロイは通るが操作が一部失敗">症状3: デプロイは通るが操作が一部失敗</h3>
<p>これは IAM 権限不足の可能性が高いです。CloudTrail の <code>eventName</code> と <code>errorCode</code> を見て、必要最小限のアクションだけ追加します。闇雲に <code>*</code> を付けないこと。</p>
<h2 id="7-段階的な移行計画既存運用からの切替">7. 段階的な移行計画（既存運用からの切替）</h2>
<p>実務では一気に切り替えず、以下の順が安全です。</p>
<ol>
<li>OIDC ロールを作成（既存シークレットは残す）</li>
<li>staging workflow だけ OIDC に切替</li>
<li>1週間監視（失敗率・デプロイ時間・CloudTrail）</li>
<li>production を OIDC 化</li>
<li>最後に長期シークレットを削除</li>
</ol>
<p>削除前に「どの workflow がどの secret を参照しているか」を grep で確認しておくと事故が減ります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git grep -n <span style="color:#e6db74">&#34;AWS_ACCESS_KEY_ID\|AWS_SECRET_ACCESS_KEY&#34;</span> .github/workflows
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="8-監査ログ設計何を見れば安全性が上がるか">8. 監査ログ設計：何を見れば安全性が上がるか</h2>
<p>最低限、次をダッシュボード化すると運用しやすいです。</p>
<ul>
<li><code>AssumeRoleWithWebIdentity</code> 実行回数（日次）</li>
<li>失敗イベント数（権限エラー/条件不一致）</li>
<li>production deploy 実行者（workflow + sha + actor）</li>
<li>1回のデプロイで変更された主要リソース数</li>
</ul>
<p>CloudTrail + CloudWatch Logs Insights での簡易クエリ例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span>fields <span style="color:#f92672">@</span><span style="color:#66d9ef">timestamp</span>, userIdentity.sessionContext.sessionIssuer.userName, eventName, errorCode
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> filter eventSource <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;sts.amazonaws.com&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> filter eventName <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AssumeRoleWithWebIdentity&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> sort <span style="color:#f92672">@</span><span style="color:#66d9ef">timestamp</span> <span style="color:#66d9ef">desc</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> <span style="color:#66d9ef">limit</span> <span style="color:#ae81ff">50</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="まとめ">まとめ</h2>
<p>OIDC は「設定が新しいから導入する」ものではなく、<strong>長期鍵を削減して事故確率を下げるための運用設計</strong>です。導入時にやるべきことはシンプルで、次の3つに集約できます。</p>
<ol>
<li>IAM 信頼ポリシーを repo/branch 単位で厳密化する</li>
<li>workflow 側で id-token 権限と環境保護を設定する</li>
<li>CloudTrail で AssumeRole の監査を継続する</li>
</ol>
<p>ここまで実施すれば、CI/CD のセキュリティは「頑張って守る」状態から、「漏えいしにくい仕組みで守る」状態へ進化します。まずは staging 1本から置き換えるのがおすすめです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>OIDC</category>
      <category>Security</category>
      <category>AWS</category>
      <category>CI/CD</category>
    </item>
    <item>
      <title>FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</link>
      <pubDate>Wed, 04 Mar 2026 09:35:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</guid>
      <description>FastAPIで安全に認証・認可を実装するために、トークン設計、ローテーション、RBAC、監査、障害時運用まで具体手順で解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi認証認可の本番設計jwt運用権限制御監査ログまで含めた実装パターン">FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</h1>
<p>FastAPI は実装が速い反面、認証・認可を最小構成のまま本番に出してしまい、後からセキュリティ事故に発展するケースが少なくありません。特に「JWT を入れたから安全」という誤解は危険です。</p>
<p>本記事では、<strong>開発速度を落とさずに本番で耐える認証基盤</strong>を作るための設計を、コード例と運用手順込みで解説します。</p>
<h2 id="1-認証と認可を分離して設計する">1. 認証と認可を分離して設計する</h2>
<p>最初に押さえるべきは責務分離です。</p>
<ul>
<li>認証（Authentication）: 誰かを確認する</li>
<li>認可（Authorization）: 何をしてよいか判定する</li>
</ul>
<p>この2つを混ぜると、実装も監査も破綻します。FastAPI では dependency を分け、<code>get_current_user</code> と <code>require_permission</code> を独立させるのが基本です。</p>
<h2 id="2-jwt-は短命--リフレッシュ--失効管理で使う">2. JWT は「短命 + リフレッシュ + 失効管理」で使う</h2>
<p>アクセストークンを長寿命にすると、漏えい時の被害が大きくなります。実運用では以下が標準です。</p>
<ul>
<li>Access Token: 5〜15分</li>
<li>Refresh Token: 7〜30日</li>
<li>Refresh Token は DB 保存し、ローテーション時に旧トークンを失効</li>
</ul>
<p><code>sub</code> だけでなく、<code>jti</code>（トークンID）や <code>scope</code> を持たせると管理しやすくなります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime, timedelta, timezone
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> jwt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ALGORITHM <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HS256&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create_access_token</span>(user_id: str, scopes: list[str], secret: str) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    now <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now(timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>    payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;sub&#34;</span>: user_id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;scope&#34;</span>: <span style="color:#e6db74">&#34; &#34;</span><span style="color:#f92672">.</span>join(scopes),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iat&#34;</span>: int(now<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;exp&#34;</span>: int((now <span style="color:#f92672">+</span> timedelta(minutes<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>))<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;jti&#34;</span>: <span style="color:#e6db74">&#34;generated-uuid&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> jwt<span style="color:#f92672">.</span>encode(payload, secret, algorithm<span style="color:#f92672">=</span>ALGORITHM)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-鍵管理とローテーション">3. 鍵管理とローテーション</h2>
<p>秘密鍵を <code>.env</code> に固定して数年運用するのは典型的な事故パターンです。最低限、次を実施します。</p>
<ul>
<li>KMS/Vault など外部シークレット管理を利用</li>
<li><code>kid</code> をヘッダに持たせ、複数鍵を並行運用</li>
<li>鍵ローテーション手順を runbook 化</li>
</ul>
<p>ローテーションの要点:</p>
<ol>
<li>新鍵を追加（検証側は新旧どちらも受理）</li>
<li>発行側を新鍵へ切替</li>
<li>旧鍵の有効期限を過ぎたら削除</li>
</ol>
<p>この手順にすると、無停止で切替できます。</p>
<h2 id="4-fastapi-dependencyで認可を明示化">4. FastAPI Dependencyで認可を明示化</h2>
<p>ロジック中で <code>if user.role == &quot;admin&quot;</code> を乱立させると、抜け漏れが起こります。権限チェックは dependency 化し、ルート定義に明示します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> Depends, HTTPException, status
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">require_permission</span>(required: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">checker</span>(user<span style="color:#f92672">=</span>Depends(get_current_user)):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> required <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> user<span style="color:#f92672">.</span>permissions:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> HTTPException(
</span></span><span style="display:flex;"><span>                status_code<span style="color:#f92672">=</span>status<span style="color:#f92672">.</span>HTTP_403_FORBIDDEN,
</span></span><span style="display:flex;"><span>                detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;insufficient permissions&#34;</span>
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> user
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> checker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.delete</span>(<span style="color:#e6db74">&#34;/projects/</span><span style="color:#e6db74">{project_id}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">delete_project</span>(
</span></span><span style="display:flex;"><span>    project_id: str,
</span></span><span style="display:flex;"><span>    user<span style="color:#f92672">=</span>Depends(require_permission(<span style="color:#e6db74">&#34;project:delete&#34;</span>))
</span></span><span style="display:flex;"><span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ルート単位で要件が見えるため、レビュー効率と監査性が上がります。</p>
<h2 id="5-rbacabac-の使い分け">5. RBAC/ABAC の使い分け</h2>
<p>小規模なら RBAC（role-based）で十分ですが、顧客単位データや組織階層があると ABAC（属性ベース）を併用した方が安全です。</p>
<ul>
<li>RBAC: <code>admin</code>, <code>editor</code>, <code>viewer</code></li>
<li>ABAC: <code>tenant_id</code>, <code>resource_owner_id</code>, <code>department</code></li>
</ul>
<p>実務では「ロールで粗く許可し、属性で絞る」が扱いやすいです。</p>
<h2 id="6-マルチテナントで必須の防御">6. マルチテナントで必須の防御</h2>
<p>マルチテナント API では、ID 推測よりも<strong>テナント境界漏れ</strong>が主要リスクです。対策は次の通りです。</p>
<ul>
<li>すべての DB クエリに <code>tenant_id</code> 条件を必須化</li>
<li>管理者 API でも境界を明示的に超える操作だけ許可</li>
<li>監査ログに <code>tenant_id</code>, <code>actor_id</code>, <code>resource_id</code> を残す</li>
</ul>
<p>SQLAlchemy でも repository 層で共通フィルタを強制すると漏れを減らせます。</p>
<h2 id="7-監査ログを設計段階で入れる">7. 監査ログを設計段階で入れる</h2>
<p>認証系は障害後に「誰が何をしたか」が必要になります。後付けだと間に合いません。最低限、次を記録します。</p>
<ul>
<li>ログイン成功/失敗（IP, user-agent, reason）</li>
<li>権限エラー（403）</li>
<li>重要操作（削除、権限変更、請求情報更新）</li>
<li>トークン失効・再発行</li>
</ul>
<p>フォーマットは JSON 構造化に統一し、SIEM や OpenSearch に流せる形にしておくと分析が速いです。</p>
<h2 id="8-レート制限とブルートフォース対策">8. レート制限とブルートフォース対策</h2>
<p>パスワード認証がある場合、レート制限なしは危険です。<code>slowapi</code> などを使い、ログイン系エンドポイントに制限を入れます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> slowapi <span style="color:#f92672">import</span> Limiter
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> slowapi.util <span style="color:#f92672">import</span> get_remote_address
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>limiter <span style="color:#f92672">=</span> Limiter(key_func<span style="color:#f92672">=</span>get_remote_address)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.post</span>(<span style="color:#e6db74">&#34;/auth/login&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@limiter.limit</span>(<span style="color:#e6db74">&#34;5/minute&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">login</span>(<span style="color:#f92672">...</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>さらに次を組み合わせると強化できます。</p>
<ul>
<li>失敗回数に応じた遅延（progressive delay）</li>
<li>CAPTCHA（必要時のみ）</li>
<li>異常IP/ASN の遮断</li>
</ul>
<h2 id="9-よくある実装ミス">9. よくある実装ミス</h2>
<h3 id="ミスa-署名検証はしているが-audiss-未検証">ミスA: 署名検証はしているが <code>aud/iss</code> 未検証</h3>
<p>結果:</p>
<ul>
<li>他システム向けトークンを誤受理</li>
</ul>
<p>対処:</p>
<ul>
<li>issuer/audience を厳格検証</li>
<li>想定外クレームは拒否</li>
</ul>
<h3 id="ミスb-refresh-token-の使い回しを検知しない">ミスB: refresh token の使い回しを検知しない</h3>
<p>結果:</p>
<ul>
<li>漏えい時に長期間乗っ取られる</li>
</ul>
<p>対処:</p>
<ul>
<li>ローテーション時に旧トークン失効</li>
<li>再利用検知時はセッション全失効</li>
</ul>
<h3 id="ミスc-認可チェックが一部エンドポイントで抜ける">ミスC: 認可チェックが一部エンドポイントで抜ける</h3>
<p>結果:</p>
<ul>
<li>水平権限昇格</li>
</ul>
<p>対処:</p>
<ul>
<li>dependency ベースで強制</li>
<li>重要ルートにセキュリティテスト追加</li>
</ul>
<h2 id="10-テスト戦略必須">10. テスト戦略（必須）</h2>
<p>認証はユニットテストだけでなく、統合テストで権限境界を確認します。</p>
<ul>
<li>有効トークン/期限切れ/改ざんトークン</li>
<li>role ごとのアクセス可否</li>
<li>tenant 越境アクセス拒否</li>
<li>refresh token 再利用検知</li>
</ul>
<p>pytest では fixture で role 別トークンを用意し、回帰を防ぎます。</p>
<h2 id="11-障害時-runbook最低限">11. 障害時 runbook（最低限）</h2>
<p>インシデント時に迷わないよう、次を文書化しておきます。</p>
<ol>
<li>鍵漏えい疑い時の全トークン失効手順</li>
<li>認証基盤障害時のフェイル動作（許可しすぎを防ぐ）</li>
<li>監査ログの検索手順</li>
<li>関係者通知テンプレート</li>
</ol>
<p>特に「認証サーバーが落ちたとき、API をどうするか」は事前に決めておく必要があります。</p>
<h2 id="12-導入チェックリスト">12. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> access token は短寿命（&lt;=15分）</li>
<li><input disabled="" type="checkbox"> refresh token は DB 管理 + ローテーション</li>
<li><input disabled="" type="checkbox"> 鍵ローテーション手順がある</li>
<li><input disabled="" type="checkbox"> ルート単位で認可 dependency が明示されている</li>
<li><input disabled="" type="checkbox"> tenant 境界を DB レイヤーで強制している</li>
<li><input disabled="" type="checkbox"> 監査ログ（認証/認可/重要操作）を構造化保存</li>
<li><input disabled="" type="checkbox"> レート制限と異常検知がある</li>
<li><input disabled="" type="checkbox"> 権限境界の統合テストがある</li>
</ul>
<p>FastAPI の認証・認可は、フレームワーク機能だけでは守り切れません。<strong>トークン寿命設計、鍵運用、境界強制、監査、テスト、runbook</strong>まで含めて初めて、本番で信頼できるセキュリティ基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Security</category>
      <category>JWT</category>
      <category>OAuth2</category>
      <category>Python</category>
    </item>
    <item>
      <title>MCPサーバー本番設計ガイド：AIエージェント連携を安全・安定に運用するアーキテクチャ</title>
      <link>https://www.ai2core.com/posts/2026-03-01-mcp-server-production-architecture/</link>
      <pubDate>Sun, 01 Mar 2026 09:35:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-01-mcp-server-production-architecture/</guid>
      <description>Model Context Protocol（MCP）サーバーを本番運用するための設計指針を、権限分離、監査、失敗制御、運用チェックリストまで実装レベルで解説。</description>
      <content:encoded><![CDATA[<h1 id="mcpサーバー本番設計ガイドaiエージェント連携を安全安定に運用するアーキテクチャ">MCPサーバー本番設計ガイド：AIエージェント連携を安全・安定に運用するアーキテクチャ</h1>
<p>MCP（Model Context Protocol）は、LLM と外部ツールを接続する強力な仕組みです。便利な一方で、本番運用では「権限の過剰付与」「監査不能」「障害時の暴走」が起きやすく、設計を誤ると一気にリスクが跳ね上がります。</p>
<p>本記事では、MCP サーバーを業務利用する前提で、<strong>安全性・可観測性・運用性</strong>を満たす設計パターンをまとめます。PoC から本番へ上げる際のチェックリストとして使える構成にしています。</p>
<h2 id="1-mcp本番運用で先に決めるべきこと">1. MCP本番運用で先に決めるべきこと</h2>
<p>最初に決めるべきは、技術スタックではなく「権限境界」です。</p>
<ul>
<li>どのエージェントが、どのツールを使えるか</li>
<li>書き込み系操作（作成・更新・削除）の承認方式</li>
<li>外部送信（メール、投稿、通知）の監査ルール</li>
<li>失敗時の停止条件（fail-open か fail-closed か）</li>
</ul>
<p>ここを決めずに実装を始めると、あとから制約を入れられず、結果として運用停止になります。</p>
<h2 id="2-推奨アーキテクチャcontrol-plane-と-tool-plane-の分離">2. 推奨アーキテクチャ：Control Plane と Tool Plane の分離</h2>
<p>MCP 構成は最低でも2層に分けると安全です。</p>
<ol>
<li><strong>Control Plane</strong>: 認証、認可、監査、レート制御</li>
<li><strong>Tool Plane</strong>: 実際のツール実行（DB、GitHub、Browser、Messaging）</li>
</ol>
<h3 id="2-1-なぜ分離するのか">2-1. なぜ分離するのか</h3>
<p>Tool 実装に認可ロジックを埋め込むと、ツール追加のたびにセキュリティ品質がブレます。Control Plane で一元化すれば、ポリシー変更時も1箇所で反映できます。</p>
<h3 id="2-2-リクエストフロー例">2-2. リクエストフロー例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Agent -&gt; MCP Gateway(Control Plane)
</span></span><span style="display:flex;"><span>      -&gt; Policy Engine (allow/deny, scope check)
</span></span><span style="display:flex;"><span>      -&gt; Tool Adapter (Tool Plane)
</span></span><span style="display:flex;"><span>      -&gt; Audit Logger
</span></span></code></pre></td></tr></table>
</div>
</div><p>deny の場合も必ず監査ログに記録し、試行の痕跡を残します。</p>
<h2 id="3-認可設計rbacだけでは足りない">3. 認可設計：RBACだけでは足りない</h2>
<p>本番では RBAC（役割）に加えて ABAC（属性）を使うと事故が減ります。</p>
<ul>
<li>RBAC: <code>writer</code>, <code>reviewer</code>, <code>admin</code></li>
<li>ABAC: 時間帯、環境（prod/staging）、対象リポジトリ、操作種別</li>
</ul>
<p>例:</p>
<ul>
<li>staging では <code>write</code> 可、prod は <code>read</code> のみ</li>
<li>23:00〜08:00 の外部送信を自動 deny</li>
<li><code>delete</code> は人間承認トークン必須</li>
</ul>
<p>このルールを YAML 等で宣言的に持つと、監査とレビューが容易になります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">policies</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">deny-night-send</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">action</span>: <span style="color:#ae81ff">message.send</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">time_between</span>: [<span style="color:#e6db74">&#34;23:00&#34;</span>, <span style="color:#e6db74">&#34;08:00&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">effect</span>: <span style="color:#ae81ff">deny</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">allow-read-docs</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">action</span>: <span style="color:#ae81ff">docs.read</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">env</span>: [<span style="color:#e6db74">&#34;prod&#34;</span>, <span style="color:#e6db74">&#34;staging&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">effect</span>: <span style="color:#ae81ff">allow</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="4-書き込み操作のガードレール">4. 書き込み操作のガードレール</h2>
<p>削除や公開投稿など不可逆操作は、二段階に分けます。</p>
<ol>
<li><strong>Planフェーズ</strong>: 何を変更するか差分提示</li>
<li><strong>Applyフェーズ</strong>: 承認トークン付きで実行</li>
</ol>
<h3 id="4-1-git操作の安全化">4-1. Git操作の安全化</h3>
<ul>
<li><code>main</code> 直 push 禁止（PR 経由のみ）</li>
<li>commit message に実行主体IDを埋め込む</li>
<li>重要ディレクトリは CODEOWNERS レビュー必須</li>
</ul>
<p>これにより「誰が」「なぜ」「どの差分を」適用したか追跡できます。</p>
<h2 id="5-可観測性最低限必要な監査ログ項目">5. 可観測性：最低限必要な監査ログ項目</h2>
<p>MCP の障害は、アプリログだけでは追えません。次を構造化ログで保存します。</p>
<ul>
<li>request_id</li>
<li>agent_id / session_id</li>
<li>tool_name</li>
<li>action（read/write/delete/send）</li>
<li>policy_decision（allow/deny）</li>
<li>latency_ms</li>
<li>result（success/failure）</li>
<li>redacted_input_hash</li>
</ul>
<p>入力本文を丸ごと保存すると個人情報漏えいリスクが高いので、ハッシュ化・マスキングが原則です。</p>
<h2 id="6-障害設計mcpが壊れても全体を止めない">6. 障害設計：MCPが壊れても全体を止めない</h2>
<h3 id="6-1-circuit-breaker-を必ず入れる">6-1. Circuit Breaker を必ず入れる</h3>
<p>下流ツール（例: GitHub API）が遅延した際、MCP 全体が巻き込まれないようにします。</p>
<ul>
<li>連続失敗 N 回で open</li>
<li>cool-down 後に half-open</li>
<li>成功確認後に close</li>
</ul>
<h3 id="6-2-タイムアウト予算">6-2. タイムアウト予算</h3>
<p>MCP リクエストは複数ツールを跨ぐため、全体予算を先に決めます。</p>
<ul>
<li>全体: 10 秒</li>
<li>認可: 500ms</li>
<li>ツール実行: 7 秒</li>
<li>ログ書き込み: 1 秒</li>
</ul>
<p>予算超過時は中断し、部分成功を明示するレスポンスを返すほうが、黙って待つより運用しやすいです。</p>
<h2 id="7-実運用チェックリスト導入前に必須">7. 実運用チェックリスト（導入前に必須）</h2>
<h3 id="セキュリティ">セキュリティ</h3>
<ul>
<li><input disabled="" type="checkbox"> デフォルト deny（明示 allow のみ）</li>
<li><input disabled="" type="checkbox"> 外部送信系に承認フローあり</li>
<li><input disabled="" type="checkbox"> APIキー/トークンのローテーション手順あり</li>
<li><input disabled="" type="checkbox"> 監査ログの改ざん防止（WORM/署名）</li>
</ul>
<h3 id="信頼性">信頼性</h3>
<ul>
<li><input disabled="" type="checkbox"> タイムアウト・再試行・サーキットブレーカ設定済み</li>
<li><input disabled="" type="checkbox"> ツールごとのレート制限あり</li>
<li><input disabled="" type="checkbox"> 障害時の degraded mode 定義済み</li>
<li><input disabled="" type="checkbox"> 週次で復旧訓練（ゲームデイ）実施</li>
</ul>
<h3 id="運用">運用</h3>
<ul>
<li><input disabled="" type="checkbox"> Runbook とオンコール体制がある</li>
<li><input disabled="" type="checkbox"> 重大操作の通知先が明確</li>
<li><input disabled="" type="checkbox"> 監査レポートを定期出力できる</li>
<li><input disabled="" type="checkbox"> ポリシー変更はPRレビュー必須</li>
</ul>
<h2 id="8-pocから本番へ上げるときの移行手順">8. PoCから本番へ上げるときの移行手順</h2>
<p>おすすめは次の4段階です。</p>
<ol>
<li><strong>Read-only段階</strong>: 読み取り専用ツールのみ許可</li>
<li><strong>限定Write段階</strong>: staging への書き込みのみ解放</li>
<li><strong>承認付きProd段階</strong>: prod 書き込みは人間承認必須</li>
<li><strong>自動化段階</strong>: 低リスク操作だけ自動承認</li>
</ol>
<p>この順序なら、事故を起こさず運用知見を溜められます。</p>
<h2 id="まとめ">まとめ</h2>
<p>MCP は「つなげる技術」ですが、本番では「制御する技術」に重心があります。成功する導入の共通点は次の3つです。</p>
<ul>
<li>権限境界を最初に設計する</li>
<li>監査可能な実行経路を持つ</li>
<li>障害時に安全側へ倒れる設計にする</li>
</ul>
<p>AI エージェント活用は今後さらに広がります。だからこそ、機能追加より先に運用設計を固めることが、長期的な速度と安全性を両立する最短ルートです。今日から始めるなら、まずは「default deny + 監査ログ項目定義」この2つを先に確定してください。</p>
<h2 id="9-監査レビューを回すための実務フロー">9. 監査レビューを回すための実務フロー</h2>
<p>ログを取って終わりでは意味がありません。週次または隔週で、次の観点で監査レビューを実施します。</p>
<ul>
<li>deny された操作のうち、正当要求だったものはないか</li>
<li>承認付き操作で承認理由が空欄になっていないか</li>
<li>失敗率の高いツールに設計欠陥がないか</li>
<li>夜間・休日の危険操作が発生していないか</li>
</ul>
<p>監査レビューはセキュリティ部門だけでなく、実際に運用する開発チームが同席することで改善速度が上がります。ポリシーは「守らせるもの」ではなく「運用と一緒に育てるもの」と捉えるのが重要です。</p>
<h2 id="10-導入初期に決めておくべきslo">10. 導入初期に決めておくべきSLO</h2>
<p>MCP 基盤にも SLO を置くと、感覚論で運用しなくて済みます。例として以下が扱いやすいです。</p>
<ul>
<li>正常リクエスト成功率: 99.5%以上</li>
<li>ポリシー判定レイテンシ p95: 300ms 以下</li>
<li>重大操作の監査ログ欠損率: 0%</li>
<li>障害発生時の検知時間: 5分以内</li>
</ul>
<p>このSLOをダッシュボード化し、週次で逸脱をレビューするだけで、MCP運用の成熟度は大きく上がります。PoC段階から数値を持っておくと、本番移行時の説得材料としても有効です。</p>
<p>補足として、導入時は「全ツール同時公開」を避け、1ツールずつトラフィックを段階開放するのが安全です。障害発生時の切り戻し対象が明確になり、原因分析時間を大幅に短縮できます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>MCP</category>
      <category>AI Agent</category>
      <category>Architecture</category>
      <category>Security</category>
    </item>
    <item>
      <title>GitHub Actionsセルフホストランナー防衛術：CI/CDの供給網リスクを減らす実装ガイド</title>
      <link>https://www.ai2core.com/posts/2026-02-28-github-actions-selfhosted-runner-security/</link>
      <pubDate>Sat, 28 Feb 2026 13:30:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-28-github-actions-selfhosted-runner-security/</guid>
      <description>セルフホストランナー運用で発生する主要リスクを洗い出し、実装可能な防御策を段階的に紹介。</description>
      <content:encoded><![CDATA[<h1 id="github-actionsセルフホストランナー防衛術cicdの供給網リスクを減らす実装ガイド">GitHub Actionsセルフホストランナー防衛術：CI/CDの供給網リスクを減らす実装ガイド</h1>
<p>セルフホストランナーは高速で柔軟です。特定ツールチェーンや社内ネットワーク接続が必要な環境では、ほぼ必須といえます。一方で、設定を誤ると CI/CD が攻撃経路になります。</p>
<p>近年のインシデントでは、依存パッケージ汚染だけでなく「Actions workflow の権限過多」「fork 由来PRでの秘密情報流出」「ランナー残存データ」が問題化しています。</p>
<p>本記事では、セルフホストランナーを安全に運用するための防衛策を、設計レイヤごとに整理します。</p>
<h2 id="脅威モデルを先に定義する">脅威モデルを先に定義する</h2>
<p>まず守る対象を明確にします。</p>
<ul>
<li>リポジトリのソースコード</li>
<li>Secrets（クラウド鍵、署名鍵、トークン）</li>
<li>配布物の完全性（改ざん防止）</li>
<li>社内ネットワーク接続経路</li>
</ul>
<p>攻撃経路は主に次です。</p>
<ol>
<li>悪意ある PR が workflow を悪用</li>
<li>Marketplace Action の supply chain 汚染</li>
<li>ランナー上に残る credential / build artifact</li>
<li>過剰な <code>GITHUB_TOKEN</code> 権限</li>
</ol>
<p>この4点を潰す設計が防御の中心になります。</p>
<h2 id="1-ランナーは使い捨てを前提にする">1. ランナーは「使い捨て」を前提にする</h2>
<p>長寿命ランナーは便利ですが、攻撃後の残留リスクが高いです。可能ならジョブ単位で破棄できる ephemeral 構成を採用します。</p>
<ul>
<li>Kubernetes + Actions Runner Controller</li>
<li>VMテンプレートから都度起動</li>
<li>ジョブ終了後に完全破棄</li>
</ul>
<p>少なくとも <code>/tmp</code> と workspace を確実に消去し、Docker layer cache の共有範囲を制御してください。</p>
<h2 id="2-workflow-権限を最小化する">2. workflow 権限を最小化する</h2>
<p><code>permissions: write-all</code> は禁止レベルです。workflowごとに最小権限を明記します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull-requests</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>特に <code>id-token: write</code> は OIDC 連携に必要ですが、不要ジョブで許可しないこと。権限漏れがクラウド侵害に直結します。</p>
<h2 id="3-fork-pr-の実行ポリシーを分離する">3. fork PR の実行ポリシーを分離する</h2>
<p>外部 fork からの PR で secrets を使うジョブを実行しない設計が必須です。</p>
<p>推奨:</p>
<ul>
<li><code>pull_request</code> イベント: テストのみ、secrets 無し</li>
<li><code>pull_request_target</code>: 原則禁止、必要なら厳格レビュー</li>
<li>デプロイ系は <code>push</code>（保護ブランチ）だけ</li>
</ul>
<p>また、<code>workflow_run</code> を使って「検証済み成果物だけを次段へ渡す」2段構成にすると安全性が上がります。</p>
<h2 id="4-action-の固定と検証">4. Action の固定と検証</h2>
<p><code>uses: actions/checkout@v4</code> のようなタグ指定だけでは、将来更新の影響を受けます。高リスク工程では commit SHA 固定を検討します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@8ade135a...</span> <span style="color:#75715e"># SHA pin</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>加えて、許可済み Action の allowlist を組織ポリシー化します。無制限に Marketplace Action を使わせると監査不能になります。</p>
<h2 id="5-secrets-管理は-oidc-中心へ">5. Secrets 管理は OIDC 中心へ</h2>
<p>長期固定鍵を GitHub Secrets に置く運用は、漏洩時の被害が大きいです。クラウド連携は OIDC フェデレーションに移行し、短命トークンを発行します。</p>
<p>利点:</p>
<ul>
<li>鍵配布不要</li>
<li>期限短い</li>
<li>リポジトリ/ブランチ条件で絞れる</li>
</ul>
<p>AWS なら role trust policy に <code>sub</code> 条件を入れ、「main ブランチの release workflow だけ許可」といった制御が可能です。</p>
<h2 id="6-ネットワーク分離と出口制御">6. ネットワーク分離と出口制御</h2>
<p>セルフホストランナーが社内フラットネットワークに直結している構成は危険です。ランナー専用サブネットを作り、次を実施します。</p>
<ul>
<li>egress allowlist（必要ドメインのみ）</li>
<li>社内DBへの直接接続禁止</li>
<li>管理プレーンと実行プレーン分離</li>
</ul>
<p>「CIだから社内に近いほど便利」は短期的発想です。侵害前提で最小到達範囲に設計します。</p>
<h2 id="7-監査ログと改ざん検知">7. 監査ログと改ざん検知</h2>
<p>必要なログ:</p>
<ul>
<li>workflow 実行者</li>
<li>使用ランナーID</li>
<li>取得したクラウド権限</li>
<li>成果物ハッシュ</li>
</ul>
<p>さらに、リリース成果物に SBOM と署名（cosign / sigstore）を付与し、配布前検証を自動化します。これで supply chain の追跡性が大きく向上します。</p>
<h2 id="8-実装チェックリストそのまま使える">8. 実装チェックリスト（そのまま使える）</h2>
<ul>
<li><input disabled="" type="checkbox"> セルフホストランナーは ephemeral 運用</li>
<li><input disabled="" type="checkbox"> workflow permissions を最小権限化</li>
<li><input disabled="" type="checkbox"> fork PR と deploy workflow を分離</li>
<li><input disabled="" type="checkbox"> 高リスク Action は SHA pin</li>
<li><input disabled="" type="checkbox"> OIDC による短命認証へ移行</li>
<li><input disabled="" type="checkbox"> ネットワーク egress 制限</li>
<li><input disabled="" type="checkbox"> artifact 署名と SBOM 生成</li>
<li><input disabled="" type="checkbox"> 監査ログを90日以上保持</li>
</ul>
<h2 id="9-段階導入プラン4週間">9. 段階導入プラン（4週間）</h2>
<ul>
<li>Week1: 権限棚卸し、<code>write-all</code> 排除</li>
<li>Week2: fork PR ポリシー分離、allowlist導入</li>
<li>Week3: OIDC移行、固定鍵削減</li>
<li>Week4: ephemeral runner 化、署名/SBOM実装</li>
</ul>
<p>一気に変えると運用停止リスクがあるため、週単位で区切るのが現実的です。</p>
<h2 id="まとめ">まとめ</h2>
<p>セルフホストランナーは強力ですが、セキュリティ設計を誤ると CI/CD が最も危険な入口になります。ポイントは「最小権限」「使い捨て」「短命認証」「監査可能性」の4つです。</p>
<p>まずは <code>permissions</code> の最小化と fork PR 分離から始めてください。ここを押さえるだけでも、供給網リスクは大幅に下げられます。</p>
<h2 id="実運用での検知ルール例siem連携">実運用での検知ルール例（SIEM連携）</h2>
<p>防御策を実装しても、検知が弱いと侵害を見逃します。次のイベントを SIEM 側で高優先度アラート化してください。</p>
<ul>
<li>深夜帯の workflow 権限変更</li>
<li>普段使わないランナーラベルでのジョブ実行</li>
<li>release workflow で未知の Action 呼び出し</li>
<li>OIDC 経由で想定外クラウドロールを取得</li>
</ul>
<p>検知時には自動で <code>repository dispatch</code> を使い、該当リポジトリのデプロイを一時停止する仕組みを入れると被害拡大を防げます。</p>
<h2 id="インシデント対応runbookの最小構成">インシデント対応Runbookの最小構成</h2>
<p>供給網インシデントは初動が遅れると致命傷になります。Runbook には最低限次を含めます。</p>
<ol>
<li>影響範囲特定（対象リポジトリ、workflow、artifact）</li>
<li>該当 Secrets/Role の無効化</li>
<li>ランナー群の全廃棄と再構築</li>
<li>直近リリース成果物のハッシュ再検証</li>
<li>監査ログ保全と関係者通知</li>
</ol>
<p>この手順を平時に演習しておくことで、実際の障害時に迷いを減らせます。</p>
<h2 id="組織導入で効くポリシー">組織導入で効くポリシー</h2>
<ul>
<li>新規 workflow はセキュリティレビュー必須</li>
<li>Action 追加時はリスク評価テンプレート提出</li>
<li>重要リポジトリは branch protection + required review を強制</li>
<li>セルフホストランナーの管理責任者を明確化</li>
</ul>
<p>技術対策だけでは継続しません。責任分界とレビュー手順を運用ルール化することが、防御の持続性を高めます。</p>
<h2 id="まとめ運用視点">まとめ（運用視点）</h2>
<p>最終的に重要なのは「侵害されないこと」ではなく「侵害されても被害を限定し、速く復旧できること」です。セルフホストランナーを使うなら、最初からゼロトラスト前提で設計し、検知・対応まで含めた体制を整えてください。</p>
<h3 id="最後に">最後に</h3>
<p>現場では「便利だから後で固める」が最も危険です。セルフホストランナーは導入初日から最小権限と隔離を前提に設計し、定期監査で逸脱を戻す運用を続けてください。</p>
<h3 id="追加の運用tip">追加の運用Tip</h3>
<p>新しいリポジトリを作るたびに同じ議論をしないため、テンプレートリポジトリへ安全な workflow 雛形を同梱しておくと効果的です。初期状態を安全側に固定するだけで、運用負荷と事故率の両方を下げられます。</p>
<p>継続的な棚卸しを習慣化してください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>Security</category>
      <category>CI/CD</category>
      <category>DevSecOps</category>
    </item>
    <item>
      <title>FastAPI本番運用ハードニング完全ガイド：セキュリティ・性能・障害対応を実装で固める</title>
      <link>https://www.ai2core.com/posts/2026-02-28-fastapi-production-hardening-guide/</link>
      <pubDate>Sat, 28 Feb 2026 09:15:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-28-fastapi-production-hardening-guide/</guid>
      <description>FastAPIを本番運用する際に必要なセキュリティ、性能最適化、観測性、デプロイ手順を具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi本番運用ハードニング完全ガイドセキュリティ性能障害対応を実装で固める">FastAPI本番運用ハードニング完全ガイド：セキュリティ・性能・障害対応を実装で固める</h1>
<p>FastAPI は開発速度が高く、PoC から本番まで一気に進めやすいフレームワークです。しかし、早く作れることと安全に運用できることは別問題です。実際の障害は、コードの正しさよりも運用の隙から発生します。</p>
<p>本記事では、FastAPI を本番で安心して運用するためのハードニング手順を、実装可能な形でまとめます。対象は「すでにAPIが動いているが、運用強度を上げたい」チームです。</p>
<h2 id="1-入口防御tlsヘッダレート制限">1. 入口防御：TLS、ヘッダ、レート制限</h2>
<h3 id="tls終端とforwardedヘッダ">TLS終端とForwardedヘッダ</h3>
<p>ロードバランサ配下で動かす場合、<code>X-Forwarded-For</code> と <code>X-Forwarded-Proto</code> の扱いを明確にします。誤るとクライアントIPが取れず、監査も制限も機能しません。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> starlette.middleware.trustedhost <span style="color:#f92672">import</span> TrustedHostMiddleware
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI()
</span></span><span style="display:flex;"><span>app<span style="color:#f92672">.</span>add_middleware(TrustedHostMiddleware, allowed_hosts<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;api.example.com&#34;</span>, <span style="color:#e6db74">&#34;*.example.com&#34;</span>])
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>allowed_hosts</code> をワイルドにしすぎると Host Header Injection の温床になります。</p>
<h3 id="セキュリティヘッダ">セキュリティヘッダ</h3>
<p>最低限次を返します。</p>
<ul>
<li><code>Strict-Transport-Security</code></li>
<li><code>X-Content-Type-Options: nosniff</code></li>
<li><code>X-Frame-Options: DENY</code></li>
<li><code>Referrer-Policy</code></li>
</ul>
<p>APIでも無関係ではありません。管理画面やドキュメントUIを守る意味があります。</p>
<h3 id="レート制限">レート制限</h3>
<p>ブルートフォースと突発負荷に備え、IPまたはAPIキー単位でレート制限を設定します。</p>
<ul>
<li>認証系: 5 req/min</li>
<li>通常API: 60 req/min</li>
<li>高負荷検索: 20 req/min</li>
</ul>
<p>Redis バックエンド方式にして、アプリ再起動でカウンタが失われないようにします。</p>
<h2 id="2-認証認可の落とし穴を塞ぐ">2. 認証・認可の落とし穴を塞ぐ</h2>
<h3 id="jwt検証の必須項目">JWT検証の必須項目</h3>
<p><code>exp</code> だけ見て通す実装は危険です。少なくとも次を検証します。</p>
<ul>
<li><code>iss</code>（発行者）</li>
<li><code>aud</code>（想定利用先）</li>
<li><code>nbf</code>（有効開始）</li>
<li><code>kid</code> に基づく鍵ローテーション</li>
</ul>
<h3 id="認可はエンドポイント単位ではなくリソース単位">認可は「エンドポイント単位」ではなく「リソース単位」</h3>
<p><code>/users/{id}</code> のアクセス時に、path パラメータの <code>id</code> とトークンの主体を照合しない事故は頻発します。FastAPI の dependency で統一的に実施します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">authorize_user_resource</span>(current_user, target_user_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> (current_user<span style="color:#f92672">.</span>is_admin <span style="color:#f92672">or</span> current_user<span style="color:#f92672">.</span>user_id <span style="color:#f92672">==</span> target_user_id):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> HTTPException(status_code<span style="color:#f92672">=</span><span style="color:#ae81ff">403</span>, detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;forbidden&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-入出力の安全化pydanticだけでは不十分">3. 入出力の安全化：Pydanticだけでは不十分</h2>
<p>Pydantic は型安全に強いですが、ビジネス制約は別で実装が必要です。</p>
<ul>
<li>文字列長上限</li>
<li>許可文字セット</li>
<li>SQL/NoSQLインジェクションの危険文字</li>
<li>HTML/Markdown サニタイズ</li>
</ul>
<p>特に検索APIやエクスポートAPIは、クエリ文字列が巨大化しやすく DoS の入口になります。<code>max_length</code> を必ず定義してください。</p>
<h2 id="4-性能ハードニングワーカdbタイムアウト">4. 性能ハードニング：ワーカ・DB・タイムアウト</h2>
<h3 id="uvicorngunicorn-構成">Uvicorn/Gunicorn 構成</h3>
<p>CPUコア数に応じて worker を決めます。目安は <code>workers = 2 * core + 1</code> から開始し、実測で調整。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>gunicorn app.main:app -k uvicorn.workers.UvicornWorker --workers <span style="color:#ae81ff">5</span> --bind 0.0.0.0:8000 --timeout <span style="color:#ae81ff">30</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="db接続プール">DB接続プール</h3>
<p><code>asyncpg</code> や SQLAlchemy async engine のプール上限を設定しないと、ピーク時に接続飽和します。</p>
<ul>
<li>min: 5</li>
<li>max: 30（DB性能と相談）</li>
<li>pool timeout: 5s</li>
</ul>
<h3 id="タイムアウト戦略">タイムアウト戦略</h3>
<p>上流・下流の timeout を揃えないと、雪崩障害が発生します。</p>
<ul>
<li>外部API呼び出し: connect 1s / read 3s</li>
<li>DBクエリ: statement timeout 2s（重処理は別キュー）</li>
<li>API全体: 10s で fail fast</li>
</ul>
<h2 id="5-例外設計と障害時の挙動">5. 例外設計と障害時の挙動</h2>
<p>本番障害では「500が出ること」より「500の意味が不明」なことが問題です。エラーレスポンス形式を固定し、trace_id を必ず返します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">&#34;error&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;code&#34;</span>: <span style="color:#e6db74">&#34;INTERNAL_ERROR&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;unexpected error&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trace_id&#34;</span>: <span style="color:#e6db74">&#34;8f3d...&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>内部例外はそのまま返さず、ログ側に stack trace を記録。ユーザーには安全な文言のみ返却します。</p>
<h2 id="6-可観測性ログメトリクストレース">6. 可観測性：ログ・メトリクス・トレース</h2>
<h3 id="構造化ログ">構造化ログ</h3>
<p>JSON ログを標準化し、次を必須項目にします。</p>
<ul>
<li>timestamp</li>
<li>level</li>
<li>service</li>
<li>trace_id</li>
<li>user_id（可能なら）</li>
<li>endpoint</li>
<li>latency_ms</li>
</ul>
<h3 id="メトリクス">メトリクス</h3>
<p>最低限:</p>
<ul>
<li>RPS</li>
<li>エラー率（4xx/5xx）</li>
<li>P50/P95/P99 latency</li>
<li>DB遅延</li>
<li>外部API失敗率</li>
</ul>
<h3 id="トレース">トレース</h3>
<p>OpenTelemetry で endpoint → service → DB をつなぐと、障害切り分けが劇的に速くなります。</p>
<h2 id="7-デプロイ戦略壊さずに出す">7. デプロイ戦略：壊さずに出す</h2>
<p>推奨は Blue/Green か Canary。FastAPI 単体の問題より、周辺設定差異が事故の原因になります。</p>
<p>リリース前チェックリスト:</p>
<ol>
<li>DB migration の後方互換性</li>
<li>依存ライブラリ脆弱性スキャン</li>
<li>load test（代表3API）</li>
<li>rollback 手順の実行確認</li>
<li>feature flag で段階有効化</li>
</ol>
<h2 id="8-運用で効くインシデント訓練">8. 運用で効くインシデント訓練</h2>
<p>月1回、次の擬似障害を実施すると運用強度が上がります。</p>
<ul>
<li>DB遅延 3秒化</li>
<li>外部API 30% 失敗</li>
<li>メモリリーク発生</li>
<li>JWT鍵ローテーション失敗</li>
</ul>
<p>重要なのは、復旧時間だけでなく「誰が何を見て判断したか」を記録することです。Runbook の更新まで含めて初めて訓練が完結します。</p>
<h2 id="9-すぐ使える最小ハードニングチェック">9. すぐ使える最小ハードニングチェック</h2>
<ul>
<li><input disabled="" type="checkbox"> Host ヘッダ制限</li>
<li><input disabled="" type="checkbox"> JWT <code>iss/aud/exp/nbf</code> 検証</li>
<li><input disabled="" type="checkbox"> 全エンドポイントに認可 dependency</li>
<li><input disabled="" type="checkbox"> 外部API timeout/retry/circuit breaker</li>
<li><input disabled="" type="checkbox"> JSON 構造化ログ + trace_id</li>
<li><input disabled="" type="checkbox"> P95 latency 監視とアラート</li>
<li><input disabled="" type="checkbox"> rollback 手順が5分で実行可能</li>
</ul>
<p>この 7 項目が揃うだけで、障害時の被害規模は大きく下がります。</p>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI は高速開発の武器ですが、本番運用では「早く作る」より「安全に壊れる」設計が重要です。入口防御、認証認可、性能制御、観測性、リリース運用をセットで整備すれば、チームは安心して機能開発に集中できます。</p>
<p>もし何から始めるか迷うなら、まずは trace_id 付きの構造化ログと timeout 統一から着手してください。最小の投資で、運用の見通しが一気に良くなります。</p>
<h2 id="10-セキュアな開発フローを維持するためのci設定">10. セキュアな開発フローを維持するためのCI設定</h2>
<p>本番ハードニングはコードだけでなく、CI フローで担保する必要があります。推奨するジョブは次の通りです。</p>
<ol>
<li>依存脆弱性スキャン（pip-audit / osv-scanner）</li>
<li>SAST（bandit など）</li>
<li>型チェック（mypy）</li>
<li>負荷テストのスモーク（k6）</li>
<li>OpenAPI 差分チェック（破壊的変更検出）</li>
</ol>
<p>特に OpenAPI 差分チェックは有効です。意図しないレスポンス変更を早期に検知でき、フロントエンド障害を防げます。</p>
<h2 id="11-バックアップと復旧を設計に含める">11. バックアップと復旧を設計に含める</h2>
<p>API 運用は「壊れない」ではなく「壊れても戻せる」が現実的です。最低限次を決めておきます。</p>
<ul>
<li>DB バックアップ頻度（例: 15分ごと増分、日次フル）</li>
<li>復旧目標（RTO/RPO）</li>
<li>復旧手順の担当と実行順</li>
</ul>
<p>復旧訓練をしていないバックアップは、存在しないのと同じです。四半期に一度は検証環境でリストア演習を行ってください。</p>
<h2 id="12-監査対応を見据えたログ保全">12. 監査対応を見据えたログ保全</h2>
<p>B2B API では監査要件が後から増えることが多いです。最初から次を満たす設計にしておくと後で困りません。</p>
<ul>
<li>監査ログとアプリログを分離</li>
<li>重要操作（権限変更、削除、課金操作）の証跡保存</li>
<li>ログ保持期間の明確化（例: 180日）</li>
<li>改ざん検知（WORM ストレージや署名）</li>
</ul>
<p>「誰が、いつ、何をしたか」を追えることは、障害解析だけでなく法務リスク低減にも直結します。</p>
<h2 id="最終まとめ">最終まとめ</h2>
<p>FastAPI の本番運用は、フレームワーク知識だけでは足りません。セキュリティ、性能、可観測性、復旧性を一体で設計することが重要です。チェックリスト化し、CI と運用手順へ落とし込むことで、安定した開発速度を維持できます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Python</category>
      <category>Security</category>
      <category>SRE</category>
    </item>
  </channel>
</rss>
