Skip to content

scheiblingco/acmeproxy.go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

acmeproxy.go

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's httpreq provider, acme.sh (dns_acmeproxy), Traefik, and Caddy;

  • the acme-dns interface (/update) - spoken by acme-dns clients such as cert-manager, lego's acmedns provider, 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)

How it works

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
Loading
  1. Your ACME client asks the proxy to present a validation token, using either protocol.
  2. 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.
  3. 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.

Features

  • 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

Quick start

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.com

Accounts 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.yaml

Terminate TLS in-process (server.tls) or behind a reverse proxy - clients will be sending credentials, so don't run it plaintext beyond localhost.

Pointing clients at it

lego (httpreq)

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 run

Traefik (httpreq) and Caddy (acmeproxy) use the same three settings.

acme.sh

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_acmeproxy

acme-dns clients

Registration 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 run

cert-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.

Configuration

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.

Comparison

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).

License

See LICENSE.

About

Go reimplementation of acmeproxy/acmeproxy.pl

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages