Skip to content

Windows Platform APIs

Overview

Windows provides a layered security model for agent identity and credential management, built around three core pillars:

  1. Credential Manager + DPAPI — Secure storage of secrets (tokens, passwords, keys) tied to user or machine identity via the Win32 Credential APIs and the Data Protection API.
  2. Windows Services + Task Scheduler — Background execution as distinct identities (LocalSystem, NetworkService, LocalService, or custom service accounts).
  3. UAC + Windows Hello — Consent prompts and biometric authentication for human-in-the-loop authorization flows.

All of these are accessible from Python via pywin32 (which wraps most Win32 APIs), keyring (cross-platform abstraction over Credential Manager), and ctypes (for anything not directly wrapped).

Key Python Packages

Package Purpose Install
pywin32 Full Win32 API bindings (win32cred, win32crypt, win32security, win32service) pip install pywin32
pywin32-ctypes Lightweight ctypes-based subset (no C extensions) pip install pywin32-ctypes
keyring Cross-platform credential storage (uses WinCred backend on Windows) pip install keyring
fido2 FIDO2/WebAuthn support (Windows Hello integration) pip install fido2

Credential Manager

Architecture

Windows Credential Manager (wincred.h) provides a per-user encrypted vault for storing credentials. It is the OS-native way to persist secrets like tokens, passwords, and API keys. Under the hood, credential blobs are encrypted with DPAPI, binding them to the user's login credentials.

Credential Types

Constant Value Use Case
CRED_TYPE_GENERIC 1 Application-defined secrets (tokens, API keys) — this is what Entraclaw should use
CRED_TYPE_DOMAIN_PASSWORD 2 Windows domain authentication (NTLM/Kerberos)
CRED_TYPE_DOMAIN_CERTIFICATE 3 Certificate-based domain auth
CRED_TYPE_DOMAIN_VISIBLE_PASSWORD 4 Passport/Live ID

Persistence Levels

Constant Behavior
CRED_PERSIST_SESSION Deleted on logoff — good for ephemeral tokens
CRED_PERSIST_LOCAL_MACHINE Survives reboots, bound to this machine
CRED_PERSIST_ENTERPRISE Roams with domain profile (requires AD roaming profiles)

Use CredGetSessionTypes() to query which persistence levels are available on a given system for each credential type.

Win32 API (C)

#include <windows.h>
#include <wincred.h>
#pragma comment(lib, "Advapi32.lib")

// --- Store a credential ---
void StoreCredential(void) {
    CREDENTIALW cred = {0};
    cred.Type = CRED_TYPE_GENERIC;
    cred.TargetName = L"Entraclaw/AgentToken";
    cred.UserName = L"agent-abc123";
    const char* token = "eyJhbGciOiJSUzI1NiIs...";
    cred.CredentialBlobSize = (DWORD)strlen(token);
    cred.CredentialBlob = (LPBYTE)token;
    cred.Persist = CRED_PERSIST_LOCAL_MACHINE;

    if (!CredWriteW(&cred, 0)) {
        printf("CredWrite failed: %d\n", GetLastError());
    }
}

// --- Read a credential ---
void ReadCredential(void) {
    PCREDENTIALW pCred = NULL;
    if (CredReadW(L"Entraclaw/AgentToken", CRED_TYPE_GENERIC, 0, &pCred)) {
        printf("User: %ws\n", pCred->UserName);
        printf("Token: %.*s\n", pCred->CredentialBlobSize, pCred->CredentialBlob);
        CredFree(pCred);
    }
}

// --- Delete a credential ---
void DeleteCredential(void) {
    CredDeleteW(L"Entraclaw/AgentToken", CRED_TYPE_GENERIC, 0);
}

Python via win32cred (pywin32)

import win32cred

# --- Store ---
target = "Entraclaw/AgentToken"
token = "eyJhbGciOiJSUzI1NiIs..."

credential = {
    'Type': win32cred.CRED_TYPE_GENERIC,
    'TargetName': target,
    'UserName': 'agent-abc123',
    'CredentialBlob': token,
    'Persist': win32cred.CRED_PERSIST_LOCAL_MACHINE,
    'Comment': 'Entraclaw agent OBO token',
}
win32cred.CredWrite(credential, 0)

# --- Read ---
cred = win32cred.CredRead(target, win32cred.CRED_TYPE_GENERIC)
token = cred['CredentialBlob'].decode('utf-16-le')
print(f"Token: {token}")

# --- Delete ---
win32cred.CredDelete(target, win32cred.CRED_TYPE_GENERIC, 0)

# --- Enumerate all generic credentials ---
creds = win32cred.CredEnumerate(Filter=None, Flags=0)
for c in creds:
    if c['Type'] == win32cred.CRED_TYPE_GENERIC:
        print(f"  {c['TargetName']}: {c['UserName']}")

Python via pywin32-ctypes (lighter weight, no C extensions)

import win32ctypes.pywin32.win32cred as win32cred

target = "Entraclaw/AgentToken"
token = "eyJhbGciOiJSUzI1NiIs..."

# Store — note: CredentialBlob must be bytes in UTF-16LE
credential = {
    'Type': win32cred.CRED_TYPE_GENERIC,
    'TargetName': target,
    'UserName': 'agent-abc123',
    'CredentialBlob': token.encode('utf-16-le'),
    'Persist': win32cred.CRED_PERSIST_LOCAL_MACHINE,
}
win32cred.CredWrite(credential, 0)

# Read
cred = win32cred.CredRead(target, win32cred.CRED_TYPE_GENERIC)
retrieved = cred['CredentialBlob'].decode('utf-16-le')
import keyring

# Store
keyring.set_password("entraclaw", "agent-abc123", "eyJhbGciOiJSUzI1NiIs...")

# Retrieve
token = keyring.get_password("entraclaw", "agent-abc123")

# Delete
keyring.delete_password("entraclaw", "agent-abc123")

On Windows, keyring uses WinVaultKeyring backend by default, which maps to the same Credential Manager APIs. Service name → TargetName, username → UserName.

Tradeoff: keyring is the simplest and most portable, but win32cred gives access to metadata fields (Comment, Attributes, LastWritten) and persistence control that keyring abstracts away.


DPAPI (Data Protection API)

Architecture

DPAPI provides symmetric encryption tied to Windows user credentials. It is the mechanism that Credential Manager itself uses under the hood. Direct DPAPI usage is useful when you need to encrypt arbitrary data (config files, local caches, etc.) beyond what fits in a credential blob.

Scopes

Scope Flag Who Can Decrypt
CurrentUser 0 (default) Only the same Windows user on the same machine
LocalMachine CRYPTPROTECT_LOCAL_MACHINE (0x4) Any user/process on the same machine

For Entraclaw agents, CurrentUser scope is strongly preferred — it ensures that even a local admin on the machine cannot trivially decrypt the agent's secrets without impersonating the specific user.

How It Works

  1. Each user has a master key derived from their Windows password, stored in %APPDATA%\Microsoft\Protect\{SID}\.
  2. CryptProtectData() generates a session key, encrypts data, and wraps the session key with the master key.
  3. CryptUnprotectData() reverses the process — requires the same user context.
  4. In Active Directory environments, a domain backup key on the DC can recover any user's master key. If this backup key is compromised, all DPAPI secrets in the domain are at risk.

Win32 API (C)

#include <windows.h>
#include <dpapi.h>
#pragma comment(lib, "Crypt32.lib")

BOOL EncryptSecret(const BYTE* data, DWORD dataLen, DATA_BLOB* out) {
    DATA_BLOB input = { dataLen, (BYTE*)data };
    DATA_BLOB entropy = { 8, (BYTE*)"entraclaw" };  // optional extra entropy

    return CryptProtectData(
        &input,
        L"Entraclaw Agent Secret",  // description (stored in cleartext!)
        &entropy,                   // optional entropy
        NULL,                       // reserved
        NULL,                       // prompt struct (NULL = no UI)
        0,                          // flags (0 = CurrentUser scope)
        out                         // output blob
    );
}

BOOL DecryptSecret(DATA_BLOB* encrypted, DATA_BLOB* out) {
    DATA_BLOB entropy = { 8, (BYTE*)"entraclaw" };
    LPWSTR description = NULL;

    BOOL result = CryptUnprotectData(
        encrypted, &description, &entropy,
        NULL, NULL, 0, out
    );
    if (description) LocalFree(description);
    return result;
}

Python via win32crypt (pywin32)

import win32crypt

# --- Encrypt (CurrentUser scope) ---
plaintext = b'{"access_token": "eyJ...", "refresh_token": "dGhpcyBpcyBh..."}'
entropy = b"entraclaw-agent"

# CryptProtectData returns (description, encrypted_bytes)
desc, encrypted = win32crypt.CryptProtectData(
    plaintext,
    "Entraclaw Agent Config",  # description (stored in cleartext)
    entropy,                   # optional entropy
    None,                      # reserved
    None,                      # prompt struct
    0                          # flags: 0 = CurrentUser scope
)

# --- Decrypt ---
desc, decrypted = win32crypt.CryptUnprotectData(
    encrypted,
    entropy,   # must match what was used for encryption
    None,      # reserved
    None,      # prompt struct
    0          # flags
)
print(decrypted.decode('utf-8'))

# --- Machine-scope encryption (any user on this machine can decrypt) ---
CRYPTPROTECT_LOCAL_MACHINE = 0x4
desc, encrypted_machine = win32crypt.CryptProtectData(
    plaintext, "Shared Secret", None, None, None,
    CRYPTPROTECT_LOCAL_MACHINE
)

DPAPI + File Storage Pattern

For data too large for Credential Manager (e.g., cached tokens, agent config):

import win32crypt
import os

CONFIG_PATH = os.path.join(os.environ['LOCALAPPDATA'], 'Entraclaw', 'agent_config.enc')

def save_encrypted(data: bytes, path: str):
    _, encrypted = win32crypt.CryptProtectData(data, "Entraclaw", b"entropy", None, None, 0)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, 'wb') as f:
        f.write(encrypted)

def load_decrypted(path: str) -> bytes:
    with open(path, 'rb') as f:
        encrypted = f.read()
    _, decrypted = win32crypt.CryptUnprotectData(encrypted, b"entropy", None, None, 0)
    return decrypted

Windows Services

Service Identity Options

Identity Local Privilege Network Auth DPAPI Scope Best For
LocalSystem Highest (root-equivalent) Machine credentials Machine-scope keys (weak protection) Avoid unless absolutely necessary
NetworkService Low Machine credentials (COMPUTERNAME$) Has own profile, but limited Network-facing services
LocalService Lowest Anonymous Has own profile, limited Local-only services
Custom user Configurable User's credentials Full user-scope DPAPI Recommended for Entraclaw

Recommendation for Entraclaw: Create a dedicated entraclaw-agent user account with minimal privileges. This gives the agent its own DPAPI master key, its own Credential Manager vault, and proper identity isolation.

Python Windows Service with pywin32

import win32serviceutil
import win32service
import win32event
import servicemanager
import time
import os

class EntraclawAgentService(win32serviceutil.ServiceFramework):
    _svc_name_ = 'EntraclawAgent'
    _svc_display_name_ = 'Entraclaw Autonomous Agent'
    _svc_description_ = 'Background service for the Entraclaw autonomous agent.'

    def __init__(self, args):
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.stop_event = win32event.CreateEvent(None, 0, 0, None)
        self.running = True

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.stop_event)
        self.running = False

    def SvcDoRun(self):
        servicemanager.LogMsg(
            servicemanager.EVENTLOG_INFORMATION_TYPE,
            servicemanager.PYS_SERVICE_STARTED,
            (self._svc_name_, '')
        )
        self.main()

    def main(self):
        """Main agent loop."""
        log_dir = os.path.join(os.environ.get('LOCALAPPDATA', 'C:\\'), 'Entraclaw')
        os.makedirs(log_dir, exist_ok=True)
        log_path = os.path.join(log_dir, 'agent.log')

        while self.running:
            # Agent work happens here
            with open(log_path, 'a') as f:
                f.write(f'Agent heartbeat: {time.ctime()}\n')

            # Wait for stop signal or timeout (10 seconds)
            result = win32event.WaitForSingleObject(self.stop_event, 10000)
            if result == win32event.WAIT_OBJECT_0:
                break

if __name__ == '__main__':
    win32serviceutil.HandleCommandLine(EntraclawAgentService)

Service Lifecycle Commands

# Install the service
python entraclaw_service.py install

# Set to run as a specific user (recommended)
sc.exe config EntraclawAgent obj= ".\entraclaw-agent" password= "SecurePassword"

# Start / Stop / Remove
python entraclaw_service.py start
python entraclaw_service.py stop
python entraclaw_service.py remove

# Debug mode (runs in console for development)
python entraclaw_service.py debug

Alternative: NSSM (Non-Sucking Service Manager)

NSSM wraps any executable (including Python scripts) as a Windows service without requiring pywin32 service code. This is simpler but less integrated.

# Install (from admin shell)
nssm install EntraclawAgent "C:\path\to\venv\Scripts\python.exe" "C:\path\to\agent.py"

# Configure working directory
nssm set EntraclawAgent AppDirectory "C:\path\to\project"

# Configure logging
nssm set EntraclawAgent AppStdout "C:\path\to\logs\stdout.log"
nssm set EntraclawAgent AppStderr "C:\path\to\logs\stderr.log"

# Set service account
nssm set EntraclawAgent ObjectName ".\entraclaw-agent"

# Auto-start on boot
nssm set EntraclawAgent Start SERVICE_AUTO_START

# Set environment variables
nssm set EntraclawAgent AppEnvironmentExtra ENTRACLAW_ENV=production

# Control
nssm start EntraclawAgent
nssm stop EntraclawAgent
nssm restart EntraclawAgent
nssm edit EntraclawAgent     # Opens GUI for configuration
nssm remove EntraclawAgent

NSSM advantages: Auto-restart on failure, stdout/stderr capture, GUI editor, no code changes needed. Disadvantages: External dependency, less control over service events (pause, custom commands).


Task Scheduler

When to Use Task Scheduler vs. Services

Aspect Windows Service Task Scheduler
Execution model Always running (daemon) Triggered by schedule/event
User session Runs without user login Can run without login (with config)
Credential access Service account's vault Logged-in user's vault (or specified user)
Restart on failure Built-in recovery options Limited retry options
Best for Persistent agents, message queues Periodic tasks, token refresh, cleanup

