Configuração ACME#

O módulo ACME no Angie permite a aquisição automática de certificados usando o protocolo ACME. O protocolo ACME suporta vários métodos de verificação de domínio (também chamados de "validação"); este módulo implementa validação HTTP, validação DNS, validação ALPN e validação baseada em hooks através de um serviço externo personalizado.

Etapas de configuração#

Etapas gerais para habilitar solicitações de certificado na configuração:

  • Configure um cliente ACME no bloco http usando a diretiva acme_client, que especifica um nome único de cliente e outros parâmetros. Múltiplos clientes ACME podem ser configurados.

  • Especifique os domínios para os quais os certificados são solicitados: Um único certificado será emitido para todos os nomes de domínio listados nas diretivas server_name dentro de todos os blocos server que usam diretivas acme apontando para o mesmo cliente ACME.

    Endereços IP (IPv4 ou IPv6) especificados em server_name também são identificadores de certificado válidos, exceto com validação DNS.

    Observe que autoridades certificadoras públicas podem emitir certificados para endereços IP apenas sob perfis de certificado específicos; por exemplo, a Let's Encrypt atualmente exige um perfil que permita identificadores IP (consulte sua documentação de perfis). Para solicitar um, defina o parâmetro profile= de acme_client.

  • Configure o tratamento de solicitações e callbacks ACME: Isso é necessário para verificar a propriedade do domínio. A configuração depende do método de validação de domínio escolhido:

    Método

    Requisitos do Usuário

    Multi-domínio

    Domínios Wildcard

    Validação HTTP

    Abrir a porta 80 (ou a especificada em acme_http_port) para conexões de entrada no servidor Angie.

    ✔

    Validação DNS

    Abrir a porta 53 (ou a especificada em acme_dns_port) para conexões de entrada no servidor Angie.

    Definir um registro NS para o subdomínio _acme-challenge. apontando para seu servidor Angie.

    ✔

    ✔

    Validação ALPN

    Abrir a porta 443 (ou a porta TLS usada pelo seu servidor) para conexões de entrada no servidor Angie.

    ✔

    Validação por hook

    Criar um serviço externo (script ou aplicação) que possa, mediante solicitação do Angie, atualizar registros DNS ou servir uma resposta especial via servidor web.

    ✔

    ✔

  • Configure SSL usando o certificado e chave obtidos: O módulo torna certificados e chaves disponíveis como variáveis incorporadas que podem ser usadas na configuração para preencher ssl_certificate e ssl_certificate_key.

    Para instruções de configuração SSL, consulte Configuração SSL.

Dica

O processo de aquisição e renovação de certificados depende de muitos serviços e pode levar algum tempo. Seja paciente, e se encontrar problemas ou tiver dúvidas, verifique o log de depuração.

Detalhes de implementação#

Chaves de cliente e certificados são armazenados em codificação PEM dentro de subdiretórios do diretório especificado pela --http-acme-client-path opção de compilação:

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

  account.key  certificate.pem  private.key

Nota

Esses arquivos persistem no disco entre reinicializações. Na inicialização, o cliente reutiliza um certificado armazenado que ainda é válido em vez de solicitar um novo, evitando o atraso de emissão e requisições desnecessárias à CA, que podem estar sujeitas a limites de taxa. Em um contêiner, coloque o diretório de armazenamento (por padrão /var/lib/angie/acme/) em um volume persistente para que os certificados emitidos sobrevivam à recriação do contêiner; consulte executando o Angie em um contêiner.

Nota

Recarregar a configuração (angie -s reload) reavalia cada cliente ACME em relação aos certificados armazenados em disco. Um cliente cujo certificado não é válido no momento -- incluindo um cuja solicitação anterior terminou em erro -- é solicitado novamente de imediato, independentemente de retry_after_error. O atraso de repetição (e retry_after_error=off) aplica-se apenas enquanto o Angie continua em execução; não sobrevive a uma recarga. Portanto, recarregar repetidamente enquanto uma solicitação continua falhando pode enviar solicitações frequentes à CA, sujeitas aos limites de taxa mencionados acima.

