ACME Configuration#
The ACME module in Angie enables automatic certificate
retrieval using the ACME protocol. The ACME protocol supports
various domain verification methods (also called "validation"); this module
implements HTTP validation, DNS validation, ALPN validation, and
hook-based validation
through a custom external service. General steps to enable certificate requests in the configuration: Configure an ACME client in the Specify the domains for which certificates are requested: A single
certificate will be issued for all domain names listed in server_name
directives within all IP addresses (IPv4 or IPv6) specified in server_name are also valid
certificate identifiers, except with DNS validation. Note that public certificate authorities may issue certificates for IP
addresses only under specific certificate profiles; for example, Let's
Encrypt currently requires a profile that permits IP identifiers (see its
profiles documentation).
To request one, set the Set up request handling and ACME callbacks: This is required to verify
domain ownership. The setup depends on the chosen domain validation method: Method User Requirements Multi-domain Wildcard Domains Open port 80 (or the one specified in acme_http_port)
for incoming connections on the Angie server. ✔ Open port 53 (or the one specified in acme_dns_port)
for incoming connections on the Angie server. Set an NS record for the ✔ ✔ Open port 443 (or the TLS port used by your server)
for incoming connections on the Angie server. ✔ Create an external service (script or application)
that can, on request from Angie, update DNS records
or serve a special response via the web server. ✔ ✔ Configure SSL using the obtained certificate and key: The module makes
certificates and keys available as embedded variables
that can be used in configuration to populate
ssl_certificate and ssl_certificate_key. For SSL setup instructions, refer to SSL Configuration. Tip The certificate acquisition and renewal process depends on many services
and may take some time. Be patient, and if you encounter problems or have
doubts, check the debug log. Client keys and certificates are stored in PEM encoding within subdirectories of the
directory specified by the Note These files persist on disk between restarts. On startup, the client reuses
a stored certificate that is still valid instead of requesting a new one,
avoiding both the issuance delay and unnecessary requests to the CA, which
may be subject to rate limits. In a container, place the
storage directory (by default Note Reloading the configuration ( The ACME client requires an account on the CA server. To create and manage this
account, the client uses a private key ( Note If you already have an account key, place it in the client's subdirectory
before starting to reuse the account. Alternatively, specify the key file
using the Some CAs require the ACME account to be linked with an existing external account
through External Account Binding (EAB) before
they will issue certificates. To enable this, configure the The ACME client also uses a separate key ( At startup, the client requests a certificate if one doesn't exist, signing and
sending a CSR for all domains under its management to the CA server. The server
verifies domain ownership using HTTP or DNS
validation and issues a certificate, which the client saves
locally ( As mentioned earlier, a single certificate covers all domain names managed by
the same ACME client, potentially resulting in a multi-domain certificate. The
list of all names covered by the certificate can be found in the Subject
Alternative Name (SAN) section of the obtained certificate. To check this from
the command line: When a certificate is about to expire or the domain list changes, the client
signs and sends another CSR to the CA server. The server re-verifies ownership
and issues a new certificate, which the client installs locally, replacing the
previous one. In the configuration, the obtained certificate and its
corresponding key are available through the prefix variables
$acme_cert_<name> and $acme_cert_key_<name>. Their values are the
contents of the respective files, which should be used with the
ssl_certificate and ssl_certificate_key directives: Note The ACME client resolves the CA server's host name through the
resolver of the same context, which by default queries the DNS
servers listed in The acme directive serves only to collect domain names
for certificate requests.
It does not control where the certificate can be used:
any For example, if you have a wildcard server block
that already covers all subdomains,
additional server blocks for specific subdomains
do not need the To control the exact set of domain names in a certificate
without relying on automatic collection from all server blocks,
create a dedicated Other Each acme_client manages a single certificate. To obtain several
independent certificates (for example, for unrelated domains that should not
share one certificate), configure a separate acme_client for each in the
Validation is automatic. When Angie orders a certificate, the ACME server
requests a token file over HTTP from Angie at
In this example, the ACME client named As noted earlier, port 80 must be open to handle HTTP ACME calls.
If no server is configured to listen on the HTTP challenge port,
the module creates a dedicated listener on port 80 (or the one set in
acme_http_port). A separate server block is not required. Validation is automatic, but it needs DNS records on your side. When Angie
orders a certificate, the ACME server sends a DNS query for a TXT record
under the For the query to reach Angie, your DNS must delegate the
DNS validation proves control of a domain name, so it cannot validate IP
address identifiers: Angie ignores an IP address in server_name for a
client configured with Note The Angie server must be reachable from the internet
on UDP port 53 (or the one specified in acme_dns_port).
If the server is behind a firewall,
make sure this port is open for incoming connections. For example, to verify the domain This configuration delegates DNS resolution for
Warning NS record propagation can take anywhere from a few minutes to 48 hours
depending on TTL and DNS provider. It is recommended to verify the
configuration is correct before requesting a certificate. To verify that DNS is configured correctly, you can use the following commands: This method allows requesting wildcard certificates, for example, a certificate
that includes the entry Warning The applicability of this scenario largely depends on the capabilities
provided by your DNS provider; some providers do not allow such
configurations. Overall, the configuration is similar to the example in the previous section.
There is no need for HTTP-specific settings; instead, it's sufficient to set
In this example, the ACME client named Validation is handled automatically. The ACME server connects using TLS and
requests the To enable this method, configure The configuration is similar to the previous sections; it is enough to set
In this example, the ACME client named Unlike the previous methods, this validation requires additional effort. The
ACME server performs standard HTTP validation or
DNS validation, but instead of interacting directly
with the Angie server, it communicates with an external service managed by the
Angie server using hook calls (acme_hook). This service configures a
separate DNS or HTTP server where the ACME server sends its requests. Once the ACME server receives the expected response from the configured DNS or
HTTP server, it confirms domain ownership. When certificate issuance or renewal requires domain verification,
Angie generates an internal request
to the named The general pattern is: Create a named Configure a request handler in the same Pass ACME variables
to the handler using the mechanism it supports,
for example The handler must return a Regardless of the handler used,
the hook For DNS validation, the handler must use In this example, the ACME client The A named The following Perl script demonstrates a corresponding external FastCGI service: Here, Another example, using PHP-FPM: Parameters passed can be accessed in PHP via Here the hook is a script run by the CGI module; it adds
and removes the The module is loaded in the octoDNS builds a zone from its sources in order and merges them, so the hook
keeps the challenge record in a source directory of its own,
The zone file holds the records you maintain yourself: To check the octoDNS configuration by hand, run octodns-sync without
The script reads the ACME variables from its environment
and writes the challenge record into the As soon as the If the value does not appear in time, the script answers The wait limit comes from the Setting it to The $acme_hook_domain variable arrives without the The script runs as the user of the Angie worker processes, The script's standard error goes to the Angie error log
at the To put all of this in place on a Debian-based system: Install the CGI module: Install octoDNS and the Cloudflare provider
into a virtual environment of their own: Create the directories; the module package
does not create Create Save the script shown above as Add the configuration shown above, then test it and
reload Angie: The client requests the certificate immediately after the reload; its state
and the certificate status appear in the /status/http/acme_clients/ API section, and the issued certificate is
saved in the client's storage directory. The Stream ACME module enables automated
certificate issuance and usage for TCP traffic.
For it to work correctly, you must first configure its HTTP counterpart:
the ACME client must be declared in the By default, HTTP validation mode is used to obtain certificates.
As mentioned in the HTTP Validation section,
this requires an HTTP server listening on port 80: You can also use DNS validation
by configuring If you previously used certbot
to obtain and renew SSL certificates from Let's Encrypt
before migrating from nginx to Angie,
follow these steps to transition to using our ACME module. Suppose you configured certificates as follows: The configuration automatically created by this command
is typically located in In the example above, the highlighted lines need to be modified.
Depending on your circumstances and preferences, configure
HTTP validation
or DNS validation using the ACME module. The resulting Angie configuration
might look something like this: Remember to reload the configuration
after making changes: Once you have verified that this configuration works,
you can delete the certbot certificates
and disable or remove certbot entirely from the server
if it is no longer used elsewhere,
for example:Configuration Steps#
http block using the
acme_client directive, which specifies a unique client name and other
parameters. Multiple ACME clients can be configured.server blocks that use acme directives
pointing to the same ACME client.profile= parameter of acme_client._acme-challenge. subdomain
pointing to your Angie server.Implementation Details#
--http-acme-client-path build option:$ ls /var/lib/angie/acme/example/
account.key certificate.pem private.key
/var/lib/angie/acme/) on a persistent
volume so that issued certificates survive container recreation; see
running Angie in a container.angie -s reload) re-evaluates every ACME
client against the certificates stored on disk. A client whose certificate is
not currently valid (including one whose previous request ended in an error)
is requested again immediately, regardless of retry_after_error. The retry delay (and retry_after_error=off) applies only
while Angie keeps running; it does not survive a reload. Reloading repeatedly
while a request keeps failing can therefore send frequent requests to the CA,
subject to the rate limits noted above.account.key). If no key exists,
it is generated at startup. The client then uses this key to register the
account with the server.account_key parameter in acme_client.eab
parameter of acme_client with the key identifier and MAC key the CA
provides; see the directive's description for the exact syntax.private.key) for Certificate
Signing Requests (CSRs). This certificate key is automatically created at
startup if needed.certificate.pem).$ openssl x509 -in certificate.pem -noout -text | grep -A5 "Subject Alternative Name"
server {
listen 443 ssl;
server_name example.com www.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
/etc/resolv.conf; configure the directive
explicitly only to use other servers or parameters. On a host without IPv6
connectivity, the resolver may still issue AAAA (IPv6) queries for the CA
and fail to connect; disable them with the ipv6=off parameter, for
example resolver conf ipv6=off;.Domain Collection vs. Certificate Usage#
server block can reference the obtained certificate
through the $acme_cert_<name> variable,
regardless of whether the block contains an acme directive.acme directive:http {
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
# This block lists the domains for the certificate request
server {
listen 443 ssl;
server_name example.com *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
# This block uses the same certificate but does not
# add its server_name to the certificate request
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
Explicit Domain List#
server block
that contains only the server_name and acme directives.
To prevent this block from handling real traffic,
bind it to a Unix domain socket:# Dedicated block that defines the certificate's domain list
server {
listen unix:/tmp/acme_example.sock;
server_name example.com www.example.com;
acme example;
}
server blocks can then use the certificate
through the $acme_cert_<name> variable
without affecting which domains are requested.Separate Certificates for Different Domains#
http block, and point the acme directive of every server
block at the relevant client by name:http {
# Two independent clients, each managing its own certificate
acme_client shop https://acme-v02.api.letsencrypt.org/directory
challenge=http;
acme_client blog https://acme-v02.api.letsencrypt.org/directory
challenge=http;
server {
listen 443 ssl;
server_name shop.example.com www.shop.example.com;
acme shop;
ssl_certificate $acme_cert_shop;
ssl_certificate_key $acme_cert_key_shop;
}
server {
listen 443 ssl;
server_name blog.example.com;
acme blog;
ssl_certificate $acme_cert_blog;
ssl_certificate_key $acme_cert_key_blog;
}
}
HTTP Validation#
/.well-known/acme-challenge/<TOKEN>. Angie answers these requests
itself, so you need not serve that path yourself. The ACME server then compares
the token it receives with the one it issued and, if they match, accepts the
domain as yours.Configuration Example#
example manages certificates for
example.com and www.example.com (note that wildcard certificates
aren't supported with HTTP validation):http {
acme_client example https://acme-v02.api.letsencrypt.org/directory;
server {
listen 80; # Not required: if nothing else listens on the HTTP
# challenge port, the module opens it itself
# (see the 'acme_http_port' directive)
listen 443 ssl;
server_name example.com www.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
DNS Validation#
_acme-challenge. subdomain of the domain being validated.
Angie answers these queries itself: it acts as the authoritative name server for
that subdomain and returns the TXT record the ACME server expects. The ACME
server compares the record it receives with the value it issued and, if they
match, accepts the domain as yours._acme-challenge. subdomain to the Angie server, as shown below. Angie
answers with a very short TTL by default; some DNS providers filter out
responses with a TTL that low, so raise it with acme_dns_ttl if
validation never completes.challenge=dns, logging a warning at startup.
A server block that lists an IP address must still name at least one
domain the client can validate.example.com using an Angie server at
IP address 203.0.113.10, your domain's DNS configuration should include
the following records:_acme-challenge.example.com. 60 IN NS ns.example.com.
ns.example.com. 60 IN A 203.0.113.10
_acme-challenge.example.com to ns.example.com, ensuring
ns.example.com is accessible by mapping it to the IP address
(203.0.113.10).$ dig NS _acme-challenge.example.com +short # Check NS record for _acme-challenge subdomain
ns.example.com.
$ dig A ns.example.com +short # Check A record for name server
203.0.113.10
$ nc -zv 203.0.113.10 53 # Check DNS server accessibility on port 53
*.example.com in the Subject Alternative Name
(SAN) section. To explicitly request a certificate for a subdomain, such as
www.example.com, you must separately verify that subdomain using the
method described above.Configuration Example#
challenge=dns for the acme_client directive.example manages certificates for
example.com and *.example.com:http {
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
server {
server_name example.com *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
ALPN Validation#
acme-tls/1 protocol via ALPN. The module serves a temporary
certificate for the validation request.challenge=alpn in the
acme_client directive and ensure your TLS listener is reachable on
port 443 (or the port used for TLS).Configuration Example#
challenge=alpn for the acme_client directive and ensure the TLS
server is reachable on port 443.example manages a certificate
for example.com and www.example.com:http {
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=alpn;
server {
listen 443 ssl;
server_name example.com www.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
Hook-Based Validation#
location containing the acme_hook directive.
How this request is handled depends entirely on the other directives
configured in the same location.location with the acme_hook directive.location
using whatever module fits your setup:
fastcgi_pass for FastCGI,
proxy_pass for HTTP,
cgi_pass for CGI scripts, etc.fastcgi_param for FastCGI
or cgi_set_var for CGI.2xx status code,
which can be sent via the Status header.
Any other code from the add hook aborts the renewal attempt;
from the remove hook, the same code is only logged as a warning,
and the renewal continues.
Output from the handler is ignored.Minimal Configuration#
location follows this structure:location @acme_hook_location {
acme_hook example;
# Handler directive (fastcgi_pass, proxy_pass, cgi_pass, ...)
# Pass ACME variables using the handler's mechanism:
# ACME_HOOK — $acme_hook_name ("add" or "remove")
# ACME_CHALLENGE — $acme_hook_challenge ("dns" or "http")
# ACME_DOMAIN — $acme_hook_domain
# ACME_TOKEN — $acme_hook_token
# ACME_KEYAUTH — $acme_hook_keyauth
}
ACME_HOOK
to determine the action:
when it is add,
create a TXT record for _acme-challenge.ACME_DOMAIN
with the value from ACME_KEYAUTH;
when it is remove, delete that record.FastCGI Example#
example is configured for domain
verification using DNS callbacks, indicated by the challenge=dns
parameter in the acme_client directive.server block applies to all subdomains of example.com (e.g.,
*.example.com) and uses the ACME client example to manage
certificates, as specified by the acme directive.location block handles the hook calls.
The acme_hook directive associates it
with the ACME client example.
Hook requests are sent to a local FastCGI server
on port 9000 using fastcgi_pass.
The fastcgi_param directives pass
the ACME variables to the external service.acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
server {
listen 80;
server_name *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
location @acme_hook_location {
acme_hook example;
fastcgi_pass localhost:9000;
fastcgi_param ACME_CLIENT $acme_hook_client;
fastcgi_param ACME_HOOK $acme_hook_name;
fastcgi_param ACME_CHALLENGE $acme_hook_challenge;
fastcgi_param ACME_DOMAIN $acme_hook_domain;
fastcgi_param ACME_TOKEN $acme_hook_token;
fastcgi_param ACME_KEYAUTH $acme_hook_keyauth;
include fastcgi.conf;
}
}
#!/usr/bin/perl
use strict; use warnings;
use FCGI;
my $socket = FCGI::OpenSocket(":9000", 5);
my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket);
while ($request->Accept() >= 0) {
print "\r\n";
my $client = $ENV{ACME_CLIENT};
my $hook = $ENV{ACME_HOOK};
my $challenge = $ENV{ACME_CHALLENGE};
my $domain = $ENV{ACME_DOMAIN};
my $token = $ENV{ACME_TOKEN};
my $keyauth = $ENV{ACME_KEYAUTH};
if ($hook eq 'add') {
DNS_set_TXT_record("_acme-challenge.$domain.", $keyauth);
} elsif ($hook eq 'remove') {
DNS_clear_TXT_record("_acme-challenge.$domain.");
}
};
FCGI::CloseSocket($socket);
DNS_set_TXT_record() and DNS_clear_TXT_record() are
functions assumed to add and remove TXT records in the configuration of an
external DNS server that the ACME server queries. These records must contain the
data provided by the Angie server to allow the external DNS server to
successfully pass validation, similar to the process described in
DNS Validation. The implementation details of such functions are beyond
the scope of this guide; for example, parameters can also be passed through the
request URI:# ...
location @acme_hook_location {
acme_hook example uri=/acme_hook/$acme_hook_name?domain=$acme_hook_domain&key=$acme_hook_keyauth;
fastcgi_pass localhost:9000;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param ACME_CLIENT $acme_hook_client;
fastcgi_param ACME_CHALLENGE $acme_hook_challenge;
fastcgi_param ACME_TOKEN $acme_hook_token;
include fastcgi.conf;
}
PHP-FPM Example#
location @acme_hook_location {
acme_hook example;
root /var/www/dns;
fastcgi_pass unix:/run/php-fpm/php-dns.sock;
fastcgi_index hook.php;
fastcgi_param SCRIPT_FILENAME /var/www/dns/hook.php;
include fastcgi_params;
fastcgi_param ACME_CLIENT $acme_hook_client;
fastcgi_param ACME_HOOK $acme_hook_name;
fastcgi_param ACME_CHALLENGE $acme_hook_challenge;
fastcgi_param ACME_DOMAIN $acme_hook_domain;
fastcgi_param ACME_TOKEN $acme_hook_token;
fastcgi_param ACME_KEYAUTH $acme_hook_keyauth;
}
[dns]
listen = /run/php-fpm/php-dns.sock
listen.mode = 0666
user = angie
group = angie
chdir = /var/www/dns
# ...
$_SERVER['...'].CGI and octoDNS Example#
_acme-challenge TXT record through octoDNS, which keeps DNS as code: a zone is
described in YAML and synchronized to a DNS provider. Cloudflare is shown here;
any other octoDNS provider works the same way once it is declared under
providers and named in targets. The API token needs the
Zone:Read, DNS:Read, and DNS:Edit permissions for the
zone.main context. The ACME client
example requests a single certificate for example.com and
*.example.com, verified through DNS callbacks as indicated by the
challenge=dns parameter in the acme_client directive. A named
location block handles the hook calls: the acme_hook directive
associates it with the client, the cgi_set_var directives pass the ACME
variables under the standard names, and
cgi_pass runs the script for each call. Of those names, this script
uses ACME_CHALLENGE, ACME_HOOK, ACME_DOMAIN, and
ACME_KEYAUTH.# in the main context
load_module modules/ngx_http_cgi_module.so;
acme_client example https://acme-v02.api.letsencrypt.org/directory
challenge=dns;
server {
listen 443 ssl;
server_name example.com *.example.com;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
location @acme_hook_location {
acme_hook example;
cgi_set_var ACME_CLIENT $acme_hook_client;
cgi_set_var ACME_HOOK $acme_hook_name;
cgi_set_var ACME_CHALLENGE $acme_hook_challenge;
cgi_set_var ACME_DOMAIN $acme_hook_domain;
cgi_set_var ACME_TOKEN $acme_hook_token;
cgi_set_var ACME_KEYAUTH $acme_hook_keyauth;
cgi_pass /usr/share/angie/cgi-bin/acme-octodns;
}
}
/etc/octodns/acme, and certificate renewals never rewrite the zone file
under /etc/octodns/zones. The ignore_missing_zones option
(octoDNS 1.16 and later) lets /etc/octodns/acme stay empty between
renewals. The zone file itself must not define _acme-challenge records.providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: /etc/octodns/zones
acme:
class: octodns.provider.yaml.YamlProvider
directory: /etc/octodns/acme
ignore_missing_zones: true
cloudflare:
class: octodns_cloudflare.CloudflareProvider
token: YOUR_API_TOKEN
zones:
example.com.:
sources:
- config
- acme
targets:
- cloudflare
'':
type: A
value: 203.0.113.10
www:
type: A
value: 203.0.113.10
--doit.acme source:The acme-octodns script
#!/opt/octodns/bin/python3
# DNS-01 hook for the Angie ACME client: keeps the _acme-challenge TXT record
# in a separate octodns source directory and publishes it with octodns-sync.
import fcntl
import os
import subprocess
import sys
import time
import dns.message
import dns.query
import dns.resolver
import yaml
CONFIG = "/etc/octodns/octodns.yaml"
ACME_DIR = "/etc/octodns/acme"
SYNC = "/opt/octodns/bin/octodns-sync"
TTL = 120
WAIT = int(os.environ.get("ACME_WAIT", "300"))
def respond(code, message=""):
if message:
print(message, file=sys.stderr)
sys.stdout.write(f"Status: {code}\r\n\r\n")
sys.exit(0)
def find_zone(domain):
with open(CONFIG) as f:
zones = [z.rstrip(".") for z in yaml.safe_load(f)["zones"]]
match = [z for z in zones if domain == z or domain.endswith("." + z)]
if not match:
respond(500, f"no zone for {domain} in {CONFIG}")
return max(match, key=len)
def update(zone, name, value, add):
# One record may hold several values: a certificate for example.com and
# *.example.com is validated with two TXT values on the same name.
path = f"{ACME_DIR}/{zone}.yaml"
with open(f"{ACME_DIR}/.lock", "w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
data = {}
if os.path.exists(path):
with open(path) as f:
data = yaml.safe_load(f) or {}
values = set(data.get(name, {}).get("values", []))
(values.add if add else values.discard)(value)
if values:
data[name] = {"type": "TXT", "ttl": TTL, "values": sorted(values)}
else:
data.pop(name, None)
if data:
with open(path, "w") as f:
yaml.safe_dump(data, f)
elif os.path.exists(path):
os.remove(path)
cmd = [SYNC, f"--config-file={CONFIG}", "--doit", zone + "."]
run = subprocess.run(cmd, capture_output=True, text=True)
if run.returncode != 0:
respond(500, "octodns-sync failed:\n" + run.stdout + run.stderr)
def has_txt(server, fqdn, value):
try:
reply = dns.query.udp(dns.message.make_query(fqdn, "TXT"), server, timeout=3)
except Exception:
return False
return any(value == b"".join(getattr(r, "strings", ())).decode()
for rrset in reply.answer for r in rrset)
def wait_visible(zone, fqdn, value):
# The ACME server asks the zone's authoritative servers, so poll them
# directly: a resolver may have cached the record's absence.
deadline = time.monotonic() + WAIT
while time.monotonic() < deadline:
try:
servers = [str(a) for ns in dns.resolver.resolve(zone, "NS")
for a in dns.resolver.resolve(str(ns.target), "A")]
if servers and all(has_txt(s, fqdn, value) for s in servers):
return True
except Exception:
pass
time.sleep(5)
return False
def main():
hook = os.environ.get("ACME_HOOK")
domain = os.environ.get("ACME_DOMAIN", "")
value = os.environ.get("ACME_KEYAUTH", "")
if os.environ.get("ACME_CHALLENGE") != "dns" or hook not in ("add", "remove"):
respond(500, "the hook supports only DNS validation")
zone = find_zone(domain)
rel = domain[: -len(zone) - 1]
name = "_acme-challenge" + (f".{rel}" if rel else "")
update(zone, name, value, hook == "add")
if hook == "add" and WAIT and not wait_visible(zone, f"{name}.{zone}.", value):
update(zone, name, value, False)
respond(500, f"{name}.{zone} is not visible on the name servers after {WAIT}s")
respond(200)
try:
main()
except Exception as e:
respond(500, f"{type(e).__name__}: {e}")
add hook returns a 2xx code, Angie asks the ACME
server to validate the challenge and polls the result for 60 seconds. Angie
never waits for the record to propagate, so the script does that itself. It
queries the zone's authoritative name servers directly, since a resolver may
have cached the record's absence, and returns as soon as every one of them
serves the value, waiting up to 300 seconds by default.Status: 500,
and Angie aborts the attempt and retries it after the interval set by the
retry_after_error parameter. Angie does not call the
remove hook after a failed add, so the script deletes its own
value before answering.ACME_WAIT environment variable. The hook
location above does not set it; to change the limit, add it there,
passing the value through a variable like the others:set $acme_wait 600;
cgi_set_var ACME_WAIT $acme_wait;
0 disables the wait, which suits a self-hosted
authoritative server where an API write is visible immediately.*. prefix, so
a certificate for example.com and *.example.com produces two
add calls for the same _acme-challenge.example.com name. The
script keeps a list of values on one record, and on remove it drops only
the value from that call; once no values are left, it deletes the record and the
file that held it under /etc/octodns/acme.angie. It
needs read access to the octoDNS configuration, which holds the token, and write
access to /etc/octodns/acme; nothing else. The CGI process does not
inherit Angie's environment, which is why the token lives in the configuration
file and the script uses absolute paths.warn level, where hook failures are diagnosed; these messages
appear only when the log level is warn or less severe.$ sudo apt-get install angie-module-cgi
$ sudo apt-get install python3-venv
$ sudo python3 -m venv /opt/octodns
$ sudo /opt/octodns/bin/pip install 'octodns>=1.16' octodns-cloudflare
/usr/share/angie/cgi-bin:$ sudo install -d /etc/octodns/zones /usr/share/angie/cgi-bin
$ sudo install -d -o angie -g angie -m 750 /etc/octodns/acme
/etc/octodns/octodns.yaml and
/etc/octodns/zones/example.com.yaml as shown above, then restrict the
configuration to the angie group, since it holds the API token:$ sudo chgrp angie /etc/octodns/octodns.yaml
$ sudo chmod 640 /etc/octodns/octodns.yaml
acme-octodns, then install it:$ sudo install -m 755 acme-octodns /usr/share/angie/cgi-bin/
$ sudo angie -t
$ sudo kill -HUP $(cat /run/angie.pid)
ACME in the Stream Module#
http context,
and the stream block itself must be placed after the http block
in the configuration.Configuration Example#
# HTTP part
http {
# ACME client for the stream part
acme_client example https://acme-v02.api.letsencrypt.org/directory;
# Server for HTTP validation
server {
listen 80;
return 444;
}
}
# Stream part
stream {
server {
listen 12345 ssl;
proxy_pass backend_upstream;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
server_name example.com www.example.com;
acme example; # reference to the ACME client defined in the HTTP part
}
upstream backend_upstream {
server 127.0.0.1:54321;
}
}
challenge=dns in the acme_client directive;
in that case, the server will not be needed.Migrating from certbot#
$ sudo certbot --nginx -d example.com -d www.example.com
/etc/nginx/sites-available/example.conf
and looks something like this:server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
root /var/www/example;
index index.html;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
http {
acme_client example https://acme-v02.api.letsencrypt.org/directory;
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
root /var/www/example;
index index.html;
acme example;
ssl_certificate $acme_cert_example;
ssl_certificate_key $acme_cert_key_example;
}
}
$ sudo kill -HUP $(cat /run/angie.pid)
$ sudo rm -rf /etc/letsencrypt
$ sudo systemctl stop certbot.timer
$ sudo systemctl disable certbot.timer
$ # -- or --
$ sudo rm /etc/cron.d/certbot
$ sudo apt remove certbot
$ # -- or --
$ sudo dnf remove certbot