From 4c1b3c74f62880660d17ce1bd6acc3e534ee684b Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Sun, 18 Oct 2026 13:13:47 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E9=8D=B5=E3=81=AE=E6=9C=9F=E9=99=90?= =?UTF-8?q?=E3=81=AE=E7=9B=A3=E8=A6=96(expiry.yml)=E3=82=92=20pulumi.yml?= =?UTF-8?q?=20=E3=81=AB=E3=81=BE=E3=81=A8=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit どちらも月曜の朝に同じ鍵(NetBird・Cloudflare・Entra の preview / apply)で入っていた。 中身は pulumi/expiry.sh に出し、pulumi.yml の `expiry` ジョブから呼ぶ。投稿は 9:00 から 13:00(JST)に移る。PR では投稿せずログに出すだけ(前と同じ)。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JbmWyUB9jcBcYCnNyiGBSV --- .github/workflows/expiry.yml | 123 ----------------------------------- .github/workflows/pulumi.yml | 50 ++++++++++++++ docs/cloudflare-tokens.md | 2 +- pulumi/expiry.sh | 68 +++++++++++++++++++ 4 files changed, 119 insertions(+), 124 deletions(-) delete mode 100644 .github/workflows/expiry.yml create mode 100755 pulumi/expiry.sh diff --git a/.github/workflows/expiry.yml b/.github/workflows/expiry.yml deleted file mode 100644 index a6f2f403..00000000 --- a/.github/workflows/expiry.yml +++ /dev/null @@ -1,123 +0,0 @@ -# **鍵の期限切れを Matrix の部屋 server に知らせる。** 毎週月曜 9:00 (JST)。 -# -# 期限を API から読めるものだけを見る。日付を手で書くファイルは置かない (書き忘れと書き換え忘れが起きる)。 -# **鍵は自分の期限だけを読む。** NetBird の Auditor は利用者の一覧は読めても他人のトークンは読めない (403) ので、 -# preview 用と apply 用をそれぞれのジョブで使う。apply 用は Environment `pulumi-apply` (main だけ) にあるので PR では見ない。 -# - NetBird のサービスユーザーのトークン (pulumi/README.md「NetBird」) -# - Cloudflare の API トークン (期限が付いていれば) -# - Entra のアプリ登録のクライアントシークレットと証明書 (テナントの全部。共用の Main が切れると SSO が全部止まる)。 -# preview の ID (Application.Read.All) で GitHub の OIDC から入るので鍵は要らない。preview のジョブだけで見る -# 期限の無いもの (WINGET_TOKEN など) は対象外。 -# -# 30 日を切ったか切れていたら投稿する。PR では投稿せずにログに出すだけ。手動実行で `report` を付けると全部を投稿する。 -# -# 秘密: -# MATRIX_SERVER_WEBHOOK_URL hookshot の部屋 server の受け口 (https://m.doany.io/webhook/) -# PREVIEW_NB_PAT / PREVIEW_CLOUDFLARE_API_TOKEN、NB_PAT / CLOUDFLARE_API_TOKEN (pulumi-apply) pulumi.yml と同じ -name: expiry - -on: - schedule: - - cron: "0 0 * * 1" - workflow_dispatch: - inputs: - report: - description: 期限が遠いものも含めて全部を投稿する (動作確認用) - type: boolean - default: false - pull_request: - paths: - - .github/workflows/expiry.yml - -permissions: - contents: read - # Entra へ OIDC で入る (preview のジョブだけ) - id-token: write - -jobs: - check: - strategy: - fail-fast: false - matrix: - key: ${{ github.event_name == 'pull_request' && fromJSON('["preview"]') || fromJSON('["preview", "apply"]') }} - runs-on: ubuntu-latest - environment: ${{ matrix.key == 'apply' && 'pulumi-apply' || '' }} - env: - KEY: ${{ matrix.key }} - NB_PAT: ${{ matrix.key == 'apply' && secrets.NB_PAT || secrets.PREVIEW_NB_PAT }} - CF_TOKEN: ${{ matrix.key == 'apply' && secrets.CLOUDFLARE_API_TOKEN || secrets.PREVIEW_CLOUDFLARE_API_TOKEN }} - CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - WEBHOOK: ${{ secrets.MATRIX_SERVER_WEBHOOK_URL }} - DRY_RUN: ${{ github.event_name == 'pull_request' }} - REPORT_ALL: ${{ inputs.report == true }} - WARN_DAYS: "30" - steps: - - if: matrix.key == 'preview' - uses: azure/login@v3 - with: - client-id: ${{ vars.PREVIEW_AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - allow-no-subscriptions: true - - run: | - set -euo pipefail - now=$(date -u +%s) - items=$(mktemp) # 1 行 1 件: 残り日数名前期限 - - add() { # $1 名前 $2 期限 (ISO 8601) - local exp; exp=$(date -u -d "$2" +%s) - # 切り捨てを負の側にも効かせる (切れて 1 日未満を「あと 0 日」にしない) - local d=$(( exp - now )); d=$(( d < 0 ? -((-d + 86399) / 86400) : d / 86400 )) - printf '%s\t%s\t%s\n' "$d" "$1" "$(date -u -d "$2" +%Y-%m-%d)" >> "$items" - } - - # NetBird: この鍵のサービスユーザー (gitops-pulumi-) のトークン。名前で選ぶ: - # サービスユーザーは users/current が 403 で自分を引けず、apply 用 (Admin) は他人のトークンも読めて二重に数えてしまう - nb() { curl -fsS -H "Authorization: Token $NB_PAT" -H "Accept: application/json" "https://nb.doany.io/api/$1"; } - u=$(nb "users?service_user=true" | jq -r --arg n "gitops-pulumi-$KEY" '.[] | select(.name == $n) | .id') - [ -n "$u" ] || { echo "NetBird: サービスユーザー gitops-pulumi-$KEY が無い" >&2; exit 1; } - # 先に変数へ受ける (`< <(…)` だと失敗しても落ちない) - tokens=$(nb "users/$u/tokens" | jq -r '.[] | select(.expiration_date) | [.name, .expiration_date] | @tsv') - while IFS=$'\t' read -r tname exp; do - if [ -n "$tname" ]; then add "NetBird gitops-pulumi-$KEY / $tname" "$exp"; fi - done <<< "$tokens" - - # Cloudflare: この鍵自身 (期限が無ければ null)。アカウントのトークンは accounts//tokens/verify でしか確かめられない - cf() { curl -fsS -H "Authorization: Bearer $CF_TOKEN" "https://api.cloudflare.com/client/v4/$1/tokens/verify"; } - cf_exp=$( { cf user 2>/dev/null || cf "accounts/$CF_ACCOUNT"; } | jq -r '.result.expires_on // empty') - [ -n "$cf_exp" ] && add "Cloudflare ($KEY 用)" "$cf_exp" - - # Entra: テナントの全アプリ登録のシークレットと証明書 (preview のジョブだけ。apply と二重に数えない) - if [ "$KEY" = preview ]; then - creds=$(az rest --method GET --url 'https://graph.microsoft.com/v1.0/applications?$select=displayName,passwordCredentials,keyCredentials&$top=999' | - jq -r '.value[] | .displayName as $app | ((.passwordCredentials // []) + (.keyCredentials // []))[] | select(.endDateTime) | [$app, (.displayName // .keyId), .endDateTime] | @tsv') - while IFS=$'\t' read -r app cname exp; do - if [ -n "$app" ]; then add "Entra $app / $cname" "$exp"; fi - done <<< "$creds" - fi - - sort -n "$items" | tee /dev/stderr > "$items.sorted" - if [ "$REPORT_ALL" = true ]; then - due=$(cat "$items.sorted") - else - due=$(awk -F'\t' -v w="$WARN_DAYS" '$1 <= w' "$items.sorted") - fi - if [ -z "$due" ]; then echo "期限が近いものは無い"; exit 0; fi - - lines=$(printf '%s\n' "$due" | awk -F'\t' '{ s = ($1 < 0) ? "切れている" : "あと " $1 " 日"; printf "- **%s**: %s (%s)\n", $2, $3, s }') - if [ "$REPORT_ALL" = true ]; then - text=$(printf ':key: 鍵の期限 (%s 用、全部)\n\n%s' "$KEY" "$lines") - else - text=$(printf ':key: 鍵の期限 (%s 用、%s 日以内)\n\n%s\n\n作り直して差し替える。手順は gitops の pulumi/README.md' "$KEY" "$WARN_DAYS" "$lines") - fi - if [ "$DRY_RUN" = true ]; then - echo "--- 投稿する内容 (PR なので投稿しない)"; echo "$text"; exit 0 - fi - jq -n --arg text "$text" '{text: $text}' | curl -fsS -X POST -H "Content-Type: application/json" --data-binary @- "$WEBHOOK" - - # 確かめられなかったことも知らせる (鍵が切れて 401 になったときなど。黙って落ちると気づかない) - - if: failure() && github.event_name != 'pull_request' - env: - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - jq -n --arg text ":warning: 鍵の期限を確かめられなかった ($KEY 用)。鍵が切れているかもしれない: $RUN_URL" '{text: $text}' | - curl -fsS -X POST -H "Content-Type: application/json" --data-binary @- "$WEBHOOK" diff --git a/.github/workflows/pulumi.yml b/.github/workflows/pulumi.yml index 299997ac..fa1351b7 100644 --- a/.github/workflows/pulumi.yml +++ b/.github/workflows/pulumi.yml @@ -5,6 +5,7 @@ # main からしか使えない) # 毎週月曜 → preview --refresh --expect-no-changes。Cloudflare 側を直接いじったら落ちる。 # Pulumi で表せない残り(カスタムドメイン・容量)も pulumi/report.ts で見る +# 鍵の期限も見る(pulumi/expiry.sh。30 日を切ったら Matrix の部屋 server に投稿) # # 書き込みの鍵は R2 の設定を書き換えられる(= バックアップのバケットを消せる)。 # 守りは 2 つ: Environment で main に限ること、doany-restic に `protect` が付いていること @@ -17,6 +18,7 @@ # PREVIEW_CLOUDFLARE_API_TOKEN / PREVIEW_AWS_ACCESS_KEY_ID / PREVIEW_AWS_SECRET_ACCESS_KEY # R2 の設定の読み取り + doany-pulumi の読み書き(state のロックに要る) # PREVIEW_NB_PAT NetBird の読み取り (サービスユーザー gitops-pulumi-preview、Auditor) +# MATRIX_SERVER_WEBHOOK_URL 鍵の期限の投稿先(expiry) # Environment `pulumi-apply` の秘密: # CLOUDFLARE_API_TOKEN / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY # R2 の設定の書き込み + doany-pulumi の読み書き @@ -45,6 +47,11 @@ on: schedule: - cron: "0 4 * * 1" workflow_dispatch: + inputs: + report: + description: 鍵の期限を、遠いものも含めて全部投稿する (expiry の動作確認用) + type: boolean + default: false permissions: contents: read @@ -173,3 +180,46 @@ jobs: cloud-url: ${{ secrets.PULUMI_BACKEND_URL }} pulumi-version-file: pulumi/.pulumi-version always-include-summary: true + + # 鍵の期限(pulumi/expiry.sh)。30 日を切ったか切れていたら Matrix の部屋 server に投稿する。 + # PR では投稿せずログに出すだけ。手動実行で `report` を付けると全部を投稿する。 + # 鍵は自分の期限しか読めないので、preview 用と apply 用をそれぞれで見る。apply 用は Environment `pulumi-apply` + # (main だけ)にあるので PR では見ない。 + expiry: + if: github.event_name != 'push' + strategy: + fail-fast: false + matrix: + key: ${{ github.event_name == 'pull_request' && fromJSON('["preview"]') || fromJSON('["preview", "apply"]') }} + runs-on: ubuntu-latest + environment: ${{ matrix.key == 'apply' && 'pulumi-apply' || '' }} + permissions: + contents: read + # Entra へ OIDC で入る (preview だけ) + id-token: write + env: + KEY: ${{ matrix.key }} + NB_PAT: ${{ matrix.key == 'apply' && secrets.NB_PAT || secrets.PREVIEW_NB_PAT }} + CF_TOKEN: ${{ matrix.key == 'apply' && secrets.CLOUDFLARE_API_TOKEN || secrets.PREVIEW_CLOUDFLARE_API_TOKEN }} + CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # hookshot の部屋 server の受け口 (https://m.doany.io/webhook/) + WEBHOOK: ${{ secrets.MATRIX_SERVER_WEBHOOK_URL }} + DRY_RUN: ${{ github.event_name == 'pull_request' }} + REPORT_ALL: ${{ inputs.report == true }} + WARN_DAYS: "30" + steps: + - uses: actions/checkout@v7 + - if: matrix.key == 'preview' + uses: azure/login@v3 + with: + client-id: ${{ vars.PREVIEW_AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + allow-no-subscriptions: true + - run: pulumi/expiry.sh + # 確かめられなかったことも知らせる (鍵が切れて 401 になったときなど。黙って落ちると気づかない) + - if: failure() && github.event_name != 'pull_request' + env: + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + jq -n --arg text ":warning: 鍵の期限を確かめられなかった ($KEY 用)。鍵が切れているかもしれない: $RUN_URL" '{text: $text}' | + curl -fsS -X POST -H "Content-Type: application/json" --data-binary @- "$WEBHOOK" diff --git a/docs/cloudflare-tokens.md b/docs/cloudflare-tokens.md index 0b0e19fa..99efa9f5 100644 --- a/docs/cloudflare-tokens.md +++ b/docs/cloudflare-tokens.md @@ -16,7 +16,7 @@ Pulumi で作ると値を Infisical に書く鍵が CI に要る。その鍵は | `gitops pulumi apply` / `preview` | アカウント | pulumi/README.md「鍵と state」 | GitHub の secrets | gitops の Pulumi | アカウントのトークンは `tools/t.ps1 cf accounts tokens list`、ユーザーのトークンは画面の **My Profile → API Tokens** で見る。 -**どれも期限を付けていない。** 期限の監視(`.github/workflows/expiry.yml`)が見るのは Pulumi 用だけ。 +**どれも期限を付けていない。** 期限の監視(`.github/workflows/pulumi.yml` の `expiry`、`pulumi/expiry.sh`)が見るのは Pulumi 用だけ。 ## 作り直す diff --git a/pulumi/expiry.sh b/pulumi/expiry.sh new file mode 100755 index 00000000..72fde50f --- /dev/null +++ b/pulumi/expiry.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# 鍵の期限を見て、30 日を切ったか切れていたら Matrix の部屋 server に知らせる。pulumi.yml の expiry ジョブから呼ぶ。 +# +# 期限を API から読めるものだけを見る。日付を手で書くファイルは置かない (書き忘れと書き換え忘れが起きる)。 +# **鍵は自分の期限だけを読む。** NetBird の Auditor は利用者の一覧は読めても他人のトークンは読めない (403) ので、 +# preview 用と apply 用をそれぞれのジョブで使う (KEY=preview|apply)。 +# - NetBird のサービスユーザーのトークン (README.md「NetBird」) +# - Cloudflare の API トークン (期限が付いていれば) +# - Entra のアプリ登録のクライアントシークレットと証明書 (テナントの全部。共用の Main が切れると SSO が全部止まる)。 +# preview のジョブだけで見る (az login 済みであること) +# 期限の無いもの (WINGET_TOKEN など) は対象外。 +# +# env: KEY NB_PAT CF_TOKEN CF_ACCOUNT WEBHOOK DRY_RUN REPORT_ALL WARN_DAYS +set -euo pipefail +now=$(date -u +%s) +items=$(mktemp) # 1 行 1 件: 残り日数名前期限 + +add() { # $1 名前 $2 期限 (ISO 8601) + local exp; exp=$(date -u -d "$2" +%s) + # 切り捨てを負の側にも効かせる (切れて 1 日未満を「あと 0 日」にしない) + local d=$(( exp - now )); d=$(( d < 0 ? -((-d + 86399) / 86400) : d / 86400 )) + printf '%s\t%s\t%s\n' "$d" "$1" "$(date -u -d "$2" +%Y-%m-%d)" >> "$items" +} + +# NetBird: この鍵のサービスユーザー (gitops-pulumi-) のトークン。名前で選ぶ: +# サービスユーザーは users/current が 403 で自分を引けず、apply 用 (Admin) は他人のトークンも読めて二重に数えてしまう +nb() { curl -fsS -H "Authorization: Token $NB_PAT" -H "Accept: application/json" "https://nb.doany.io/api/$1"; } +u=$(nb "users?service_user=true" | jq -r --arg n "gitops-pulumi-$KEY" '.[] | select(.name == $n) | .id') +[ -n "$u" ] || { echo "NetBird: サービスユーザー gitops-pulumi-$KEY が無い" >&2; exit 1; } +# 先に変数へ受ける (`< <(…)` だと失敗しても落ちない) +tokens=$(nb "users/$u/tokens" | jq -r '.[] | select(.expiration_date) | [.name, .expiration_date] | @tsv') +while IFS=$'\t' read -r tname exp; do + if [ -n "$tname" ]; then add "NetBird gitops-pulumi-$KEY / $tname" "$exp"; fi +done <<< "$tokens" + +# Cloudflare: この鍵自身 (期限が無ければ null)。アカウントのトークンは accounts//tokens/verify でしか確かめられない +cf() { curl -fsS -H "Authorization: Bearer $CF_TOKEN" "https://api.cloudflare.com/client/v4/$1/tokens/verify"; } +cf_exp=$( { cf user 2>/dev/null || cf "accounts/$CF_ACCOUNT"; } | jq -r '.result.expires_on // empty') +[ -n "$cf_exp" ] && add "Cloudflare ($KEY 用)" "$cf_exp" + +# Entra: テナントの全アプリ登録のシークレットと証明書 (preview のジョブだけ。apply と二重に数えない) +if [ "$KEY" = preview ]; then + creds=$(az rest --method GET --url 'https://graph.microsoft.com/v1.0/applications?$select=displayName,passwordCredentials,keyCredentials&$top=999' | + jq -r '.value[] | .displayName as $app | ((.passwordCredentials // []) + (.keyCredentials // []))[] | select(.endDateTime) | [$app, (.displayName // .keyId), .endDateTime] | @tsv') + while IFS=$'\t' read -r app cname exp; do + if [ -n "$app" ]; then add "Entra $app / $cname" "$exp"; fi + done <<< "$creds" +fi + +sort -n "$items" | tee /dev/stderr > "$items.sorted" +if [ "$REPORT_ALL" = true ]; then + due=$(cat "$items.sorted") +else + due=$(awk -F'\t' -v w="$WARN_DAYS" '$1 <= w' "$items.sorted") +fi +if [ -z "$due" ]; then echo "期限が近いものは無い"; exit 0; fi + +lines=$(printf '%s\n' "$due" | awk -F'\t' '{ s = ($1 < 0) ? "切れている" : "あと " $1 " 日"; printf "- **%s**: %s (%s)\n", $2, $3, s }') +if [ "$REPORT_ALL" = true ]; then + text=$(printf ':key: 鍵の期限 (%s 用、全部)\n\n%s' "$KEY" "$lines") +else + text=$(printf ':key: 鍵の期限 (%s 用、%s 日以内)\n\n%s\n\n作り直して差し替える。手順は gitops の pulumi/README.md' "$KEY" "$WARN_DAYS" "$lines") +fi +if [ "$DRY_RUN" = true ]; then + echo "--- 投稿する内容 (PR なので投稿しない)"; echo "$text"; exit 0 +fi +jq -n --arg text "$text" '{text: $text}' | curl -fsS -X POST -H "Content-Type: application/json" --data-binary @- "$WEBHOOK" + From f0eb3bff24513bee2c16bd33c32582354111872a Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Sun, 18 Oct 2026 13:38:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?pulumi/README.md=20=E3=81=AE=20expiry.yml?= =?UTF-8?q?=20=E3=81=AE=E5=8F=82=E7=85=A7=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JbmWyUB9jcBcYCnNyiGBSV --- pulumi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulumi/README.md b/pulumi/README.md index a8782151..b635c9be 100644 --- a/pulumi/README.md +++ b/pulumi/README.md @@ -48,7 +48,7 @@ state は R2 の別バケット **`doany-pulumi`**(APAC)にあり、パスフレ | どこ | 何 | | --- | --- | -| Actions secrets | `PULUMI_BACKEND_URL` / `PULUMI_CONFIG_PASSPHRASE` / `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_NOTIFY_WEBHOOK_URL`(notifications.ts の送り先)/ `MATRIX_SERVER_WEBHOOK_URL`(expiry.yml の送り先)/ `CLOUDFLARE_ZONE_ID`(dns.ts)/ `PREVIEW_CLOUDFLARE_API_TOKEN` / `PREVIEW_AWS_ACCESS_KEY_ID` / `PREVIEW_AWS_SECRET_ACCESS_KEY` | +| Actions secrets | `PULUMI_BACKEND_URL` / `PULUMI_CONFIG_PASSPHRASE` / `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_NOTIFY_WEBHOOK_URL`(notifications.ts の送り先)/ `MATRIX_SERVER_WEBHOOK_URL`(pulumi.yml の `expiry` の送り先)/ `CLOUDFLARE_ZONE_ID`(dns.ts)/ `PREVIEW_CLOUDFLARE_API_TOKEN` / `PREVIEW_AWS_ACCESS_KEY_ID` / `PREVIEW_AWS_SECRET_ACCESS_KEY` | | Environment `pulumi-apply` | `CLOUDFLARE_API_TOKEN` / `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | | 手元 | パスフレーズの写し `~/.config/doany/pulumi-passphrase` |