Microsoft Entra IDの削除済みObjectIDは何だった?ユーザー・サービスプリンシパル・マネージドIDを特定する手順とKQL調査

Azure RBAC のロール割り当てで「Identity not found / Unknown」と表示され、手元に残ったのは ObjectID だけ——その正体がユーザーなのかサービス プリンシパルなのかマネージド ID なのか、KQL で追えるのか悩むケースがあります。本記事では判別できる条件と、できない時の安全な対処、再発防止の運用まで整理します。

目次

起きている現象:「Identity not found」かつ「Unknown」タイプのロール割り当て

管理グループ/サブスクリプション/リソース(またはリソース グループ)の IAM でロール割り当てを見たとき、割り当て先のプリンシパルが Identity not found、種類が Unknown と表示されることがあります。

Microsoft のドキュメントでは、この症状の代表的な原因として次の 2 つが挙げられています。

  • 原因 1(待てば直る可能性):招待直後などでレプリケーションが間に合っていない
  • 原因 2(オーファン化):ユーザー/グループ/サービス プリンシパル/マネージド ID を削除したのに、RBAC ロール割り当てだけが残っている

質問のように「削除済みの ObjectID だけが残っている」ケースは、多くが 原因 2(オーファン化)に該当します。ただし、後述のとおり「そもそもその GUID が実在したことがない(作成時に間違えて別の GUID を入れた)」パターンもあり得ます。

先に結論:特定できるかどうかは “2つの期限” で決まる

削除済み ObjectID の「正体(ユーザー/サービス プリンシパル/マネージド ID)」を後から突き止められるかは、ざっくり次の条件で決まります。

状況「正体」を特定できる可能性主な調査先
削除から 30 日以内で、削除済みコンテナーに残っている高い(種類まで分かる)Microsoft Entra の削除済み(deletedItems / リサイクル)
削除から時間が経っているが、監査ログ/サインインログが残っている中(ログに type/displayName が残っていれば分かる)AuditLogs / Sign-in Logs(Log Analytics)
削除済みコンテナーにもなく、ログ保持期限も過ぎている低い(実質不可能)台帳や IaC の履歴がない限り困難

つまり「KQL で必ず判別できる」わけではなく、ログ(または削除済みコンテナー)に情報が残っている期間だけ追跡できます。期限を過ぎると、RBAC 側に残るのは GUID(principalId)だけなので、復元的に断定できません。

混同しやすい前提:ObjectID と AppId(Client ID)は別物

質問の KQL では InitiatedBy.app.appId と ObjectID を比較していますが、ここが混乱ポイントになりがちです。Entra の「アプリ」には少なくとも 2 種類の GUID が登場します。

呼び名意味どこで出やすいかRBAC ロール割り当て(principalId)に入るのは?
ObjectIDディレクトリ オブジェクトの ID(ユーザー、グループ、サービス プリンシパル等)ユーザー詳細、Enterprise Applications、Managed Identity、ログの TargetResources.id などこれ
AppId(Client ID)アプリ登録(application)の appId。いわゆるクライアント IDアプリ登録の画面、トークン要求、ログの initiatedBy.app.appId など基本的に入らない(入れると Unknown の原因になる)

Log Analytics の AuditLogs テーブルでは、InitiatedBy は「ユーザーまたはアプリが開始した」ことを示す動的フィールドで、ターゲット側(変更された対象)は TargetResources に入ります。

そのため、「削除されたプリンシパルの ObjectID が何者か」を追う場合は、まず TargetResources[].id を中心に追い、必要に応じて InitiatedBy 側(user.id / app.servicePrincipalId など)も併用するのが安全です。

Unknown が出る“もう1つの原因”:削除ではなく「そもそも GUID が間違っている」

現場で意外と多いのが、次のようなパターンです。

  • RBAC 付与時に service principal の ObjectID ではなく appId(Client ID) を principalId として渡してしまった
  • IaC(ARM/Bicep/Terraform)の変数がすり替わっていて、実在しない GUID が principalId に入った

実際、ロール割り当てリソース(Microsoft.Authorization/roleAssignments)は principalId に GUID を入れるだけでデプロイが通り、結果として 存在しない principalId のロール割り当てが生まれてしまうケースがあります。こうなると、削除済みを探しても当然見つかりません。

「削除済みオブジェクトを探したが何も出ない」場合、削除以前に“誤った GUID を割り当てた”可能性も並行で疑う価値があります。

調査手順:上から順に潰すと迷いにくい

今すぐ確認:これは一時的(レプリケーション)?それともオーファン?

招待したゲストや新規作成直後のタイミングでは、一時的に Identity not found が出て、しばらくすると解消する場合があります(レプリケーション遅延)。