For Entraclaw, a hybrid approach works well: - Service for the main agent loop (always running, processing tasks) - Scheduled task for periodic maintenance (token refresh, log rotation)

Creating Tasks via schtasks.exe

# Run a Python script every hour under the current user
schtasks /create /tn "Entraclaw\TokenRefresh" /tr "C:\path\to\venv\Scripts\python.exe C:\path\to\refresh_token.py" /sc hourly /ru "%USERNAME%" /rl HIGHEST

# Run at system startup (requires admin)
schtasks /create /tn "Entraclaw\AgentStart" /tr "C:\path\to\start_agent.py" /sc onstart /ru "entraclaw-agent" /rp "Password" /rl HIGHEST

# Run when user logs on
schtasks /create /tn "Entraclaw\AgentUserStart" /tr "C:\path\to\agent.py" /sc onlogon

# Query task status
schtasks /query /tn "Entraclaw\TokenRefresh" /fo LIST /v

# Delete a task
schtasks /delete /tn "Entraclaw\TokenRefresh" /f

Creating Tasks via PowerShell (more control)

$action = New-ScheduledTaskAction `
    -Execute "C:\path\to\venv\Scripts\python.exe" `
    -Argument "C:\path\to\refresh_token.py" `
    -WorkingDirectory "C:\path\to\project"

$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Hours 1) -Once -At (Get-Date)

$principal = New-ScheduledTaskPrincipal `
    -UserId "entraclaw-agent" `
    -LogonType ServiceAccount `
    -RunLevel Highest

