開発効率化

GitHub資格情報失効入門——漏えい時のbreak-glass運用を作る

GitHubのself-service credential revocationを題材に、Next.js、Firestore、GitHub Actionsで漏えい時の受付、台帳、訓練を作る実践手順を解説します。

2026年6月25日
GitHubSecurityIncident ResponseFirestoreGitHub Actions
GitHub資格情報失効入門——漏えい時のbreak-glass運用を作る

はじめに

GitHubのPersonal Access TokenやSSH鍵を運用していて、こんな不安はありませんか?

状況起きやすい問題
開発者の端末からPATが漏えいしたどの組織に効く資格情報か切り分ける前に時間が過ぎる
退職者や紛失端末の対応が手順書頼み誰が、いつ、何を止めたかが残りにくい
GitHub ActionsのSecretを多数使っている漏えい時にCI/CDまで止める判断が遅れる

2026年6月24日、GitHubは公式Changelogで「Self-service credential revocation for incident response」を発表しました。Enterprise owner、または Manage enterprise credentials 権限を持つメンバーが、特定ユーザーまたは全ユーザーに対してSSO認可の失効や、Enterprise Managed Usersではトークン・SSH鍵の削除を実行できるという内容です。

本記事では、架空の予約管理SaaS「ReserveKit」を題材に、Next.js + Firestore + GitHub Actions + TypeScriptで、資格情報漏えい時のbreak-glass運用を準備します。読み終えると、「どのボタンを押すか」だけでなく、押す前後に何を記録し、どこまで自動化してよいかを設計できるようになります。

参考にした一次情報は次の通りです。

何ができるようになったのか

今回の発表で重要なのは、漏えいした資格情報を「探してから個別に消す」だけでなく、Enterpriseの管理者が高インパクトな封じ込めを選べるようになった点です。

操作主な対象注意点
特定ユーザーのSSO認可を失効PAT、SSH鍵、OAuth/GitHub Appユーザーアクセストークン資格情報そのものは削除されません
特定ユーザーのキーとトークンを削除Enterprise Managed Users新しい資格情報の再発行と差し替えが必要です
全ユーザーへの一括操作重大インシデント時自動化やCIを壊す可能性が高く、訓練なしでは危険です
POST /credentials/revoke値が分かっているPATやOAuthトークンなど認証ヘッダーを付けると403になります

GitHub Docsでは、初動で脅威を深掘りしすぎず、実在する脅威を否定できない場合は封じ込めに進む考え方が示されています。一方で、一括操作は復旧に大きな手間がかかる可能性があります。つまり、実務で必要なのは「押すボタン」だけではなく、判断、記録、復旧の準備です。

ハンズオン1: Firestoreにインシデント台帳を作る

ReserveKitでは、GitHub側の破壊的操作をアプリから直接実行しません。まずは受付と台帳だけをNext.jsに置き、操作は権限を持つ担当者がGitHub Enterpriseの画面または公式APIで実行します。

// src/lib/security/createCredentialIncident.ts
import { FieldValue, getFirestore } from "firebase-admin/firestore";

type CredentialIncidentInput = {
  reporterId: string;
  githubLogin?: string;
  suspectedCredentialType:
    | "classic_pat"
    | "fine_grained_pat"
    | "ssh_key"
    | "oauth_token"
    | "github_app_user_token"
    | "unknown";
  evidenceUrl?: string;
  summary: string;
};

export async function createCredentialIncident(input: CredentialIncidentInput) {
  const db = getFirestore();

  const incidentRef = await db.collection("credentialIncidents").add({
    ...input,
    status: "triage",
    severity: "unknown",
    actions: [],
    createdAt: FieldValue.serverTimestamp(),
    updatedAt: FieldValue.serverTimestamp(),
  });

  return incidentRef.id;
}

evidenceUrl にはGitHub Issue、社内アラート、secret scanning alertなどのURLを入れます。ここに漏えいしたトークン値そのものを保存してはいけません。Firestoreは台帳であり、秘匿情報の退避場所ではないからです。

ハンズオン2: Server Actionで受付フォームを作る

App RouterではServer Actionを使い、入力を最小限に絞ります。架空チケットは RK-001 から RK-030 のように扱い、実在の顧客名や契約番号は入れません。

// src/app/(admin)/security/incidents/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { createCredentialIncident } from "@/lib/security/createCredentialIncident";
import { requireAdminSession } from "@/lib/auth/requireAdminSession";

export async function reportCredentialIncident(formData: FormData) {
  const session = await requireAdminSession();

  const summary = String(formData.get("summary") ?? "").trim();
  if (summary.length < 20) {
    throw new Error("summary must be at least 20 characters");
  }

  await createCredentialIncident({
    reporterId: session.user.id,
    githubLogin: String(formData.get("githubLogin") ?? "").trim() || undefined,
    suspectedCredentialType:
      (formData.get("credentialType") as "classic_pat" | "fine_grained_pat" | "ssh_key" | "oauth_token" | "github_app_user_token" | "unknown") ?? "unknown",
    evidenceUrl: String(formData.get("evidenceUrl") ?? "").trim() || undefined,
    summary,
  });

  revalidatePath("/admin/security/incidents");
}

この画面の目的は、失効操作の自動実行ではありません。インシデント受付を標準化し、誰がトリアージを始めたかを残すことです。緊急時ほど、強い権限を持つ操作と業務アプリの通常権限を混ぜないほうが安全です。

ハンズオン3: 値が分かるトークンだけAPIで失効する

GitHubのRevocation APIは、漏えいした資格情報の値が分かっている場合に使えます。公式Docsでは POST https://api.github.com/credentials/revoke が案内されており、Personal Access Token classic、fine-grained PAT、OAuth app access token、GitHub App user access token、GitHub App refresh tokenを受け付けます。

重要な注意点があります。このAPIは認証なしで呼び出す設計で、認証ヘッダーを付けたリクエストは403になります。また、失効した資格情報はGitHub側で復活できません。CIから自動実行するのではなく、インシデント対応者が値を確認してからローカルで実行する運用にします。

// scripts/revoke-github-credentials.ts
const credentials = JSON.parse(process.env.GITHUB_CREDENTIALS_JSON ?? "[]") as string[];
const confirmation = process.env.REVOKE_CONFIRMATION;

if (confirmation !== "I_UNDERSTAND_CREDENTIALS_WILL_BE_REVOKED") {
  throw new Error("Set REVOKE_CONFIRMATION before revoking credentials");
}

if (credentials.length === 0) {
  throw new Error("GITHUB_CREDENTIALS_JSON must contain at least one credential");
}

const response = await fetch("https://api.github.com/credentials/revoke", {
  method: "POST",
  headers: {
    Accept: "application/vnd.github+json",
    "X-GitHub-Api-Version": "2026-03-10",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ credentials }),
});

if (response.status !== 202) {
  throw new Error(`GitHub revocation failed: ${response.status}`);
}

console.log(`Accepted revocation request for ${credentials.length} credential(s)`);

実行例は次の通りです。シェル履歴にトークン値を残さないため、JSONファイルから読み込む形にしています。

GITHUB_CREDENTIALS_JSON="$(cat ./tmp/exposed-credentials.json)" \
REVOKE_CONFIRMATION="I_UNDERSTAND_CREDENTIALS_WILL_BE_REVOKED" \
yarn tsx scripts/revoke-github-credentials.ts

GitHub Actionsで月次訓練を回す

GitHub Actionsでは失効そのものではなく、訓練を自動化します。手順書、Firestore台帳、担当者変数がそろっているかを毎月確認し、欠けていれば失敗させます。

name: credential-incident-drill

on:
  schedule:
    - cron: "0 0 1 * *"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  drill:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "yarn"

      - run: yarn install --frozen-lockfile

      - run: yarn tsx scripts/check-credential-incident-readiness.ts
        env:
          SECURITY_RUNBOOK_PATH: docs/security/github-credential-incident.md
          INCIDENT_OWNER_GITHUB: ${{ vars.INCIDENT_OWNER_GITHUB }}

check-credential-incident-readiness.ts は、runbookファイルの存在、担当者変数、Firestoreの直近訓練記録を確認するだけで十分です。資格情報を失効する処理をCIへ入れると、誤実行時の被害が大きくなります。

実務で決めておくこと

決めることReserveKitでの例
初動の起票先credentialIncidents とGitHub Issue RK-SEC-*
特定ユーザー対応Enterprise設定から対象ユーザーのSSO認可を失効
EMUでの削除判断CTOとセキュリティ責任者の2名承認
全体break-glass本番デプロイ停止、Firestore管理画面凍結、影響チーム通知を同時に実施
復旧新しいPATやSSH鍵を発行し、GitHub Actions Secretsを棚卸し

GitHub credential types referenceでは、GITHUB_TOKEN、GitHub App installation token、deploy keyなど、今回のEnterpriseレベル操作で影響を受けない資格情報も明記されています。漏えい時には「全部止まったはず」と思い込まず、資格情報の種類ごとに封じ込め策を確認します。

まとめ

GitHubのself-service credential revocationは、漏えい時の初動を強くする機能です。ただし、強い機能ほど、平時の設計がないと危険です。

本記事では、Next.jsで受付を作り、Firestoreに判断と対応履歴を残し、GitHub Actionsで月次訓練を回す構成を紹介しました。失効操作は公式UIまたは公式APIに寄せ、アプリ側は台帳と運用に徹するのが現実的です。

まずは自社のGitHub資格情報を、PAT、SSH鍵、GitHub App、deploy key、Actions GITHUB_TOKEN に分類してみてください。そこまで見えると、どのケースで「特定ユーザーの失効」にするか、「全体break-glass」に進むかを落ち着いて判断できます。