しかし「数日経っても消えない」「削除作業をした覚えがある」「ObjectID だけが残っている」なら、オーファンの可能性が高いです。

手順 1:その ObjectID が “現在も存在するか” を確認する(Graph が最短)

最短で白黒つけるなら Microsoft Graph で directoryObjects を直接引きます。

  • 存在する:レスポンスに @odata.type が出る(user / servicePrincipal / group など)
  • 存在しない:404(または権限不足)

例(REST)

GET https://graph.microsoft.com/v1.0/directoryObjects/<ObjectID>

権限が不足していると「Unknown」に見えることもあります。Az PowerShell のコマンドは、ディレクトリ参照に失敗した場合(オブジェクトが見つからない、または権限不足)に ObjectType を Unknown として扱う旨が明記されています。

したがって、調査用アカウントが Directory.Read.All 相当の閲覧権限を持たない状態で「Unknown だったから削除済み」と断定するのは危険です。Graph で 404 を確認する、もしくは Entra 管理者権限で同じ確認をするのが堅いです。

手順 2:削除から 30 日以内なら「削除済みコンテナー」で種類を特定できる

アプリ/サービス プリンシパルはソフト削除され、削除済みコンテナーに最大 30 日残ります。30 日を過ぎると完全削除されます。

Microsoft Graph の deletedItems は、少なくとも application / servicePrincipal / group / user などの削除済みオブジェクトを扱えます。

例(REST):削除済みオブジェクトを直接引く

GET https://graph.microsoft.com/v1.0/directory/deletedItems/<ObjectID>

例(REST):削除済みの一覧から探す(件数が多い場合はフィルタやページングが必要)

GET https://graph.microsoft.com/v1.0/directory/deletedItems

ここでレスポンスが得られれば、@odata.type やプロパティで「user」「servicePrincipal」などが確定します。この段階で “正体” はほぼ確定します。

なお、マネージド ID も Entra 的には(実体として)サービス プリンシパルに紐づきます。Microsoft の FAQ では、マネージド ID のサービス プリンシパルもソフト削除され、30 日以内はリサイクル ビンから閲覧できる一方で、復元や完全削除はできず、30 日後に自動で完全削除されると説明されています。

「ユーザー/サービス プリンシパル/マネージド ID」を見分けたい場合、削除から 30 日以内であれば deletedItems 側の情報が最も信頼できます。

手順 3:AuditLogs(Entra 監査ログ)を “展開して” 判別する

質問の KQL は方向性として近いのですが、TargetResources は dynamic(配列)なので、「含まれていそう」を文字列検索するより、配列を展開して id を厳密一致させた方が「type」を取り出しやすくなります。

まずは TargetResources を展開して、type / displayName を抜く

let oid = "<ObjectID>";
AuditLogs
| mv-expand tr = TargetResources
| extend TargetId = tostring(tr.id)
| where TargetId == oid
| project
    TimeGenerated,
    ActivityDisplayName,
    OperationName,
    Result,
    TargetType = tostring(tr.type),
    TargetDisplayName = tostring(tr.displayName),
    TargetUPN = tostring(tr.userPrincipalName),
    InitiatedBy
| sort by TimeGenerated desc

AuditLogs の TargetResources には「どのリソースが変更されたか」が入り、リソース種別は User / App / Group / Policy などになり得ます。

ここで TargetType が User と出れば「ユーザーだった」可能性が高く、ServicePrincipal(または App) と出れば「サービス プリンシパル系」だった可能性が高い、というところまでは進めます。

InitiatedBy 側も一緒に拾う(user.id / app.servicePrincipalId / app.appId)

let oid = "<ObjectID>";
AuditLogs
| mv-expand tr = TargetResources
| extend
    TargetId = tostring(tr.id),
    InitiatorUserId = tostring(InitiatedBy.user.id),
    InitiatorSpId   = tostring(InitiatedBy.app.servicePrincipalId),
    InitiatorAppId  = tostring(InitiatedBy.app.appId)
| where TargetId == oid
   or InitiatorUserId == oid
   or InitiatorSpId == oid
   or InitiatorAppId == oid
| project
    TimeGenerated,
    ActivityDisplayName,
    OperationName,
    Result,
    TargetType = tostring(tr.type),
    TargetDisplayName = tostring(tr.displayName),
    TargetId,
    InitiatorUserId,
    InitiatorSpId,
    InitiatorAppId,
    InitiatedBy
| sort by TimeGenerated desc

このクエリの狙いは、「変更された対象が oid」なのか、「操作した主体が oid」なのかを分けて見ることです。特にアプリ系は AppId と ServicePrincipal の ObjectID が別なので、InitiatedBy.app.appId だけ追っても空振りすることがあります。