$settings = New-ScheduledTaskSettingsSet `
    -AllowStartIfOnBatteries `
    -DontStopIfGoingOnBatteries `
    -StartWhenAvailable `
    -RestartCount 3 `
    -RestartInterval (New-TimeSpan -Minutes 1)

Register-ScheduledTask `
    -TaskName "Entraclaw\TokenRefresh" `
    -Action $action `
    -Trigger $trigger `
    -Principal $principal `
    -Settings $settings `
    -Description "Periodic token refresh for Entraclaw agent"

Python Task Scheduler via COM API

import win32com.client

scheduler = win32com.client.Dispatch('Schedule.Service')
scheduler.Connect()
root = scheduler.GetFolder('\\')

# Create a new task
task_def = scheduler.NewTask(0)
task_def.RegistrationInfo.Description = 'Entraclaw token refresh'

# Create trigger (daily at 3 AM)
trigger = task_def.Triggers.Create(2)  # 2 = TASK_TRIGGER_DAILY
trigger.StartBoundary = '2025-01-01T03:00:00'
trigger.DaysInterval = 1

# Create action
action = task_def.Actions.Create(0)  # 0 = TASK_ACTION_EXEC
action.Path = r'C:\path\to\venv\Scripts\python.exe'
action.Arguments = r'C:\path\to\refresh_token.py'
action.WorkingDirectory = r'C:\path\to\project'

# Register (TASK_CREATE_OR_UPDATE=6, TASK_LOGON_PASSWORD=1)
root.RegisterTaskDefinition(
    'Entraclaw\\TokenRefresh',
    task_def,
    6,                    # TASK_CREATE_OR_UPDATE
    'entraclaw-agent',     # user
    'password',           # password
    1                     # TASK_LOGON_PASSWORD
)

SSPI (Security Support Provider Interface)

Architecture

SSPI is Microsoft's implementation of GSSAPI — a pluggable authentication framework that abstracts protocol details (Kerberos, NTLM, etc.) behind a common API. It is the mechanism Windows uses for all integrated authentication.

Security Support Providers (SSPs)

SSP Protocol When Used Strength
Kerberos Kerberos v5 (RFC 4120) Domain-joined machines, Active Directory Strong: mutual auth, delegation, tickets
NTLM Challenge-response Legacy/workgroup, fallback Moderate: no mutual auth, replay-vulnerable
Negotiate SPNEGO (RFC 2478) Default in most Windows auth Auto-selects Kerberos → NTLM fallback
Schannel TLS/SSL HTTPS, encrypted channels Transport-level security
CredSSP Kerberos + TLS RDP, WinRM, PowerShell remoting Delegation with encryption

How Negotiate Works

  1. Application requests authentication using the "Negotiate" package.
  2. SSPI examines the target (SPN, network context) and tries Kerberos first.
  3. If Kerberos is unavailable (no domain, no SPN, workgroup), falls back to NTLM.
  4. The application never needs to know which protocol was selected.

Relevance to Entraclaw

SSPI is primarily relevant for: - Agent-to-agent authentication in enterprise/AD environments - Service-to-service auth where Entraclaw agents need to call Windows-authenticated APIs - OBO (On-Behalf-Of) token flows — Kerberos delegation (S4U2Proxy) allows a service to act on behalf of a user

Python SSPI Access

import sspi
import sspicon

# Create a client security context (Negotiate = auto Kerberos/NTLM)
client_auth = sspi.ClientAuth("Negotiate", targetspn="HTTP/server.domain.com")

# Generate the initial auth token
err, buffers = client_auth.authorize(None)
token = buffers[0].Buffer  # Send this to the server

# Server side
server_auth = sspi.ServerAuth("Negotiate")
err, buffers = server_auth.authorize(token)
# After completion, server_auth.ctxt contains the authenticated identity

The sspi module is part of pywin32. For HTTP-based auth, libraries like requests-negotiate-sspi or requests-kerberos handle SSPI integration automatically.


Architecture

User Account Control (UAC) creates a split-token model: even administrator accounts run with standard-user privileges by default. Elevation to full admin requires an explicit consent prompt.

Elevation Flow

Application requests admin privileges
    → CreateProcess returns ERROR_ELEVATION_REQUIRED
    → ShellExecuteEx with "runas" verb triggers AppInfo service
    → AppInfo displays consent/credential prompt on Secure Desktop
    → User consents → new process created with full admin token
    → User denies → ERROR_CANCELLED returned
Setting Effect Security
Prompt for consent on secure desktop Dimmed screen, UAC dialog only Recommended (default)
Prompt for credentials on secure desktop Must enter admin password Highest security
Prompt for consent (no secure desktop) Normal dialog box Lower (malware can click)
Elevate without prompting No UI at all Dangerous — never use

Registry: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\ConsentPromptBehaviorAdmin

Programmatic Elevation (Python)

import ctypes
import sys
import subprocess

def is_elevated() -> bool:
    """Check if the current process has admin privileges."""
    try:
        return ctypes.windll.shell32.IsUserAnAdmin() != 0
    except Exception:
        return False

def run_elevated(script_path: str, args: str = ""):
    """Re-launch a Python script with elevation (triggers UAC prompt)."""
    # ShellExecuteEx with "runas" verb
    ctypes.windll.shell32.ShellExecuteW(
        None,          # parent window
        "runas",       # verb — triggers UAC
        sys.executable,  # application
        f'"{script_path}" {args}',  # arguments
        None,          # working directory
        1              # SW_SHOWNORMAL
    )

# Usage pattern for Entraclaw consent
if not is_elevated():
    print("Agent needs elevated privileges for initial setup.")
    run_elevated(__file__, "--setup")
    sys.exit(0)
else:
    print("Running with admin privileges — performing setup...")

Application Manifest for Auto-Elevation

For compiled executables, embed a manifest requesting elevation:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <!-- Options: asInvoker | highestAvailable | requireAdministrator -->
        <requestedExecutionLevel level="highestAvailable" uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

UAC is not suitable for ongoing agent consent (it's designed for one-shot elevation). For Entraclaw, the consent flow should be:

  1. Installation — One-time UAC elevation to install the service and create the service account.
  2. Ongoing consent — Use a custom UI (tray app, toast notification) rather than UAC. Windows Toast Notifications API can present approve/deny actions.
  3. Sensitive operations — Trigger Windows Hello biometric prompt (see below).

Windows Hello

Architecture

Windows Hello provides biometric (fingerprint, face) and PIN-based authentication, exposed through the WebAuthn/FIDO2 APIs. It acts as a platform authenticator — the biometric check happens locally, and the result is a cryptographic assertion.

Integration Model

Windows Hello is accessed through the WebAuthn API, not directly. The flow is:

Agent requests user consent
    → Agent's local web UI calls navigator.credentials.get() (browser)
    → OR agent calls WebAuthNAuthenticatorGetAssertion (Win32 native)
    → Windows Hello prompt appears (face/fingerprint/PIN)
    → Returns signed assertion (not biometric data)
    → Agent verifies assertion server-side

Native Win32 WebAuthn API

Windows 10 1903+ provides webauthn.h:

#include <webauthn.h>
#pragma comment(lib, "WebAuthn.lib")

// Check if WebAuthn is available
DWORD apiVersion = WebAuthNGetApiVersionNumber();
BOOL available = WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(&pbAvailable);

// Request assertion (simplified)
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS options = {
    .dwVersion = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_1,
    .dwTimeoutMilliseconds = 30000,
    .dwUserVerificationRequirement = WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED
};

Python Integration via FIDO2

from fido2.server import Fido2Server
from fido2.webauthn import (
    PublicKeyCredentialRpEntity,
    UserVerificationRequirement,
)

rp = PublicKeyCredentialRpEntity(name="Entraclaw Agent", id="localhost")
server = Fido2Server(rp)

# Registration (one-time setup)
registration_data, state = server.register_begin(
    user={"id": b"agent-abc123", "name": "entraclaw-agent", "displayName": "Entraclaw Agent"},
    credentials=[],
    user_verification=UserVerificationRequirement.REQUIRED,
)
# Client (browser) handles the Windows Hello prompt and returns attestation

# Authentication (each consent request)
auth_data, state = server.authenticate_begin(
    credentials=registered_credentials,
    user_verification=UserVerificationRequirement.REQUIRED,
)
# Client triggers Windows Hello → returns assertion → server.authenticate_complete()

Practical Considerations for Entraclaw

  • Windows Hello requires a browser or native WebAuthn caller — it cannot be triggered from a pure CLI/headless Python process.
  • For a headless agent, the pattern is:
  • Agent runs as a background service.
  • When consent is needed, agent notifies a tray application (user-session process).
  • Tray app opens a local web page that triggers WebAuthn.
  • Result is passed back to the agent service via IPC (named pipe, localhost HTTP).
  • Alternative: Use the Windows Security Center API for simpler PIN verification without the full WebAuthn ceremony.

Windows Security Tokens & Process Identity

Access Tokens

Every Windows process has a primary access token that defines its security identity:

  • User SID — The security identifier of the user/account
  • Group SIDs — All groups the user belongs to
  • Privileges — Specific rights (e.g., SeBackupPrivilege, SeDebugPrivilege)
  • Default DACL — Permissions applied to objects created by this process
  • Token type — Primary (process) or Impersonation (thread)

Impersonation

A thread can temporarily assume another user's identity via an impersonation token. This is how services act on behalf of users:

import win32security
import win32con
import win32api

# Log on as a specific user
handle = win32security.LogonUser(
    'entraclaw-agent',                    # username
    '.',                                  # domain (. = local)
    'password',                           # password
    win32con.LOGON32_LOGON_INTERACTIVE,   # logon type
    win32con.LOGON32_PROVIDER_DEFAULT     # logon provider
)

# Impersonate that user (current thread now runs as entraclaw-agent)
win32security.ImpersonateLoggedOnUser(handle)
print(f"Now running as: {win32api.GetUserName()}")

# Do work that requires entraclaw-agent's credentials/DPAPI keys...

# Revert to original identity
win32security.RevertToSelf()
handle.Close()

Token Queries

import win32security
import win32api
import win32process

# Get current process token
token = win32security.OpenProcessToken(
    win32api.GetCurrentProcess(),
    win32con.TOKEN_QUERY
)

# Get the user SID from the token
user_sid, _ = win32security.GetTokenInformation(token, win32security.TokenUser)
account, domain, type = win32security.LookupAccountSid(None, user_sid)
print(f"Running as: {domain}\\{account}")

# Get privileges
privileges = win32security.GetTokenInformation(token, win32security.TokenPrivileges)
for priv_luid, priv_attr in privileges:
    name = win32security.LookupPrivilegeName(None, priv_luid)
    enabled = bool(priv_attr & win32security.SE_PRIVILEGE_ENABLED)
    print(f"  {name}: {'enabled' if enabled else 'disabled'}")

Integration Patterns

┌─────────────────────────────────────────────────────┐
│                   Windows Machine                    │
│                                                      │
│  ┌──────────────────────┐  ┌──────────────────────┐ │
│  │   Entraclaw Service   │  │   Entraclaw Tray App  │ │
│  │   (Background)       │  │   (User Session)     │ │
│  │                      │  │                      │ │
│  │  • Runs as dedicated │  │  • Shows status icon │ │
│  │    service account   │  │  • Displays consent  │ │
│  │  • Processes tasks   │  │    prompts           │ │
│  │  • Manages agent ID  │  │  • Triggers Windows  │ │
│  │                      │  │    Hello for auth    │ │
│  │  Credentials:        │  │  • Toast notifs for  │ │
│  │  • Credential Mgr    │  │    approvals         │ │
│  │  • DPAPI-encrypted   │  │                      │ │
│  │    config files      │  │  IPC: Named Pipes    │ │
│  │                      │  │  or localhost HTTP   │ │
│  └──────────┬───────────┘  └──────────┬───────────┘ │
│             │                         │              │
│             └─────────┬───────────────┘              │
│                       │                              │
│              ┌────────▼────────┐                     │
│              │ Credential Mgr  │                     │
│              │ (WinCred API)   │                     │
│              │                 │                     │
│              │ Agent tokens,   │                     │
│              │ OBO tokens,     │                     │
│              │ refresh tokens  │                     │
│              └─────────────────┘                     │
└─────────────────────────────────────────────────────┘

Implementation Checklist

1. Credential Storage Layer

# credential_store_windows.py — Entraclaw credential abstraction

import platform
if platform.system() != 'Windows':
    raise ImportError("This module is Windows-only")

import win32cred
import win32crypt
import json
import os

CREDENTIAL_PREFIX = "Entraclaw"

class WindowsCredentialStore:
    """Windows-native credential storage using Credential Manager + DPAPI."""

    def store_token(self, agent_id: str, token_data: dict) -> None:
        """Store an agent token in Credential Manager."""
        target = f"{CREDENTIAL_PREFIX}/{agent_id}/token"
        blob = json.dumps(token_data)
        credential = {
            'Type': win32cred.CRED_TYPE_GENERIC,
            'TargetName': target,
            'UserName': agent_id,
            'CredentialBlob': blob,
            'Persist': win32cred.CRED_PERSIST_LOCAL_MACHINE,
            'Comment': f'Entraclaw agent token for {agent_id}',
        }
        win32cred.CredWrite(credential, 0)

    def get_token(self, agent_id: str) -> dict | None:
        """Retrieve an agent token from Credential Manager."""
        target = f"{CREDENTIAL_PREFIX}/{agent_id}/token"
        try:
            cred = win32cred.CredRead(target, win32cred.CRED_TYPE_GENERIC)
            blob = cred['CredentialBlob'].decode('utf-16-le')
            return json.loads(blob)
        except Exception:
            return None

    def delete_token(self, agent_id: str) -> None:
        """Remove an agent token from Credential Manager."""
        target = f"{CREDENTIAL_PREFIX}/{agent_id}/token"
        try:
            win32cred.CredDelete(target, win32cred.CRED_TYPE_GENERIC, 0)
        except Exception:
            pass

    def store_encrypted_config(self, agent_id: str, config: dict) -> None:
        """Store larger config data as DPAPI-encrypted file."""
        config_dir = os.path.join(
            os.environ.get('LOCALAPPDATA', ''),
            'Entraclaw', agent_id
        )
        os.makedirs(config_dir, exist_ok=True)
        path = os.path.join(config_dir, 'config.enc')

        plaintext = json.dumps(config).encode('utf-8')
        _, encrypted = win32crypt.CryptProtectData(
            plaintext, f"Entraclaw:{agent_id}", None, None, None, 0
        )
        with open(path, 'wb') as f:
            f.write(encrypted)

    def load_encrypted_config(self, agent_id: str) -> dict | None:
        """Load DPAPI-encrypted config."""
        path = os.path.join(
            os.environ.get('LOCALAPPDATA', ''),
            'Entraclaw', agent_id, 'config.enc'
        )
        if not os.path.exists(path):
            return None
        with open(path, 'rb') as f:
            encrypted = f.read()
        _, decrypted = win32crypt.CryptUnprotectData(encrypted, None, None, None, 0)
        return json.loads(decrypted.decode('utf-8'))

2. Service Installation Script

# install_service.py — One-time setup requiring UAC elevation

import ctypes
import sys
import subprocess
import os

def is_admin():
    return ctypes.windll.shell32.IsUserAnAdmin() != 0

def create_service_account():
    """Create a dedicated Windows user for the agent service."""
    import secrets
    password = secrets.token_urlsafe(32)

    # Create user
    subprocess.run([
        'net', 'user', 'entraclaw-agent', password,
        '/add', '/comment:"Entraclaw Agent Service Account"',
        '/passwordchg:no', '/expires:never'
    ], check=True)

    # Grant "Log on as a service" right
    # (Requires ntrights.exe or secedit — simplified here)
    print(f"Service account created. Password stored in Credential Manager.")
    return password

def install():
    if not is_admin():
        # Re-launch with elevation
        ctypes.windll.shell32.ShellExecuteW(
            None, "runas", sys.executable, f'"{__file__}"', None, 1
        )
        return

    password = create_service_account()

    # Install the service
    subprocess.run([
        sys.executable, 'entraclaw_service.py', 'install',
        '--username', '.\\entraclaw-agent',
        '--password', password,
        '--startup', 'auto',
    ], check=True)

    print("Entraclaw agent service installed successfully.")

if __name__ == '__main__':
    install()
# tray_app.py — System tray application for user consent
# Requires: pip install pystray pillow

import pystray
from PIL import Image, ImageDraw
import threading
import json
from http.server import HTTPServer, BaseHTTPRequestHandler

class ConsentHandler(BaseHTTPRequestHandler):
    """Local HTTP server for consent requests from the agent service."""

    def do_POST(self):
        if self.path == '/consent':
            length = int(self.headers['Content-Length'])
            body = json.loads(self.rfile.read(length))

            # Show consent dialog (Windows toast notification or dialog)
            approved = show_consent_dialog(
                action=body['action'],
                resource=body['resource'],
                agent_id=body['agent_id'],
            )

            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({'approved': approved}).encode())

def show_consent_dialog(action: str, resource: str, agent_id: str) -> bool:
    """Show a Windows dialog for user consent."""
    import ctypes
    result = ctypes.windll.user32.MessageBoxW(
        0,
        f"Entraclaw agent '{agent_id}' wants to:\n\n"
        f"Action: {action}\n"
        f"Resource: {resource}\n\n"
        f"Allow this action?",
        "Entraclaw Agent Consent",
        0x00000004 | 0x00000030  # MB_YESNO | MB_ICONWARNING
    )
    return result == 6  # IDYES

def create_tray_icon():
    # Create a simple icon
    img = Image.new('RGB', (64, 64), color='blue')
    draw = ImageDraw.Draw(img)
    draw.rectangle([16, 16, 48, 48], fill='white')

    icon = pystray.Icon(
        "entraclaw",
        img,
        "Entraclaw Agent",
        menu=pystray.Menu(
            pystray.MenuItem("Status: Running", lambda: None),
            pystray.MenuItem("View Logs", lambda: None),
            pystray.MenuItem("Quit", lambda icon, _: icon.stop()),
        )
    )
    return icon

if __name__ == '__main__':
    # Start consent HTTP server in background
    server = HTTPServer(('127.0.0.1', 19876), ConsentHandler)
    threading.Thread(target=server.serve_forever, daemon=True).start()

    # Run tray icon (blocks)
    icon = create_tray_icon()
    icon.run()

Community Learnings & Gotchas

DPAPI Scope Issues

  • Problem: DPAPI secrets are bound to the user's Windows password. If an admin resets the user's password (vs. the user changing it themselves), the DPAPI master key is lost and all encrypted data becomes unrecoverable.
  • Mitigation: In AD environments, the domain backup key can recover master keys. For local accounts, there is no recovery. Always maintain a separate backup of critical secrets.

  • Problem: Service accounts (SYSTEM, NetworkService) have DPAPI keys, but they're derived from machine secrets rather than passwords, making them weaker.

  • Mitigation: Use a dedicated user account for the agent service, not a built-in service account.

Credential Manager Limits

  • CredentialBlob size: Maximum is 512 bytes for CRED_TYPE_GENERIC credentials on older Windows versions (2560 bytes on newer). Long tokens may need to be split or stored via DPAPI files instead.
  • TargetName uniqueness: TargetName + Type must be unique per user. Use a consistent naming convention: Entraclaw/{agent_id}/{purpose}.

Service Account Quirks

  • Services running as LocalSystem cannot access the interactive user's Credential Manager vault — they have their own (mostly empty) vault.
  • Services don't get a desktop session by default. Any UI (dialogs, console output) requires "Allow service to interact with desktop" (deprecated) or a separate user-session process.
  • The service's %USERPROFILE% and %APPDATA% point to C:\Windows\system32\config\systemprofile when running as SYSTEM, not the user's actual profile.

Credential Isolation

  • Each Windows user has their own isolated Credential Manager vault. A credential stored by User A is invisible to User B, even if B is an administrator.
  • Exception: LocalMachine-scope DPAPI allows any local process to decrypt. Use CurrentUser scope for agent secrets.

Attack Surface

  • Mimikatz and similar tools can extract DPAPI master keys with local admin access. DPAPI is defense-in-depth, not an absolute barrier.
  • Credential Guard (Windows 10 Enterprise) isolates DPAPI keys in a VSM (Virtual Secure Mode) enclave, significantly hardening against extraction.
  • The AD domain backup key is a single point of compromise — if leaked, all domain-joined DPAPI secrets are exposed. Microsoft recommends abandoning the domain in this scenario (no supported key rotation).

Python-Specific Issues

  • pywin32 requires a matching Python architecture (x86 vs x64) and version.
  • pywin32-ctypes is lighter but doesn't wrap all APIs (no win32service, win32security, sspi).
  • The keyring library may use WinVaultKeyring or Windows Credential Locker depending on the Windows version — test on target platforms.
  • When encoding credential blobs: pywin32's CredWrite handles strings directly, but pywin32-ctypes requires explicit UTF-16LE encoding.

Open Questions

  1. Service vs. scheduled task for token refresh? A service is always-on but consumes resources. A scheduled task is lighter but has delayed startup. For an autonomous agent, the service model is likely required, with periodic token refresh on a timer within the service.

  2. Credential Manager vs. DPAPI file for OBO tokens? Credential Manager is simpler but has size limits. DPAPI files handle arbitrary data. Consider: tokens in Credential Manager (small, high-value), config/cache in DPAPI files.

  3. How to handle Windows password resets? If the domain admin resets the service account's password, DPAPI master keys are lost. Options: (a) store a backup key in the cloud, (b) use machine-scope DPAPI for the most critical bootstrap secret, (c) implement a re-enrollment flow.

  4. Consent UX without a browser? Windows Hello requires WebAuthn (browser-based) or the native webauthn.dll API. For a pure-desktop consent flow, consider using MessageBoxW for simple approve/deny, with Windows Hello reserved for high-security operations where a browser component is acceptable.

  5. Cross-platform credential abstraction? The keyring library provides the best cross-platform API (macOS Keychain on Mac, SecretService on Linux, WinCred on Windows), but loses access to platform-specific features. Consider keyring as the default with platform-specific backends for advanced features.

  6. Credential Guard compatibility? If the target machine has Credential Guard enabled, does it affect DPAPI access for service accounts? Need to test on Enterprise SKUs.

  7. MSA/gMSA for service identity? Group Managed Service Accounts (gMSA) provide automatic password rotation and are the enterprise best practice for service identity in AD environments. Investigate whether the Entraclaw agent service can run as a gMSA.


Sources

Microsoft Documentation

Python Libraries

Tools

Security Research

Community Discussions