Appearance
Privileged Role and Service Principal Cleanup — PROD Subscriptions
Story: PIMA-011 (PROD-027) Scope: PROD (ciriusgroup.com) subscriptions — identity, prod, dashboard, firewall, logging, dev, dmz, management Owner: Rory (architectural decisions) / Kobe (implementation) Status: Analysis complete — remediation actions require Rory approval
Purpose
This document inventories privileged principals (users, service principals, managed identities) that hold broad administrative access across PROD subscriptions and classifies what each should have under least-privilege.
Generated in response to PIMA finding PROD-027 flagging service principals and role assignments with excessive admin scope. This is an analysis + remediation proposal only — actual cleanup requires a separate change approved by Rory, documented in its own PR, and rolled out one principal at a time with verification.
What Counts as Over-Provisioned Access
Any of the following, on any PROD subscription, is a PIMA finding that must be justified or remediated:
| Pattern | Why it's a problem |
|---|---|
Owner at subscription scope on a non-human identity | SP compromise = full tenant write; no role separation |
Contributor at subscription scope on a non-human identity | Broad enough to disable diag settings, delete resources, alter NSGs |
User Access Administrator on anything other than break-glass | Privilege escalation — can grant itself Owner |
Directory-wide Graph application permissions (e.g. RoleManagement.ReadWrite.Directory, Application.ReadWrite.All) | Tenant-wide identity control |
Managed identity with Owner/Contributor at subscription scope | Indistinguishable from SP compromise via metadata service |
| Role assignment with no corresponding resource in code (orphaned) | Unknown why it exists, cannot be audited against expected baseline |
| Permanent PIM assignments for human identities (non-break-glass) | Should be eligible + JIT, not active |
Exception: Break-glass accounts (documented in security/lpriv-baseline.md) are exempt from this baseline. They are permanently unsuppressible and permanently over-provisioned by design.
Right-Sized Access by Scope
| Scope level | When to use | Example |
|---|---|---|
| Resource | Access to a single resource only | AcrPull on ciriusagentsprod |
| Resource group | Access spans resources within a logical boundary | Contributor on rg-logging-logs for the SecOps platform |
| Subscription | Access genuinely spans an entire subscription and narrower scope breaks workflow | Reader on subscription for discovery; CI deploy role |
| Management group / tenant | Cross-subscription admin — avoid except for tenant-wide controls | Entra break-glass only |
The default is resource-scoped. Only widen the scope when the narrower scope demonstrably breaks the workflow and that reason is captured in lpriv-baseline.md.
Known Privileged Service Principals
github-deploy-main (per subscription)
Purpose: OIDC-federated SP used by GitHub Actions to run terraform apply after PR merge. One assignment per subscription (azure-infra, aws-infra, azure-dde-infra repos).
Current state (PROD subscriptions): Owner at subscription scope.
PIMA classification: Over-provisioned.
Why: CI apply only needs to manage resources inside resource groups that belong to the environment. Owner at subscription scope grants Microsoft.Authorization/* — it can grant itself additional roles, alter RBAC, change subscription-level policies, and delete management locks. A compromise of the GitHub OIDC trust or the Actions runner would yield full subscription takeover.
What it should have:
| Role | Scope | Rationale |
|---|---|---|
Contributor | Each managed resource group (rg-logging-logs, rg-firewall-hub, rg-identity, rg-prod-apps, etc.) | CRUD on the resources the repo owns |
Network Contributor | Each managed VNet resource group | Needed for VNet peerings crossing RGs |
User Access Administrator | NONE | CI should never grant roles; if new RBAC needed, open a PR with an explicit role assignment |
Role Based Access Control Administrator | Scoped only to specific managed identities/SPs the repo creates | Allows RBAC writes only within a narrow managed set, not subscription-wide |
Blocker: A proper cleanup requires enumerating every resource group the CI pipeline touches (via terraform state list across the three infra repos) and confirming all of them are either explicitly listed or created by the same SP. Proposed approach:
- Run
terraform state list | awk -F. '{print $1"."$2}' | sort -uacross each repo to get the resource group set - Draft a custom role definition granting
Contributor-equivalent permissions scoped to those RGs - Assign the custom role at the RG level for each listed RG
- Remove the subscription-level
Ownerassignment as a second, separate PR so rollback is one click
Do NOT proceed without Rory's approval of the resource group inventory — missing a resource group will break terraform apply on that repo.
kobe-sp / kobe-bot
Purpose: Service principal used by the Kobe agent (Cloud PC Ubuntu EC2) for read-only Azure discovery and state access.
Current expected state (per security/lpriv-baseline.md): Reader at subscription scope on relevant subscriptions.
PIMA classification: Within baseline — low-risk.
What to verify:
- Confirm no
Contributor,Owner, orUser Access Administratorassignments exist - Confirm the SP is NOT a member of any Entra role-assignable group that carries broader rights (e.g. Security Administrator, Global Reader — Global Reader is acceptable)
- Confirm Terraform state storage RBAC is scoped to the specific storage accounts, not subscription-wide
Remediation: None expected if baseline holds. If PIMA flags this principal, re-read security/lpriv-baseline.md to confirm the finding against expected state before adjusting anything.
bedrock-hub (App Registration)
Purpose: App reg used by the SecOps platform Container App (ca-secops-prod) for API integration with Azure — known-good rules sync, incident ingestion from agents, CMDB queries, change management ticket creation.
Current expected state (per security/lpriv-secops.md and lpriv-baseline.md):
| Resource | Role |
|---|---|
ca-secops-prod (Container App) | Specific reader permissions for self-introspection |
cirius-openai-kv-prod (Key Vault) | Key Vault Secrets User on specific secrets |
cirius-logging-law-central | Log Analytics Reader (resource scope) |
psql-secops-prod | No direct RBAC — uses SP credential stored in KV |
PIMA classification: Low-risk if scoped as above. Over-provisioned if subscription scope appears anywhere.
What to verify:
- Run
az role assignment list --assignee <bedrock-hub-app-id> --alland confirm every assignment has a scope narrower than the subscription - Any
Microsoft.Web/sites/*orMicrosoft.App/*wildcard at subscription scope should be narrowed to the specific Container App or removed - Graph API permissions should be minimal —
User.Read, maybeAuditLog.Read.Allfor Entra correlation; NEVERRoleManagement.ReadWrite.DirectoryorApplication.ReadWrite.All
id-secops-prod / id-agents-prod (Managed Identities)
Covered in detail in security/lpriv-baseline.md. Currently within baseline. PIMA should not be flagging these unless an assignment has drifted.
Drift check: Compare live assignments against the tables in lpriv-baseline.md and investigate any delta.
MDR / Arctic Wolf
Arctic Wolf has full Azure access by design — the managed detection and response provider acts first without approval. This is intentional and outside PIMA scope. If PIMA flags Arctic Wolf principals, mark the finding as KNOWN_GOOD with justification "MDR contractual access — Rory approved 2024". Do not narrow.
Proposed Remediation Sequence
All actions require Rory approval on a per-step basis.
Step 1: Baseline reconciliation (no changes)
- Run
az role assignment list --all --subscription <sub>on each PROD subscription - For each non-break-glass principal with
Owner,Contributor, orUser Access Administratorat subscription scope:- Confirm the principal's purpose
- Cross-reference against
lpriv-baseline.md - Classify: within baseline, drift (needs correction), or over-provisioned by design
- Produce a delta report: list of principals × scope × current role × expected role
Step 2: Harvest actual resource group dependencies for github-deploy-main
- Across each infra repo (
azure-infra,azure-dde-infra), runterraform state list | awk -F. '{print $1"."$2}' | grep -E '^azurerm_resource_group' | sort -u - Additionally find any resource whose parent is a resource group not listed by the above — these are implicitly managed RGs
- Consolidate into a canonical list of resource groups each SP needs
Contributoron
Step 3: Custom role definition
Draft azurerm_role_definition.github_deploy scoped to the canonical RG list. The role should grant the same Microsoft.Compute/*, Microsoft.Network/*, Microsoft.Storage/*, Microsoft.Insights/*, etc. actions as built-in Contributor but explicitly deny Microsoft.Authorization/*/write and */delete.
Step 4: Apply custom role assignments (one subscription at a time)
Per subscription, in a separate PR each:
- Add
azurerm_role_assignmentresources for the custom role on each listed RG - Leave the subscription-level
Ownerassignment in place for one full CI cycle to confirm the new roles are sufficient - In a follow-up PR, remove the
Ownerassignment
Step 5: Verify
- Run a representative
terraform applyon each infra repo and confirm it succeeds - Run
az role assignment list --assignee <github-deploy-main SP>and confirm the SP no longer holdsOwnerat subscription scope - PIMA agent should clear PROD-027 on its next run
Current Detection
The PIMA / LPRIV agents run quarterly against lpriv-baseline.md. Any deviation opens a SecOps story in the PIMA project. Stories should reference this document for remediation guidance.
Agent: ops-automation/audits/pim-audit.ymlOutput: Findings posted via POST /api/ingest/findings with framework=LPRIVEscalation: Rory reviews weekly; approved remediations become their own SecOps stories
Out of Scope for This Document
- Human identity PIM assignments (eligible vs. active) — covered by
lpriv-baseline.md - DDE tenant privileged roles — separate analysis in a future PIMA story
- AWS IAM role cleanup — covered by
aws/*documentation and separate AWS-scoped PIMA findings - Break-glass account review — permanently over-provisioned by design; reviewed under a different control
References
security/lpriv-baseline.md— expected least-privilege state (source of truth)security/lpriv-secops.md— SecOps-specific identity documentationsecurity/lpriv-ops-automation.md— ops-automation SP details- SecOps story: PIMA-011 (PROD-027)
- CLAUDE.md in
azure-infra— GitHub deploy SP conventions
Last updated: 2026-04-16 — initial analysis for PIMA-011