O cliente ACME requer uma conta no servidor CA. Para criar e gerenciar esta conta, o cliente usa uma chave privada (account.key). Se nenhuma chave existir, ela é gerada na inicialização. O cliente então usa esta chave para registrar a conta no servidor.

Nota

Se você já possui uma chave de conta, coloque-a no subdiretório do cliente antes de iniciar para reutilizar a conta. Alternativamente, especifique o arquivo de chave usando o parâmetro account_key em acme_client.

Algumas CAs exigem que a conta ACME seja vinculada a uma conta externa existente por meio de External Account Binding (EAB) antes de emitir certificados. Para habilitar isso, configure o parâmetro eab de acme_client com o identificador de chave e a chave MAC fornecidos pela CA; consulte a descrição da diretiva para a sintaxe exata.

O cliente ACME também usa uma chave separada (private.key) para Solicitações de Assinatura de Certificado (CSRs). Esta chave de certificado é automaticamente criada na inicialização se necessário.

Na inicialização, o cliente solicita um certificado se um não existir, assinando e enviando um CSR para todos os domínios sob seu gerenciamento para o servidor CA. O servidor verifica a propriedade do domínio usando validação HTTP ou validação DNS e emite um certificado, que o cliente salva localmente (certificate.pem).

Como mencionado anteriormente, um único certificado cobre todos os nomes de domínio gerenciados pelo mesmo cliente ACME, potencialmente resultando em um certificado multi-domínio. A lista de todos os nomes cobertos pelo certificado pode ser encontrada na seção Subject Alternative Name (SAN) do certificado obtido. Para verificar isso pela linha de comando:

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

Quando um certificado está prestes a expirar ou a lista de domínios muda, o cliente assina e envia outro CSR para o servidor CA. O servidor re-verifica a propriedade e emite um novo certificado, que o cliente instala localmente, substituindo o anterior.

Na configuração, o certificado obtido e sua chave correspondente estão disponíveis através das variáveis de prefixo $acme_cert_<name> e $acme_cert_key_<name>. Seus valores são o conteúdo dos respectivos arquivos, que devem ser usados com as diretivas ssl_certificate e 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

O cliente ACME resolve o nome do host do servidor CA por meio do resolver do mesmo contexto, que por padrão consulta os servidores DNS listados em /etc/resolv.conf; configure a diretiva explicitamente apenas para usar outros servidores ou parâmetros. Em um host sem conectividade IPv6, o resolver ainda pode emitir consultas AAAA (IPv6) para a CA e falhar ao conectar; desabilite-as com o parâmetro ipv6=off, por exemplo resolver conf ipv6=off;.

Coleta de domínios versus uso do certificado#

A diretiva acme serve apenas para coletar nomes de domínio para solicitações de certificado. Ela não controla onde o certificado pode ser usado: qualquer bloco server pode referenciar o certificado obtido através da variável $acme_cert_<name>, independentemente de o bloco conter ou não uma diretiva acme.

Por exemplo, se você tem um bloco server com curinga que já cobre todos os subdomínios, blocos server adicionais para subdomínios específicos não precisam da diretiva 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 domínios#

Para controlar o conjunto exato de nomes de domínio em um certificado sem depender da coleta automática a partir de todos os blocos server, crie um bloco server dedicado que contenha apenas as diretivas server_name e acme. Para evitar que esse bloco trate tráfego real, vincule-o a um socket de domínio 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;
}

Outros blocos server podem então usar o certificado através da variável $acme_cert_<name> sem afetar quais domínios são solicitados.

Certificados separados para domínios diferentes#

Cada acme_client gerencia um único certificado. Para obter vários certificados independentes (por exemplo, para domínios não relacionados que não devem compartilhar um único certificado), configure um acme_client separado para cada um no bloco http e aponte a diretiva acme de cada bloco server para o cliente correspondente pelo nome:

http {

    # Dois clientes independentes, cada um gerenciando seu próprio 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;
    }
}

Validação HTTP#

A validação é automática. Quando o Angie solicita um certificado, o servidor ACME requisita um arquivo de token via HTTP ao Angie no endereço /.well-known/acme-challenge/<TOKEN>. O próprio Angie responde a essas solicitações, então você não precisa servir esse caminho. Em seguida, o servidor ACME compara o token recebido com o que emitiu e, se coincidirem, aceita o domínio como seu.

Exemplo de configuração#

Neste exemplo, o cliente ACME chamado example gerencia certificados para example.com e www.example.com (note que certificados wildcard não são suportados com validação HTTP):

http {

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

    server {

        listen 80; # Não é necessário: se nada mais escutar na porta
                   # de desafio HTTP, o módulo a abre por conta própria
                   # (veja a diretiva '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 observado anteriormente, a porta 80 deve estar aberta para tratar chamadas HTTP ACME. Se nenhum servidor estiver configurado para escutar na porta de desafio HTTP, o módulo cria um listener dedicado na porta 80 (ou na definida em acme_http_port). Um bloco server separado não é necessário.

Validação DNS#

A validação é automática, mas exige registros DNS do seu lado. Quando o Angie solicita um certificado, o servidor ACME envia uma consulta DNS por um registro TXT sob o subdomínio _acme-challenge. do domínio sendo validado. O Angie responde a essas consultas por conta própria: ele atua como o servidor de nomes autoritativo para esse subdomínio e retorna o registro TXT que o servidor ACME espera. O servidor ACME compara o registro recebido com o valor emitido e, se coincidirem, aceita o domínio como seu.

Para que a consulta chegue ao Angie, seu DNS deve delegar o subdomínio _acme-challenge. ao servidor Angie, conforme mostrado abaixo. Por padrão, o Angie responde com um TTL muito baixo; alguns provedores de DNS filtram respostas com um TTL tão baixo, portanto aumente-o com acme_dns_ttl se a validação nunca for concluída.

A validação DNS comprova o controle de um nome de domínio, portanto não pode validar identificadores de endereço IP: o Angie ignora um endereço IP em server_name para um cliente configurado com challenge=dns, registrando um aviso na inicialização. Um bloco server que lista um endereço IP ainda deve nomear pelo menos um domínio que o cliente possa validar.

Nota

O servidor Angie deve estar acessível pela internet na porta UDP 53 (ou na especificada em acme_dns_port). Se o servidor estiver atrás de um firewall, certifique-se de que esta porta esteja aberta para conexões de entrada.

Por exemplo, para verificar o domínio example.com usando um servidor Angie no endereço IP 203.0.113.10, a configuração DNS do seu domínio deve incluir os seguintes registros:

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

Esta configuração delega a resolução DNS para _acme-challenge.example.com para ns.example.com, garantindo que ns.example.com seja acessível mapeando-o para o endereço IP (203.0.113.10).

Aviso

A propagação de registros NS pode levar de alguns minutos a 48 horas dependendo do TTL e do provedor DNS. É recomendado verificar se a configuração está correta antes de solicitar um certificado.

Para verificar se o DNS está configurado corretamente, você pode usar os seguintes comandos:

$ dig NS _acme-challenge.example.com +short  # Verifica o registro NS para o subdomínio _acme-challenge

  ns.example.com.

$ dig A ns.example.com +short  # Verifica o registro A para o servidor de nomes

  203.0.113.10

$ nc -zv 203.0.113.10 53  # Verifica a acessibilidade do servidor DNS na porta 53

Este método permite solicitar certificados wildcard, por exemplo, um certificado que inclui a entrada *.example.com na seção Subject Alternative Name (SAN). Para solicitar explicitamente um certificado para um subdomínio, como www.example.com, você deve verificar separadamente esse subdomínio usando o método descrito acima.

Aviso

A aplicabilidade deste cenário depende amplamente das capacidades fornecidas pelo seu provedor DNS; alguns provedores não permitem tais configurações.

Exemplo de configuração#

No geral, a configuração é similar ao exemplo da seção anterior. Não há necessidade de configurações específicas do HTTP; em vez disso, é suficiente definir challenge=dns para a diretiva acme_client.

Neste exemplo, o cliente ACME chamado example gerencia certificados para example.com e *.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;
    }
}

Validação ALPN#

A validação é tratada automaticamente. O servidor ACME se conecta usando TLS e solicita o protocolo acme-tls/1 via ALPN. O módulo serve um certificado temporário para a solicitação de validação.

Para habilitar este método, configure challenge=alpn na diretiva acme_client e garanta que seu listener TLS esteja acessível na porta 443 (ou a porta usada para TLS).

Exemplo de configuração#

A configuração é similar às seções anteriores; basta definir challenge=alpn para a diretiva acme_client e garantir que o servidor TLS esteja acessível na porta 443.

Neste exemplo, o cliente ACME chamado example gerencia um certificado para example.com e 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;
    }
}

Validação baseada em hooks#

Diferentemente dos métodos anteriores, esta validação requer esforço adicional. O servidor ACME executa validação HTTP padrão ou validação DNS, mas em vez de interagir diretamente com o servidor Angie, ele se comunica com um serviço externo gerenciado pelo servidor Angie usando chamadas de hook (acme_hook). Este serviço configura um servidor DNS ou HTTP separado onde o servidor ACME envia suas solicitações.

Uma vez que o servidor ACME recebe a resposta esperada do servidor DNS ou HTTP configurado, ele confirma a propriedade do domínio.

Quando a emissão ou renovação de certificado requer verificação de domínio, o Angie gera uma solicitação interna para o location nomeado que contém a diretiva acme_hook. Como essa solicitação é tratada depende inteiramente das outras diretivas configuradas no mesmo location.

O padrão geral é:

  1. Crie um location nomeado com a diretiva acme_hook.

  2. Configure um manipulador de solicitações no mesmo location usando o módulo que melhor se adequar à sua configuração: fastcgi_pass para FastCGI, proxy_pass para HTTP, cgi_pass para scripts CGI, etc.

  3. Passe as variáveis ACME para o manipulador usando o mecanismo que ele suporta, por exemplo fastcgi_param para FastCGI ou cgi_set_var para CGI.

O manipulador deve retornar um código de status 2xx, que pode ser enviado via o cabeçalho Status. Qualquer outro código do hook add interrompe a tentativa de renovação; no caso do hook remove, o mesmo código é apenas registrado como aviso, e a renovação continua. A saída do manipulador é ignorada.

Configuração mínima#

Independentemente do manipulador utilizado, o location do hook segue esta estrutura:

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 validação DNS, o manipulador deve usar ACME_HOOK para determinar a ação: quando ele for add, crie um registro TXT para _acme-challenge.ACME_DOMAIN com o valor de ACME_KEYAUTH; quando for remove, exclua esse registro.

Exemplo com FastCGI#

Neste exemplo, o cliente ACME example está configurado para verificação de domínio usando callbacks DNS, indicado pelo parâmetro challenge=dns na diretiva acme_client.

O bloco server se aplica a todos os subdomínios de example.com (por exemplo, *.example.com) e usa o cliente ACME example para gerenciar certificados, conforme especificado pela diretiva acme.

Um bloco location nomeado trata as chamadas de hook. A diretiva acme_hook o associa ao cliente ACME example. As solicitações de hook são enviadas para um servidor FastCGI local na porta 9000 usando fastcgi_pass. As diretivas fastcgi_param passam as variáveis ACME para o serviço 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;
    }
}

O seguinte script Perl demonstra um serviço FastCGI externo correspondente:

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

Aqui, DNS_set_TXT_record() e DNS_clear_TXT_record() são funções assumidas para adicionar e remover registros TXT na configuração de um servidor DNS externo que o servidor ACME consulta. Estes registros devem conter os dados fornecidos pelo servidor Angie para permitir que o servidor DNS externo passe com sucesso na validação, similar ao processo descrito em Validação DNS. Os detalhes de implementação de tais funções estão além do escopo deste guia; por exemplo, parâmetros também podem ser passados através da URI da solicitação:

# ...

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

Exemplo com PHP-FPM#

Outro exemplo, 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
# ...

Parâmetros passados podem ser acessados em PHP via $_SERVER['...'].

Exemplo com CGI e octoDNS#

Aqui o hook é um script executado pelo módulo CGI; ele adiciona e remove o registro TXT _acme-challenge por meio do octoDNS, que mantém o DNS como código: uma zona é descrita em YAML e sincronizada com um provedor DNS. O Cloudflare é usado aqui como exemplo; qualquer outro provedor octoDNS funciona da mesma forma, uma vez declarado em providers e nomeado em targets. O token da API precisa das permissões Zone:Read, DNS:Read e DNS:Edit para a zona.

O módulo é carregado no contexto main. O cliente ACME example solicita um único certificado para example.com e *.example.com, verificado por meio de callbacks DNS, conforme indicado pelo parâmetro challenge=dns na diretiva acme_client. Um bloco location nomeado trata as chamadas de hook: a diretiva acme_hook o associa ao cliente, as diretivas cgi_set_var passam as variáveis ACME sob os nomes padrão, e cgi_pass executa o script para cada chamada. Dentre esses nomes, este script usa ACME_CHALLENGE, ACME_HOOK, ACME_DOMAIN e 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;
    }
}

O octoDNS constrói uma zona a partir de suas origens, em ordem, e as mescla; por isso o hook mantém o registro do desafio em um diretório de origem próprio, /etc/octodns/acme, e as renovações de certificado nunca reescrevem o arquivo de zona em /etc/octodns/zones. A opção ignore_missing_zones (octoDNS 1.16 e posteriores) permite que /etc/octodns/acme permaneça vazio entre renovações. O próprio arquivo de zona não deve 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

O arquivo de zona contém os registros que você mesmo mantém:

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

Para verificar manualmente a configuração do octoDNS, execute octodns-sync sem --doit.

O script lê as variáveis ACME do seu ambiente e grava o registro do desafio na origem acme:

O 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}")

Assim que o hook add retorna um código 2xx, o Angie solicita ao servidor ACME que valide o desafio e verifica repetidamente o resultado durante 60 segundos. O Angie nunca espera a propagação do registro; é o próprio script que faz isso. Ele consulta diretamente os servidores de nomes autoritativos da zona, já que um resolver pode ter armazenado em cache a ausência do registro, e retorna assim que cada um deles estiver servindo o valor, aguardando até 300 segundos por padrão.

Se o valor não aparecer a tempo, o script responde com Status: 500, e o Angie interrompe a tentativa e a repete após o intervalo definido pelo parâmetro retry_after_error. O Angie não chama o hook remove após uma falha do add, então o script exclui seu próprio valor antes de responder.

O limite de espera vem da variável de ambiente ACME_WAIT. O location do hook acima não a define; para alterar o limite, adicione-a ali, passando o valor por meio de uma variável como as demais:

set $acme_wait 600;
cgi_set_var ACME_WAIT $acme_wait;

Defini-lo como 0 desativa a espera, o que é adequado para um servidor autoritativo auto-hospedado, onde uma gravação via API fica visível imediatamente.

A variável $acme_hook_domain chega sem o prefixo *., então um certificado para example.com e *.example.com produz duas chamadas de add para o mesmo nome _acme-challenge.example.com. O script mantém uma lista de valores em um único registro e, em remove, remove apenas o valor daquela chamada; quando não sobra nenhum valor, ele exclui o registro e o arquivo que o continha em /etc/octodns/acme.

O script é executado como o usuário dos processos worker do Angie, angie. Ele precisa de acesso de leitura à configuração do octoDNS, que contém o token, e de acesso de gravação a /etc/octodns/acme; nada além disso. O processo CGI não herda o ambiente do Angie, por isso o token fica no arquivo de configuração e o script usa caminhos absolutos.

A saída de erro padrão do script vai para o log de erro do Angie no nível warn, onde as falhas do hook são diagnosticadas; essas mensagens aparecem apenas quando o nível de log é warn ou menos severo.

Para colocar tudo isso em prática em um sistema baseado em Debian:

  1. Instale o módulo CGI:

    $ sudo apt-get install angie-module-cgi
    
  2. Instale o octoDNS e o provedor Cloudflare em um ambiente virtual próprio:

    $ sudo apt-get install python3-venv
    $ sudo python3 -m venv /opt/octodns
    $ sudo /opt/octodns/bin/pip install 'octodns>=1.16' octodns-cloudflare
    
  3. Crie os diretórios; o pacote do módulo não cria /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. Crie /etc/octodns/octodns.yaml e /etc/octodns/zones/example.com.yaml como mostrado acima, depois restrinja a configuração ao grupo angie, já que ela contém o token da API:

    $ sudo chgrp angie /etc/octodns/octodns.yaml
    $ sudo chmod 640 /etc/octodns/octodns.yaml
    
  5. Salve o script mostrado acima como acme-octodns, depois instale-o:

    $ sudo install -m 755 acme-octodns /usr/share/angie/cgi-bin/
    
  6. Adicione a configuração mostrada acima, depois teste-a e recarregue o Angie:

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

    O cliente solicita o certificado imediatamente após a recarga; seu estado e o status do certificado aparecem na seção da API /status/http/acme_clients/, e o certificado emitido é salvo no diretório de armazenamento do cliente.

ACME no módulo stream#

O módulo stream ACME habilita emissão automatizada de certificados e uso para tráfego TCP. Para que funcione corretamente, você deve primeiro configurar sua contraparte HTTP: o cliente ACME deve ser declarado no contexto http, e o próprio bloco stream deve ser colocado após o bloco http na configuração.

Exemplo de configuração#

Por padrão, o modo de validação HTTP é usado para obter certificados. Como mencionado na seção Validação HTTP, isso requer um servidor HTTP escutando na porta 80:

# Parte HTTP
http {

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

    # Servidor para validação 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; # referência ao cliente ACME definido na parte HTTP
    }

    upstream backend_upstream {

        server 127.0.0.1:54321;
    }
}

Você também pode usar validação DNS configurando challenge=dns na diretiva acme_client; nesse caso, o servidor não será necessário.

Migrando do certbot#

Se você usou anteriormente o certbot para obter e renovar certificados SSL do Let's Encrypt antes de migrar do nginx para o Angie, siga estes passos para fazer a transição para usar nosso módulo ACME.

Suponha que você configurou os certificados da seguinte forma:

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

A configuração criada automaticamente por este comando está tipicamente localizada em /etc/nginx/sites-available/example.conf e se parece com algo assim:

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

No exemplo acima, as linhas destacadas precisam ser modificadas. Dependendo das suas circunstâncias e preferências, configure a validação HTTP ou a validação DNS usando o módulo ACME.

A configuração do Angie resultante pode se parecer com isto:

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

Lembre-se de recarregar a configuração após fazer as alterações:

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

Uma vez que você tenha verificado que esta configuração funciona, você pode excluir os certificados do certbot e desabilitar ou remover o certbot inteiramente do servidor se ele não for mais usado em outro lugar, por exemplo:

$ sudo rm -rf /etc/letsencrypt

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

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