A minimal, credential-scoping HTTP API for ACME DNS-01 challenges, written in Go (Gin).
Instead of handing every server in your fleet full DNS provider credentials, you run one acmeproxy.go instance that holds them. ACME clients get a narrowly scoped account that can only set the DNS-01 validation token for the domains they are allowed to. The proxy fulfills those requests through go-acme/lego's DNS provider packages, so it supports the same ~200 DNS providers lego does.
It is a spiritual successor to acmeproxy.pl and understands two client protocols:
-
the acmeproxy interface (
/present,/cleanup) - spoken by lego'shttpreqprovider, acme.sh (dns_acmeproxy), Traefik, and Caddy; -
the acme-dns interface (
/update) - spoken by acme-dns clients such as cert-manager, lego'sacmednsprovider, and certbot hooks.
Unlike acmeproxy.pl there is no acme.sh shell-out, and unlike acme-dns there is no built-in DNS server: every record is written to your real DNS zone through a lego provider. The proxy keeps nothing on disk* - configuration lives in a single YAML (or JSON) file, and runtime state lives in memory, Valkey, or PostgreSQL.
(* Unless you configure valkey to do so)
flowchart LR
C[ACME client<br/>lego, Traefik, cert-manager, ...] -->|scoped credentials| P[acmeproxy.go]
P -->|provider API| D[(DNS zone)]
CA[Certificate authority] -->|DNS-01 lookup| D
- Your ACME client asks the proxy to present a validation token, using either protocol.
- The proxy authenticates the account (Basic auth or
X-Api-User/X-Api-Key), checks the source-IP allowlist, and verifies the target domain is within the account's grant. - It writes the TXT record at
_acme-challenge.<domain>via the configured lego provider, and the CA validates as usual.
CNAME delegation is supported. If _acme-challenge.domainA.com is a CNAME to
acme.domainB.com, the proxy (via lego's built-in CNAME following) writes the token at the
target - so it only ever needs provider credentials for a dedicated challenge zone, and
your production zones stay untouchable. This gives you acme-dns's delegation security model
without running DNS infrastructure.
- Two wire-compatible client interfaces: acmeproxy.pl-style and acme-dns-style
- All ~200 lego DNS providers, configured per domain suffix - multiple providers (or multiple accounts at one provider) side by side
- Universal accounts: every account works with both interfaces, defined only in the config file - no open registration, no account database
- Per-account domain scoping and CIDR source allowlists; bcrypt-hashed secrets
- Automatic cleanup: a reaper removes challenge records that acme-dns-style clients leave behind
- State in memory, Valkey, or PostgreSQL - never on disk; stateless replicas for HA
- Single YAML/JSON configuration file
Create config.yaml:
server:
listen: ":9090"
storage:
backend: memory
providers:
- name: cf
type: cloudflare
domains:
- example.com
settings:
CLOUDFLARE_DNS_API_TOKEN: "${CF_TOKEN}"
accounts:
- username: web01
password: "change-me"
domains:
- int.example.comAccounts are universal: web01 can authenticate with Basic auth on the acmeproxy
interface and with X-Api-User/X-Api-Key on the acme-dns interface, with the same
domain grant on both.
Run it:
acmeproxy -config config.yaml
# or
docker run -v ./config.yaml:/config.yaml -e CF_TOKEN=... -p 9090:9090 \
ghcr.io/scheibling/acmeproxy.go -config /config.yamlTerminate TLS in-process (server.tls) or behind a reverse proxy - clients will be sending
credentials, so don't run it plaintext beyond localhost.
HTTPREQ_ENDPOINT=https://acmeproxy.int.example.com:9090 \
HTTPREQ_USERNAME=web01 \
HTTPREQ_PASSWORD=change-me \
lego --dns httpreq --domains app.int.example.com --email you@example.com runTraefik (httpreq) and Caddy (acmeproxy) use the same three settings.
export ACMEPROXY_ENDPOINT="https://acmeproxy.int.example.com:9090"
export ACMEPROXY_USERNAME="web01"
export ACMEPROXY_PASSWORD="change-me"
acme.sh --issue -d app.int.example.com --dns dns_acmeproxyRegistration is disabled by design - accounts come from the proxy's config, and any
account works here. Give the client the account credentials in the shape it expects, with
subdomain set to the domain being validated. For lego's acmedns provider that's a
pre-filled storage file:
{
"app.int.example.com": {
"username": "web01",
"password": "change-me",
"subdomain": "app.int.example.com",
"fulldomain": "_acme-challenge.app.int.example.com"
}
}ACME_DNS_API_BASE=https://acmeproxy.int.example.com:9090 \
ACME_DNS_STORAGE_PATH=./acme-dns-accounts.json \
lego --dns acmedns --domains app.int.example.com --email you@example.com runcert-manager's acme-dns solver and certbot acme-dns hooks work the same way: point them at the proxy's base URL with pre-provisioned credentials.
The full annotated schema lives in TECHNICAL_OVERVIEW.md §9. At a glance:
| Section | Purpose |
|---|---|
server |
Listen address, optional in-process TLS, trusted proxies for real-IP handling |
logging |
Level and format (text/json) |
storage |
memory, valkey, or postgres + connection settings |
reaper |
Interval and max age for removing abandoned challenge records |
providers |
lego provider name, its settings (lego env-var keys), and the domain suffixes it serves |
accounts |
Universal accounts, valid on both interfaces: username, password or bcrypt hash, granted domains, CIDR allowlist |
${VAR} references in the file are interpolated from the environment, so secrets can stay
out of the file itself.
| acmeproxy.pl | acme-dns | acmeproxy.go | |
|---|---|---|---|
| Client protocol | httpreq | acme-dns | both |
| Record fulfillment | acme.sh scripts | own DNS server (port 53) | lego providers |
| Account creation | config file | open /register + database |
config file only |
| CNAME delegation | - | core model | supported via lego |
| Cleanup of stale records | on /cleanup |
never | /cleanup + automatic reaper |
| State on disk | acme.sh data | SQLite by default | none |
See TECHNICAL_OVERVIEW.md for architecture, the exact wire formats, and how the lego integration works (including the one small patch that lets lego providers accept a precomputed challenge token).
See LICENSE.