Duo Admin API Integration

Configure HTTP Pull to retrieve security logs and authentication events from Duo Admin API with HMAC-SHA1 authentication, pagination support, and comprehensive monitoring capabilities.

Overview

Duo Security’s Admin API provides programmatic access to authentication logs, administrator actions, telephony records, and security events from your Duo-protected infrastructure. The Edge Delta HTTP Pull source integrates seamlessly with all five Duo Admin API log endpoints, enabling real-time security monitoring and compliance reporting through your observability platform.

Key Benefits

  • Security Monitoring - Track multi-factor authentication events across your organization
  • Compliance Auditing - Maintain complete audit trails of administrative actions
  • Cost Analysis - Monitor telephony usage and credits for voice/SMS authentications
  • Threat Detection - Identify anomalous authentication patterns and potential breaches
  • Activity Tracking - Comprehensive visibility into user and admin activities

Quick Reference - Duo Log Types

Log Type API Version Timestamp Format Pull Interval Primary Use Case
Authentication v2 Milliseconds 5m MFA events, login tracking
Administrator v1 Seconds 6m Admin actions, config changes
Telephony v2 Milliseconds 7m Voice/SMS usage, costs
Activity v2 Milliseconds 8m User activity patterns
Offline Enrollment v1 Seconds 10m Hardware token enrollment

Authentication

Duo Admin API requires HMAC-SHA1 signature authentication for all requests. Unlike simple token-based authentication, Duo’s approach requires constructing a canonical request string and generating a cryptographic signature.

Prerequisites

  1. Create a Duo Admin API application in your Duo Admin Panel
  2. Note your Integration Key (starts with DI)
  3. Securely store your Secret Key
  4. Record your API hostname (e.g., api-xxxxxxxx.duosecurity.com)

Environment Variables

Configure these environment variables before deploying your Edge Delta pipeline:

# Duo API Credentials
export DUO_IKEY="DIXXXXXXXXXXXXXXXXXX"       # Your Integration Key
export DUO_SKEY="your_secret_key_here"       # Your Secret Key (keep secure!)
export DUO_API_HOST="api-xxxxxxxx.duosecurity.com"  # Your API hostname

# Optional: Customize pull intervals
export DUO_AUTH_PULL_INTERVAL="5m"           # Authentication logs interval
export DUO_ADMIN_PULL_INTERVAL="6m"          # Administrator logs interval
export DUO_TELEPHONY_PULL_INTERVAL="7m"      # Telephony logs interval
export DUO_ACTIVITY_PULL_INTERVAL="8m"       # Activity logs interval
export DUO_OFFLINE_PULL_INTERVAL="10m"       # Offline enrollment interval

HMAC-SHA1 Signature Construction

The Duo API authentication process requires building a canonical request string and signing it with your secret key.

Understanding the HMAC Components:

  1. Date Header: RFC 2822 formatted timestamp

    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    
  2. Canonical String: Concatenate with newlines

    date\nmethod\nhost\npath\nparams
    
  3. HMAC Signature: Sign with secret key using SHA1

    EDXHmac(canonical_string, secret_key, "sha1", "hex")
    
  4. Authorization Header: Basic auth with ikey:signature

    Authorization: Concat(["Basic ", EDXEncode(Concat([ikey, ":", signature], ""), "base64", false)], "")
    

Authentication Logs

Monitor multi-factor authentication events including success/failure rates, authentication methods, and device information.

Duo API Reference: Authentication Logs Endpoint

nodes:
- name: duo_authentication_logs
  type: http_pull_input
  endpoint: https://api-xxxxxxxx.duosecurity.com/admin/v2/logs/authentication
  method: GET
  pull_interval: 5m

  header_expressions:
    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    Accept: '"application/json"'
    Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v2/logs/authentication", Concat(["limit=1000&mintime=", String(Int(UnixSeconds(Now()) - 360) * 1000), "&sort=ts%3Adesc"], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

  parameter_expressions:
    limit: '"1000"'
    sort: '"ts:desc"'
    mintime: String(Int(UnixSeconds(Now()) - 360) * 1000)

Key Fields in Response:

  • user: Username and user key
  • factor: Authentication method (push, sms, phone, hardware_token)
  • result: Success, failure, or denied
  • application: Protected application name
  • access_device: Browser and IP information
  • auth_device: Authentication device details

Administrator Logs

Track administrative actions and configuration changes within your Duo environment.

Duo API Reference: Administrator Logs Endpoint

nodes:
- name: duo_administrator_logs
  type: http_pull_input
  endpoint: https://api-xxxxxxxx.duosecurity.com/admin/v1/logs/administrator
  method: GET
  pull_interval: 6m

  header_expressions:
    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    Accept: '"application/json"'
    Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v1/logs/administrator", Concat(["mintime=", String(Int(UnixSeconds(Now()) - 420))], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

  parameter_expressions:
    mintime: String(Int(UnixSeconds(Now()) - 420))

Note: v1 API uses seconds, not milliseconds for timestamps.

Key Fields in Response:

  • username: Administrator who performed the action
  • action: Type of administrative action
  • object: Target of the action
  • description: Detailed description of the change

Telephony Logs

Monitor voice call and SMS authentication events, including credit usage.

Duo API Reference: Telephony Logs Endpoint

nodes:
- name: duo_telephony_logs
  type: http_pull_input
  endpoint: https://api-xxxxxxxx.duosecurity.com/admin/v2/logs/telephony
  method: GET
  pull_interval: 7m

  header_expressions:
    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    Accept: '"application/json"'
    Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v2/logs/telephony", Concat(["limit=1000&mintime=", String(Int(UnixSeconds(Now()) - 480) * 1000), "&maxtime=", String(Int(UnixSeconds(Now())) * 1000), "&sort=ts%3Adesc"], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

  parameter_expressions:
    limit: '"1000"'
    sort: '"ts:desc"'
    mintime: String(Int(UnixSeconds(Now()) - 480) * 1000)
    maxtime: String(Int(UnixSeconds(Now())) * 1000)

Key Fields in Response:

  • type: Call or SMS
  • phone: Phone number used
  • credits: Credits consumed for the call/SMS
  • context: Authentication context

Activity Logs

Track user and administrator activity within Duo-protected applications.

Duo API Reference: Activity Logs Endpoint

nodes:
- name: duo_activity_logs
  type: http_pull_input
  endpoint: https://api-xxxxxxxx.duosecurity.com/admin/v2/logs/activity
  method: GET
  pull_interval: 8m

  header_expressions:
    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    Accept: '"application/json"'
    Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v2/logs/activity", Concat(["limit=1000&mintime=", String(Int(UnixSeconds(Now()) - 540) * 1000), "&maxtime=", String(Int(UnixSeconds(Now())) * 1000), "&sort=ts%3Adesc"], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

  parameter_expressions:
    limit: '"1000"'
    sort: '"ts:desc"'
    mintime: String(Int(UnixSeconds(Now()) - 540) * 1000)
    maxtime: String(Int(UnixSeconds(Now())) * 1000)

Key Fields in Response:

  • action: Activity type
  • actor: Entity performing the action
  • target: Entity being acted upon
  • context: Additional context information

Offline Enrollment Logs

Monitor offline enrollment activities for hardware tokens and mobile authenticators.

Duo API Reference: Offline Enrollment Logs Endpoint

nodes:
- name: duo_offline_enrollment_logs
  type: http_pull_input
  endpoint: https://api-xxxxxxxx.duosecurity.com/admin/v1/logs/offline_enrollment
  method: GET
  pull_interval: 10m

  header_expressions:
    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    Accept: '"application/json"'
    Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v1/logs/offline_enrollment", Concat(["mintime=", String(Int(UnixSeconds(Now()) - 720))], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

  parameter_expressions:
    mintime: String(Int(UnixSeconds(Now()) - 720))

Note: v1 API uses seconds for timestamps.

Key Fields in Response:

  • user: User being enrolled
  • action: Enrollment action type
  • description: Enrollment details

Pagination

Duo Admin API uses offset-based pagination for large result sets. The API returns metadata with pagination information:

{
  "metadata": {
    "next_offset": 1000,
    "prev_offset": 0,
    "total_objects": 5432
  },
  "response": [...]
}

To handle pagination in Edge Delta:

pagination:
  url_json_path: "$['metadata']['next_offset']"
  max_parallel_requests: 3

# Include offset in parameters if continuing from previous page
parameter_expressions:
  limit: '"1000"'
  offset: "0"  # Start from 0, use next_offset for subsequent pages

Best Practices:

  • Use limit=1000 for optimal performance
  • Process pages sequentially to maintain order
  • Monitor total_objects to track data volume
  • Implement retry logic for partial page failures

