Warden — client access

Give us eyes. Not hands.

Read-only access to one Entra tenant takes about ten minutes and can be revoked in one click. This page is the whole procedure — the exact permissions, how to create them, how to send us the credential, and how to check afterwards that we did only what you authorised.

We will never ask you to send us a client secret in a web form.

If anything claiming to be Subtle Cyber asks for a secret by email link, portal page or chat message, that is not us — tell us through a channel you already have for us. A client secret for an app with directory read permissions is a real credential against a real tenant. Typing one into a form sends it to a web server, into that server's access logs, and usually onward into someone's inbox. We are not going to build the thing we are hired to find.

The request form on this page takes the Tenant ID and Application (client) ID, which are public identifiers and safe to email. The secret itself goes out of band, as below.

Step 1 — what you're granting

Five Microsoft Graph application permissions. All read.

Warden asks for these five and stops if the token it is issued holds anything more. Nothing here grants the ability to create, modify or delete a single object — no role assignment, no policy change, no user edit.

The five Microsoft Graph application permissions Warden requires
Permission Type What it lets Warden read
Directory.Read.All Application Users, groups, group membership, administrative units, service principals.
Policy.Read.All Application Conditional Access, authentication methods, authorisation policy, cross-tenant policy, consent policy.
RoleManagement.Read.Directory Application Role definitions, assignments, and PIM schedule state.
Application.Read.All Application App registrations and their credentials, passwords and federation metadata.
AuditLog.Read.All Application Directory audit history, for showing when a policy changed, not only what it says now.
What is deliberately not requested No *.ReadWrite.All of any kind. No RoleManagement.ReadWrite.Directory. No AppRoleAssignment.ReadWrite.All. No User.AuthenticationMethod reads, no mailbox or file access, and no key vault or subscription access. Warden runs in its own tenant and holds no admin role in yours.
The honest caveat on breadth Directory.Read.All reads the whole directory, which is genuinely broad: profile attributes, group structure, everything. The alternative would be narrower roles that change as Microsoft renames them, and read-only is the part that matters most. We would rather name the scope plainly than pretend it is small.

Step 2 — create the access

Portal, CLI or PowerShell. Whichever your team actually uses.

You need permission to create an app registration, and someone with Privileged Role Administrator or Global Administrator to grant admin consent. On many tenants that is you; on managed ones it is a change request, so it is worth raising early.

Recommended

Entra admin centre

Ten to fifteen minutes. This is the path we would take, and the only one where you can see every permission you are ticking.

  1. Create the app registration

    Entra admin centre → Identity → App registrations → New registration.

    Name it something an auditor will thank you for later: sc-warden-assessment-contoso. Under Supported account types, choose Accounts in this organisational directory only — single tenant. Leave the redirect URI blank: there is no interactive sign-in in this mode.

  2. Record the two IDs

    The Overview page shows Application (client) ID and Directory (tenant) ID. Both are public identifiers. Copy them somewhere — they are what you send us.

  3. Add the five permissions

    API permissions → Add a permission → Microsoft Graph → Application permissions. Search each name and tick it:

    • Directory.Read.All
    • Policy.Read.All
    • RoleManagement.Read.Directory
    • Application.Read.All
    • AuditLog.Read.All

    Click Add permissions. Do not add anything by searching "Directory" and grabbing what looks reasonable — the read scopes have write siblings with almost identical names, and that is how a read-only grant turns into a write one.

  4. Grant admin consent

    Click Grant admin consent for [your tenant] and confirm. Until you do, every call returns AadStsFromArag_token_cannot_be_resolved or a consent error, and Warden will report the tenant as unreadable rather than guessing.

    If the button is greyed out, you are signed in without a role that can consent. That is a normal finding, not an error: send the app's object ID to whoever holds Privileged Role Administrator.

  5. Create the credential

    Certificates & secrets → New client secret. Description: subtle-cyber-warden. Expires: 60 days or 90 days.

    Avoid Never. It is convenient right up until the day nobody can say which of nine secrets is still live. An assessment that outlives its secret gets a new secret, not an extension.

    Copy the Value, not the Secret ID, the moment it appears. It is shown once; Secret ID is a different identifier and will not authenticate.

  6. Send us everything except the secret

    Use the request form or email us directly. The secret travels separately, out of band.

For scripting it

Azure CLI

Same result, and auditable as a change record. Requires the az CLI and consent rights.

create-warden-sp.sh
set -euo pipefail

# Sign in first: az login --tenant <your-tenant>
TENANT=$(az account show --query tenantId -o tsv)
APP_NAME="sc-warden-assessment"

# 1. Create the app and its service principal.
APP_ID=$(az ad app create \
  --display-name "$APP_NAME" \
  --available-to-other-tenants false \
  --query appId -o tsv)
SP_ID=$(az ad sp create --id "$APP_ID" --query id -o tsv)

# 2. Microsoft Graph's service principal for THIS tenant. Its app ID is the
#    well-known multi-tenant Graph identity; the object ID is per-tenant, so we
#    look it up rather than hard-coding one.
GRAPH_SP=$(az ad sp list --all \
  --filter "appId eq '00000003-0000-0000-0000-c00000000000'" \
  --query '[0].id' -o tsv)

# 3. Resolve each role GUID from that service principal, so the GUIDs come from
#    your own directory rather than from a blog post written against a different
#    API version. If a role does not resolve, stop rather than half-configure.
ROLES=(Directory.Read.All Policy.Read.All RoleManagement.Read.Directory \
       Application.Read.All AuditLog.Read.All)
IDS=()
for R in "${ROLES[@]}"; do
  GUID=$(az ad sp show --id "$GRAPH_SP" \
    --query "appRoles[?value=='$R'].id | [0]" -o tsv)
  [ -n "$GUID" ] || { echo "role $R not found on Graph SP" >&2; exit 1; }
  IDS+=("$GUID")
done

# 4. Declare the required resource access on the app, then grant admin consent.
#    Declaring is not sufficient: without consent the API still refuses.
PERMS=""
for GUID in "${IDS[@]}"; do
  PERMS="$PERMS $GUID=Role"
done
# shellcheck disable=SC2086
az ad app permission add \
  --id "$APP_ID" \
  --api 00000003-0000-0000-0000-c00000000000 \
  --api-permissions $PERMS

az ad app permission admin-consent --id "$APP_ID"

# 5. Credential expiring in 90 days, printed once. GNU and BSD date differ.
END=$(date -u -d '+90 days' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
      || date -u -v+90d +%Y-%m-%dT%H:%M:%SZ)
SECRET=$(az ad app credential reset --id "$APP_ID" --append \
  --display-name "subtle-cyber-warden" --end-date "$END" --query password -o tsv)

echo "Tenant ID : $TENANT"
echo "Client ID : $APP_ID"
echo "Object ID : $SP_ID"
echo
echo "Send the three lines above to Subtle Cyber in ordinary email."
echo "Store the secret in your vault, then share it out of band:"
printf '%s' "$SECRET" | tr -d '\n' | xclip -selection clipboard 2>/dev/null || printf '\n%s\n' "$SECRET"

PowerShell

Microsoft Graph PowerShell SDK

The SDK equivalent, for teams whose standard tooling is PowerShell. Review it before running it: we use this shape ourselves, but your SDK version and conditional-access stance may disagree with us.

New-WardenAccess.ps1
Connect-MgGraph -Scopes 'Application.ReadWrite.All','AppRoleAssignment.ReadWrite.All','RoleManagement.ReadWrite.Directory'
$ErrorActionPreference = 'Stop'

$AppName = 'sc-warden-assessment'
$Roles   = @(
  'Directory.Read.All',
  'Policy.Read.All',
  'RoleManagement.Read.Directory',
  'Application.Read.All',
  'AuditLog.Read.All'
)

# Microsoft Graph's service principal in this tenant.
$GraphSp = Get-MgServicePrincipal `
  -Filter "appId eq '00000003-0000-0000-0000-c00000000000'"

$App = New-MgApplication -DisplayName $AppName `
  -SignInAudience AzureADMyOrg -IsFallbackPublicClient:$false
New-MgServicePrincipal -AppId $App.AppId | Out-Null

foreach ($Role in $Roles) {
  $RoleEntry = $GraphSp.AppRoles | Where-Object Value -eq $Role
  if (-not $RoleEntry) { throw "Graph app role '$Role' not found" }
  New-MgServicePrincipalAppRoleAssignment `
    -ServicePrincipalId $GraphSp.Id `
    -PrincipalId        (Get-MgServicePrincipal -Filter "appId eq '$($App.AppId)'").Id `
    -ResourceId         $GraphSp.Id `
    -AppRoleId          $RoleEntry.Id
}

# Credential, valid 90 days. The SecretText is returned once and cannot be
# retrieved afterwards.
$Cred = Add-MgApplicationPassword -ApplicationId $App.AppId `
  -DisplayName 'subtle-cyber-warden' -EndDateTime (Get-Date).AddDays(90)

Write-Host 'Client ID :' $App.AppId
Write-Host 'Tenant ID :' (Get-MgContext).TenantId
Write-Host 'Send those two to Subtle Cyber. The secret below does NOT go in email:'
$Cred.SecretText

If your tenant requires application registration to go through an approval workflow, the app appears in Enterprise applications → Application registrations as pending until it is approved. Warden treats an unapproved app as "cannot read this tenant" and says so, rather than reporting an empty tenant.

Step 3 — the credential

How to share the secret.

Once the app registration exists and we have confirmed the permissions look right, the secret moves through a channel that does not persist it in a mail store.

Shared vault entry

Our preference where you have one. Create a BeyondTrust, CyberArk, 1Password, Entra PIM-for-groups or AWS Secrets Manager entry and grant access to the named Warden operator, scoped to that one secret. Revocation is then deleting one permission rather than chasing copies.

One-time encrypted link

A self-destructing note works if it is genuinely one-time and you send the password through a different channel than the link. Microsoft Purview Message Encryption or an O365 Encrypt-Only label both do this properly, and both give you an audit record of the send.

Live, then revoke

For a short engagement, some teams prefer to read it out over a call or paste it into a disappearing message, then delete the credential the moment the run completes. Highest friction, smallest window. Sometimes the right trade.

The stronger option

Certificates, if your policy permits it.

A certificate credential has no secret to leak: you hold the private key and never transmit anything sensitive. Where a client can do this, we would rather they did.

On the same Certificates & secrets blade, choose Upload certificate instead of creating a new client secret. Send us the .cer public certificate or just its SHA-1 thumbprint, and we will confirm we are talking about the same credential before a run.

Generate one locally, valid for a bounded period:

warden-cert.sh
# Private key never leaves this machine.
openssl req -newkey rsa:2048 -nodes -keyout warden.key \
  -x509 -days 180 -out warden.crt \
  -subj "/CN=sc-warden-assessment"

# Thumbprint, to send us for confirmation.
openssl x509 -in warden.crt -noout -fingerprint -sha1

# Combine into PEM for the Warden operator, then share the .pem
# exactly like a secret — it contains the private key.
cat warden.key warden.crt > warden.pem

AZURE_CLIENT_CERTIFICATE_PATH is the environment variable Warden reads a PEM bundle from, with AZURE_CLIENT_CERTIFICATE_PASSWORD if the key is encrypted. Like the secret path, credentials are read from the environment or an interactive prompt — never from a command-line argument, where they would land in shell history and ps output.

Step 4 — check our work

Verify us, then revoke us.

You should not have to take an assurance company's word that it stayed read-only. Both halves of that are things you can prove from your own tenant.

  1. Watch what it did

    Entra admin centre → Monitoring & health → Sign-in logs, filter on the app's Application ID. Every token issuance appears there with the resource and the scopes granted. Cross-check it against the five on this page.

  2. Confirm nothing was written

    Check Audit logs for the same app ID. A Warden run should produce no directory write events. If it shows any, that is a bug or an over-granted consent, and either way you want to know during the engagement rather than after it.

  3. Revoke

    Certificates & secrets → delete the secret, or remove the certificate. Consent can be withdrawn separately under API permissions → Remove permission. Takes seconds, needs no notice to us, and we consider it good practice to do it between engagements rather than leaving grants dormant.

New and existing clients

Tell us what you've set up.

New client or returning, same form. If you already have a Warden app registration from a previous engagement, put its client ID in and we will re-use it where the permissions still match.

Handover request

This tells us what to expect and who to reply to. It takes about two minutes and does not commit you to anything.

Which best describes you?

How do you want to give us access?

Certificate is the stronger option and lasts longer. Secret is the quickest and what most people start with. The instructions below follow whichever you pick.

Entra admin centre → Identity → Overview → ID (tenant). A primary domain ending .onmicrosoft.com works equally well.

Overview page of the app registration. This is a public identifier — safe to send us in ordinary email.

60 or 90 days is the right answer for an assessment. If the engagement outlives the secret, we ask for a new one rather than extending it.

We never ask for a private key. You send us the public certificate or have already uploaded it to the app registration; the thumbprint lets both sides confirm they mean the same one.

For delegated access the assessor signs in interactively with a device code and holds Global Reader, activated in PIM for the collection window where you use it.

Plain English is fine, and the specific worry is genuinely useful — it changes which checks we prioritise.