On this page

AI coding assistants need Contributor access to deploy infrastructure. Backup automation needs Key Vault secrets at 2 AM. Security scanners need Reader access on a schedule.

The easy answer is standing permissions-give each service principal what it needs and move on. But that leaves dozens of non-human identities with 24/7 access to sensitive resources, and most of them use that access for minutes per day.

Zero Standing Privilege (ZSP) flips this: no identity starts with access to anything. Permissions are granted just-in-time, scoped to the task, and automatically revoked.

This post walks through building a ZSP gateway using Azure Functions that manages time-bounded access for AI agents, automation workflows, and service principals. We’ll cover the NHI access pattern in detail, then briefly show how the same gateway handles human admins too.

Hands-on Lab: Deployment steps, architecture notes, and supporting assets are in the companion lab.


Why NHI Security Matters Now

For every human in an Azure tenant, there may be 50-100 or more non-human identities (industry reports range from 17:1 to over 100:1 depending on methodology):

  • AI coding agents requesting temporary access to deploy infrastructure
  • Backup automation needing Key Vault secrets only during backup windows
  • CI/CD pipelines requiring Contributor access for deployments
  • Security scanners needing read access on a schedule
  • Agentic workflows chaining multiple Azure services together

Most have standing access they use for minutes per day-or never.

A service principal that runs a nightly backup has 24/7 Key Vault access for a 5-minute task. That’s 23 hours and 55 minutes of unnecessary exposure. An AI agent with permanent Contributor access is a credential theft away from a full environment compromise.

Diagram showing timeline of standing privilege exposure vs actual access need
Standing privilege: 24/7 access for a 5-minute job. ZSP: access only during execution.

The Risk Surface

ScenarioStanding Privilege RiskZSP Mitigation
Stolen SP credentialsImmediate Key Vault accessCredentials alone insufficient-no standing permissions
Compromised AI agentAttacker inherits Contributor roleAgent has zero access until workflow triggers grant
Lateral movementPivot via always-on service accountsService accounts have zero access between runs
Supply chain attackCompromised dependency has ambient accessNo ambient access to exploit

Microsoft PIM helps with human admin access, but it doesn’t solve the NHI problem. Service principals can’t activate PIM roles. They need a different pattern.


Architecture: ZSP Gateway

The ZSP gateway is an Azure Function App that brokers all privileged access. It exposes two endpoints:

  • /api/nhi-access - Grants time-bounded Azure RBAC role assignments to service principals
  • /api/admin-access - Grants temporary Entra group membership to human admins

Both patterns use one Azure Durable Functions saga for preflight, grant, wait, and revoke. The HTTP trigger records the lifecycle first; it never grants privilege before Durable history exists.

Architecture diagram showing ZSP Gateway with NHI and admin access paths, Durable Functions timer for revocation, and Log Analytics audit trail
ZSP Gateway: AI agents and service principals use /api/nhi-access for RBAC assignments. Human admins use /api/admin-access for group membership. All access is time-bounded and logged.

Infrastructure

The lab deploys with Bicep + PowerShell:

  • Azure Function App (Flex Consumption, Python 3.11) with system-assigned managed identity
  • Key Vault and Storage Account as target resources for demo
  • Application Insights and Log Analytics for observability
  • Data Collection Endpoint + Rule (DCE/DCR) for custom audit logging to ZSPAudit_CL
  • Entra ID groups with directory role assignments (for human admin path)
  • Backup service principal with zero initial permissions (for NHI demo)

The managed identity has GroupMember.ReadWrite.All, Directory.Read.All, and RoleManagement.ReadWrite.Directory Graph API permissions, User Access Administrator on the resource group for managing role assignments (in production, consider the more restrictive Role Based Access Control Administrator role), and Monitoring Metrics Publisher on the DCR for sending audit logs. The RoleManagement.ReadWrite.Directory permission is required because the ZSP groups are role-assignable-standard group membership permissions (GroupMember.ReadWrite.All) are insufficient for managing membership of role-assignable groups.

Audit ingestion is a privilege-safety dependency, not optional telemetry. The /api/health readiness endpoint returns HTTP 503 and status: degraded when the DCR endpoint or immutable DCR ID is missing or malformed. Admission, scheduled requests, and the grant activities repeat that check so no accepted Durable instance can grant after the configuration becomes invalid.


NHI Access: The Core Pattern

