wulf-pulse/docs/DUO_INTEGRATION.md
lorentz 02958e429c docs: Add Duo Security integration guide
Covers architecture, data synced (6 tables), sync process, rate limiting,
incremental auth logs, company matching, API endpoints, UI elements,
bypass vs disabled user separation, env vars, and file inventory.
2026-03-27 11:48:30 -04:00

12 KiB

Duo Security Integration

Overview

Pulse syncs data from Duo Security across all managed child accounts using two Duo APIs:

  • Accounts API — Lists child accounts under the parent MSP account
  • Admin API — Pulls users, phones, auth logs, groups, and integrations from each child (and the parent)

The parent Accounts API credentials can sign requests against any child account's API hostname, so no per-child API keys are needed.


Architecture

┌──────────────────┐     POST /accounts/v1/account/list     ┌──────────────┐
│   Duo Accounts   │ ◄──────────────────────────────────────│              │
│       API        │     (returns 32 child accounts)        │              │
└──────────────────┘                                        │  DuoClient   │
                                                            │  (HMAC-SHA1  │
┌──────────────────┐     GET /admin/v1/users, phones, ...   │   signing)   │
│   Duo Admin API  │ ◄──────────────────────────────────────│              │
│  (per-child host │     (with account_id param)            │              │
│   + parent)      │                                        └──────┬───────┘
└──────────────────┘                                               │
                                                                   │
                                                            ┌──────▼───────┐
                                                            │ DuoSyncService│
                                                            │  (orchestrate │
                                                            │   full sync)  │
                                                            └──────┬───────┘
                                                                   │ upsert
                                                            ┌──────▼───────┐
                                                            │  PostgreSQL  │
                                                            │  (6 tables)  │
                                                            └──────────────┘

Data Synced

1. Accounts (duo_accounts)

Field Description
account_id Duo's unique account identifier
name Account display name
api_hostname API hostname for Admin API calls
autotask_company_id FK to matched Autotask company
user_count Number of users in this account
integration_count Number of integrations
edition Duo edition (if available)
is_parent true for the parent Wulf Consulting account

Source: Accounts API POST /accounts/v1/account/list + parent account row added manually.

2. Users (duo_users)

Field Description
user_id Duo user ID
username Login username
email Email address
realname Display name
status active, bypass, disabled, locked out
is_enrolled Whether the user has completed enrollment
last_login Last authentication timestamp
phones_count Number of registered devices
groups JSONB array of group memberships
aliases JSONB array of username aliases

Source: Admin API GET /admin/v1/users per account.

3. Phones (duo_phones)

Field Description
phone_id Duo phone ID
name Device name
number Phone number
type mobile, landline, etc.
platform Apple iOS, Google Android, etc.
model Device model
os_version Operating system version
activated Whether Duo Mobile is activated
last_seen Last device activity
capabilities JSONB array of supported auth methods
users JSONB array of associated users

Source: Admin API GET /admin/v1/phones per account.

4. Auth Logs (duo_auth_logs)

Field Description
txid Unique transaction ID
timestamp When the authentication occurred
user_name Username that authenticated
factor Auth method: duo_push, phone, sms, passcode, etc.
result success, denied, fraud
reason Detailed reason for result
application_name Which integration triggered the auth
access_device_ip IP address of the access device (INET type)
access_device_location JSONB with city/state/country of access device
auth_device_ip IP of the authenticating device
event_type authentication, enrollment

Source: Admin API GET /admin/v2/logs/authentication per account. Uses incremental sync — only pulls logs newer than the last synced timestamp (default: 30-day lookback on first sync).

5. Groups (duo_groups)

Field Description
group_id Duo group ID
name Group name
description Group description
member_count Number of members
status Group status

Source: Admin API GET /admin/v1/groups per account.

6. Integrations (duo_integrations)

Field Description
integration_key Duo integration key
name Integration name
type Integration type (e.g., websdk, rdgateway)
enabled Whether the integration is active
notes Admin notes

Source: Admin API GET /admin/v1/integrations per account.


Company Matching

After syncing accounts, the service automatically matches Duo account names to Autotask companies:

  1. Exact matchduo_accounts.name = companies.company_name
  2. Case-insensitive containment — company name contains account name or vice versa
  3. No match — left unlinked (can be manually linked later)

Current match rate: 30 of 32 child accounts (94%).


Sync Frequency

The sync is currently on-demand only — triggered via API call or the UI "Sync Now" button.

To schedule automatic syncing, add an entry to sync_schedules:

INSERT INTO sync_schedules (id, sync_type, cron_expression, enabled, description)
VALUES ('duo-sync', 'duo', '0 3 * * *', true, 'Duo Security full sync daily at 3am');

A full sync takes approximately 75 seconds (sequential processing across 33 accounts to respect Duo rate limits).


Sync Process

  1. List child accounts via Accounts API
  2. For each child account (sequentially, to respect rate limits):
    • Sync users → upsert into duo_users
    • Sync phones → upsert into duo_phones
    • Sync groups → upsert into duo_groups
    • Sync integrations → upsert into duo_integrations
    • Sync auth logs (incremental) → insert new logs into duo_auth_logs
  3. Sync parent account (same as child but uses parent host directly)
  4. Run company matching — link Duo accounts to Autotask companies
  5. Update account stats — refresh user_count and integration_count

Rate Limiting

  • The client handles HTTP 429 responses automatically
  • Reads the Retry-After header and waits before retrying
  • Child accounts are processed sequentially (not in parallel) to avoid hitting rate limits

Incremental Auth Logs

  • On first sync: pulls last 30 days of auth logs
  • On subsequent syncs: uses MAX(timestamp) from existing logs as mintime
  • Auth logs v2 uses a different pagination model (next_offset array) which is handled separately from standard v1 pagination

API Endpoints

Internal (session-authenticated via middleware bypass)

Method Endpoint Description
POST /api/duo/sync Trigger a full sync (non-blocking)
GET /api/duo/sync Check sync status (inProgress, currentSyncId)
GET /api/duo/status Connection status + record counts (accounts, users, phones, auth logs, groups, integrations, bypass, disabled)
GET /api/duo/accounts List all accounts with stats and matched Autotask company
GET /api/duo/accounts/[id]/users List users for a specific account
GET /api/duo/users/flagged List bypass and disabled users separately with account names

OpenClaw (external, API key auth via x-openclaw-key header)

Method Endpoint Description
POST /api/openclaw/sync/duo Trigger full sync from external agent

UI Elements

Sync Overview Card (/admin/sync)

The Duo card appears on the integrations overview page with:

  • Category: 2FA / MFA
  • Color: Green theme
  • Stats shown: Accounts / Users, Phones, Auth logs
  • Status icon:
    • Green checkmark — all clear
    • Red alert — bypass users detected (MFA not enforced)
    • Gray clock — not yet synced

Duo Detail Page (/admin/sync/duo)

Full management page with:

  • Header — Duo logo, title, "Sync Now" button with progress polling
  • Stat cards (6) — Accounts, Users, Phones, Auth Logs, Groups, Integrations
  • Last sync timestamp
  • Bypass Users panel (expandable, red) — Clickable warning banner shows users with status = 'bypass'. These users can authenticate without MFA — this is a security risk. Table shows: user, email, account, enrolled status, last login, notes.
  • Disabled Users panel (expandable, muted gray) — Clickable info banner shows users with status = 'disabled'. These users are locked out and cannot authenticate — not a security concern. Same table columns.
  • Parent Account section — Name, user count, integrations, last sync
  • Child Accounts table — Sortable list with name, user count, integrations, matched Autotask company (with checkmark), last sync time

Environment Variables

Variable Description
DUOACCOUNTS_INTEGRATION_KEY Parent Accounts API integration key
DUOACCOUNTS_SECRET_KEY Parent Accounts API secret key
DUOACCOUNTS_API_HOSTNAME Parent Accounts API hostname
DUOADMIN_INTEGRATION_KEY Parent Admin API integration key (used for parent account only)
DUOADMIN_SECRET_KEY Parent Admin API secret key
DUOADMIN_API_HOSTNAME Parent Admin API hostname

Important: The Accounts API credentials (not Admin API) are used for all child account data access. The client signs requests against each child's api_hostname and passes account_id as a parameter.


Files

File Purpose
lib/services/duo-client.ts API client — HMAC-SHA1 signing, GET/POST, pagination, rate-limit handling
lib/services/duo-sync-service.ts Sync orchestration — full sync, per-account data sync, company matching
migrations/058_create_duo_tables.sql Database schema — 6 tables with indexes and foreign keys
app/api/duo/sync/route.ts Sync trigger + status endpoint
app/api/duo/status/route.ts Connection test + record counts
app/api/duo/accounts/route.ts Account listing endpoint
app/api/duo/accounts/[id]/users/route.ts Per-account user listing
app/api/duo/users/flagged/route.ts Bypass + disabled user listing
app/api/openclaw/sync/duo/route.ts External sync trigger (OpenClaw)
app/admin/sync/duo/page.tsx Admin detail page
app/admin/sync/page.tsx Overview card (Duo entry)

Current Data Volume

Table Records
duo_accounts 33 (32 children + 1 parent)
duo_users 832
duo_phones 925
duo_auth_logs ~5,900+ (growing with each sync)
duo_groups 46
duo_integrations 78