2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub App認証で自作ランナーを作ろうとして "401 Bad credentials" の沼に沈んだ話

2
Posted at

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"
}

試したこと(全部ダメ)

  1. App IDの確認: 何度見ても合っている。
  2. 秘密鍵の再生成: GitHub上で再発行してSSMに入れ直したがダメ。
  3. 時刻同期: サーバーの時刻ズレを疑ったが正常。
  4. 権限設定: 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段階認証が必要でした。

  1. JWT を使って、「私のAppがこの組織にインストールされているID (Installation ID)」を特定する。
  2. そのIDを使って アクセストークン (Installation Token) を発行する。
  3. そのトークンを使って、初めて ランナー登録トークン が取れる。

私は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 は「鍵が違う」だけでなく、「手順が違う」「承認されていない」ときにも出るので、諦めずに仕様と承認フローを確認することが大事でした!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?