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.
Related modules
HTTP ACME module: obtains and renews certificates.
Stream ACME module: stream servers need a client from
http.HTTP module: server_name gives the certificate its names.
HTTP SSL module: ssl_certificate loads the ACME certificate.
FastCGI module: passes hook calls to FastCGI and PHP-FPM.
CGI module: runs the octoDNS hook script.
Configuration Steps#
General steps to enable certificate requests in the configuration:
Configure an ACME client in the
httpblock using the acme_client directive, which specifies a unique client name and other parameters. Multiple ACME clients can be configured.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
serverblocks that use acme directives pointing to the same ACME client.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
profile=parameter of acme_client.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
_acme-challenge.subdomain pointing to your Angie server.✔
✔
Open port 443 for incoming connections on the Angie server; the ACME server connects only to this port.
✔
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.
Implementation Details#
Client keys and certificates are stored in PEM encoding within subdirectories of the
directory specified by the --http-acme-client-path build option:
$ ls /var/lib/angie/acme/example/
account.key certificate.pem private.key
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 /var/lib/angie/acme/) on a persistent
volume so that issued certificates survive container recreation; see
running Angie in a container.
Note
Reloading the configuration (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.
The ACME client requires an account on the CA server. To create and manage this
account, the client uses a private key (account.key). If no key exists,
it is generated at startup. The client then uses this key to register the
account with the server.
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 account_key parameter in acme_client.
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 eab
parameter of acme_client with the key identifier and MAC key the CA
provides; see the directive's description for the exact syntax.
The ACME client also uses a separate key (private.key) for Certificate
Signing Requests (CSRs). This certificate key is automatically created at
startup if needed.
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 (certificate.pem).
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:
$ openssl x509 -in certificate.pem -noout -text | grep -A5 "Subject Alternative Name"
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:
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;
}
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 /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#
The acme directive serves only to collect domain names
for certificate requests.
It does not control where the certificate can be used:
any server block can reference the obtained certificate
through the $acme_cert_<name> variable,
regardless of whether the block contains an acme directive.
For example, if you have a wildcard server block
that already covers all subdomains,
additional server blocks for specific subdomains
do not need the 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#
To control the exact set of domain names in a certificate
without relying on automatic collection from all server blocks,
create a dedicated 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;
}
Other server blocks can then use the certificate
through the $acme_cert_<name> variable
without affecting which domains are requested.
Separate Certificates for Different Domains#
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
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#
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./.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#
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 _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.
For the query to reach Angie, your DNS must delegate the
_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.
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 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.
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 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
This configuration delegates DNS resolution for
_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).
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:
$ 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
This method allows requesting wildcard certificates, for example, a certificate
that includes the entry *.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.
Warning
The applicability of this scenario largely depends on the capabilities provided by your DNS provider; some providers do not allow such configurations.
Configuration Example#
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
challenge=dns for the acme_client directive.
In this example, the ACME client named 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#
Validation is handled automatically. The ACME server connects using TLS and
requests the acme-tls/1 protocol via ALPN. The module serves a temporary
certificate for the validation request.
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 challenge=alpn in the
acme_client directive and ensure that a TLS listener is reachable on
port 443: the ACME server connects only to this port. A server that listens on
another port needs external port 443 forwarded to it.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#
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 location containing the acme_hook directive.
How this request is handled depends entirely on the other directives
configured in the same location.
The general pattern is:
Create a named
locationwith the acme_hook directive.Configure a request handler in the same
locationusing whatever module fits your setup: fastcgi_pass for FastCGI, proxy_pass for HTTP,cgi_passfor CGI scripts, etc.Pass ACME variables to the handler using the mechanism it supports, for example
fastcgi_paramfor FastCGI orcgi_set_varfor CGI.
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.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#
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 described
in the HTTP Validation section, the ACME server must be able to reach
port 80; if no HTTP server listens there, Angie opens the port itself, so the
port 80 server in this example is optional: You can also use DNS validation
by configuring You can also use ALPN validation. With 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;
# Optional: Angie opens port 80 itself if no server listens there
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, port 80 is not used.challenge=alpn, the stream
server answers the validation request itself if it terminates TLS with the
certificate and key from the client's variables, as the example above does.
The ACME server connects only to port 443, so the stream server must listen
there (listen 443 ssl instead of 12345), and port 80 is not
used.
Migrating from certbot#
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:
$ sudo certbot --nginx -d example.com -d www.example.com
The configuration automatically created by this command
is typically located in /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;
}
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:
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;
}
}
Remember to reload the configuration after making changes:
$ sudo kill -HUP $(cat /run/angie.pid)
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:
$ 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