How NHI Access Works

  1. A workflow (timer, API call, or AI agent) calls /api/nhi-access
  2. The gateway validates allowlists and starts a Durable lifecycle
  3. The endpoint returns HTTP 202 with management URLsβ€”this is acceptance, not active access
  4. The orchestrator performs preflight, creates the scoped assignment, publishes customStatus.status = "active", waits, and revokes
  5. Everything is logged to Log Analytics

The Request

NHI_RESPONSE="$(curl --fail --silent --show-error -X POST "$FUNCTION_URL/api/nhi-access" \
  -H "Content-Type: application/json" \
  -H "x-functions-key: $FUNCTION_KEY" \
  -d '{
    "sp_object_id": "BACKUP_SP_OBJECT_ID",
    "scope": "/subscriptions/.../providers/Microsoft.KeyVault/vaults/<keyvault-name>",
    "role": "Key Vault Secrets User",
    "duration_minutes": 10,
    "workflow_id": "nightly-backup"
  }')"

STATUS_URL="$(echo "$NHI_RESPONSE" | jq -r '.statusQueryGetUri')"

The HTTP 202 response is a Durable management payload (URLs abbreviated):

{
  "id": "b10a200905204d0bb10d54fc4e1a73e0",
  "statusQueryGetUri": "https://.../runtime/webhooks/durabletask/instances/...",
  "terminatePostUri": "https://...",
  "purgeHistoryDeleteUri": "https://..."
}

Poll the status URL and do not use the entitlement until the custom status is active:

while true; do
  STATUS="$(curl --fail --silent --show-error "$STATUS_URL")"
  echo "$STATUS" | jq '{runtimeStatus, customStatus}'
  [ "$(echo "$STATUS" | jq -r '.customStatus.status // empty')" = "active" ] && break
  case "$(echo "$STATUS" | jq -r '.runtimeStatus')" in
    Failed|Terminated|Completed) exit 1 ;;
  esac
  sleep 2
done

Only customStatus.status == "active" establishes that the assignment exists. A failed, terminated, or already completed lifecycle must not be treated as access. The same lifecycle later removes its assignment and ends with revoked status.

The Grant Logic

The HTTP handler validates and schedules only. Inside the Durable lifecycle, a grant activity creates the role assignment via the Azure SDK; the orchestration owns the absolute expiry and compensating revocation:

# nhi_access.py (simplified from lab code)

from azure.mgmt.authorization import AuthorizationManagementClient
from azure.mgmt.authorization.models import RoleAssignmentCreateParameters
from azure.identity import DefaultAzureCredential
import uuid
from datetime import datetime, timedelta, timezone

ROLE_DEFINITIONS = {
    "Key Vault Secrets User": "4633458b-17de-408a-b874-0445c86b69e6",
    "Key Vault Reader": "21090545-7ca7-4776-b22c-e363652d74d2",
    "Storage Blob Data Reader": "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1",
    "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe",
    "Reader": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
}

async def grant_nhi_access(sp_object_id, scope, role_name, duration_minutes, workflow_id):
    credential = DefaultAzureCredential()
    subscription_id = scope.split("/")[2]  # extract from scope
    auth_client = AuthorizationManagementClient(credential, subscription_id)

    role_guid = ROLE_DEFINITIONS[role_name]
    assignment_name = str(uuid.uuid4())
    full_role_id = f"/subscriptions/{subscription_id}/providers/Microsoft.Authorization/roleDefinitions/{role_guid}"

    assignment = auth_client.role_assignments.create(
        scope=scope,
        role_assignment_name=assignment_name,
        parameters=RoleAssignmentCreateParameters(
            role_definition_id=full_role_id,
            principal_id=sp_object_id,
            principal_type="ServicePrincipal"
        )
    )

    return {
        "status": "granted",
        "assignment_id": assignment.id,
        "assignment_name": assignment_name,
        "sp_object_id": sp_object_id,
        "scope": scope,
        "role": role_name,
        "expires_at": (datetime.now(timezone.utc) + timedelta(minutes=duration_minutes)).isoformat(),
        "duration_minutes": duration_minutes,
        "workflow_id": workflow_id
    }

The key design decision: the gateway should be the only identity with permission to create role assignments. Service principals start at zero and can’t escalate themselves. In production, enable Entra authentication on the Function App so only approved callers can request access - function keys alone aren’t sufficient to enforce this boundary.

Scheduled Access with Timer Triggers

For predictable workloads like nightly backups, the gateway uses timer-triggered functions:

@app.timer_trigger(schedule="%BACKUP_JOB_SCHEDULE%", arg_name="timer", run_on_startup=False)
@app.durable_client_input(client_name="client")
async def backup_job_access_grant(timer: func.TimerRequest, client):
    """Record one lifecycle for the backup bundle; grant nothing here."""
    duration = int(os.environ.get("BACKUP_JOB_DURATION_MINUTES", 35))
    grants = []
    for scope, role in (
        (os.environ["KEYVAULT_RESOURCE_ID"], "Key Vault Secrets User"),
        (os.environ["STORAGE_RESOURCE_ID"], "Storage Blob Data Contributor"),
    ):
        grants.append(validate_nhi_request({
            "sp_object_id": os.environ["BACKUP_SP_OBJECT_ID"],
            "scope": scope,
            "role": role,
            "duration_minutes": duration,
            "workflow_id": "nightly-backup",
        }, _maximum_access_duration()))
    instance_id = await client.start_new(
        "access_lifecycle_orchestrator",
        client_input={
            "access_type": "nhi_bundle",
            "duration_minutes": duration,
            "workflow_id": "nightly-backup",
            "grants": grants,
        },
    )

The timer validates the full bundle and starts one durable saga; it does not make out-of-band grants. The pattern is still to open the access window shortly before the job and revoke after the configured duration, leaving the service principal with zero permissions for 23+ hours per day.

Timeline showing SP with zero access, then brief window of access during job, then back to zero
NHI access timeline: 23+ hours of zero privilege, brief access window during job execution.

Automatic Revocation

Grant and revocation live in the same Durable Functions orchestrator:

@app.orchestration_trigger(context_name="context")
def access_lifecycle_orchestrator(context: df.DurableOrchestrationContext):
    request = context.get_input()
    expiry_time = context.current_utc_datetime + timedelta(
        minutes=request["duration_minutes"]
    )

    context.set_custom_status({"status": "granting"})
    grant = yield context.call_activity_with_retry(
        "grant_nhi_access_activity", retry_options, request
    )
    context.set_custom_status({
        "status": "active",
        "expires_at": expiry_time.isoformat(),
        "grants": [grant],
    })
    yield context.create_timer(expiry_time)
    context.set_custom_status({"status": "revoking"})
    yield context.call_activity_with_retry(
        "revoke_role_assignment_activity", retry_options, grant
    )
    context.set_custom_status({"status": "revoked"})

The orchestrator derives an absolute expiry_time from context.current_utc_datetime, which remains deterministic during replay. Durable Functions survive Function App restarts: recorded grant history and the timer replay together instead of leaving an untracked, out-of-band assignment. Durable Functions timers in Python have a maximum duration of 6 daysβ€”more than enough for access windows measured in minutes or hours, but worth knowing if you extend durations.

One subtlety: the expiry_time must be timezone-aware (utc) because Durable Functions compares it against context.current_utc_datetime, which is always timezone-aware. Synchronous activity functions run in a thread pool, so they use asyncio.new_event_loop() to call async SDK methods:

@app.activity_trigger(input_name="activityPayload")
def revoke_role_assignment_activity(activityPayload: str):
    import asyncio
    input_data = json.loads(activityPayload) if isinstance(activityPayload, str) else activityPayload

    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(revoke_nhi_access(
            assignment_id=input_data["assignment_id"]
        ))
        loop.run_until_complete(log_access_event(
            event_type="AccessRevoke",
            identity_type="nhi",
            principal_id=input_data["sp_object_id"],
            target=input_data["scope"],
            target_type="AzureResource",
            role=input_data.get("role"),
            result="Success"
        ))
    finally:
        loop.close()

    return {"status": "revoked"}

The input_name="activityPayload" with type str is required-the Azure Functions .NET host serializes the input as a JSON string, so the Python worker must accept str and deserialize it.


AI Agent Integration Patterns

The /api/nhi-access endpoint is designed for machine callers. In the examples below, request_access_and_wait sends the function key in x-functions-key, accepts only HTTP 202, polls the returned status URL, and returns only after customStatus.status becomes active. A production helper must fail closed on failed, terminated, or unexpectedly completed instances.

Pattern 1: AI Coding Agent Deploying Infrastructure

An AI coding assistant may need temporary deployment access. Contributor is not enabled by the lab’s default role allowlist; add any elevated custom role to both the role-definition map and deployment allowlist only after constraining scope and reviewing the threat model:

# AI agent workflow
async def deploy_infrastructure(agent_context):
    await request_access_and_wait({
        "sp_object_id": agent_context.service_principal_id,
        "scope": f"/subscriptions/{SUB_ID}/resourceGroups/{RG_NAME}",
        "role": "Approved Deployment Custom Role",
        "duration_minutes": 30,
        "workflow_id": f"agent-deploy-{agent_context.session_id}"
    })

    # Run only after durable status confirms active access.
    await run_bicep_deployment(agent_context.template)

Pattern 2: Security Scanner on Schedule

A scanning agent needs Reader access across resource groups:

# Timer trigger starts a durable lifecycle, waits for active, then scans.
@app.timer_trigger(schedule="0 0 */6 * * *", arg_name="timer")  # Every 6 hours
async def security_scan_access(timer):
    for rg in RESOURCE_GROUPS_TO_SCAN:
        await request_access_and_wait({
            "sp_object_id": SCANNER_SP_ID,
            "scope": rg,
            "role": "Reader",
            "duration_minutes": 60,
            "workflow_id": "security-scan",
        })

Pattern 3: Event-Driven Access

An AI agent responds to incidents and needs temporary elevated access:

# Event Grid trigger when security alert fires
@app.event_grid_trigger(arg_name="event")
async def incident_response_access(event):
    alert = event.get_json()

    await request_access_and_wait({
        "sp_object_id": INCIDENT_RESPONSE_SP_ID,
        "scope": alert["resource_id"],
        "role": "Reader",
        "duration_minutes": 120,
        "workflow_id": f"incident-{alert['id']}",
    })

Audit Trail

Every access grant and revocation is logged to a custom Log Analytics table (ZSPAudit_CL) via the Azure Monitor Ingestion API. The pipeline uses a Data Collection Endpoint (DCE) and Data Collection Rule (DCR) to route structured audit events into Log Analytics. This is critical for NHI access since there’s no human to ask “why did you need this?”

Every new event also carries LifecycleId, the Durable orchestration instance ID, and EntitlementId, the deterministic admin owner key or complete Azure role-assignment resource ID. That pair is the exact correlation key for a grant and its revoke; principal and target are not unique across repeated lifecycles.

What Gets Logged

Every grant and revocation produces a log entry in ZSPAudit_CL. This representative pair uses the current schema (identifiers are redacted):

{
  "TimeGenerated": "2026-01-28T04:56:49.158538Z",
  "EventType": "AccessGrant",
  "IdentityType": "nhi",
  "PrincipalId": "<backup-service-principal-object-id>",
  "Target": "/subscriptions/.../Microsoft.KeyVault/vaults/zsp-lab-kv",
  "TargetType": "AzureResource",
  "Role": "Key Vault Secrets User",
  "DurationMinutes": 2,
  "WorkflowId": "nightly-backup",
  "LifecycleId": "b10a200905204d0bb10d54fc4e1a73e0",
  "EntitlementId": "/subscriptions/<subscription-id>/.../providers/Microsoft.Authorization/roleAssignments/<assignment-guid>",
  "ExpiresAt": "2026-01-28T04:58:48.598527",
  "Result": "Success"
}
{
  "TimeGenerated": "2026-01-28T04:58:55.570995Z",
  "EventType": "AccessRevoke",
  "IdentityType": "nhi",
  "PrincipalId": "<backup-service-principal-object-id>",
  "Target": "/subscriptions/.../Microsoft.KeyVault/vaults/zsp-lab-kv",
  "TargetType": "AzureResource",
  "Role": "Key Vault Secrets User",
  "LifecycleId": "b10a200905204d0bb10d54fc4e1a73e0",
  "EntitlementId": "/subscriptions/<subscription-id>/.../providers/Microsoft.Authorization/roleAssignments/<assignment-guid>",
  "ExpiresAt": "2026-01-28T04:58:48.598527",
  "Result": "Success"
}

The 2-minute gap between grant (04:56:49) and revoke (04:58:55) matches the requested duration_minutes: 2.

Log Analytics query results showing ZSPAudit_CL table with AccessGrant and AccessRevoke events for both NHI and human identities
Real audit data from the lab: grants and revocations for both NHI (service principals) and human admin access, with timestamps, roles, and workflow IDs.

Useful KQL Queries

All NHI access grants (last 24 hours):

ZSPAudit_CL
| where TimeGenerated > ago(24h)
| where IdentityType == "nhi"
| where EventType == "AccessGrant"
| project TimeGenerated, PrincipalId, Target, Role, DurationMinutes, WorkflowId, LifecycleId, EntitlementId
| order by TimeGenerated desc

NHI access outside expected windows:

ZSPAudit_CL
| where IdentityType == "nhi"
| where EventType == "AccessGrant"
| extend Hour = datetime_part("hour", TimeGenerated)
| where Hour < 1 or Hour > 3  // Expected window is 1-3 AM
| project TimeGenerated, PrincipalId, Target, WorkflowId, LifecycleId, EntitlementId

Unusual access patterns (more than 5 grants per hour for same SP):

ZSPAudit_CL
| where TimeGenerated > ago(7d)
| where IdentityType == "nhi"
| where EventType == "AccessGrant"
| summarize count() by bin(TimeGenerated, 1h), PrincipalId
| where count_ > 5

WorkflowId explains which automation requested access. LifecycleId plus EntitlementId proves whether that exact entitlement lifecycle has a successful revoke. The lab’s overdue-grant hunt uses a leftanti join on both keys. Older rows missing either key are labeled LegacyUncorrelated and sent to manual review; they are never guessed into a match using principal and target.

Log Analytics workbook showing ZSP access grants, revocations, and anomalies
ZSP audit dashboard: track all privileged access with identity type, duration, and workflow ID.

Bonus: This Also Works for Human Admins

The same gateway handles temporary admin access via Entra group membership. The pattern is simpler: empty security groups hold directory roles, and the gateway temporarily adds users.

How Admin Access Works

  1. Create Entra security groups like SG-Intune-Admins-ZSP and assign them directory roles
  2. Groups start empty - no one has the role
  3. Admin calls /api/admin-access with justification
  4. A Durable Entity claims the user/group owner lock; a concurrent lifecycle for the same pair is rejected
  5. The lifecycle adds the user, reports active status, waits, verifies exact ownership, removes the membership, and releases the lock
ADMIN_RESPONSE="$(curl --fail --silent --show-error -X POST "$FUNCTION_URL/api/admin-access" \
  -H "Content-Type: application/json" \
  -H "x-functions-key: $FUNCTION_KEY" \
  -d '{
    "user_id": "YOUR_USER_OBJECT_ID",
    "group_id": "INTUNE_ADMIN_GROUP_ID",
    "duration_minutes": 15,
    "justification": "Deploying new compliance policy - INC0012345"
  }')"

ADMIN_STATUS_URL="$(echo "$ADMIN_RESPONSE" | jq -r '.statusQueryGetUri')"

The HTTP 202 response only accepts the request. Poll ADMIN_STATUS_URL and wait for customStatus.status == "active", just as with NHI access.

Owner Lock and Manual Recovery

Microsoft Graph does not attach a per-membership owner token to a group edge. The lab therefore serializes each user/group pair through an admin_entitlement_owner Durable Entity. The lifecycle verifies the exact orchestration owner before compensation or expiry revocation, and retains the lock when safe cleanup cannot be proven.

If the entity confirms another owner, status becomes ownership_lost. If owner verification itself throws, status becomes ownership_unverified. Both paths attempt a correlated failed-revoke audit event, retain the lock, and deliberately leave the membership untouched. Stop new requests for that pair. Correlate the instance history, Entra audit logs, and current membership; manually remove the member only if the failed lifecycle created it. Once the entitlement is confirmed absent, repair or purge that owner entity (or reset/redeploy the task hub in this disposable lab), then verify again. Never clear the lock first, and never change privileged ZSP group memberships manually while a lifecycle is active.

This is the same broad pattern CyberArk uses for ZSP in their M365 implementation. It doesn’t require PIM eligible assignments and provides clear audit trails, but the role-assignable groups and allowed roles must remain explicitly constrained.


Deploying the Lab

Prerequisites

  • Azure subscription with Owner access
  • Azure CLI configured (az login)
  • PowerShell 7+ (pwsh)
  • Entra ID P1 or P2 license (for group-based role assignment)
  • Privileged Role Administrator directory role (required to create role-assignable Entra groups)

