docker-duoauthproxy
Containerized Duo Authentication Proxy with multi-arch builds, Trivy scanning and SBOM attestation.
What it solves
Duo Authentication Proxy is a service by Duo Security (Cisco) that acts as an intermediary between your local authentication infrastructure (Active Directory, RADIUS) and the Duo cloud service. It enables adding two-factor authentication (2FA) to existing VPN, SSH or web applications — without modifying the application itself.
However, the official Duo Authentication Proxy has no container distribution. Installation requires building from source, manual INI file configuration, and managing the service on the host system. This Docker image simplifies everything down to a single docker run command.
The image was designed for Docker Swarm mode — full Docker Secrets support, health checks and the ability to scale replicas without configuration changes.
Authentication flow
Typical deployment in a network with FortiGate, FreeRADIUS and MariaDB:
- Client connects to FortiGate VPN with username and password
- FortiGate sends a RADIUS request to Duo Auth Proxy
- Duo Auth Proxy verifies the password against FreeRADIUS (which authenticates via MariaDB)
- After successful password verification, requests 2FA via Duo Cloud (push notification, SMS, OTP)
- After 2FA approval, returns RADIUS Accept back to FortiGate
Quick start
docker run -d \
--name duoauthproxy \
-p 1812:1812/udp \
-e RADIUS_HOST=10.10.10.2 \
-e RADIUS_SECRET=radiussecret \
-e DUO_IKEY=DIXXXXXXXXXXXXXXXXXX \
-e DUO_SKEY=YourSecretKeyHere \
-e DUO_API_HOST=api-XXXXXXXX.duosecurity.com \
-e RADIUS_CLIENT_IP_1=192.168.1.10 \
-e RADIUS_CLIENT_SECRET_1=clientsecret \
ghcr.io/ict-solutions-dev/duoauthproxy:1.2.0-duo6.6.0
The entire configuration is done through environment variables — no manual editing of config files. The entrypoint script at startup:
- Reads variables (including
_FILEvariants for Docker secrets) - Validates inputs — checks required variables, sequential host ordering, allowed values
- Generates
authproxy.cfgfrom variables - Tests connectivity to Duo API
- Starts the proxy daemon
Configuration via environment variables
Instead of a classic INI file, the entire configuration is ENV-first:
| Variable | Description |
|---|---|
RADIUS_HOST | RADIUS server hostname or IP |
RADIUS_SECRET | RADIUS server shared secret |
DUO_IKEY | Duo integration key |
DUO_SKEY | Duo secret key |
DUO_API_HOST | Duo API hostname |
RADIUS_CLIENT_IP_1 | First RADIUS client IP address |
RADIUS_CLIENT_SECRET_1 | First RADIUS client shared secret |
Supports up to 6 RADIUS servers and 6 clients via numbered variables (RADIUS_HOST_2, RADIUS_CLIENT_IP_3, …).
Optional variables like RADIUS_FAILMODE (safe/secure), RADIUS_PASS_THROUGH_ATTRS or RADIUS_CLIENT_TYPE (radius_client, ad_client, duo_only_client) enable advanced configuration without touching config files.
Security
Security was a priority from the first commit:
- Non-root container — runs as user
duo(UID/GID 35505), not root - Docker Secrets support — every sensitive variable supports the
_FILEsuffix. Instead of-e RADIUS_SECRET=password, use-e RADIUS_SECRET_FILE=/run/secrets/radius_secretand the secret is read from a file. If both variants are set, the container refuses to start - Log redaction — the entrypoint script masks all sensitive values in startup logs
- Trivy vulnerability scanning — every build is scanned for known vulnerabilities, results uploaded to GitHub Security tab
- SBOM generation — each build produces a Software Bill of Materials in SPDX format via Anchore
- Build provenance attestation — GitHub Attestations verify the image was actually built in the CI pipeline
# Docker Compose with Docker Secrets
services:
duoauthproxy:
image: ghcr.io/ict-solutions-dev/duoauthproxy:1.2.0-duo6.6.0
restart: unless-stopped
ports:
- "1812:1812/udp"
environment:
RADIUS_HOST: 10.10.10.2
RADIUS_SECRET_FILE: /run/secrets/radius_secret
DUO_IKEY_FILE: /run/secrets/duo_ikey
DUO_SKEY_FILE: /run/secrets/duo_skey
DUO_API_HOST: api-XXXXXXXX.duosecurity.com
RADIUS_CLIENT_IP_1: 192.168.1.10
RADIUS_CLIENT_SECRET_FILE: /run/secrets/radius_client_secret
secrets:
- radius_secret
- duo_ikey
- duo_skey
- radius_client_secret
secrets:
radius_secret:
file: ./secrets/radius_secret.txt
duo_ikey:
file: ./secrets/duo_ikey.txt
duo_skey:
file: ./secrets/duo_skey.txt
radius_client_secret:
file: ./secrets/radius_client_secret.txt
CI/CD pipeline
The GitHub Actions workflow runs on push to develop, tag creation (v*), and manual dispatch. Pipeline:
- Lint — Dockerfile checked via hadolint
- Build — multi-arch image (
linux/amd64+linux/arm64) via Docker Buildx - Push — published to GitHub Container Registry
- Scan — Trivy scans for vulnerabilities
- SBOM — generated in SPDX format
- Attestation — build provenance via GitHub Attestations
Versioning
The image tag contains both versions — the project and the upstream Duo:
- Major bump — breaking changes (renamed env vars, new entrypoint)
- Minor bump — new features (new env vars, RadSec support)
- Patch bump — bugfixes, Duo version bump, base image update
Documentation and support
Complete documentation including all environment variables, examples and advanced configuration is available in the README on GitHub. If you run into an issue or have a question, feel free to open an issue or contact me directly.