Rate Limiting

Duo enforces rate limits to ensure API stability. Follow these guidelines to avoid throttling:

Log Type Pull Interval Lookback Window Why This Interval?
Authentication 5 minutes 6 minutes High-priority security events need timely detection
Administrator 6 minutes 7 minutes Less frequent but critical configuration changes
Telephony 7 minutes 8 minutes Balance between cost tracking and API load
Activity 8 minutes 9 minutes Lower priority general activity monitoring
Offline Enrollment 10 minutes 12 minutes Infrequent enrollment events

Critical Rate Limit Guidelines

Limit Type Value Impact Recommendation
Hard Limit 50 calls/min 429 errors Never exceed for bulk operations
Soft Limit 1 call/min/endpoint Throttling risk Stay well below this
Log Delay 2 minutes Missing recent data Account for in lookback window
Retry Backoff 30s → 5m Recovery time Implement exponential backoff

Rate Limit Configuration Example

# Implement exponential backoff on rate limit errors
retry_http_code: [429, 503]
retry_max_attempts: 3
retry_initial_interval: 30s
retry_max_interval: 5m

Troubleshooting

401 Unauthorized: Invalid Signature

Error: {"code": 40103, "message": "Invalid signature in request credentials"}

Common Causes & Solutions:

  1. Incorrect Date Format

    • ✅ Correct: Mon, 02 Jan 2006 15:04:05 -0700
    • ❌ Wrong: 2006-01-02T15:04:05Z
    • Ensure RFC 2822 format exactly
  2. Secret Key Issues

    • Verify DUO_SKEY environment variable is set correctly
    • Check for extra spaces or newlines in the secret key
    • Regenerate keys if compromised
  3. Canonical String Mismatch

    • Ensure query parameters are URL-encoded (:%3A)
    • Verify host matches exactly (include subdomain)
    • Check path starts with /
  4. Clock Skew

    • Synchronize system time with NTP
    • Maximum allowed drift is 5 minutes

429 Too Many Requests

Error: {"code": 42901, "message": "Too Many Requests"}

Solutions:

  1. Increase Pull Intervals

    pull_interval: 10m  # Increase from 5m
    
  2. Implement Staggered Starts

    • Don’t start all pulls simultaneously
    • Use different intervals for each log type
  3. Add Retry Logic

    retry_http_code: [429]
    retry_max_attempts: 3
    retry_initial_interval: 60s
    
  4. Monitor API Usage

    • Check Duo Admin Panel for API usage metrics
    • Set up alerts for rate limit approaches

Timestamp Format Mismatches

Error: {"code": 40002, "message": "Invalid request parameters", "message_detail": "mintime"}

Common Mistakes:

  1. v1 vs v2 API Confusion

    • v1 endpoints (Administrator, Offline): Use seconds
      mintime: String(Int(UnixSeconds(Now()) - 420))
      
    • v2 endpoints (Authentication, Telephony, Activity): Use milliseconds
      mintime: String(Int(UnixSeconds(Now()) - 360) * 1000)
      
  2. Missing Multiplication

    • ❌ Wrong: String(Int(UnixSeconds(Now()) - 360)) for v2
    • ✅ Right: String(Int(UnixSeconds(Now()) - 360) * 1000) for v2
  3. Parameter Availability

    • v1 endpoints: Only support mintime
    • v2 endpoints: Support both mintime and maxtime

Complete Pipeline Example

Here’s a production-ready configuration combining all five Duo log types with proper error handling.

version: v3

settings:
  tag: duo-security-logs
  log:
    level: info

nodes:
# Edge Delta system monitoring
- name: ed_system_stats
  type: ed_system_stats_input

# Authentication Logs - Primary security events
- name: duo_auth_logs
  type: http_pull_input
  endpoint: https://api-xxxxxxxx.duosecurity.com/admin/v2/logs/authentication
  method: GET
  pull_interval: 5m
  retry_http_code: [429, 503]
  retry_max_attempts: 3

  header_expressions:
    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    Accept: '"application/json"'
    Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v2/logs/authentication", Concat(["limit=1000&mintime=", String(Int(UnixSeconds(Now()) - 360) * 1000), "&sort=ts%3Adesc"], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

  parameter_expressions:
    limit: '"1000"'
    sort: '"ts:desc"'
    mintime: String(Int(UnixSeconds(Now()) - 360) * 1000)

