acme

package module
v0.0.0-...-a035c6c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 10, 2026 License: MIT Imports: 28 Imported by: 0

README

acme

Name

acme - enables dynamic DNS updates via HTTP API with authentication for ACME DNS-01 challenges.

Description

The acme plugin provides a dynamic DNS API server for handling ACME DNS-01 challenges. It's designed to be compatible with go-acme (Lego) which is used by Traefik and other popular ACME clients. This plugin only answers queries for _acme-challenge subdomains and provides a REST API for dynamically updating the corresponding TXT records, which are used by ACME clients to validate domain ownership for certificate issuance. All other DNS queries are passed to the next plugin in the chain.

Features

  • RESTful HTTP API: Simple REST API for managing TXT records
  • Flexible Authentication: Support for Basic Auth, API headers, and query parameters
  • IP-based Access Control: Restrict API access by IP address or CIDR ranges
  • Account Management: Create and manage accounts with domain restrictions
  • Multiple Storage Options: SQLite database with in-memory option (coming soon)
  • Go-ACME Compatibility: Works with Lego library used by Traefik and other tools
  • Proxy Support: Header-based client IP detection for reverse proxy setups
  • ACME-Subdomain Specific: Only answers _acme-challenge queries, passing all others to the next plugin
  • Selective Fallthrough: Configurable fallthrough behavior for ACME challenge domains
  • Wildcard Certificate Support: Easily manage wildcard certificates with DNS-01 challenges

Syntax

acme [ZONES...] {
    [endpoint ADDRESS]
    [db TYPE PATH]
    [extract_ip_from_header HEADER]
    [allowfrom [CIDR...]]
    [require_auth]
    [account USERNAME PASSWORD [ZONE] [CIDR...]]
    [enable_registration]
    [fallthrough [ZONES...]]
}
  • ZONES zones the acme plugin will be authoritative for. If empty, the zones from the server block are used.
  • endpoint specifies the ADDRESS for the API server. If not specified, the API server will not be started and the database will operate in read-only mode (useful when delegating a zone but still want to use the plugin for DNS-01 challenges).
  • db selects the database backend:
    • sqlite with a PATH to the database file (default: "acme.db" in the current directory).
    • badger with a PATH to the database directory.
    • memory for an in-memory database (coming soon).
  • extract_ip_from_header extracts the client IP address from the specified HTTP header instead of using the TCP remote address.
  • allowfrom lists IP addresses or CIDR ranges allowed to access the API globally.
  • require_auth requires authentication for API record updates. When enabled, username/password authentication is required for updating or deleting TXT records. When disabled (default), records can be updated without authentication, but global IP restrictions from allowfrom are still enforced if set.
  • account registers an account with:
    • USERNAME - User identifier for authentication
    • PASSWORD - Password for authentication
    • [ZONE] - Optional domain name zone the account is authorized to manage
    • [CIDR...] - Optional list of IP addresses or CIDR ranges allowed to access with this account
  • enable_registration allows new account registrations via the API.
  • fallthrough [ZONES...] routes queries to the next plugin when a request is for a TXT record of _acme-challenge subdomain, but no record is found. If specific ZONES are listed, fallthrough will only happen for those specific zones. Without this option, the plugin will respond with NXDOMAIN if no record is found.

Important Notes:

  • This plugin only answers queries for _acme-challenge subdomains - all other queries are passed to the next plugin
  • Always include the trailing dot (.) after domain names to ensure proper fully qualified domain names (FQDNs)
  • When no IP restrictions are specified for an account or globally, access will be allowed to all by default. Make sure to only expose the API to trusted networks in this case.

Examples

Basic configuration with default settings:

auth.example.org {
    acme {
        endpoint 0.0.0.0:8080
        fallthrough
    }
    forward . 8.8.8.8
}

DNS-only mode (no API server):

auth.example.org {
    acme {
        db badger /var/lib/coredns/acme.db
        fallthrough
    }
    forward . 8.8.8.8
}

Secure production setup with TLS and multiple accounts for different zones:

example.org {
    tls /etc/coredns/certs/cert.pem /etc/coredns/certs/key.pem

    acme subdomain.example.org {
        db sqlite /var/lib/coredns/acme.db
        endpoint 0.0.0.0:8443
        extract_ip_from_header X-Forwarded-For
        allowfrom 10.0.0.0/8 192.168.0.0/16
        require_auth
        account user1 strong-password1 one.subdomain.example.org
        account user2 strong-password2 two.subdomain.example.org 10.1.0.0/16 192.168.1.0/24
    }

    # Logging
    log {
        class error
    }

    # Forward regular DNS queries
    forward . 1.1.1.1 8.8.8.8 {
        policy random
        health_check 10s
    }
}

