GitHub App認証で自作ランナーを作ろうとして "401 Bad credentials" の沼に沈んだ話
はじめに
AWS EC2 で GitHub Actions の Self-hosted Runner を動かし、使い終わったら捨てるという構成(Ephemeral Runner)を組もうとしました。
「秘密鍵をSSMに入れて、起動時にスクリプトでトークン取得すれば一発でしょ?」
そう思っていた時期が私にもありました。
結果として、GitHub App認証の複雑な仕様 にハマり、半日を溶かしました。同じ悲劇を繰り返さないために、私が踏み抜いた地雷と解決策を共有します。
遭遇した地雷:無限 "Bad credentials"
GitHub Appを作成し、秘密鍵 (private-key.pem) をEC2に配置。シェルスクリプトでJWT(JSON Web Token)を生成してAPIを叩きました。
# JWTを使って登録トークンを取得しようとする
curl -X POST \
-H "Authorization: Bearer ${JWT}" \
"[https://api.github.com/orgs/MY-ORG/actions/runners/registration-token](https://api.github.com/orgs/MY-ORG/actions/runners/registration-token)"
返ってきたレスポンスは非情な 401 Bad credentials。
{
"message": "Bad credentials",
"documentation_url": "[https://docs.github.com/rest](https://docs.github.com/rest)",
"status": "401"
}
試したこと(全部ダメ)
- App IDの確認: 何度見ても合っている。
- 秘密鍵の再生成: GitHub上で再発行してSSMに入れ直したがダメ。
- 時刻同期: サーバーの時刻ズレを疑ったが正常。
-
権限設定:
Self-hosted runnersの権限をRead & writeにした。
「JWTは生成できているのに、なぜ認証してくれないんだ……」と絶望しました。
真の原因:2つの「認証の罠」
トラブルシュートの末、原因は 「権限の承認漏れ」 と 「認証方式の勘違い」 の複合技だと判明しました。
1. 権限変更後の「Accept」忘れ
GitHub AppのPermission(Self-hosted runners 等)を後から変更した場合、Organizationの設定画面でその変更を承認(Accept) しないと、APIアクセスは拒否され続けます。
- 場所: Organization Settings > GitHub Apps > Configure
- 症状: 画面上部に黄色いバナーで「A GitHub App is requesting additional permissions」と出ている。
これをポチッと押さない限り、いくら正しい鍵を使っても 401 になります。これが一番の盲点でした。
2. Organizationの操作には「Installation Token」が必要
もう一つの落とし穴です。
Organization(組織)レベルの操作を行う場合、JWTだけでは権限不足で弾かれることがあり、以下の2段階認証が必要でした。
- JWT を使って、「私のAppがこの組織にインストールされているID (Installation ID)」を特定する。
- そのIDを使って アクセストークン (Installation Token) を発行する。
- そのトークンを使って、初めて ランナー登録トークン が取れる。
私はJWT(Appそのものの証明書)を使って、直接組織内部の操作を行おうとして門前払いされていたわけです。
最終的な解決コード
最終的に動いたスクリプトがこちらです。
依存パッケージとして openssl jq が必要です。
#!/bin/bash
set -e
# 設定値
APP_ID="YOUR_APP_ID"
PEM_FILE="./private-key.pem"
ORG_NAME="YOUR_ORG_NAME"
# --- 1. JWT生成 (Appとしての認証) ---
b64enc() { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
header=$(echo -n '{"typ":"JWT","alg":"RS256"}' | b64enc)
now=$(date +%s)
payload=$(echo -n "{\"iat\":$(($now - 60)),\"exp\":$(($now + 600)),\"iss\":\"$APP_ID\"}" | b64enc)
signature=$(echo -n "${header}.${payload}" | openssl dgst -sha256 -sign "$PEM_FILE" | b64enc)
JWT="${header}.${payload}.${signature}"
# --- 2. Installation ID の取得 ---
# ここで「このAppが組織のどこにインストールされているか」特定する
INSTALLATION_ID=$(curl -s -H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
"[https://api.github.com/app/installations](https://api.github.com/app/installations)" | jq -r '.[0].id')
# --- 3. Installation Token (アクセストークン) の取得 ---
# 組織を操作するための「一時的な通行証」を発行
INSTALL_TOKEN=$(curl -s -X POST \
-H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
"[https://api.github.com/app/installations/$](https://api.github.com/app/installations/$){INSTALLATION_ID}/access_tokens" | jq -r .token)
# --- 4. ランナー登録トークンの取得 ---
# Installation Token を使って、ようやく目的のブツを入手
REG_TOKEN=$(curl -s -X POST \
-H "Authorization: token ${INSTALL_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"[https://api.github.com/orgs/$](https://api.github.com/orgs/$){ORG_NAME}/actions/runners/registration-token" | jq -r .token)
echo "Success! Registration Token: ${REG_TOKEN}"
まとめ
- GitHub App の権限を変えたら、インストール先のOrganization設定画面で承認 を忘れない。
- Organization の操作には、JWTではなく Installation Token を経由する。
エラーメッセージ Bad credentials は「鍵が違う」だけでなく、「手順が違う」「承認されていない」ときにも出るので、諦めずに仕様と承認フローを確認することが大事でした!