# Administrator Logs - Admin actions
- name: duo_admin_logs
  type: http_pull_input
  endpoint: https://api-xxxxxxxx.duosecurity.com/admin/v1/logs/administrator
  method: GET
  pull_interval: 6m
  retry_http_code: [429, 503]

  header_expressions:
    Date: FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700")
    Accept: '"application/json"'
    Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v1/logs/administrator", Concat(["mintime=", String(Int(UnixSeconds(Now()) - 420))], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

  parameter_expressions:
    mintime: String(Int(UnixSeconds(Now()) - 420))

# Add Telephony, Activity, and Offline Enrollment with similar patterns...

# Processing pipeline
- name: duo_processor
  type: sequence

- name: duo_enrichment
  type: ottl
  statements: |
    // Add common fields
    set(attributes["source"], "duo")
    set(attributes["log_type"], attributes["_http_pull_source"])

    // Parse timestamps correctly based on API version
    set(attributes["timestamp"], Int(cache["ts"]) * 1000000) where cache["ts"] != nil
    set(attributes["timestamp"], Int(cache["timestamp"]) * 1000000000) where cache["timestamp"] != nil    

# Output destination
- name: edge_delta_output
  type: ed_output

# Links connecting the pipeline
links:
- from: ed_system_stats
  to: edge_delta_output
- from: duo_auth_logs
  to: duo_processor
- from: duo_admin_logs
  to: duo_processor
- from: duo_processor
  to: duo_enrichment
- from: duo_enrichment
  to: edge_delta_output

Security Best Practices

Credential Management

  1. Use Environment Variables

    • Store DUO_IKEY and DUO_SKEY in environment variables
    • Never hardcode credentials in configuration files
    • Use EDXEnv() function to reference credentials
  2. Rotate Keys Regularly

    • Follow your organization’s key rotation policy
    • Update both Edge Delta and Duo when rotating
    • Test new keys in a non-production environment first
  3. Limit API Permissions

    • Create dedicated API applications for Edge Delta
    • Grant minimum required permissions
    • Use read-only access for log retrieval
  4. Monitor API Usage

    • Set up alerts for unusual API activity
    • Review Duo Admin Panel for access patterns
    • Monitor for authentication failures in Edge Delta logs

Network Security

  1. Use HTTPS Exclusively

    • All Duo API calls must use HTTPS
    • Verify SSL certificates
    • Don’t disable certificate validation
  2. Implement IP Restrictions

    • Configure API access from specific IP ranges in Duo
    • Use Edge Delta’s static IP addresses if available
    • Monitor for access from unexpected locations

Copy-Paste Ready Authorization Headers

Authentication Logs (v2 - milliseconds)

Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v2/logs/authentication", Concat(["limit=1000&mintime=", String(Int(UnixSeconds(Now()) - 360) * 1000), "&sort=ts%3Adesc"], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

Administrator Logs (v1 - seconds)

Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v1/logs/administrator", Concat(["mintime=", String(Int(UnixSeconds(Now()) - 420))], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

Telephony Logs (v2 - milliseconds)

Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v2/logs/telephony", Concat(["limit=1000&mintime=", String(Int(UnixSeconds(Now()) - 480) * 1000), "&maxtime=", String(Int(UnixSeconds(Now())) * 1000), "&sort=ts%3Adesc"], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

Activity Logs (v2 - milliseconds)

Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v2/logs/activity", Concat(["limit=1000&mintime=", String(Int(UnixSeconds(Now()) - 540) * 1000), "&maxtime=", String(Int(UnixSeconds(Now())) * 1000), "&sort=ts%3Adesc"], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

Offline Enrollment Logs (v1 - seconds)

Authorization: Concat(["Basic ", EDXEncode(Concat([EDXEnv("DUO_IKEY", ""), ":", EDXHmac(Format("%s\n%s\n%s\n%s\n%s", [FormatTime(Now(), "Mon, 02 Jan 2006 15:04:05 -0700"), "GET", EDXEnv("DUO_API_HOST", ""), "/admin/v1/logs/offline_enrollment", Concat(["mintime=", String(Int(UnixSeconds(Now()) - 720))], "")]), EDXEnv("DUO_SKEY", ""), "sha1", "hex")], ""), "base64", false)], "")

Additional Resources

Support

For assistance with Duo API integration: