Deploy with Microsoft Entra ID

Use Microsoft Entra ID users and groups with DefenseClaw on Entra-joined Windows, on Linux (Azure Entra SSH sign-in, Himmelblau, or Microsoft Entra Domain Services with SSSD) and on macOS with Platform SSO. Get Entra groups to the operating system, map users and groups to guardrail profiles, and check what DefenseClaw records. Starter scripts and configs included.

DefenseClaw never calls Microsoft Entra ID or Microsoft Graph. A user signs in to the computer with Entra ID, the computer gives that user an operating system account, and DefenseClaw reads the account the way it reads any other: the SID and sign-in token on Windows, NSS on Linux, Open Directory on macOS. So deploying DefenseClaw for Entra users is first an Entra and operating system task, and a groups assignment can name an Entra group only where the operating system knows the membership. This guide gives the steps in order, the starter scripts that do them, and what DefenseClaw records at the end. The background on what is read from where is in How DefenseClaw knows who a user is.

Guardrail profiles apply to per-user installs and to the standalone enterprise profile. The Secure Client profile rejects them and keeps its behavior as it was, so nothing here applies to a Secure Client computer.

What was run live

On live Entra tenants with Claude Code (Haiku 4.5 on Amazon Bedrock), each as the real user in an interactive session:

  • Entra-joined Windows 11: a per-user install signed in over remote desktop as an Entra user, and the standalone enterprise profile with a groups assignment by Entra group SID.
  • Ubuntu 22.04 on Azure with Entra SSH sign-in (AADSSHLoginForLinux), per user.
  • Ubuntu 24.04 with Himmelblau 5.0.0 (nightly), per user and with the standalone enterprise package (deb): a group member got the group profile.
  • RHEL 9.8 joined to a Microsoft Entra Domain Services managed domain with realmd and SSSD, per user and with the standalone enterprise package (rpm): a group member got the group profile, with qualified and with short names.
  • macOS 15.8 enrolled in Intune with Company Portal and Platform SSO, per user and with the standalone enterprise package (pkg): no Entra group reached the Mac; a local group that carried the membership selected the profile.

Not tested: a users assignment on the standalone profile on Windows, delivering the local group membership through Intune (Windows) or the bridge script through Intune (macOS), hybrid-joined Windows, Entra Kerberos, Himmelblau 4.x stable, Himmelblau's offline break-glass, and an Entra outage on Windows or macOS. Each is marked where it comes up.

Pick your setup

SetupUsers sign in withDefenseClaw seesA groups assignment can name
Entra-joined Windows 10 or 11Entra ID at the Windows sign-in screenDirectory entra_id, the user SID, the UPN and the tenant IDStandalone enterprise profile: an Entra group by SID, once a built-in local group lists it (step 2). Per-user install: no groups, use users
Azure Linux VM with Entra SSH sign-inaz ssh vm, with the aad moduleDirectory entra_id, source nss_aad, the UPNNo Entra group: the module gives the account only a private group. Use users, or one of the two routes below
Linux with Himmelblaussh with the Entra password, an MFA code and a Hello PINDirectory entra_id, source himmelblau; the UPN when accounts are named by itThe user's Entra groups, by name (Himmelblau)
Linux joined to Microsoft Entra Domain Servicesssh with the Entra password, through SSSDDirectory active_directory, source sssd (sssd_infopipe on standalone enterprise), the UPN, the domainThe synced Entra groups, as name@domain unless SSSD uses short names (Entra Domain Services)
Mac with Platform SSO (Company Portal)Entra ID at the macOS login windowStandalone enterprise: directory entra_id, source macos_platform_sso, the UPN. Per-user: directory localNo Entra group: Platform SSO gives none. A local group that carries the membership (step 4)
Linux desktop enrolled in IntuneA local account; Intune enrolls the device and does not sign users inDirectory local, expected. Not observed with DefenseClaw installedLocal groups
Hybrid-joined Windows, Entra KerberosThe readers exist (identity sources)Not run on a live tenant

Why a group assignment matches no one

The usual cause is that the operating system does not know the Entra group, so DefenseClaw does not either. Check with the OS's own tools as the user: whoami /groups on Windows, id on Linux and macOS. If the Entra group is not listed there, no DefenseClaw setting can match it: use the route for your platform below, or a users assignment.

Starter kit

The scripts and example configs are in packaging/identity/entra. Every script has --help (or Get-Help), reads credentials from the environment and never from arguments, and changes nothing until you say so: the Python and shell scripts print a plan unless you pass --apply, and the PowerShell script supports -WhatIf.

FileUse
entra_setup.pyMicrosoft Graph helper: create groups and users from a JSON file, read the Windows SID of a group or user, compute a SID from an object id offline
setup-entra-ssh-linux.shAzure CLI wrapper: enable Entra SSH sign-in on an Azure Linux VM and grant the sign-in role
Add-EntraGroupToLocalGroup.ps1Put an Entra group's SID into a built-in local group, the step that makes the group appear in sign-in tokens
Get-DefenseClawEntraIdentity.ps1Read-only: show the join state, the Entra accounts, the token's groups and what DefenseClaw answers
setup-himmelblau.shInstall Himmelblau on Ubuntu 24.04, write himmelblau.conf (UPN account names, the groups allowed to sign in) and restart it safely; check is read-only
join-entra-domain-services.shJoin a Linux host to an Entra Domain Services managed domain with realmd and SSSD, switch between qualified and short names, clear the SSSD cache; check is read-only
macos-entra-group-bridge.shAdd a Mac user to, or remove them from, the local group that stands for an Entra group (an Intune shell script that names the account, or by hand)
macos-platform-sso.settings-catalog.example.jsonThe Intune settings catalog body for Platform SSO with Company Portal that the live test used
admin-config.windows.example.yaml, per-user-profiles.example.yaml, tenant.example.jsonStarting points for the standalone config, a per-user config and the tenant setup file

1. Prepare the tenant

DefenseClaw needs nothing special from the tenant. You need the groups and users your profiles will name, and the SID of each group.

The live tests ran on a tenant with no paid licence. They turned security defaults off so a scripted password sign-in works; that is a test convenience, not a DefenseClaw requirement.

Create an app registration with the application permissions Group.ReadWrite.All, User.ReadWrite.All and Organization.Read.All (read-only check needs Organization.Read.All and Policy.Read.All; sids needs Group.Read.All and User.Read.All), grant admin consent, and give the script its credentials through the environment:

export AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
export AZURE_CLIENT_ID=00000000-0000-0000-0000-000000000000
read -rs AZURE_CLIENT_SECRET && export AZURE_CLIENT_SECRET   # type it; it stays out of your history

python3 entra_setup.py check                                  # tenant, domains, security defaults
cp tenant.example.json tenant.json                            # edit the domain, groups and users
python3 entra_setup.py apply --config tenant.json             # a preview; changes nothing
python3 entra_setup.py apply --config tenant.json --apply --password-file users.txt
python3 entra_setup.py sids --group defenseclaw-ml-team

A token in GRAPH_ACCESS_TOKEN works instead of the app registration. The script creates objects that do not exist and leaves the others alone, so it is safe to run again. New users get a generated password that goes only to the --password-file (mode 0600), never to the screen.

sids prints the object ID and the Windows SID of the group:

group defenseclaw-ml-team
      object id  6f1c9a0e-3b2d-4c58-9e71-a4b5c6d7e8f9
      SID        S-1-12-1-1864145422-1280850733-3047453086-4192786374

Windows shows an Entra group only by this SID. The SID is four 32-bit words of the object ID, so entra_setup.py sid-from-object-id 6f1c9a0e-... computes it without a tenant. On a live tenant the Graph securityIdentifier of two groups and a user matched the computed value, and the SIDs of that user and of her group matched the ones in her Windows sign-in token.

2. Windows: Entra-joined computers

Join and sign in

Join the computer to Entra ID in Settings > Accounts > Access work or school > Join this device to Microsoft Entra ID. The user who joins is a local administrator by default, so remove that account from Administrators if your users should be standard users. On an Azure VM, the AADLoginForWindows extension joins it, and each user needs the Virtual Machine User Login role.

Users sign in with their Entra ID; over remote desktop the live test signed in as AzureAD\alice@contoso.onmicrosoft.com. Windows gives the account a name of its own, not the UPN (AzureAD\EntraAlice for entra-alice@... in the live test), and a SID that starts with S-1-12-1-.

See what the computer exposes

Run the read-only diagnostic as the user you want to check. Run it again as SYSTEM to read the identity store, which the live test read as SYSTEM. If the script came from the Internet, run Unblock-File .\Get-DefenseClawEntraIdentity.ps1 first, or launch a trusted copy with powershell.exe -ExecutionPolicy Bypass -File .\Get-DefenseClawEntraIdentity.ps1. An unattended run of an Internet-zone script can hang at a security prompt.

.\Get-DefenseClawEntraIdentity.ps1
.\Get-DefenseClawEntraIdentity.ps1 -GroupSid S-1-12-1-1864145422-1280850733-3047453086-4192786374

It prints the dsregcmd /status join state, the Entra accounts in the identity store (SID, UPN, provider), the tenant, the groups in the sign-in token with the Entra SIDs marked, the built-in local groups that list an Entra SID, and, when DefenseClaw is installed, the answer of guardrail profile explain. With -GroupSid it says whether each group is in this token.

Put an Entra group into the sign-in token

An Entra-joined computer puts an Entra group's SID into a sign-in token only when a built-in local group lists it. Without that, a groups assignment or an enrollment group filter that names the group matches no one. The rule and its limits are in Entra groups on Windows.

For a fleet, use an Intune policy; see Prepare your Intune tenant. Delivery through Intune has not been tested. To make the membership on one computer, run the script elevated. Unblock a downloaded copy with Unblock-File .\Add-EntraGroupToLocalGroup.ps1 or launch it with powershell.exe -ExecutionPolicy Bypass -File .\Add-EntraGroupToLocalGroup.ps1 -GroupSid SID. An unattended Internet-zone script can hang at a security prompt. Intune platform scripts receive no arguments; use Account protection for fleet delivery:

.\Add-EntraGroupToLocalGroup.ps1 -GroupSid S-1-12-1-1864145422-1280850733-3047453086-4192786374 -WhatIf
.\Add-EntraGroupToLocalGroup.ps1 -GroupSid S-1-12-1-1864145422-1280850733-3047453086-4192786374

The script adds the SID to the built-in Users group with the Windows API NetLocalGroupAddMembers. It is idempotent: it reports Added, AlreadyMember, Removed or NotMember. -Remove undoes it. It warns when you name a group other than the six Windows reads, and when you name Administrators, because that makes every member of the Entra group a local administrator.

The script ran on Windows Server 2025 in Windows PowerShell 5.1 and PowerShell 7 against a throwaway local group. The same API call put an Entra group into the sign-in token of an Entra-joined Windows 11 user in the live test. After the membership exists, the user signs out and back in. net localgroup may not list an Entra SID the computer cannot resolve; use the diagnostic's -GroupSid check, which reads the token.

Install DefenseClaw

Install per user as in Install, or deploy the standalone enterprise profile with Setup or your MDM (Enterprise on Windows, Intune on Windows).

A per-user install has no group list on Windows, so only users, connectors and agents assignments can match there. The standalone enterprise profile reads each user's token groups through the SYSTEM enumerator.

3. Linux

Linux has three ways to sign Entra users in, and only two of them give the host the user's Entra groups:

RouteEntra groups on the hostTested with DefenseClaw
Azure Entra SSH sign-in (aad module)None: each account has only its private groupUbuntu 22.04, per user
HimmelblauThe user's Entra groups, named in id and initgroupsUbuntu 24.04, Himmelblau 5.0.0 nightly, per user and standalone enterprise
Entra Domain Services with SSSDThe synced Entra groups, as Active Directory groupsRHEL 9.8, per user and standalone enterprise

Azure VMs with Entra SSH sign-in

Enable the sign-in on the VM. The script enables the VM's system-assigned identity, installs the AADSSHLoginForLinux extension and grants the Virtual Machine User Login role (--admin grants Virtual Machine Administrator Login, which carries sudo rights). It prints a plan until you add --apply:

./setup-entra-ssh-linux.sh -g my-rg -n my-vm --user alice@contoso.onmicrosoft.com --group defenseclaw-ml-team
./setup-entra-ssh-linux.sh -g my-rg -n my-vm --user alice@contoso.onmicrosoft.com --group defenseclaw-ml-team --apply

Users sign in with the Azure CLI:

az extension add --name ssh
az ssh vm -g my-rg -n my-vm

The aad module adds itself to passwd and group in /etc/nsswitch.conf, and the account is named by the UPN:

$ id
uid=9246366(alice@contoso.onmicrosoft.com) gid=9246366(alice@contoso.onmicrosoft.com) groups=9246366(alice@contoso.onmicrosoft.com)
$ getent group defenseclaw-ml-team
$

The module derives the uid from the user. The account has only its private group: getent group finds no Entra group. So a groups assignment cannot name an Entra group on this setup. Assign profiles by users, or add a local group of your own.

Install DefenseClaw per user as that user, as in Install. The Entra role is only for sign-in; DefenseClaw does not use it. The standalone enterprise package on an Entra Linux VM was not tested.

Enrolling a Linux desktop in Intune is a different path: the device gets an Entra identity for compliance, but the accounts on it stay local. See Prepare your Intune tenant.

Himmelblau

Himmelblau is an open-source (GPL-3.0) NSS, PAM and SSH module that signs Entra users in to Linux and reads their Entra groups. It needs no app registration. The live test used the 5.0.0 nightly build (5.0.0-ubuntu24.04~20261007) on Ubuntu 24.04, the only free channel at the time; the 4.0.5 stable packages need a paid entitlement token (or a build from source) and were not tested.

Before users sign in. Entra refuses remote sign-in through Himmelblau for a user who has no MFA method registered (AADSTS50072, "Remote session with unenrolled MFA user denied"), even with security defaults off and no Conditional Access policy. Have each user register an authenticator app at Security info. The first sign-in then asks for the Entra password, the code and a new Windows Hello PIN of at least six characters (an empty PIN is refused), and joins the computer to Entra: it creates a device object, so your users need permission to join devices.

Install and configure (as root; the script prints its plan first):

GROUP_OBJECT_ID=00000000-0000-0000-0000-000000000000   # the Entra group object id
sudo ./setup-himmelblau.sh install --domain contoso.onmicrosoft.com --allow-group "$GROUP_OBJECT_ID"
sudo ./setup-himmelblau.sh install --domain contoso.onmicrosoft.com --allow-group "$GROUP_OBJECT_ID" --apply
./setup-himmelblau.sh check --user alice@contoso.onmicrosoft.com

The script's configure, restart and check ran live on the test VM; its install step does what the live test did by hand (repository, key and packages) and was run in plan mode only. The packages (himmelblau nss-himmelblau pam-himmelblau himmelblau-sshd-config, from the signed repository at packages.himmelblau-idm.org) add himmelblau to the passwd, group and shadow lines of nsswitch.conf and to PAM, but write no /etc/himmelblau/himmelblau.conf, and the daemon does not start without its domain. The script writes these keys:

KeyWhy
domain = contoso.onmicrosoft.comThe tenant
cn_name_mapping = falseNames accounts by their UPN. With Himmelblau's default (true) accounts are named alice, carry no UPN or domain, and a DefenseClaw users entry written as a UPN never matches; explain says so. Groups match either way
pam_allow_groups = <object id>,...Only members of these Entra groups may sign in. Without it every user of the tenant can, and the daemon warns. It takes object IDs, not names
home_attr, home_alias = CN, use_etc_skel = trueHome directories named by the account

What the host shows. id names the Entra groups (groups=1608906209(ml-team) style ids and names), but getent group ml-team answers nothing: Himmelblau finds a group only by gid or object ID, by design. DefenseClaw matches the names id reports exactly, including case, spaces and accents. Quote a name with spaces in YAML, for example groups: ["Research ML Team"]. A groups: [ml-team] assignment works, and DefenseClaw does not warn that the group is unknown on a host whose group line lists Himmelblau. The records carry directory entra_id, source himmelblau, assurance verified, a posix_uid, the private group plus the Entra groups in group_count, and the SSH session.

Group changes. Himmelblau caches an account for 300 seconds (cache_timeout) and refreshes it at the next lookup after that. In the live test a removal showed in id after 1 minute 46 seconds and an addition after 4 minutes 58 seconds; a new sign-in, or sudo aad-tool cache-clear, shows it at once. DefenseClaw then picks it up within its 15-minute cache, so a removal can take about 21 minutes to reach hook decisions.

When Entra is unreachable. Cached accounts and groups keep answering, past the cache time and across a daemon restart. A new SSH sign-in of an MFA user is refused (PAM_ABORT: Network outage detected) unless Himmelblau's offline_breakglass is set up, which is off by default and was not tested. Sign-in works again as soon as Entra answers.

When the daemon is down or restarting, id shows the Entra groups as bare numbers. DefenseClaw keeps such an answer for 2 minutes instead of 15, and the next request after that waits for a fresh lookup, so a group member gets the group profile again within about 2 minutes of the daemon's return.

Restart the daemon with the script. On the 5.0.0 nightly, a second systemctl restart himmelblaud himmelblaud-tasks left the units stuck (inactive (dead-resources-pinned)), and the host answered no Entra account until systemctl clean --what=fdstore himmelblaud.service. setup-himmelblau.sh restart --apply stops the sockets and daemons, clears the stored descriptors and starts them again; configure restarts the same way.

Other notes from the live test: a VM without a TPM uses a software HSM; debug = true is very verbose; the test VM (Standard_B2als_v2) cost about US$0.04 an hour while running.

A configure --apply run writes the keys above and removes any other himmelblau.conf settings. Its preview names the unmanaged keys, and each apply keeps a timestamped backup. An omitted --allow-group preserves an existing pam_allow_groups sign-in restriction; use --allow-all to remove it explicitly. Review the backup for site-specific settings before running it again.

Remove Himmelblau

Before removing a host, use Entra admin center to find and delete that host's Entra device object. Keep user homes until their owners have copied any needed data. On the host, stop the Himmelblau services and remove all five packages:

sudo systemctl stop himmelblaud.service himmelblaud-tasks.service
sudo apt-get purge himmelblau nss-himmelblau pam-himmelblau himmelblau-sshd-config himmelblau-apparmor
sudo rm /etc/apt/sources.list.d/himmelblau.list /etc/apt/keyrings/himmelblau.gpg
sudo rm -r /etc/himmelblau /var/lib/himmelblaud
sudo apt-get update

Review /etc/nsswitch.conf and PAM for any remaining Himmelblau entries before the next sign-in. Remove an Entra user's home only after checking its contents.

Microsoft Entra Domain Services with SSSD

Microsoft Entra Domain Services runs a managed Active Directory domain that syncs the tenant's users and groups. A Linux host joins it with realm join like any AD domain, SSSD reads the Entra groups as AD groups, and DefenseClaw reports the account as active_directory.

Cost. The managed domain bills while it exists: about US$0.15 an hour (roughly US$110 a month) for the Standard SKU in the live test, plus the VMs.

Create the managed domain (once per tenant, in the Azure portal or with ARM; the live test used the ARM API, Microsoft.AAD/domainServices version 2022-12-01):

  1. Register the Microsoft.AAD resource provider. Create the Domain Controller Services service principal, the AAD DC Administrators group and an admin user in it. The live test's automation identity needed the Entra roles Application Administrator and Groups Administrator for this.
  2. Create a VNet with a subnet for the domain and one for the VMs.
  3. Create the managed domain with TLS 1.0 disabled (domainSecuritySettings.tlsV1: Disabled): with the default the create failed after 19 seconds. It took about 67 minutes to finish; a second domain controller appeared later.
  4. Point the VNet's DNS servers at both domain controllers.
  5. Reset or change each user's password once the domain exists, so it gets the NTLM and Kerberos hashes. A self-service change worked within 5 minutes; an administrator reset took about 11 minutes. Repeated failed kinit attempts lock the account for 30 minutes.

Join a host (as root; RHEL 9.8 in the live test):

sudo ./join-entra-domain-services.sh join --domain contoso.onmicrosoft.com --admin dsadmin@contoso.onmicrosoft.com --short-names
sudo ./join-entra-domain-services.sh join --domain contoso.onmicrosoft.com --admin dsadmin@contoso.onmicrosoft.com --short-names --apply
./join-entra-domain-services.sh check --user alice --group ml-team

It installs realmd, SSSD and adcli, and runs realm join --membership-software=adcli -U dsadmin@CONTOSO.ONMICROSOFT.COM contoso.onmicrosoft.com, which asks for the password. The computer account lands in OU=AADDC Computers, and SSSD gets id_provider = ad. The live test joined with that realm join command typed by hand; the script's check and names ran live, and its join was run in plan mode only.

Qualified or short names. realm join sets use_fully_qualified_names = True, so the host knows the group only as ml-team@contoso.onmicrosoft.com and the user as alice@contoso.onmicrosoft.com. A groups: [ml-team] assignment then selects nobody; explain names the qualified group the host knows. Either write the qualified name in the assignment, or switch to short names (--short-names, or join-entra-domain-services.sh names short --apply). Both were tested live and selected the group profile.

What DefenseClaw records. Directory active_directory, source sssd, the domain, the bare account name and the uid SSSD's ID mapping assigns. On standalone enterprise the root guardian reads the UPN from SSSD InfoPipe after the reconcile that enrolls the account, and the source becomes sssd_infopipe. Until then the principal is alice@CONTOSO.ONMICROSOFT.COM; DefenseClaw refreshes such an account every 2 minutes, so the UPN replaces it within about 2 minutes of the guardian's record. A per-user install has no InfoPipe access and keeps alice@CONTOSO.ONMICROSOFT.COM.

Group changes go through three caches. Entra to the managed domain took about 20 seconds for an addition and 37 seconds for a removal. SSSD keeps an entry for entry_cache_timeout, 5,400 seconds by default: with the defaults the change had not shown after 10 minutes. After sudo sss_cache -E (join-entra-domain-services.sh flush --apply), id showed it 172 seconds after the Entra change. DefenseClaw then picks it up within its 15-minute cache. End to end: about 15 minutes with a flush, and up to about 105 minutes with SSSD's defaults. A shorter entry_cache_timeout shortens the SSSD part; that was not measured.

4. macOS: Platform SSO

Platform SSO with the Microsoft Enterprise SSO plug-in (Company Portal) signs Entra users in at the login window and links each local account to its Entra user. It does not put the user's Entra groups into the macOS group database.

The live test used macOS 15.8, Company Portal 5.2608.0 and user-driven enrollment in Intune, with the settings catalog policy in macos-platform-sso.settings-catalog.example.json (password method, shared device keys, a local account created at the login window, the account name from the UPN's short name), assigned to a device group. An existing local account was linked by registration, and a new one was created at the login window. In both cases:

  • id -Gn listed only local groups, and dsmemberutil checkmembership did not find the Entra group.
  • With the Platform SSO settings UserAuthorizationMode and NewUserAuthorizationMode set to Groups and AdditionalGroups naming the Entra groups (by name and by object ID), or with AdministratorGroups, Platform SSO created empty local groups (RealName "Platform SSO: ml-team", gid 5000 and up) and never added the user. No Entra group claim reached the Mac, and the user did not become an administrator.

So a groups assignment can match only a local group that carries the membership. macos-entra-group-bridge.sh adds the signed-in user to such a group, or removes them; it uses the group Platform SSO created when one of that name exists:

sudo ./macos-entra-group-bridge.sh add --group ml-team --user alice
sudo ./macos-entra-group-bridge.sh add --group ml-team --user alice --apply
./macos-entra-group-bridge.sh check --group ml-team --user alice

For a fleet, let Intune decide who is in the group, with one signed-in Entra user per Mac, who is also the Mac's primary user. Intune evaluates a user assignment for the primary user and runs the script as root, not as that user, so the script cannot tell who the assignment is for:

  • Fill in the settings block of a copy (ACTION=add, GROUP_NAME=ml-team, USER_NAME=<the Mac account of the Entra user>, APPLY=yes) and assign it to the Entra group, and a copy with ACTION=remove to the users who are not in it (all users, with the Entra group excluded).
  • An Intune copy without USER_NAME stops with exit 2. An add copy changes the named account only while that account is in front at the console; with another user in front (fast user switching) or the login window showing it changes nothing and exits 1. In Intune, set Script frequency for both copies (for example, daily) and Max number of times to retry if script fails for the add copy (for example, 3). The defaults run each copy once and never retry a failure. Check the script result and local group membership after sign-in; if the user was never in front during a run, rerun the copy. A remove copy acts on the named account at any time. See Microsoft's macOS shell script settings.
  • The script refuses built-in and privileged groups (admin, wheel, staff, com.apple.*, any group with an id below 500) unless --allow-system-group (ALLOW_SYSTEM_GROUP=yes) is given, and names that start with a dash or exceed 64 characters. remove leaves the group in place; delete it with dseditgroup -o delete <group> when no assignment uses it.

That delivery was not tested; the membership itself was: the member got the group profile on a per-user install and on standalone enterprise, the other user got default_profile, and removing the membership moved her back to the default.

What DefenseClaw records. Platform SSO stores each registered account's Entra UPN in its record (AltSecurityIdentities, PlatformSSO:<upn>), which root can read. The standalone enterprise enumerator reads it: the account is directory entra_id, source macos_platform_sso, with the UPN as its principal and domain, so a users assignment by UPN works there. Local accounts on the same Mac that never registered stay local. A per-user install cannot read the record and reports local.

5. Map users and groups to profiles

Write the assignments with the UPN for users and the SID for groups. Starter files: per-user-profiles.example.yaml for a user's own config and admin-config.windows.example.yaml for the standalone profile. The keys are the ones in User- and group-based policies; a UPN compares without regard to case.

guardrail:
  profiles:
    strict: {mode: action, block_at: MEDIUM}
    ml-team: {mode: observe}
  profile_assignments:
    - profile: ml-team
      match:
        users: ["alice@contoso.onmicrosoft.com"]
    - profile: ml-team                       # standalone enterprise on Windows only
      match:
        groups: ["S-1-12-1-1864145422-1280850733-3047453086-4192786374"]
  default_profile: strict
ai_discovery:
  include_user_principal: true               # put the UPN in records; off by default

A per-user config belongs to the user

A per-user install reads ~/.defenseclaw/config.yaml (Windows: %USERPROFILE%\.defenseclaw\config.yaml) from the user's own profile, so the user can change their profile. To enforce a policy the user cannot change, use the standalone enterprise profile, which reads the machine config.

Whereusers by UPNgroups by Entra group
Windows, per-userMatched in the live testNo: a per-user install has no group list
Windows, standalone enterpriseMatched before a lane-owned account's UPN rename and stopped matching after it; the account SID stayed the sameBy SID, after the local group step. Matched in the live test
Linux aad, per-userMatched in the live testNo: the module gives no Entra groups
Linux Himmelblau, per-user and standaloneWith cn_name_mapping = falseBy name, as id shows it. Matched in the live test
Linux Entra Domain Services, per-user and standaloneBy the UPN on standalone enterpriseml-team@domain, or ml-team with short names. Matched in the live test
macOS Platform SSO, standaloneBy the UPNOnly a local group that carries the membership. Matched in the live test
macOS Platform SSO, per-userNo: the account reports local without a UPNOnly a local group, as above

Set a strict default_profile: a failed lookup lands on it (why).

6. Check

Ask the gateway which profile a user gets. On Windows, name an Entra user by SID, as AzureAD\Name, by the bare name or by the UPN.

# Standalone enterprise: run in an elevated Administrator prompt.
& 'C:\Program Files\Cisco\DefenseClaw\bin\defenseclaw.exe' enterprise windows profile-explain --user 'AzureAD\Alice' --connector claudecode
# Per-user install:
defenseclaw guardrail profile explain --connector claudecode
defenseclaw guardrail profile explain --connector claudecode
# standalone enterprise, as root:
sudo /opt/defenseclaw/bin/defenseclaw-gateway enterprise linux profile-explain --user alice --connector claudecode
sudo /opt/cisco/defenseclaw/bin/defenseclaw-gateway enterprise macos profile-explain --user alice --connector claudecode

In the per-user live tests, profile named the profile from the users assignment and match said user. On the standalone profile use defenseclaw.exe enterprise windows profile-explain --user <SID> --connector claudecode from an elevated prompt. In the live test it named the group profile with match group for a member of the group, and default_profile for another Entra user.

These are the fields the records carried for an Entra user (the complete table is Entra ID fields, as seen live):

FieldWindowsLinux aad
defenseclaw.user.nameThe account name Windows assigns (EntraAlice), not the UPNThe UPN's account part
defenseclaw.user.principalThe UPNThe UPN, in lower case
defenseclaw.user.directory, .identity.sourceentra_id, windows_identity_storeentra_id, nss_aad
defenseclaw.user.tenant_idThe tenant IDNot reported
defenseclaw.user.group_count0 on a per-user install1, the private group

7. What to expect in operation

On a Mac, a new Platform SSO account can enter the full macOS Setup Assistant at first login before it reaches a desktop. Sign demonstration users in once before a timed run, or deploy an approved Setup Assistant skip payload.

  • A group change on Windows reaches DefenseClaw when the user signs in again: the token is fixed at sign-in. The enumerator decides a signed-out user from the token it saw at the last sign-in, and profile explain then says that the profile is not final. After a sign-in the enumerator reads the new token about 15 seconds later. profile-explain can already show the new profile while live hooks still use directory facts cached from the previous lookup. That gateway cache lasts up to 15 minutes. In live Entra add and removal tests, the token changed at the next sign-in (two to three minutes after the tenant change), while the hook decision changed about 13 minutes after sign-in. An AD group addition first changed a live hook decision about 14 minutes after the new record. Check profile-explain for the cached fact age and remaining time before deciding a rollout has failed. Microsoft documents that a new token can take up to 4 hours to carry a change; that was not measured here.
  • A group change on Linux takes the Himmelblau or SSSD cache time plus DefenseClaw's 15 minutes; the measured times are under Himmelblau and Entra Domain Services. On macOS a change reaches DefenseClaw when the bridge script runs again.
  • A user who has not signed in yet. An Entra user who has never signed in to a Windows computer has no account there yet, so there is no SID to match. On the Azure Linux VM, the module resolved such a user by name but not by uid, and profile explain said unixidentity: not found.
  • An Entra outage or a computer with no network. In a nine-minute Windows sign-in endpoint outage test, open sessions kept the same hook decisions, including after a gateway restart: DefenseClaw reads the identity store, join state and token locally. Windows refused a new password sign-in while the endpoint was unavailable, so no fresh offline token was tested. On Linux, a lookup that fails falls back to default_profile with the reason default_lookup_failed, after the gateway's cached answer expires (up to an hour); see the lookup rules.

Troubleshooting

SymptomCause and fix
A groups assignment with an Entra group never matches on WindowsThe group is not in the token. Name it by SID (the computer cannot resolve names), have a built-in local group list it, verify the account belongs to that Entra security group, then sign out and in if membership was recently added. Check with Get-DefenseClawEntraIdentity.ps1 -GroupSid
profile explain --user <name> on Windows says "Windows has no account by this name"The user has not signed in to this computer yet, or the name is misspelled. Pass the SID, AzureAD\Name or the UPN
group_count is 0 and groups never match on a per-user Windows installExpected: use users, or the standalone enterprise profile
getent group finds no Entra group on the Linux VMExpected: the aad module gives no Entra groups. Use users, Himmelblau or Entra Domain Services
getent group ml-team finds nothing on a Himmelblau host, but id lists the groupExpected: Himmelblau finds groups only by gid or object ID. DefenseClaw matches the name id shows
A user outside pam_allow_groups completes password, MFA and Hello PIN enrollment but SSH refuses them; the log says "User account has expired"Check that the user belongs to an allowed Entra group before enrollment, then verify the group object ID in pam_allow_groups.
SSH sign-in through Himmelblau asks for the password again; the log says "Remote session with unenrolled MFA user denied"The user has no MFA method. Register one at Security info, then sign in: password, code, then a new PIN of six or more characters
himmelblaud is inactive (dead-resources-pinned) after a restart, and no Entra account resolvesThe 5.0.0 nightly restart issue. Run setup-himmelblau.sh restart --apply
A users entry by UPN never matches on a Himmelblau host; explain says the account has no UPNcn_name_mapping is true (the default). Set it to false (setup-himmelblau.sh configure ... --apply), or name the account by its short name
explain says a group is not known to this host by that name and names ml-team@domainSSSD uses qualified names. Write the qualified name, or switch to short names
id on a Mac shows no Entra group after Platform SSO registrationExpected: Platform SSO gives no groups. Use the bridge script, or users on standalone enterprise
The records of a Platform SSO user say localA per-user install cannot read the Platform SSO link; standalone enterprise reports entra_id. An account that never registered stays local
profile explain says unixidentity: not found for an Entra user on LinuxThe OS names the account but cannot map its uid back to it, as seen for users who had not signed in. Have the user sign in, then ask again
Records have no principalai_discovery.include_user_principal is off, the default
az ssh vm is refusedMicrosoft requires the AADSSHLoginForLinux extension on the VM and a VM login role for the user. setup-entra-ssh-linux.sh without --apply lists what is missing
Add-EntraGroupToLocalGroup.ps1 says to run elevatedIt changes a local group. Run it as an administrator or as SYSTEM, or use -WhatIf

Next steps