Configuración de ACME#

El módulo ACME en Angie permite la obtención automática de certificados usando el protocolo ACME. Este protocolo admite varios métodos de verificación de dominio (también llamados "validación"); este módulo implementa la validación HTTP, la validación DNS, la validación ALPN y la validación mediante hooks a través de un servicio externo personalizado.

Pasos de configuración#

Pasos generales para habilitar solicitudes de certificados en la configuración:

  • Configurar un cliente ACME en el bloque http mediante la directiva acme_client, que especifica un nombre único de cliente y otros parámetros. Se pueden configurar varios clientes ACME.

  • Especificar los dominios para los que se solicitan certificados: Se emitirá un único certificado para todos los nombres de dominio listados en las directivas server_name dentro de todos los bloques server que usen directivas acme que apunten al mismo cliente ACME.

    Las direcciones IP (IPv4 o IPv6) especificadas en server_name también son identificadores de certificado válidos, excepto con la validación DNS.

    Tenga en cuenta que las autoridades de certificación públicas pueden emitir certificados para direcciones IP solo bajo perfiles de certificado específicos; por ejemplo, Let's Encrypt actualmente requiere un perfil que permita identificadores IP (consulte su documentación de perfiles). Para solicitarlo, establezca el parámetro profile= de acme_client.

  • Configurar el manejo de solicitudes y las devoluciones de llamada de ACME: Esto es necesario para verificar la propiedad del dominio. La configuración depende del método de validación elegido:

    Método

    Requisitos del Usuario

    Multidominio

    Dominios Wildcard

    Validación HTTP

    Abrir el puerto 80 (o el especificado en acme_http_port) para conexiones entrantes en el servidor Angie.

    ✔

    Validación DNS

    Abrir el puerto 53 (o el especificado en acme_dns_port) para conexiones entrantes en el servidor Angie.

    Configurar un registro NS para el subdominio _acme-challenge. apuntando a su servidor Angie.

    ✔

    ✔

    Validación ALPN

    Abrir el puerto 443 (o el puerto TLS usado por su servidor) para conexiones entrantes en el servidor Angie.

    ✔

    Validación mediante hooks

    Crear un servicio externo (script o aplicación) que pueda, a solicitud de Angie, actualizar registros DNS o servir una respuesta especial mediante el servidor web.

    ✔

    ✔

  • Configurar SSL usando el certificado y la clave obtenidos: El módulo pone a disposición certificados y claves como variables integradas que pueden usarse en la configuración para rellenar ssl_certificate y ssl_certificate_key.

    Para instrucciones de configuración SSL, consulte Configuración SSL.

Truco

El proceso de obtención y renovación de certificados depende de muchos servicios y puede llevar tiempo. Sea paciente y, si encuentra problemas o tiene dudas, revise el log de depuración.

Detalles de implementación#

Las claves y certificados de cliente se almacenan en codificación PEM dentro de subdirectorios del directorio especificado por la --http-acme-client-path opción de compilación:

$ ls /var/lib/angie/acme/example/

  account.key  certificate.pem  private.key

Nota

Estos archivos persisten en disco entre reinicios. Al arrancar, el cliente reutiliza un certificado almacenado que aún es válido en lugar de solicitar uno nuevo, evitando tanto el tiempo de espera de emisión como solicitudes innecesarias a la CA, que pueden estar sujetas a límites de tasa. En un contenedor, coloque el directorio de almacenamiento (por defecto /var/lib/angie/acme/) en un volumen persistente para que los certificados emitidos sobrevivan a la recreación del contenedor; consulte ejecutar Angie en un contenedor.

Nota

Recargar la configuración (angie -s reload) vuelve a evaluar cada cliente ACME frente a los certificados almacenados en disco. Un cliente cuyo certificado no es válido en ese momento -- incluido uno cuya solicitud anterior terminó en error -- se vuelve a solicitar de inmediato, independientemente de retry_after_error. El retraso de reintento (y retry_after_error=off) solo se aplica mientras Angie sigue en ejecución; no persiste tras una recarga. Por lo tanto, recargar repetidamente mientras una solicitud sigue fallando puede enviar solicitudes frecuentes a la CA, sujetas a los límites de tasa mencionados anteriormente.