Build

This plugin can be compiled as part of CoreDNS by adding the following line to the plugin.cfg file:

acme:github.com/ShrewdHydra/coredns-acme

Then compile CoreDNS:

go generate
go build

Or using make:

make

API Usage

The plugin provides a RESTful API for managing DNS records for ACME DNS-01 challenges, compatible with the Lego httpreq provider used by Traefik and other tools.

Endpoints
Account Registration
POST /register

Request:

{
  "username": "username",
  "password": "password",
  "zone": "example.org"
}

Response:

{
  "username": "username",
  "password": "password",
  "zone": "example.org"
}
Present TXT Record
POST /present

Request:

{
  "fqdn": "_acme-challenge.example.org",
  "txt": "acme-challenge-value"
}

Response:

{
  "success": true
}
Cleanup TXT Record
POST /cleanup

Request:

{
  "fqdn": "_acme-challenge.example.org",
  "txt": "acme-challenge-value"
}

Response:

{
  "success": true
}
Health Check
GET /health

Response:

OK
Traefik Integration

You can configure Traefik to use the ACME plugin by adding the following to your traefik.yml file:

# Static configuration
certificatesResolvers:
  myresolver:
    acme:
      email: [email protected]
      storage: /path/to/acme.json
      dnsChallenge:
        provider: httpreq
        resolvers:
          - "8.8.8.8:53"
          - "1.1.1.1:53"

# Environment variables for the httpreq provider
environment:
  - HTTPREQ_ENDPOINT=https://auth.example.org:8080
  - HTTPREQ_USERNAME=your_username
  - HTTPREQ_PASSWORD=your_password

Metrics

If monitoring is enabled (via the prometheus directive) the following metrics are exported:

  • coredns_acme_request_count_total{server} - counter of DNS requests served by the acme plugin, labeled by DNS server address
  • coredns_acme_api_request_count_total{server, endpoint} - counter of API requests to the acme plugin, labeled by HTTP server address and endpoint name (register, present, cleanup, health)

The server label indicates which server handled the request. See the metrics plugin for details.

Security Considerations

  • Use HTTPS for the API server in production
  • Set up proper IP restrictions to prevent unauthorized access
  • Follow the principle of least privilege when setting up accounts
  • Generate strong random passwords for API access
  • When no IP restrictions are specified, access will be allowed to all by default. Make sure to only expose the API to trusted networks in this case.
  • Ensure domain names in configuration end with a trailing dot (.) to use proper FQDNs

See Also

License

MIT License

Troubleshooting

Common Issues and Solutions
API Access Denied
  • Verify the client IP is in the allowed CIDR ranges for the account
  • Check if the extract_ip_from_header setting is properly configured for proxy environments
  • Ensure the domain being updated matches the account's allowed zones and is part of the zone the plugin is authoritative for
  • If you've enabled require_auth, authentication is mandatory for API record updates, so confirm you're using the correct credentials
  • If require_auth is disabled, you can update records without authentication, but global IP restrictions (set using allowfrom) still apply
  • If you cannot access the API at all, check if you've configured an endpoint - without one, the API server doesn't start (DNS-only mode)
DNS-Only Mode
  • If you don't specify an endpoint, the plugin will operate in DNS-only mode where:
    • No API server is started, so record updates via API are not possible
    • The database is opened in read-only mode at the driver level
    • This prevents any write operations, ensuring the database integrity
    • This is useful when you need to serve DNS challenges from a delegated zone
    • Make sure to populate the database with records from a CoreDNS instance that has the API enabled when using this mode
Database Issues
  • Verify the SQLite path is writable by the CoreDNS process
  • Check for database corruption by running:
    sqlite3 /path/to/acme.db "PRAGMA integrity_check;"
    
  • If using DNS-only mode, ensure the database was populated with records by a CoreDNS instance with API enabled
DNS Propagation Problems
  • If using a CNAME record, ensure _acme-challenge.yourdomain.com points to the correct subdomain
  • Check that the ACME client is using the correct API endpoint and credentials
  • Verify CoreDNS is correctly forwarding non-ACME queries to upstream DNS servers
Certificate Issuance Failures
  • Review the ACME client logs for specific error messages
  • Ensure the TXT record is being properly set through the API
  • Verify the domain's DNS is correctly delegated to your CoreDNS server
  • Check that the ACME challenge subdomain is accessible from the internet

Documentation

Overview

Package acme implements a CoreDNS plugin that handles ACME DNS-01 challenges. It provides a REST API for updating TXT records needed for ACME DNS-01 validation.

Index

Constants

View Source
const ACMEAccountKey key = 0

ACMEAccountKey is a context key for storing Account information

View Source
const ACMERequestKey key = 1
View Source
const TXT_LENGTH = 43

Variables

View Source
var (
	ErrNoAuthenticationCredentials = errors.New("no authentication credentials")
	ErrInvalidUsernameOrPassword   = errors.New("invalid username or password")
	ErrAuthDisabled                = errors.New("authentication disabled")
)
View Source
var (
	ErrRecordNotFound   = errors.New("record not found")
	ErrReadOnlyDatabase = errors.New("database is in read-only mode")
)
View Source
var (
	// RequestCount exports a prometheus metric that is incremented every time a DNS request is processed by the acme plugin.
	RequestCount = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: plugin.Namespace,
		Subsystem: "acme",
		Name:      "request_count_total",
		Help:      "Counter of DNS requests served by the acme plugin.",
	}, []string{"server", "type"})

	// APIRequestCount exports a prometheus metric that is incremented every time an API request is processed.
	APIRequestCount = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: plugin.Namespace,
		Subsystem: "acme",
		Name:      "api_request_count_total",
		Help:      "Counter of API requests to the acme plugin.",
	}, []string{"server", "endpoint"})
)

Variables declared for monitoring.

Functions

This section is empty.

Types

type ACME

type ACME struct {
	Next  plugin.Handler
	Fall  fall.F
	Zones []string

	AuthConfig AuthConfig
	APIConfig  APIConfig
	TLSConfig  *tls.Config
	// contains filtered or unexported fields
}

ACME is a CoreDNS plugin that implements the ACME DNS challenge protocol

func (*ACME) Auth

func (a *ACME) Auth(next http.HandlerFunc) http.HandlerFunc

Auth is middleware that authenticates API requests

func (*ACME) Name

func (a *ACME) Name() string

Name implements the plugin.Handler interface

func (*ACME) ServeDNS

func (a *ACME) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error)

ServeDNS implements the plugin.Handler interface.

func (*ACME) Shutdown

func (a *ACME) Shutdown() error

func (*ACME) Startup

func (a *ACME) Startup() error

startAPIServer starts the HTTP API server

type ACMETxt

type ACMETxt struct {
	FQDN  string `json:"fqdn"`
	Value string `json:"value"`
}

type APIConfig

type APIConfig struct {
	// APIAddr is the address of the API server
	APIAddr string
	// EnableRegistration is a flag to enable registration
	EnableRegistration bool
}

APIConfig holds API server configuration

type Account

type Account struct {
	Username   string
	Password   string
	Zone       string
	AllowedIPs CIDRList
}

Account represents an API user

type AuthConfig

type AuthConfig struct {
	// AllowedIPs is a list of IP addresses or CIDR blocks that are allowed to update records
	AllowedIPs CIDRList
	// ExtractIPFromHeader is the name of the header to use for client IP
	ExtractIPFromHeader string
	// RequireAuth determines if authentication is required for API record updates
	RequireAuth bool
}

AuthConfig holds authentication configuration

type BadgerDB

type BadgerDB struct {
	// contains filtered or unexported fields
}

BadgerDB is an implementation of the DB interface using Badger

func NewBadgerDB

func NewBadgerDB(path string) (*BadgerDB, error)

NewBadgerDB creates a new BadgerDB instance

func NewBadgerDBWithROOption

func NewBadgerDBWithROOption(path string, readOnly bool) (*BadgerDB, error)

NewBadgerDBWithROOption creates a new BadgerDB instance with specified read-only option

func (*BadgerDB) CleanupRecord

func (b *BadgerDB) CleanupRecord(fqdn, value string) error

CleanupRecord removes a TXT record for a FQDN

func (*BadgerDB) Close

func (b *BadgerDB) Close() error

Close closes the BadgerDB database

func (*BadgerDB) GetAccount

func (b *BadgerDB) GetAccount(username, zone string) (Account, error)

GetAccount retrieves an account by username and zone

func (*BadgerDB) GetRecords

func (b *BadgerDB) GetRecords(fqdn string) ([]string, error)

GetRecords retrieves all TXT values for a given FQDN

func (*BadgerDB) PresentRecord

func (b *BadgerDB) PresentRecord(fqdn, value string) error

PresentRecord adds a TXT record for a FQDN

func (*BadgerDB) RegisterAccount

func (b *BadgerDB) RegisterAccount(account Account, hashedPassword []byte) error

RegisterAccount adds or updates an account

type CIDRList

type CIDRList []string

func NewCIDRList

func NewCIDRList(cidrs string) CIDRList

NewCIDRList creates a new CIDRList from a comma-separated string

func (*CIDRList) String

func (c *CIDRList) String() string

String returns a comma-separated string of CIDR entries

type DB

type DB interface {
	GetRecords(fqdn string) ([]string, error)
	PresentRecord(fqdn string, value string) error
	CleanupRecord(fqdn string, value string) error
	RegisterAccount(account Account, hashedPassword []byte) error
	GetAccount(username, zone string) (Account, error)
	Close() error
}

DB interface for different database backends

type MemDB

type MemDB struct {
	// contains filtered or unexported fields
}

MemDB is an in-memory implementation of the DB interface

func NewMemDB

func NewMemDB() (*MemDB, error)

NewMemDB creates a new in-memory database

func (*MemDB) CleanupRecord

func (m *MemDB) CleanupRecord(fqdn string, value string) error

CleanupRecord removes a DNS record

func (*MemDB) Close

func (m *MemDB) Close() error

Close does nothing for memory DB

func (*MemDB) GetAccount

func (m *MemDB) GetAccount(username, subdomain string) (Account, error)

GetAccount retrieves an account by username and zone, doing longest zone match

func (*MemDB) GetRecords

func (m *MemDB) GetRecords(fqdn string) ([]string, error)

GetRecords retrieves DNS records by FQDN

func (*MemDB) PresentRecord

func (m *MemDB) PresentRecord(fqdn string, value string) error

PresentRecord adds or updates a DNS record

func (*MemDB) RegisterAccount

func (m *MemDB) RegisterAccount(a Account, passwordHash []byte) error

RegisterAccount creates a new account

type RegisterRequest

type RegisterRequest struct {
	Username  string   `json:"username"`
	Password  string   `json:"password"`
	Zone      string   `json:"zone"`
	AllowFrom CIDRList `json:"allowfrom,omitempty"`
}

type SQLiteDB

type SQLiteDB struct {
	// contains filtered or unexported fields
}

SQLiteDB is a SQLite implementation of the DB interface

func NewSQLiteDB

func NewSQLiteDB(path string) (*SQLiteDB, error)

NewSQLiteDB creates a new SQLite database

func NewSQLiteDBWithROOption

func NewSQLiteDBWithROOption(path string, readOnly bool) (*SQLiteDB, error)

NewSQLiteDBWithROOption creates a new SQLite database with specified read-only option

func (*SQLiteDB) CleanupRecord

func (s *SQLiteDB) CleanupRecord(fqdn, value string) error

func (*SQLiteDB) Close

func (s *SQLiteDB) Close() error

func (*SQLiteDB) Exec

func (s *SQLiteDB) Exec(query string, args ...any) (sql.Result, error)

func (*SQLiteDB) GetAccount

func (s *SQLiteDB) GetAccount(username, subdomain string) (Account, error)

GetAccount retrieves an account by username and subdomain

func (*SQLiteDB) GetRecords

func (s *SQLiteDB) GetRecords(fqdn string) ([]string, error)

GetRecord retrieves a DNS record by domain

func (*SQLiteDB) PresentRecord

func (s *SQLiteDB) PresentRecord(fqdn, value string) error

PresentRecord updates a DNS record

func (*SQLiteDB) Query

func (s *SQLiteDB) Query(query string, args ...any) (*sql.Rows, error)

func (*SQLiteDB) QueryRow

func (s *SQLiteDB) QueryRow(query string, args ...any) *sql.Row

func (*SQLiteDB) RegisterAccount

func (s *SQLiteDB) RegisterAccount(a Account, passwordHash []byte) error

RegisterAccount creates a new account

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL