Skip to main content

Duo Setup for dfe-fetcher

What a Cisco Duo administrator needs to configure so dfe-fetcher can read authentication logs from a Duo tenant via the Admin API.

Overview​

dfe-fetcher reads Duo authentication events from the Duo Admin API endpoint /admin/v2/logs/authentication. Duo does NOT use a bearer token: every request is signed with Duo's own scheme. The fetcher computes a signature over a canonical request string and sends it in a Basic auth header, with a fresh Date header per request (no token is cached).

The default is signature version 5, the scheme Duo documents and the only one some newer Admin API endpoints accept:

canonical = HTTP_DATE \n METHOD \n LOWER(HOST) \n PATH \n SORTED_QUERY
\n hex(SHA-512(body)) \n hex(SHA-512(signed x-duo- headers))
sig = hex(HMAC-SHA512(skey, canonical))
Auth = Basic base64(ikey:sig)

A GET carries no body and the fetcher sends no x-duo- headers, so both hash lines are the SHA-512 of the empty string.

Signature version 2 is Duo's legacy scheme: the first five lines alone, HMAC-SHA1. A tenant whose endpoints still verify it selects it with signature_version: v2, on the type or on one connection, as below. Leave it unset otherwise.

SHA-1 is kept out of a security purpose everywhere else in the platform, so version 2 is a deliberate exemption rather than a setting like any other. A connection that binds on it logs a warning at startup naming the connection; that warning is the signal to move the tenant to version 5 once its endpoints verify it, and it is expected to stay until then.

The admin provisions an Admin API application in the Duo Admin Panel, grants it log-read permission, and hands dfe-fetcher three values: the integration key (ikey), secret key (skey), and API hostname.

The v2 endpoint takes mintime/maxtime as millisecond Unix timestamps, returns up to limit records (default 100, max 1000), and paginates with metadata.next_offset, the two-element list the API documents, sent back comma-joined until it is null. Duo enforces a deliberate two-minute delay: authentications less than two minutes old are not yet returned. The profile's window.delay: 2m ends every window two minutes before now, so the cursor never passes events Duo has not yet published.

The source is the shipped duo profile (crates/fetcher/profiles/duo.yaml) on the signature auth mode; the sources.duo block below maps onto an instance of it at load. A 429 or 5xx is retried with backoff (honouring Retry-After), a 401 or 403 ends the tick, a 2xx carrying stat: FAIL fails the tick with Duo's message, and a tick that fails does not advance the fetch window. A missing host or key is refused at load, naming sources.duo.

Prerequisites​

  • A Duo subscription whose edition includes Admin API access.
  • A Duo administrator with the Owner (or equivalent) role - required to create applications and grant Admin API permissions.
  • Access to the Duo Admin Panel for your tenant (https://admin.duosecurity.com).

Required Permissions​

ServiceObject/EndpointPermission or ScopeNotes
Authentication logsGET /admin/v2/logs/authenticationGrant read logRead-only. The only permission dfe-fetcher needs.
Health probeGET /admin/v1/check(covered by the same application keys)Cheap auth check; no extra permission.

Grant only "Grant read log". Leave all other Admin API permission toggles unchecked (least privilege).

Source-Side Setup​

  1. Sign in to the Duo Admin Panel at https://admin.duosecurity.com as an administrator with rights to create applications.

  2. Start a new Admin API application. Either path works depending on your panel version:

    • Newer panel: Applications > Application Catalog, find Admin API, click + Add.
    • Older panel: Applications > Protect an Application, search for Admin API, then click Protect next to it.
  3. Grant the log-read permission. On the new application's page, in the Permissions section, tick Grant read log. Untick everything else. Click Save Changes.

  4. Copy the credentials. In the application's Details section, copy:

    • Integration key (the ikey)
    • Secret key (the skey) - treat as a password; shown here so capture it now
    • API hostname (the api_host, format api-XXXXXXXX.duosecurity.com)
  5. (Optional) Name the application something recognisable, e.g. dfe-fetcher-authlogs, and Save Changes again.

dfe-fetcher Configuration​

The Duo source config fields are: enabled, api_host, integration_key (ikey), secret_key (skey), credential_secret, signature_version, api_url_override (test mock only), services, topic. The only service is authentication_logs.

Config File​

sources:
duo:
enabled: true
api_host: "api-XXXXXXXX.duosecurity.com" # API hostname from Admin Panel
integration_key: "your-ikey"
secret_key: "your-skey"
services:
- name: authentication_logs
# config:
# limit: 100 # per-page size, max 1000
topic: "duo"
# filter: 'result == "FRAUD"' # hot-reloaded CEL expression

Environment Variables​

DFE_FETCHER_SOURCES__DUO__ENABLED="true"
DFE_FETCHER_SOURCES__DUO__API_HOST="api-XXXXXXXX.duosecurity.com"
DFE_FETCHER_SOURCES__DUO__INTEGRATION_KEY="your-ikey"
DFE_FETCHER_SOURCES__DUO__SECRET_KEY="your-skey"

Secrets Manager (Production)​

Keep the secret key out of the config file with credential_secret:

sources:
duo:
enabled: true
api_host: "api-XXXXXXXX.duosecurity.com"
integration_key: "your-ikey"
credential_secret: "vault:kv/data/dfe/duo:skey"
services:
- name: authentication_logs
topic: "duo"

credential_secret resolves to the secret key (skey). When set, it takes precedence over an inline secret_key. The ikey and api_host are not secret and can stay inline.

An Older Tenant​

Leave signature_version unset and the fetcher signs the version Duo documents. A tenant whose endpoints refuse it verifies the legacy scheme instead, which is selected per type or per connection:

sources:
duo:
enabled: true
api_host: "api-XXXXXXXX.duosecurity.com"
integration_key: "your-ikey"
credential_secret: "vault:kv/data/dfe/duo:skey"
signature_version: v2
services:
- name: authentication_logs
topic: "duo"

A signature the tenant does not verify comes back as a 401 whose message names the credential, and the tick fails without advancing the fetch window. A connection on v2 warns at startup, because SHA-1 signing is an exemption from the platform's crypto baseline taken for this tenant alone.

Verification​

Health check. The profile's probe signs and calls GET /admin/v1/check, which returns {"response": "valid", "stat": "OK"} for working credentials; a stat other than OK is a health error. A healthy result confirms the ikey, skey, and api_host are correct and the application is active.

Live smoke tests. crates/fetcher/tests/e2e/smoke_remote.rs has env-gated, #[ignore]d tests that hit a real tenant. They read these variables (canonical .env-cloud, fallback .env):

  • DUO_API_HOST
  • DUO_INTEGRATION_KEY
  • DUO_SECRET_KEY

Run them:

cargo test -p dfe-fetcher --test e2e duo_ -- --ignored

Results are tagged duo.authentication_logs. Zero records is normal on a quiet tenant, a short lookback, or if all events fall inside the two-minute delay window.

Common failures.

  • 40103 Invalid signature / stat != OK - the skey is wrong, clock skew between the host and Duo broke the signed Date header, or the tenant verifies the other signature version. Verify the skey, check NTP is in sync, and try signature_version: v2 on an older tenant.
  • 40101 Missing request credentials - the ikey or api_host is wrong.
  • 40301 Access denied - the Admin API application lacks Grant read log; re-check the Permissions section and save.
  • Health passes but zero records every cycle - widen the window or confirm authentications exist; remember events under two minutes old are withheld.

Cost​

dfe-fetcher only reads. The Admin API and authentication-log reads add no charge of their own, but access may require a Duo plan/edition that includes Admin API access. Confirm any cost implications against your own Duo plan.

References​