🔁

Google CloudとGraph APIの合わせ技!Entra IDアプリのシークレットを全自動ローテーション

に公開

🚀 はじめに

この記事では、Google Cloud 上の仕組み(Cloud Functions, Secret Manager, Cloud Tasks)を使って、Microsoft Entra ID (旧Azure AD) のアプリケーションシークレットを定期的に自動更新(ローテーション)する方法を、具体的なコードやコマンドを交えて紹介します。
本記事の要点は以下の2点です。

  1. 複数アプリ対応: 複数のEntra IDアプリのシークレットを一括で自動更新します。
  2. セルフローテーション: この仕組みを動かすための「管理用アプリ」のシークレットすらも、自分自身で自動更新(セルフローテーション)します。

「メインのクラウド基盤はAWS、グループウェアはGoogle Workspace。でも、Windows管理にIntuneを使っていたり、認証基盤としてEntra IDが中心にいたり…。」
そんな、現実世界のハイブリッド/マルチクラウド環境で奮闘するコーポレートエンジニア、SREの皆さんに向けて書いています。
「Entra IDのアプリシークレット、気付いたら有効期限ギリギリで、慌てて手動更新してる…」という方はもちろん、「これからIntuneやOffice 365の管理をAPIで自動化したい!」と考えている方にも、その第一歩として役立つはずです。
手動でのシークレット更新は、忘れた頃にやってくる面倒な作業。しかも、有効期限切れはサービス停止に直結する怖いイベントです。この仕組みで、そんなヒヤヒヤから解放されましょう!


🎯 この記事で実現すること

やることは意外とシンプル。Entra IDアプリの面倒なシークレット更新を、Google Cloudの力で自動化します。

  • 複数の Entra ID アプリのクライアントシークレットを定期的に自動生成(例: 100日ごと)
  • ローテーション用アプリ自身(管理用アプリ)のシークレットも自動でローテーション(セルフ更新)
  • 生成した新しいシークレットを Cloud Secret Manager に「新しいバージョン」として自動で保存
  • 古いシークレットを時間差で安全に無効化(Graceful Rotation)し、クリーンな状態を保つ
  • 実行結果(成功/失敗)を Slack に通知

🤔 なぜキーローテーションは面倒で、そして"超"重要なのか?

「シークレットの更新、大事なのはわかるけど、正直面倒…」と感じるエンジニアは多いはず。では、なぜ私たちはこの面倒な作業と向き合わなければいけないのでしょうか?

手動更新が引き起こす「あるある」な悲劇

手動更新が引き起こす「あるある」な悲劇
自動化されていない現場では、こんな悲劇が(文字通り)起こりがちです。

  • 「半年に一度の更新日、担当者は誰だっけ?」問題:

    • 半年に一度のシークレット更新日。手順書は古びたWikiの奥底に眠っていますが、肝心の更新手順は「前任の〇〇さんの記憶の中」。引き継ぎも曖昧で、気づけば形骸化。そして、ある日突然サービスが止まって初めて「ヤバい!」と気づきます。
  • シークレットの“使い回し”と連鎖拡散(別ワークフローやノーコード基盤で再利用):

    • 「便利だから」と、本番環境用のAPIシークレットをステージング環境や、あろうことか開発環境でも使い回し。さらにそのシークレットが、情シス管理外のノーコードツール(ZapierやMakeなど)にもコピペされ、どこで使われているか完全ブラックボックス化。漏洩時の影響範囲が特定できなくなります。
  • シャドー自動化(Zapier/Apps Script/個人Lambda 等)(所在不明・責任者不明):

    • 良かれと思って誰かがGAS(Google Apps Script)や個人のAWSアカウントのLambdaで組んだ「便利バッチ」が、シークレットをハードコードしたまま放置。作った担当者はとっくに異動・退職し、誰もその存在を知らないまま動き続け、ある日突然止まります。
  • チケット/チャット/メモへの貼り付け(DLP外で永続化):

    • 「一時的に」とSlackやBacklog、Jiraのチケットにシークレットを平文でペースト。DLP(データ損失防止)の監視外で、検索可能な状態で永続化されてしまいます。退職者のアカウントや過去のログを漁れば、簡単に機密情報にアクセスできる状態です。
  • 有効期限切れによるサービス停止(認証エラー→緊急対応→反映地獄):

    • そして最悪のシナリオ。深夜や休日にアラートが鳴り、原因が「認証エラー」だと判明。大慌てでシークレットを再発行。しかし、「使い回し」や「シャドー自動化」で拡散したすべての利用箇所を特定し、新しいシークレットに反映する「地獄のモグラ叩き」が待っています…。

どうせやるなら、仕組みでやる。 この記事のゴールは、そこです。

Microsoftからの「無期限は許しません」という強い意志

さらに重要な点として、Microsoft Graph APIのクライアントシークレットは、有効期限を無期限に設定できません

最大でも24ヶ月(2年) という上限が定められており、それ以降は必ず失効します。
さらにMicrosoftは、有効期限の値を12か月未満に設定することを推奨しています(出典は後述)。

これは、「シークレットは定期的に更新するものですよ」というMicrosoftからの強いメッセージです。つまり、キーローテーションは「やったほうが良い」という推奨事項ではなく、遅かれ早かれ必ず向き合わなければならない 「必須作業」 なのです。
どうせやらなければいけないのなら、手動でヒヤヒヤするより、仕組みで楽ちんかつ安全に解決してしまいましょう!


🧩 なぜクラウドをまたいで自動化するのか? 背景とアーキテクチャ

「Graph APIを叩くならAzure Functionsじゃないの?」と思うかもしれません。もちろん、それも正解の一つです。
しかし、現実の企業システムはもっと複雑です。例えば、あなたの会社がこんな構成だったとします。

  • アプリケーション基盤:
    • Google Cloud (GKE, Cloud Run, etc.) または AWS (EKS, Lambda...)
  • グループウェア:
    • Google Workspace
  • 認証基盤 :
    • Microsoft Entra ID / Google Workspace(identity) / Okta
  • 認証基盤 & PC管理:
    • Intune / Jamf / Iru (旧Kandji)

この構成、めちゃくちゃ「あるある」じゃないでしょうか?
こんな環境で、「Intuneに登録されているデバイス情報を、Google Cloud上の資産管理DBに定期的に取り込みたい」といった要件は自然と生まれます。

そのすべての起点となるのが、Microsoft Graph API です。
そして、そのAPI認証に使うシークレットの管理を自動化することは、あらゆる連携の安全性を担保する上で非常に重要になります。

この記事で紹介する「シークレットの自動ローテーション」は、いわばマルチクラウド連携における”兵站”を整える、地味ながらも超重要な仕組みなのです。


🏗️ システム構成(セルフローテーション対応)

この仕組みの重要な点は、Cloud Tasks を使って処理を時間差で実行している 部分です。これにより、ダウンタイムゼロの安全な切り替え(Graceful Rotation)を実現します。

フロー詳細(階層構造)

  • Cloud Scheduler

    • 定期的に Cloud Functions を実行します。
      phase: "add" として呼び出し)
  • Cloud Functions(フェーズ: add

    • 対象アプリ(管理用アプリ自身を含む) のリストをループ処理します。
      • Graph API を叩き、Entra ID に新しいシークレットを追加します。
        (例: 有効期限 100日
      • 返ってきた新しいシークレット値を、Cloud Secret Manager に「新しいバージョン」として追加します。
      • Cloud Tasks に、「N分後(例: 60分後)に古いシークレットバージョンを無効化してね」
        というタスク(phase: "sm_disable")を登録します。
  • Cloud Tasks

    • 指定された時間(N分)まで待機します。
  • Cloud Functions(フェーズ: sm_disable

    • Cloud Tasks から時間差で再度呼び出されます。
    • Secret Manager 上で、先ほど追加した最新バージョン以外のバージョンをすべて
      「無効(DISABLED)」状態にします。

💡 この「時間差(ディレイ)」が、ダウンタイムを防ぐための非常に重要なポイントです。
旧シークレットをすぐに削除せず、一定時間共存させることで、
各アプリが安全に新しいシークレットへ切り替えられる“グレースフル・ローテーション”を実現します。

🛠️ 事前準備:Entra ID と Google Cloud の設定

この自動化の「命綱」となる設定です。
Google Cloud側の設定は、この記事のフォーカスでもあるため記載しますが、Entra ID側の基本的なアプリ登録は公式ドキュメントを参照することで簡略化します。

1. Entra ID側:管理用アプリの作成と権限付与

まず、シークレットローテーションを実行するための「管理用アプリ」が必要です。

1-a. アプリの登録とシークレットの作成(公式Doc参照)

以下のMicrosoft公式ドキュメントを参考に、Entra IDに「管理用アプリ」を1つ新規登録し、初期ブートストラップ用のクライアントシークレットを1つ作成してください。

1-b. 必須のAPI権限(Application.ReadWrite.All)の付与

ここがこの記事の仕組みで最も重要なポイントです。
上記の手順で作成した管理用アプリに、他のアプリのシークレットを操作するための強力な権限を付与します。

  1. Azure Portalで [Microsoft Entra ID] > [アプリの登録] > [(作成した管理用アプリ)] を選択します。
  2. [APIの許可] > [アクセス許可の追加] を選択します。
  3. [Microsoft Graph] > [アプリケーションの許可] を選択します。
  4. Application.ReadWrite.All を検索してチェックを入れ、[アクセス許可の追加] をクリックします。
  5. 【最重要】 追加後、[(あなたのテナント名)に管理者の同意を与えます] ボタンをクリックして、権限を有効化します。(これを忘れると動きません)

⚠️ 警告: Application.ReadWrite.All は、テナント内の全アプリを操作できる非常に強力な権限です。この権限を持つ管理用アプリのシークレットは、Google CloudのSecret Managerで厳重に管理してください。

1-c. 必要なIDの取得

最後に、以下の情報をEntra IDのポータルから控えておきます。これらはGoogle Cloud側の環境変数として使用します。

  • 管理用アプリアプリケーション (クライアント) ID
  • 管理用アプリディレクトリ (テナント) ID
  • ローテーション対象にしたい全てのアプリ(管理用アプリ自身も含む)の オブジェクト ID
  • 手順1-aで作成した管理用アプリの「クライアントシークレットの値」

2. Google Cloud側:Secret ManagerとIAMの設定

  1. APIの有効化:
    • 以下、APIを有効化します。
      • Cloud Functions API
      • Cloud Scheduler API
      • Secret Manager API
      • Cloud Tasks API
        ...
  2. シークレットの保存 (初回):
    • Secret Managerでシークレットの「器」を作成します。
      (例: entra-rotator-admin-secret など)
    • 作成した器に、手順1-cで控えた管理用アプリのシークレット値を「最初のバージョン」として追加します。
  3. IAM権限の付与:
    • Cloud Functionsが使用するサービスアカウントに対して、以下のIAMロールを付与します。
      • シークレット マネージャのシークレット アクセサー
      • シークレット マネージャのバージョン管理者
      • Cloud Tasks 依頼元
      • Cloud Functions 起動元

⚙️ 環境構築とデプロイ

環境変数 (.env.yaml またはデプロイ時に設定)

更新対象のアプリ(管理用アプリ自身も含む)は、コード側でリストとして定義する想定です。

requirements.txt

Flask>=3.0.0
functions-framework>=3.0.0
requests>=2.31.0
msal>=1.20.0
google-cloud-secret-manager>=2.16.0
google-cloud-tasks>=2.13.0
google-cloud-logging>=3.0.0

環境変数

# Google Cloud Project
PROJECT_ID: "your-gcp-project-id"
REGION: "asia-northeast1"

# Secret Manager (管理用アプリのシークレット)
# ※ローテータ自身がこれを読み取り、自分自身の更新も行います
ADMIN_SECRET_RESOURCE_ID: "projects/your-gcp-project-id/secrets/entra-rotator-admin-secret/versions/latest"

# Entra ID (管理用アプリの情報)
ENTRA_TENANT_ID: "your-entra-tenant-id"
ENTRA_ADMIN_CLIENT_ID: "your-admin-app-client-id"

# Cloud Tasks
TASKS_QUEUE: "your-tasks-queue-name"
TASKS_LOCATION: "your-tasks-queue-location" # 例: asia-northeast1
TASK_IAM_SERVICE_ACCOUNT: "your-function-service-account@your-project.iam.gserviceaccount.com"
FUNCTION_URL: "your-cloud-function-trigger-url" # デプロイ後に設定

# Slack
SLACK_WEBHOOK_URL: "[https://hooks.slack.com/services/xxxx](https://hooks.slack.com/services/xxxx)"

🧱 実装のハイライト

☕ ちょっと寄り道:なぜmsalライブラリを使うのか?

Graph APIのようなMicrosoftのAPIを利用するには、OAuth 2.0に基づいたアクセストークンが必要です。
このトークン取得処理は、requestsライブラリで自前実装することも可能ですが、トークンのキャッシュ管理や更新処理などを考えると、かなり面倒です。
そこで登場するのが、Microsoft公式の認証ライブラリ msal (Microsoft Authentication Library for Python) です。
msalを使えば、以下のように数行のコードで、推奨される安全な方法でアクセストークンを取得できます。

import msal

# msalのクライアントを準備
app = msal.ConfidentialClientApplication(
    client_id="YOUR_CLIENT_ID",
    authority="https://login.microsoftonline.com/YOUR_TENANT_ID",
    client_credential="YOUR_CLIENT_SECRET",
)

# トークンを取得(キャッシュがあればそれを利用してくれる)
# .default スコープは、アプリに付与された全権限を要求するおまじない
token = app.acquire_token_for_client(scopes=["[https://graph.microsoft.com/.default](https://graph.microsoft.com/.default)"])

このように、面倒な処理をすべて隠蔽してくれるため、私たちは本来のビジネスロジック(今回はシークレットの追加)に集中できます。特別な理由がない限り、自前実装は避けてmsalを使いましょう。

🛡️ なぜ「即時削除」は危険なのか?

新しいシークレットを発行した直後に古いシークレットを削除すると、何が起きるでしょうか?
アプリケーションがまだ古いシークレットを掴んだままだった場合、その瞬間に認証エラーが発生し、サービス影響に直結します。
特に、コンテナやサーバレス環境では、インスタンスが新しいシークレット値(Secret Managerのlatest)を読み込むまでにタイムラグが発生することがあります。

💡 解決策:Cloud Tasksによる「安全な遅延無効化」

このコードは、その問題を以下のように解決します。

  • Entra ID側では新旧シークレットを共存させる:
    • 新しいシークレットを発行しても、Entra ID上では古いシークレットをすぐには消しません。(そもそもEntra ID側ではシークレットの「削除」は推奨されず、有効期限切れを待つのが基本です)
  • アプリケーションが参照するのはSecret Managerの latest:
    • アプリケーションは、Entra IDのシークレットを直接見るのではなく、Cloud Secret Managerの latest バージョンを参照するように構築します。
  • 時間差で「無効化」する:
    • 新しいシークレットをSecret Managerに v2 として追加したら、Cloud Tasksを使って60分後に v1 を「無効化」するタスクを投入します。60分間の猶予があれば、ほとんどのアプリケーションは新しい latest (v2) を読み込み、安全に切り替わることができます。
    • これにより、アプリケーションのダウンタイムをゼロに抑えた、非常に安全なローテーション(Graceful Rotation)が実現できるのです。

🐍 コード抜粋(イメージ)

対象アプリのリストは、コード内にハードコードするか、GCSなどから読み込む想定です。

import os
import json
from datetime import datetime, timedelta, timezone
from google.cloud import secretmanager, tasks_v2
import msal
import requests

# ... (クライアントの初期化) ...
sm_client = secretmanager.SecretManagerServiceClient()
tasks_client = tasks_v2.CloudTasksClient()

# (重要) ローテーション対象のリスト
# セルフローテーションのため、管理用アプリ自身もここに追加する
TARGETS = [
    {
        # 管理用アプリ (Self)
        "name": "Rotator-Admin-App",
        "object_id": "your-admin-app-object-id",
        "gcp_secret": "projects/your-gcp-project-id/secrets/entra-rotator-admin-secret"
    },
    {
        # 対象アプリ1
        "name": "Target-App-1",
        "object_id": "your-target-app1-object-id",
        "gcp_secret": "projects/your-gcp-project-id/secrets/entra-target-app1-secret"
    },
]

# ... (環境変数の読み込み) ...

def main(request):
    """
    HTTPトリガーのメイン関数。
    Schedulerからは {"phase": "add"} で呼ばれる。
    Tasksからは {"phase": "sm_disable", ...} で呼ばれる。
    """
    body = request.get_json(silent=True) or {}
    phase = body.get("phase")

    if phase == "add":
        return add_secrets(body)
    elif phase == "sm_disable":
        return disable_old_sm_versions(body)
    else:
        return "Invalid phase", 400

def add_secrets(body):
    """
    Phase 1: Entra IDで新シークレットを追加し、SMに新バージョンとして保存。
    その後、Phase 2 (sm_disable) のタスクをエンキューする。
    """
    # 1. 管理用トークンをMSALで取得
    admin_secret = sm_client.access_secret_version(name=ADMIN_SECRET_RESOURCE_ID).payload.data.decode("UTF-8")
    bearer_token = _get_msal_token(admin_secret)

    # 2. 対象アプリをループしてシークレットをローテーション
    sm_payload = {"phase": "sm_disable", "secrets": []}
    
    # 100日後のタイムスタンプを生成
    end_time = (datetime.now(timezone.utc) + timedelta(days=100)).isoformat()

    for t in TARGETS:
        try:
            # 3. Graph APIで新しいシークレットを追加 (100日有効)
            display_name = f"auto-rotated-{datetime.now(timezone.utc).strftime('%Y%m%d')}"
            res = _add_password(t["object_id"], display_name, end_time, bearer_token)
            
            # 4. Secret Managerに新しいバージョンとして保存
            # (重要) res["secretText"] はこのレスポンスでしか取れない!
            new_secret_text = res["secretText"]
            created_ver = _sm_add_version(t["gcp_secret"], new_secret_text)
            keep_version_num = int(created_ver.name.split("/")[-1])
            
            # 5. 遅延タスクのペイロードを作成
            sm_payload["secrets"].append({
                "secret_name": t["gcp_secret"],
                "keep_version": keep_version_num
            })
            
        except Exception as e:
            # ... (エラー通知) ...
            pass
            
    # 6. Cloud Tasksに「sm_disable」フェーズの実行を予約 (60分後)
    _enqueue_sm_disable_task(sm_payload)
    
    # ... (成功通知) ...
    return "Add phase completed.", 200


def disable_old_sm_versions(body):
    """
    Phase 2: Cloud Tasksから呼ばれ、SMの古いバージョンを無効化する。
    """
    secrets = list(body.get("secrets", []))
    total_disabled = 0
    for s in secrets:
        try:
            # 最新バージョン以外をすべて無効化する
            total_disabled += _sm_disable_older_versions(
                str(s["secret_name"]), int(s["keep_version"])
            )
        except Exception as e:
            # ... (エラー通知) ...
            pass
            
    # ... (成功通知) ...
    return f"Disable phase completed. Disabled {total_disabled} versions.", 200


def _sm_disable_older_versions(secret_name: str, keep_version: int) -> int:
    """指定されたバージョン以外をすべて無効化する"""
    count = 0
    for ver in sm_client.list_secret_versions(request={"parent": secret_name}):
        vnum = int(ver.name.rsplit("/", 1)[-1])
        # 「維持するバージョン」ではなく、かつ「有効」なものが対象
        if vnum != keep_version and ver.state.name == "ENABLED":
            sm_client.disable_secret_version(request={"name": ver.name})
            count += 1
    return count

# ... (_get_msal_token, _add_password, _sm_add_version, _enqueue_sm_disable_task などのヘルパー関数) ...

⚠️ 注意点とハマりどころ

  • Application.ReadWrite.All 権限の強さ:
    • この権限はテナント内の全アプリを操作できる、非常に強力なものです。管理用アプリのシークレット(entra-rotator-admin-secret)は厳重に管理し、アクセスを最小限に留めましょう。
  • セルフローテーションの鶏卵問題(初期ブートストラップ):
    • この仕組み自体を動かす「管理用アプリのシークレット」が必要です。最初の一回だけは手動で発行し、Secret Managerに v1 として登録する必要があります。一度登録すれば、あとはこの仕組みが自分自身のシークレットも自動で更新(v2, v3...を追加)していきます。
  • addPasswordのレスポンス:
    • Graph APIで新しいシークレットを追加(addPassword)する際、新しいシークレットの平文 (secretText) は、APIレスポンスに一度しか含まれません。 このタイミングで確実に取得してSecret Managerに保存しないと、二度と確認できないので注意してください。

🔭 今後の展開

この仕組みは、さらに発展させることができます。

  • 状態管理の堅牢化:
    • 更新対象のリスト(TARGETS)を、コード直書きではなくGCSのJSONファイルやFirestoreで管理する。
  • 証明書ベース認証への移行:
    • クライアントシークレットの代わりにクライアント証明書を使うことで、よりセキュアな認証に切り替える。(証明書のローテーションも自動化できれば最強)

✅ まとめ

  • Google CloudとGraph APIを組み合わせることで、Google Cloud+Microsoftのような現実的なマルチクラウド環境でも、安全なキーローテーションが自動化できます。
  • セルフローテーションを実装することで、「管理用シークレットの手動更新」という最後の面倒からも解放されます。
  • Cloud Tasksによる遅延実行は、ダウンタイムを防ぐ「Graceful Rotation」を実現するためのシンプルかつ強力なテクニックです。
  • Microsoftのガイダンス(12ヶ月未満推奨)にも沿って、短期間(例: 100日)でのローテーションを仕組み化しましょう。
  • 面倒な手作業はどんどん自動化して、エンジニアにしかできない価値ある仕事に集中していきましょう!

📚 参考

Microsoft ID プラットフォームにアプリケーションを登録する

クライアント シークレットの有効期間は、24 か月以下に制限することをお勧めします。
 Microsoft では、有効期限の値を 12 か月未満に設定することをお勧めします。

リクルートが実践するMicrosoft Graph APIを安心して使うためのポイント5選

Discussion