El cliente ACME requiere una cuenta en el servidor CA. Para crear y gestionar esta cuenta, el cliente utiliza una clave privada (account.key). Si no existe clave, se genera al inicio. Luego, el cliente usa esta clave para registrar la cuenta con el servidor.

Nota

Si ya dispone de una clave de cuenta, colóquela en el subdirectorio del cliente antes de iniciar, para reutilizar la cuenta. Alternativamente, especifique el archivo de clave con el parámetro account_key en acme_client.

Algunas CA requieren que la cuenta ACME esté vinculada con una cuenta externa existente mediante External Account Binding (EAB) antes de emitir certificados. Para habilitarlo, configure el parámetro eab de acme_client con el identificador de clave y la clave MAC que proporciona la CA; consulte la descripción de la directiva para conocer la sintaxis exacta.

El cliente ACME también utiliza una clave separada (private.key) para las solicitudes de firma de certificados (CSR). Esta clave de certificado se crea automáticamente al inicio si es necesario.

Al arrancar, el cliente solicita un certificado si no existe, firmando y enviando un CSR para todos los dominios bajo su gestión al servidor CA. El servidor verifica la propiedad del dominio usando validación HTTP o validación DNS y emite un certificado, que el cliente guarda localmente (certificate.pem).

Como se mencionó anteriormente, un único certificado cubre todos los nombres de dominio gestionados por el mismo cliente ACME, lo que puede dar lugar a un certificado multidominio. La lista de todos los nombres cubiertos por el certificado puede encontrarse en la sección Subject Alternative Name (SAN) del certificado obtenido. Para comprobarlo desde la línea de comandos:

$ openssl x509 -in certificate.pem -noout -text | grep -A5 "Subject Alternative Name"

Cuando un certificado está a punto de expirar o la lista de dominios cambia, el cliente firma y envía otro CSR al servidor CA. El servidor vuelve a verificar la propiedad y emite un nuevo certificado, que el cliente instala localmente, reemplazando el anterior.

En la configuración, el certificado obtenido y su clave correspondiente están disponibles mediante las variables prefijo $acme_cert_<nombre> y $acme_cert_key_<nombre>. Sus valores son los contenidos de los respectivos archivos, que deben usarse con las directivas ssl_certificate y ssl_certificate_key:

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;
}

Nota

El cliente ACME resuelve el nombre de host del servidor CA a través del resolver del mismo contexto, que por defecto consulta los servidores DNS listados en /etc/resolv.conf; configure la directiva explícitamente solo para usar otros servidores o parámetros. En un host sin conectividad IPv6, el resolver puede seguir emitiendo consultas AAAA (IPv6) para la CA y no lograr conectarse; desactívelas con el parámetro ipv6=off, por ejemplo resolver conf ipv6=off;.

Recopilación de dominios frente al uso del certificado#

La directiva acme sirve únicamente para recopilar nombres de dominio para las solicitudes de certificados. No controla dónde puede usarse el certificado: cualquier bloque server puede hacer referencia al certificado obtenido mediante la variable $acme_cert_<name>, independientemente de si el bloque contiene una directiva acme.

Por ejemplo, si tiene un bloque server con comodín que ya cubre todos los subdominios, los bloques server adicionales para subdominios específicos no necesitan la directiva acme:

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;
    }
}

Lista explícita de dominios#

Para controlar el conjunto exacto de nombres de dominio en un certificado sin depender de la recopilación automática desde todos los bloques server, cree un bloque server dedicado que contenga únicamente las directivas server_name y acme. Para evitar que este bloque maneje tráfico real, vincúlelo a un socket de dominio Unix:

# 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;
}

Otros bloques server pueden entonces usar el certificado mediante la variable $acme_cert_<name> sin afectar a qué dominios se solicitan.

Certificados independientes para diferentes dominios#

Cada acme_client gestiona un único certificado. Para obtener varios certificados independientes (por ejemplo, para dominios no relacionados que no deben compartir un mismo certificado), configure un acme_client separado para cada uno en el bloque http, y apunte la directiva acme de cada bloque server al cliente correspondiente por nombre:

http {

    # Dos clientes independientes, cada uno gestionando su propio certificado
    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;
    }
}

Validación HTTP#

La validación es automática. Cuando Angie solicita un certificado, el servidor ACME solicita un archivo token vía HTTP a Angie en /.well-known/acme-challenge/<TOKEN>. Angie responde a estas solicitudes por sí mismo, por lo que no es necesario servir esa ruta usted mismo. El servidor ACME compara entonces el token recibido con el que emitió y, si coinciden, acepta el dominio como suyo.

Ejemplo de configuración#

En este ejemplo, el cliente ACME llamado example gestiona certificados para example.com y www.example.com (tenga en cuenta que los certificados wildcard no están soportados con validación HTTP):

http {

    acme_client example https://acme-v02.api.letsencrypt.org/directory;

    server {

        listen 80; # No es necesario: si nada más escucha en el puerto de
                   # desafío HTTP, el módulo lo abre por sí mismo
                   # (ver la directiva 'acme_http_port')

        listen 443 ssl;

        server_name example.com www.example.com;
        acme example;

        ssl_certificate $acme_cert_example;
        ssl_certificate_key $acme_cert_key_example;
    }
}

Como se mencionó anteriormente, el puerto 80 debe estar abierto para manejar las llamadas HTTP de ACME. Si no hay ningún servidor configurado para escuchar en el puerto de desafío HTTP, el módulo crea un listener dedicado en el puerto 80 (o el configurado en acme_http_port). No se requiere un bloque server separado.

Validación DNS#

La validación es automática, pero requiere registros DNS de su parte. Cuando Angie solicita un certificado, el servidor ACME envía una consulta DNS para un registro TXT bajo el subdominio _acme-challenge. del dominio que se está validando. Angie responde a estas consultas por sí mismo: actúa como servidor de nombres autoritativo para ese subdominio y devuelve el registro TXT que espera el servidor ACME. El servidor ACME compara el registro recibido con el valor que emitió y, si coinciden, acepta el dominio como suyo.

Para que la consulta llegue a Angie, su DNS debe delegar el subdominio _acme-challenge. al servidor Angie, como se muestra a continuación. Angie responde con un TTL muy corto de forma predeterminada; algunos proveedores de DNS filtran las respuestas con un TTL tan bajo, así que auméntelo con acme_dns_ttl si la validación nunca se completa.

La validación DNS demuestra el control de un nombre de dominio, por lo que no puede validar identificadores de dirección IP: Angie ignora una dirección IP en server_name para un cliente configurado con challenge=dns, y registra una advertencia al iniciar. Un bloque server que incluya una dirección IP debe seguir indicando al menos un dominio que el cliente pueda validar.

Nota

El servidor Angie debe ser accesible desde Internet en el puerto UDP 53 (o el especificado en acme_dns_port). Si el servidor está detrás de un firewall, asegúrese de que este puerto esté abierto para conexiones entrantes.

Por ejemplo, para verificar el dominio example.com usando un servidor Angie en la dirección IP 203.0.113.10, la configuración DNS de su dominio debería incluir los siguientes registros:

_acme-challenge.example.com. 60    IN      NS       ns.example.com.
             ns.example.com. 60    IN       A       203.0.113.10

Esta configuración delega la resolución DNS de _acme-challenge.example.com a ns.example.com, asegurando que ns.example.com sea accesible al asignarlo a la dirección IP (203.0.113.10).

Advertencia

La propagación de registros NS puede tardar desde unos minutos hasta 48 horas dependiendo del TTL y del proveedor de DNS. Se recomienda verificar que la configuración sea correcta antes de solicitar un certificado.

Para verificar que el DNS está configurado correctamente, puede usar los siguientes comandos:

$ dig NS _acme-challenge.example.com +short  # Verificar registro NS para el subdominio _acme-challenge

  ns.example.com.

$ dig A ns.example.com +short  # Verificar registro A para el servidor de nombres

  203.0.113.10

$ nc -zv 203.0.113.10 53  # Verificar accesibilidad del servidor DNS en el puerto 53

Este método permite solicitar certificados comodín, por ejemplo, un certificado que incluya la entrada *.example.com en la sección Subject Alternative Name (SAN). Para solicitar explícitamente un certificado para un subdominio, como www.example.com, debe verificar ese subdominio por separado utilizando el método descrito anteriormente.

Advertencia

La aplicabilidad de este escenario depende en gran medida de las capacidades que ofrezca su proveedor de DNS; algunos proveedores no permiten este tipo de configuraciones.

Ejemplo de configuración#

En general, la configuración es similar al ejemplo de la sección anterior. No se necesitan ajustes específicos de HTTP; basta con establecer challenge=dns en la directiva acme_client.

En este ejemplo, el cliente ACME llamado example gestiona certificados para example.com y *.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;
    }
}

Validación ALPN#

La validación se gestiona automáticamente. El servidor ACME se conecta usando TLS y solicita el protocolo acme-tls/1 a través de ALPN. El módulo sirve un certificado temporal para la solicitud de validación.

Para habilitar este método, configure challenge=alpn en la directiva acme_client y asegúrese de que su listener TLS sea accesible en el puerto 443 (o el puerto usado para TLS).

Ejemplo de configuración#

La configuración es similar a las secciones anteriores; basta con establecer challenge=alpn en la directiva acme_client y asegurarse de que el servidor TLS sea accesible en el puerto 443.

En este ejemplo, el cliente ACME llamado example gestiona un certificado para example.com y 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;
    }
}

Validación basada en hooks#

A diferencia de los métodos anteriores, esta validación requiere un esfuerzo adicional. El servidor ACME realiza la validación HTTP o la validación DNS estándar, pero en lugar de interactuar directamente con el servidor Angie, se comunica con un servicio externo gestionado por el servidor Angie mediante llamadas hook (acme_hook). Este servicio configura un servidor DNS o HTTP separado donde el servidor ACME envía sus solicitudes.

Una vez que el servidor ACME recibe la respuesta esperada desde el servidor DNS o HTTP configurado, confirma la propiedad del dominio.

Cuando la emisión o renovación de un certificado requiere la verificación de dominio, Angie genera una solicitud interna al location con nombre que contiene la directiva acme_hook. La forma en que se gestiona esta solicitud depende enteramente de las demás directivas configuradas en el mismo location.

El patrón general es el siguiente:

  1. Cree un location con nombre con la directiva acme_hook.

  2. Configure un manejador de solicitudes en el mismo location utilizando el módulo que mejor se adapte a su configuración: fastcgi_pass para FastCGI, proxy_pass para HTTP, cgi_pass para scripts CGI, etc.

  3. Pase las variables ACME al manejador utilizando el mecanismo que admita, por ejemplo fastcgi_param para FastCGI o cgi_set_var para CGI.

El manejador debe devolver un código de estado 2xx, que puede enviarse mediante la cabecera Status. Cualquier otro código devuelto por el hook add interrumpe el intento de renovación; en el caso del hook remove, el mismo código solo se registra como advertencia y la renovación continúa. La salida del manejador se ignora.

Configuración mínima#

Independientemente del manejador utilizado, el location del hook sigue esta estructura:

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
}

Para la validación DNS, el manejador debe utilizar ACME_HOOK para determinar la acción: cuando es add, crear un registro TXT para _acme-challenge.ACME_DOMAIN con el valor de ACME_KEYAUTH; cuando es remove, eliminar dicho registro.

Ejemplo con FastCGI#

En este ejemplo, el cliente ACME example está configurado para la verificación de dominio usando callbacks DNS, indicado por el parámetro challenge=dns en la directiva acme_client.

El bloque server se aplica a todos los subdominios de example.com (p. ej., *.example.com) y usa el cliente ACME example para gestionar certificados, tal como especifica la directiva acme.

Un bloque location con nombre maneja las llamadas del hook. La directiva acme_hook lo asocia con el cliente ACME example. Las solicitudes del hook se envían a un servidor FastCGI local en el puerto 9000 mediante fastcgi_pass. Las directivas fastcgi_param pasan las variables ACME al servicio externo.

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;
    }
}

El siguiente script en Perl demuestra un servicio FastCGI externo correspondiente:

#!/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);

Aquí, DNS_set_TXT_record() y DNS_clear_TXT_record() son funciones que se supone añaden y eliminan registros TXT en la configuración de un servidor DNS externo que consulta el servidor ACME. Estos registros deben contener los datos proporcionados por el servidor Angie para permitir que el servidor DNS externo pase la validación con éxito, similar al proceso descrito en Validación DNS. Los detalles de implementación de tales funciones están fuera del alcance de esta guía; por ejemplo, los parámetros también pueden pasarse a través de la URI de la solicitud:

# ...

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;
}

Ejemplo con PHP-FPM#

Otro ejemplo, usando PHP-FPM:

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

Los parámetros pasados pueden accederse en PHP a través de $_SERVER['...'].

Ejemplo con CGI y octoDNS#

Aquí el hook es un script ejecutado por el módulo CGI; añade y elimina el registro TXT _acme-challenge mediante octoDNS, que mantiene el DNS como código: una zona se describe en YAML y se sincroniza con un proveedor DNS. Aquí se muestra Cloudflare; cualquier otro proveedor de octoDNS funciona de la misma manera una vez declarado bajo providers y nombrado en targets. El token de la API necesita los permisos Zone:Read, DNS:Read y DNS:Edit para la zona.

El módulo se carga en el contexto main. El cliente ACME example solicita un único certificado para example.com y *.example.com, verificado mediante callbacks DNS según indica el parámetro challenge=dns en la directiva acme_client. Un bloque location con nombre maneja las llamadas del hook: la directiva acme_hook lo asocia con el cliente, las directivas cgi_set_var pasan las variables ACME con los nombres estándar, y cgi_pass ejecuta el script para cada llamada. De esos nombres, este script usa ACME_CHALLENGE, ACME_HOOK, ACME_DOMAIN y 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;
    }
}

octoDNS construye una zona a partir de sus fuentes en orden y las combina, por lo que el hook mantiene el registro del desafío en un directorio de fuente propio, /etc/octodns/acme, y las renovaciones del certificado nunca reescriben el archivo de zona en /etc/octodns/zones. La opción ignore_missing_zones (octoDNS 1.16 y posteriores) permite que /etc/octodns/acme permanezca vacío entre renovaciones. El propio archivo de zona no debe definir registros _acme-challenge.

/etc/octodns/octodns.yaml#
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

El archivo de zona contiene los registros que usted mismo mantiene:

/etc/octodns/zones/example.com.yaml#
'':
  type: A
  value: 203.0.113.10
www:
  type: A
  value: 203.0.113.10

Para comprobar manualmente la configuración de octoDNS, ejecute octodns-sync sin --doit.

El script lee las variables ACME de su entorno y escribe el registro del desafío en la fuente acme:

El script acme-octodns
/usr/share/angie/cgi-bin/acme-octodns#
#!/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}")

En cuanto el hook add devuelve un código 2xx, Angie le pide al servidor ACME que valide el desafío y consulta el resultado durante 60 segundos. Angie nunca espera a que el registro se propague, así que el script se encarga de ello: consulta directamente los servidores de nombres autoritativos de la zona, ya que un resolver puede tener en caché la ausencia del registro, y deja de esperar en cuanto todos ellos entregan el valor, esperando hasta 300 segundos de forma predeterminada.

Si el valor no aparece a tiempo, el script responde Status: 500, y Angie interrumpe el intento y lo reintenta transcurrido el intervalo definido por el parámetro retry_after_error. Angie no llama al hook remove después de un add fallido, así que el script elimina su propio valor antes de responder.

El límite de espera proviene de la variable de entorno ACME_WAIT. El location del hook anterior no la define; para cambiar el límite, añádala ahí, pasando el valor mediante una variable como las demás:

set $acme_wait 600;
cgi_set_var ACME_WAIT $acme_wait;

Establecerlo en 0 desactiva la espera, lo que resulta adecuado para un servidor autoritativo autoalojado donde una escritura de la API es visible de inmediato.

La variable $acme_hook_domain llega sin el prefijo *., por lo que un certificado para example.com y *.example.com genera dos llamadas add para el mismo nombre _acme-challenge.example.com. El script mantiene una lista de valores en un único registro y, en remove, elimina solo el valor de esa llamada; cuando no queda ningún valor, elimina el registro y el archivo que lo contenía en /etc/octodns/acme.

El script se ejecuta como el usuario de los procesos de trabajo de Angie, angie. Necesita acceso de lectura a la configuración de octoDNS, que contiene el token, y acceso de escritura a /etc/octodns/acme; nada más. El proceso CGI no hereda el entorno de Angie, por lo que el token reside en el archivo de configuración y el script usa rutas absolutas.

El error estándar del script va al registro de errores de Angie con el nivel warn, donde se diagnostican los fallos del hook; estos mensajes aparecen solo cuando el nivel de registro es warn o menos severo.

Para poner todo esto en marcha en un sistema basado en Debian:

  1. Instale el módulo CGI:

    $ sudo apt-get install angie-module-cgi
    
  2. Instale octoDNS y el proveedor de Cloudflare en un entorno virtual propio:

    $ sudo apt-get install python3-venv
    $ sudo python3 -m venv /opt/octodns
    $ sudo /opt/octodns/bin/pip install 'octodns>=1.16' octodns-cloudflare
    
  3. Cree los directorios; el paquete del módulo no crea /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
    
  4. Cree /etc/octodns/octodns.yaml y /etc/octodns/zones/example.com.yaml como se muestra arriba y, a continuación, restrinja la configuración al grupo angie, ya que contiene el token de la API:

    $ sudo chgrp angie /etc/octodns/octodns.yaml
    $ sudo chmod 640 /etc/octodns/octodns.yaml
    
  5. Guarde el script mostrado arriba como acme-octodns y, a continuación, instálelo:

    $ sudo install -m 755 acme-octodns /usr/share/angie/cgi-bin/
    
  6. Añada la configuración mostrada arriba, pruébela y recargue Angie:

    $ sudo angie -t
    $ sudo kill -HUP $(cat /run/angie.pid)
    

    El cliente solicita el certificado inmediatamente después de la recarga; su estado y el estado del certificado aparecen en la sección de la API /status/http/acme_clients/, y el certificado emitido se guarda en el directorio de almacenamiento del cliente.

ACME en el módulo stream#

El módulo Stream ACME permite la emisión y uso automatizado de certificados para tráfico TCP. Para que funcione correctamente, primero debe configurar su contraparte HTTP: el cliente ACME debe declararse en el contexto http, y el bloque stream en sí debe colocarse después del bloque http en la configuración.

Ejemplo de configuración#

Por defecto, se utiliza el modo de validación HTTP para obtener certificados. Como se mencionó en la sección Validación HTTP, esto requiere un servidor HTTP escuchando en el puerto 80:

# Parte HTTP
http {

    # Cliente ACME para la parte stream
    acme_client example https://acme-v02.api.letsencrypt.org/directory;

    # Servidor para validación HTTP
    server {

        listen 80;
        return 444;
    }
}

# Parte Stream
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; # referencia al cliente ACME definido en la parte HTTP
    }

    upstream backend_upstream {

        server 127.0.0.1:54321;
    }
}

También puede usar la validación DNS configurando challenge=dns en la directiva acme_client; en ese caso, no se necesitará el servidor.

Migración desde certbot#

Si anteriormente usaba certbot para obtener y renovar certificados SSL de Let's Encrypt antes de migrar de nginx a Angie, siga estos pasos para pasar a usar nuestro módulo ACME.

Supongamos que configuró certificados de la siguiente manera:

$ sudo certbot --nginx -d example.com -d www.example.com

La configuración creada automáticamente por este comando suele encontrarse en /etc/nginx/sites-available/example.conf y se parece a lo siguiente:

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;
}

En el ejemplo anterior, las líneas resaltadas deben modificarse. Dependiendo de sus circunstancias y preferencias, configure la validación HTTP o la validación DNS usando el módulo ACME.

La configuración de Angie resultante podría verse así:

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;
    }
}

Recuerde recargar la configuración después de hacer cambios:

$ sudo kill -HUP $(cat /run/angie.pid)

Una vez haya verificado que esta configuración funciona, puede eliminar los certificados de certbot y deshabilitar o eliminar completamente certbot del servidor si ya no se usa en otro lugar, por ejemplo:

$ sudo rm -rf /etc/letsencrypt

$ sudo systemctl stop certbot.timer
$ sudo systemctl disable certbot.timer
$ # -- o --
$ sudo rm /etc/cron.d/certbot

$ sudo apt remove certbot
$ # -- o --
$ sudo dnf remove certbot