284 lines
12 KiB
Markdown
284 lines
12 KiB
Markdown
|
|
# 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 match** — `duo_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`:
|
||
|
|
|
||
|
|
```sql
|
||
|
|
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 |
|