Skip to content

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:

PatternWhy it's a problem
Owner at subscription scope on a non-human identitySP compromise = full tenant write; no role separation
Contributor at subscription scope on a non-human identityBroad enough to disable diag settings, delete resources, alter NSGs
User Access Administrator on anything other than break-glassPrivilege 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 scopeIndistinguishable 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 levelWhen to useExample
ResourceAccess to a single resource onlyAcrPull on ciriusagentsprod
Resource groupAccess spans resources within a logical boundaryContributor on rg-logging-logs for the SecOps platform
SubscriptionAccess genuinely spans an entire subscription and narrower scope breaks workflowReader on subscription for discovery; CI deploy role
Management group / tenantCross-subscription admin — avoid except for tenant-wide controlsEntra 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:

RoleScopeRationale
ContributorEach managed resource group (rg-logging-logs, rg-firewall-hub, rg-identity, rg-prod-apps, etc.)CRUD on the resources the repo owns
Network ContributorEach managed VNet resource groupNeeded for VNet peerings crossing RGs
User Access AdministratorNONECI should never grant roles; if new RBAC needed, open a PR with an explicit role assignment
Role Based Access Control AdministratorScoped only to specific managed identities/SPs the repo createsAllows 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:

  1. Run terraform state list | awk -F. '{print $1"."$2}' | sort -u across each repo to get the resource group set
  2. Draft a custom role definition granting Contributor-equivalent permissions scoped to those RGs
  3. Assign the custom role at the RG level for each listed RG
  4. Remove the subscription-level Owner assignment 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, or User Access Administrator assignments 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):

ResourceRole
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-centralLog Analytics Reader (resource scope)
psql-secops-prodNo 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> --all and confirm every assignment has a scope narrower than the subscription
  • Any Microsoft.Web/sites/* or Microsoft.App/* wildcard at subscription scope should be narrowed to the specific Container App or removed
  • Graph API permissions should be minimal — User.Read, maybe AuditLog.Read.All for Entra correlation; NEVER RoleManagement.ReadWrite.Directory or Application.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)

  1. Run az role assignment list --all --subscription <sub> on each PROD subscription
  2. For each non-break-glass principal with Owner, Contributor, or User Access Administrator at subscription scope:
    • Confirm the principal's purpose
    • Cross-reference against lpriv-baseline.md
    • Classify: within baseline, drift (needs correction), or over-provisioned by design
  3. Produce a delta report: list of principals × scope × current role × expected role

Step 2: Harvest actual resource group dependencies for github-deploy-main

  1. Across each infra repo (azure-infra, azure-dde-infra), run terraform state list | awk -F. '{print $1"."$2}' | grep -E '^azurerm_resource_group' | sort -u
  2. Additionally find any resource whose parent is a resource group not listed by the above — these are implicitly managed RGs
  3. Consolidate into a canonical list of resource groups each SP needs Contributor on

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:

  1. Add azurerm_role_assignment resources for the custom role on each listed RG
  2. Leave the subscription-level Owner assignment in place for one full CI cycle to confirm the new roles are sufficient
  3. In a follow-up PR, remove the Owner assignment

Step 5: Verify

  • Run a representative terraform apply on each infra repo and confirm it succeeds
  • Run az role assignment list --assignee <github-deploy-main SP> and confirm the SP no longer holds Owner at 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 documentation
  • security/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

Internal use only — Cirius Group