Quick Start

# From a local checkout of this repository:
cd labs/zsp-azure
./scripts/Deploy-Lab.ps1

The script handles everything:

  1. Deploys Azure resources via Bicep (Resource Group, Key Vault, Storage, Function App, Log Analytics, DCE)
  2. Creates Entra ID objects (ZSP groups, directory role assignments, backup SP)
  3. Creates the ZSPAudit_CL custom table and Data Collection Rule (DCR)
  4. Grants Graph API permissions and RBAC roles to the Function App managed identity
  5. Configures Function App settings with Entra object IDs, DCR endpoint, and schedule
  6. Deploys Function code
  7. Runs a smoke test

Test NHI Access

After deployment, the script outputs the required values:

FUNCTION_URL="https://<project>-gw-<suffix>.azurewebsites.net"
FUNCTION_KEY="<from deployment output>"
BACKUP_SP_ID="<from deployment output>"
KEYVAULT_ID="<from deployment output>"

NHI_RESPONSE="$(curl --fail --silent --show-error -X POST "$FUNCTION_URL/api/nhi-access" \
  -H "Content-Type: application/json" \
  -H "x-functions-key: $FUNCTION_KEY" \
  -d '{
    "sp_object_id": "'"$BACKUP_SP_ID"'",
    "scope": "'"$KEYVAULT_ID"'",
    "role": "Key Vault Secrets User",
    "duration_minutes": 10,
    "workflow_id": "manual-test"
  }')"

STATUS_URL="$(echo "$NHI_RESPONSE" | jq -r '.statusQueryGetUri')"

Poll STATUS_URL and wait for customStatus.status == "active" before checking or using Azure IAM. HTTP 202 alone does not prove that the role exists. After 10 minutes, the same lifecycle removes the assignment.

Verify Revocation

# Check role assignments on the Key Vault
az role assignment list \
  --assignee "$BACKUP_SP_ID" \
  --scope "$KEYVAULT_ID" \
  --query "[].roleDefinitionName"
# After expiry: returns empty list

For full deployment details, troubleshooting, and cleanup instructions, see the companion lab.

Treat both FUNCTION_KEY and the returned Durable management URLs as credentials. Send the function key in x-functions-key, not a query string, and do not publish management URLs because their query parameters authorize lifecycle operations.


Production Considerations

Authentication

The lab uses function keys for simplicity. For production, enable Entra authentication on the Function App and require OAuth tokens from approved clients.

Approval Workflows

Add human-in-the-loop for sensitive roles:

async def request_with_approval(request):
    if request.role in ["Contributor", "Owner"]:
        # Create approval request in Teams/ServiceNow
        return {"status": "pending_approval", "approval_id": "..."}
    else:
        # Auto-approve low-risk roles
        return await grant_access(request)

Break-Glass Accounts

ZSP should not create lockout scenarios. Maintain 1-2 emergency accounts with standing Global Admin, stored in a physical safe, monitored for any use.

Scope Constraints

In production, the gateway should enforce allowed scopes per service principal. Don’t let any SP request any role on any resource-maintain an allowlist.


Key Takeaways

  1. NHIs are the bigger risk surface. Organizations often have 50-100 service principals per human user, and most have standing access they rarely use.

  2. AI agents amplify the problem. Agentic workflows that chain Azure services need scoped, temporary access-not standing Contributor roles.

  3. The gateway pattern centralizes control. One identity (the Function App) manages all role assignments. Service principals can’t escalate themselves.

  4. Automatic revocation is non-negotiable. Durable Functions provide reliable timers that survive restarts.

  5. Serialize human membership ownership. A Durable Entity owner lock prevents overlapping user/group lifecycles and blocks unsafe revocation when ownership cannot be proven.

  6. Audit request purpose and exact lifecycle identity. WorkflowId explains the automation; LifecycleId plus EntitlementId correlates one grant with its revoke without guessing.

  7. This also works for humans. The same gateway handles admin access via group membership, providing one system for all privileged access.


Resources

Jerrad Dahlager

Jerrad Dahlager, CISSP, CCSP

Cloud Security Architect Β· Adjunct Instructor

Marine Corps veteran and firm believer that the best security survives contact with reality.

Have thoughts on this post? I'd love to hear from you.