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

コメント