Github AppのWebhookを使って使用できるOrganizationを制限する

Github AppのWebhookを使って使用できるOrganizationを制限する

複数のOrganizationで使用するGitHub Appを全公開で公開しつつ、特定のOrganization以外での利用を制限する方法を紹介します。Webhookとサスペンド機能を組み合わせた実装方法について解説します。
2026.10.27

こんばんは、情報システム室の夏目です。

個人的にGitHub Appを使ってGithubへのアクセストークンを一時的に作成したりしています。

一つのOrganizationのリポジトリだけならそのOrganizationでGitHub Appを作成すればいいのですが、複数のOrganizationのリポジトリに対して同一のGitHub Appを付けたくなることもありました。
一応全公開でGitHub Appを作成すればできるのですが、それだと誰でもインストール可能になってしまいます。

今回はGitHub Appを使用できるOrganizationを制限しようと思います。

結論

  • 全公開になっているGitHub Appのインストールそのものは止められない
  • API経由でサスペンド(一時停止)させて使用させないことだけならできる
  • GitHub AppのWebhookでインストールされたというイベントを取得できる
    • 特定のOrganization以外ではサスペンドさせるということもできる

全公開にしたGitHub Appのインストールを止める手段はありませんでした。
ただ、アプリのオーナーとしてサスペンドすることで使用できないようにすることはできます。

https://docs.github.com/en/apps/maintaining-github-apps/suspending-a-github-app-installation

またGitHub AppでWebhookを設定することでGithub上のイベントの通知を受け取ることができます。
受け取れるイベントにはGitHub Appのインストールに関するイベント installation があります。
これをWebhookで受け取り、どのOrganizationのリポジトリにインストールされたかを確認して、特定のOrganization以外はサスペンドさせるということができそうです。

GitHub Appのサスペンド

https://docs.github.com/en/apps/maintaining-github-apps/suspending-a-github-app-installation

インストールしているGitHub Appをサスペンドすると、そのGitHub Appはリソースにアクセスできなくなります。
例えばアプリの認証情報が漏洩した可能性があるときなどにGitHub Appをサスペンドすることがあると思われます。

GitHub Appのサスペンドは以下の二人が可能です。

  • GitHub Appのオーナーおよび指定されたアプリ管理者
  • 個人アカウントもしくは自身が所有するアカウントにGitHub Appをインストールしたユーザー

GitHub AppのオーナーはAPIでのみサスペンドが可能であり、インストールした 
ユーザーはGithubのWebコンソールからのみサスペンドが可能です。

1度サスペンドした後、サスペンドを解除することも可能です。
ただしサスペンドの解除はサスペンドと同じ方法で行う必要があります。

APIでサスペンドしたのならAPIで解除しなければならないですし、WebコンソールからサスペンドしたのならWebコンソールで解除しなければなりません。
誰がやるかによってサスペンドの手段が異なるので、最終的にはサスペンドした人が解除すると考えて良いです。

GitHub AppのWebhookとinstallationイベント

https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/using-webhooks-with-github-apps

https://docs.github.com/en/webhooks/webhook-events-and-payloads#installation

WebhookでGithub上のイベント通知を受け取ることができます。
イベントという大きなくくりの中にアクションで具体的にどんなことが行われたかが表現されます。
例えば、GitHub Appがインストールされたときには installation というイベントで created というアクションになります。

デフォルトで受け取るイベント以外は、どのイベントを受け取るかGitHub Appで設定することになります。

今回使うのは installation というイベントです。
これはデフォルトで受け取るイベントであり、外すことはできません。

このイベントはGitHub Appのインストールに関するイベントです。
アクションは5つあります。

  1. created: ユーザーまたはOrganizationにGitHub Appがインストールされた
  2. deleted: ユーザーまたはOrganizationからGitHub Appがアンインストールされた
  3. new_permissions_accepted: GitHub Appに新しい権限を付与した
  4. suspend: GitHub Appが一時停止されユーザーやOrganizationへのアクセスがブロックされた
  5. unsuspend: GitHub Appの一時停止が解除され、再度アクセスできるようになった

実装する

GitHub Appの作成とWebhookを待ち受けるAPIを作成します。
今回待ち受けるAPIはCloudflare Workersで動かし、Webhookの署名検証やGitHubのAPI Callなどはoctokitを使います。

GitHub Appを作成する

https://github.com/settings/apps/new

上記リンクからGitHub Appを作成します。

今回は auto-suspend という名前で作成します。
Homepage URL は必須なのでGithubの自分のProfileとしました。

下にスクロールして、Webhookを一旦無効化します。
現時点ではWebhookを待ち受けるAPIを作るのに必要なものが足りていないからです。

Permissionsはここでは特に付けません。
実際にはGitHub Appにやらせたいことにあわせて設定してください。

Where can this GitHub App be installed? は Any account にします。

Create GitHub App をクリックしてGitHub Appを作成します。

作成できました。

Client ID は後で使うので値を控えておきます。

秘密鍵を作成する

サスペンドはAPIで行うので一時的なPAT発行のために秘密鍵を作成します。

Credentials をクリックして画面遷移します。

Key pairsのタブをクリックして、鍵の画面に遷移します。

New key をクリックすると秘密鍵が作成され、そのファイルがダウンロードされます。

ダウンロードした秘密鍵のヘッダーは次のようになっています。

-----BEGIN RSA PRIVATE KEY-----

これは PKCS#1 形式の秘密鍵です。
octokitをCloudflare Workersで使うときは PKCS#8 形式でないと行けないので変換をします。

$ openssl pkcs8 \
    -topk8 \
    -inform PEM \
    -outform PEM \
    -nocrypt \
    -in "<秘密鍵へのパス>" \
    -out private-key-pkcs8.pem

変換後の秘密鍵のヘッダーは次のようになっています。

-----BEGIN PRIVATE KEY-----

Workerを実装する

Cloudflare WorkersでWebhookを受け付けるAPIを実装します。

雛型を作成し、octokitをインストールします。
GithubのWebhookの解釈や署名検証、APIを叩くのに使用します。

$ npx cf init trial-github-app-webhook-worker
$ cd trial-github-app-webhook-worker
$ npm install octokit

cloudflare.config.tsを編集します。

cloudflare.config.ts
 import { bindings, defineConfig } from "cf/config";

 import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };

 export default defineConfig({
   worker: {
     name: "trial-github-app-webhook-worker",
     compatibilityDate: "2026-10-23",
     entrypoint,
+    observability: {
+      enabled: true,
+      logs: { enabled: true },
+    },
+    workersDev: true,
+    previewUrls: false,
     env: {
-      WORLD: bindings.text("World"),
+      // GitHub AppのClient ID
+      CLIENT_ID: bindings.secret(),
+
+      // GitHub Appの秘密鍵 (PKCS#8)
+      PRIVATE_KEY: bindings.secret(),
+
+      // Webhookの署名検証に使う文字列
+      WEBHOOK_SECRET: bindings.secret(),
+
+      // GitHub Appを使っても良いOrganizationのアカウントIDをカンマ区切りで書く
+      TARGET_ACCOUNT_IDS: bindings.secret(),
     },
   },
 });

Secretsを4つ使用します。
デプロイ時に使用するので、 secrets.jsonというJSONファイルを作成しシークレットの値を記載します。
WEBHOOK_SECRET は好きな文字列を入れてください (私はUUIDを使用しました)。

次にTypeScriptのコードを実装します。

src/index.ts
 import { env } from "cloudflare:workers";
 import { App } from "octokit";

 export default {
   async fetch(request: Request) {
     if (request.method !== "POST") {
       return Response.json({ message: "Method Not Allowed" }, { status: 405 });
     }

+    const app = new App({
+      appId: env.CLIENT_ID,
+      privateKey: env.PRIVATE_KEY,
+      webhooks: {
+        secret: env.WEBHOOK_SECRET,
+      },
+    });

     // WebhookではOrganizationのアカウントIDが数値で来るので、数値に変換する
     const allowedOrganizations = env.TARGET_ACCOUNT_IDS.split(",").map((x) => Number(x));

+    // installationイベントのcreatedアクションのときに処理を行う
+    app.webhooks.on("installation.created", async ({ octokit, payload }) => {
+      // インストールを許容するOrganizationかチェックする
+      const accountId = payload.installation.target_id;
+      if (allowedOrganizations.includes(accountId)) {
+        return;
+      }
+
+      // Github Appのサスペンドを行う
+      const installationId = payload.installation.id;
+      await octokit.request("PUT /app/installations/{installationId}/suspended", {
+        installationId,
+      });
+    });

+    // GitHubのWebhookヘッダー, 署名検証に使う
+    const id = request.headers.get("X-GitHub-Delivery");
+    const name = request.headers.get("X-GitHub-Event");
+    const signature = request.headers.get("X-Hub-Signature-256");
+
+    if (!id || !name || !signature) {
+      return Response.json({ message: "Missing webhook headers" }, { status: 400 });
+    }
+
+    const payload = await request.text();
+
+    try {
+      // 署名検証をしつつ、事前に定義していたイベントなら処理を行う
+      await app.webhooks.verifyAndReceive({ id, name, signature, payload });

       return Response.json({ ok: true });
     } catch (e) {
       console.error(`Webhook processing failed: ${e}`, e);

       return Response.json(
         { message: "Webhook verification or processing failed" },
         { status: 400 },
       );
     }
   },
 } satisfies ExportedHandler;

重要そうなところはハイライトを入れています。

デプロイします。

$ npx cf deploy --secret-file secrets.json
│  Your Worker has access to the following bindings:
│  Binding                                  Resource                  
│  env.PRIVATE_KEY ("(hidden)")             Environment Variable      
│  env.CLIENT_ID ("(hidden)")               Environment Variable      
│  env.WEBHOOK_SECRET ("(hidden)")          Environment Variable      
│  env.TARGET_ACCOUNT_IDS ("(hidden)")      Environment Variable      
│
│  Uploaded trial-github-app-webhook-worker (6.95 sec)
│  Deployed trial-github-app-webhook-worker triggers (0.91 sec)
│    https://trial-github-app-webhook-worker.luciferous.workers.dev
│  Current Version ID: 6d9ecedd-0867-4176-9d78-a26a96ff1a04
◆  Deploy complete

これでWebhookを待ち受けるAPIができました。

Github AppにWebhookを設定する

Github Appの設定画面でWebhookを設定します。
(General -> Webhook)

Secret にはWorkerのシークレット WEBHOOK_SECRET に設定した値と同じ値を入力します。

Save changes をクリックしてWebhookを設定します。

試してみる

今回 Workerのシークレット TARGET_ACCOUNT_IDS には、自分のGithubのアカウントIDと luciferous-laboratory というOrganizationのIDを入れています。

この二つと、 luciferous-react-component というOrganizationにGithub Appをインストールしてみます。


自分のGitHubアカウントの場合


luciferous-laboratoryの場合


luciferous-react-componentの場合(インストール直後)


luciferous-react-componentの場合(リロードした後)

サスペンドされました。

まとめ

以上、Github Appを全公開にしつつ使用できるOrganziationを制限する話でした。

何かのお役に立てたら幸いです。

この記事をシェアする

関連記事