削除操作のイベントだけに絞る(ログが残っていれば最短で “正体” に近づく)

let oid = "<ObjectID>";
AuditLogs
| where OperationName has "Delete" or ActivityDisplayName has "Delete"
| mv-expand tr = TargetResources
| where tostring(tr.id) == oid
| project TimeGenerated, ActivityDisplayName, OperationName, Result,
          TargetType=tostring(tr.type), TargetDisplayName=tostring(tr.displayName), InitiatedBy
| sort by TimeGenerated desc

削除イベントが残っていれば、削除された瞬間の displayName / type を拾える可能性があります。ただし、監査ログの保持期間を過ぎている・ログ転送設定が途中からだった等の場合は、この方法では追えません。

手順 4:サインインログで “ユーザー/サービス プリンシパル/マネージド ID” を横断検索する

サインインログは、種類ごとにテーブルが分かれます。Microsoft Entra では、少なくとも「ユーザー(対話)」「ユーザー(非対話)」「サービス プリンシパル」「マネージド ID」の種別が説明されています。

Log Analytics 側でも、たとえば次のようなテーブルが用意されています(代表例)。

  • SigninLogs(主に対話ユーザー)
  • AADNonInteractiveUserSignInLogs(非対話ユーザー)
  • AADServicePrincipalSignInLogs(サービス プリンシパル)
  • AADManagedIdentitySignInLogs(マネージド ID)

横断検索(ObjectID が UserId / ServicePrincipalId に一致するか)

let oid = "<ObjectID>";

union isfuzzy=true
(
SigninLogs
| extend PrincipalId = tostring(UserId), PrincipalKind = "User(SigninLogs)"
),
(
AADNonInteractiveUserSignInLogs
| extend PrincipalId = tostring(UserId), PrincipalKind = "User(NonInteractive)"
),
(
AADServicePrincipalSignInLogs
| extend PrincipalId = tostring(ServicePrincipalId), PrincipalKind = "ServicePrincipal"
),
(
AADManagedIdentitySignInLogs
| extend PrincipalId = tostring(ServicePrincipalId), PrincipalKind = "ManagedIdentity"
)
| where PrincipalId == oid
| project TimeGenerated, PrincipalKind, PrincipalId,
Identity, ServicePrincipalName, UserPrincipalName,
AppId, ResourceDisplayName, IPAddress, ResultType, ResultDescription
| sort by TimeGenerated desc

これでヒットした場合、少なくとも「ユーザー系」「サービス プリンシパル系」「マネージド ID 系」のどれに近いかが掴めます。特に AADManagedIdentitySignInLogs は、列として ManagedServiceIdentity(マネージド ID の詳細)を持つため、マネージド ID の痕跡を拾えることがあります。

逆に、どのテーブルにもヒットしない場合は「そのプリンシパルはサインインしていない(権限付与のみで使われなかった)」か、「ログ保持期間外」か、「ログ転送していなかった」可能性が高くなります。

手順 5:マネージド ID かどうかを “Azure リソース側” から逆引きする

ここはオリジナルの実務テクニックですが、「もしマネージド ID なら Azure リソースのどこかに principalId が残っている」可能性があります。

  • ユーザー割り当てマネージド ID:それ自体が Azure リソースで、プロパティとして「関連するサービス プリンシパルの principalId」を持ちます。
  • システム割り当てマネージド ID:VM/Functions/App Service などのリソースの identity に principalId がぶら下がります(リソース削除で一緒に消えるのが基本)。

ユーザー割り当てマネージド ID(UAI)を探す発想
削除されていなければ、UAI のリソースから principalId を確認できます。REST の仕様にも「principalId はサービス プリンシパルの id」と明記されています。

Resource Graph を使える環境なら、次のように「UAI の principalId が一致するもの」を探すことで、その ObjectID が UAI 由来だったことが分かる場合があります(環境により列名や整形は調整してください)。

// Azure Resource Graph(概念例)
resources
| where type =~ "microsoft.managedidentity/userassignedidentities"
| extend principalId = tostring(properties.principalId)
| where principalId == "<ObjectID>"
| project name, id, resourceGroup, subscriptionId, location, principalId

また、システム割り当て(SAI)を疑うなら、幅広いリソースの identity.principalId を検索します。

// Azure Resource Graph(概念例)
resources
| where isnotnull(identity)
| extend principalId = tostring(identity.principalId)
| where principalId == "<ObjectID>"
| project name, type, id, resourceGroup, subscriptionId, principalId

これでヒットすれば「少なくとも当時はマネージド ID(SAI)として使われていた」可能性が高まります。ヒットしなければ、削除済み・別テナント由来・そもそも実在しない GUID などの可能性が残ります。

Unknown ロール割り当ては削除してよいのか

「Identity not found / Unknown」がオーファン(削除済み)であることが確認できた場合、そのロール割り当ては 実体が存在しないプリンシパルへの参照なので、セキュリティ上も運用上も整理対象になります。Microsoft も、使われていないロール割り当ては削除するのが良いプラクティスだと説明しています。

マネージド ID のベストプラクティスでも、もはや紐づかないロール割り当てが Unknown になることに触れ、Unknown をフィルタして削除する例が示されています。

例(Az PowerShell):Unknown を抽出して削除(実行前に必ず一覧を保存)

// まずは確認(削除しない)
Get-AzRoleAssignment | Where-Object {$_.ObjectType -eq "Unknown"} | Select-Object Scope,RoleDefinitionName,ObjectId,ObjectType

// 問題ないと判断できたら削除
Get-AzRoleAssignment | Where-Object {$_.ObjectType -eq "Unknown"} | Remove-AzRoleAssignment

ただし前述の通り、Unknown は「本当に存在しない」以外に「権限不足で種類が取れない」でも起こり得ます。削除の前に、可能なら Graph で存在確認(404)や、Entra 管理者権限での再確認を挟むと事故が減ります。

今回の質問に戻る:結局 “削除済み ObjectID の正体” はどこまで分かる?

質問文の状況(削除済みユーザー/グループ等も確認したが該当なし、AuditLogs でも判断できない)を前提にすると、現実的な落としどころは次の 3 段階です。

判定レベル分かること条件
レベル 1「もうテナント内に存在しない(or 参照不能)」RBAC が Identity not found / Unknown、Graph でも取れない
レベル 2「ユーザー系か、サービス プリンシパル系か」AuditLogs / Sign-in Logs に type や UserId/ServicePrincipalId が残っている
レベル 3「マネージド ID だった」まで断定AADManagedIdentitySignInLogs の痕跡、または Azure リソース側に principalId の紐づきが残っている

そして、削除済みコンテナー(30 日)にもログ(保持期間)にも何も残っていない場合、RBAC に残るのは GUID だけなので、ユーザーだったのかサービス プリンシパルだったのかを「確実に復元」するのは難しくなります。

再発防止:後から困らないための運用(台帳・IaC・ログ設計)

ロール割り当ては “必ず棚卸し可能” にしておく

重要な権限付与(特にサブスクリプション Owner / UAA / 重要データの DataActions など)は、次のどれかで追跡できる状態にしておくと、削除後の調査コストが激減します。

  • IaC(Bicep / Terraform)で管理:roleAssignments をコード化し、レビュー可能にする
  • 定期エクスポート:role assignment を CSV/JSON で保存しておく(principalId と表示名の対応を残す)
  • 変更申請の台帳:少なくとも「誰に(principalId)」「どのロールを」「どのスコープに」を残す

roleAssignments には principalId と principalType を持たせられます。IaC では principalType を明示しておくと、将来の読み解きが少し楽になります。

「削除したのに RBAC が残る」を前提に、削除手順を決める

運用手順としては単純で、プリンシパル削除前に RBAC を剥がすのが最もクリーンです。逆順にすると、今回のようにオーファンが残ります。

特にマネージド ID は「削除して作り直すと principalId が変わる」ため、古い role assignment が残ってしまいがちです。作り直しを含む運用では、最後に IAM の残骸を確認するチェックを入れるとよく効きます。

ログ保持と転送を “設計” しておく

Entra の AuditLogs は、InitiatedBy や TargetResources など調査に必要な情報を含みます。

ただし、それが KQL で追えるのは「ログを Log Analytics に送っている」「保持期間内」という条件付きです。長期での追跡が必要なら、Log Analytics だけでなく Storage へのアーカイブ(長期保管)や SIEM 連携も含めて設計します。

Graph Activity Logs も検討する(アプリ操作の追跡を厚くする)

「誰(どのアプリ)が Graph API で何をしたか」を追う目的なら、Microsoft Graph activity logs の活用も選択肢になります。Graph 側のアクティビティを監査トレイルとして扱えるため、アプリ主体の操作追跡に強くなります(転送先は Log Analytics / Storage / Event Hubs など)。

まとめ:今回すぐ使える最短ルート

  1. Graph で現在存在するか確認(取れれば種類が確定/取れなければ次へ)
  2. 削除から 30 日以内なら deletedItems を確認(見つかれば正体確定)
  3. AuditLogs を mv-expand して type を抜く(ログが残っていれば判定できる)
  4. Sign-in Logs を横断検索(サービス プリンシパル/マネージド ID まで寄せられる)
  5. どこにも痕跡がなければ、正体の復元は困難。Unknown の割り当ては安全性を確認したうえで整理し、今後は台帳/IaC/ログ設計で再発防止する

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次