Ansible SSL: Certificados Let's Encrypt Automáticos com AWX
Você já acordou às 3 da manhã com o alerta de que um certificado SSL expirou em produção? Ou descobriu que um cliente tentou acessar o site e viu a tela vermelha do browser dizendo “conexão não é segura”? Esse é o custo de gerenciar SSL na mão.
Neste lab eu automatizei o ciclo completo de criação e renovação de certificados SSL para três sites diferentes — fxshell1.com.br, fxshell2.com.br e fxshell3.com.br — usando Ansible + Certbot + Let’s Encrypt. Cada certificado cobre o domínio raiz e o www no mesmo cert (SAN). O AWX agenda a renovação automática toda terça-feira às 03:00 UTC; quando disparado manualmente, um survey pergunta quais tenants renovar antes de executar. Nenhum clique desnecessário, nenhuma surpresa.
Objetivo do Lab
Construir uma automação completa que:
- Instala e configura o Certbot em múltiplos servidores
- Emite um único certificado cobrindo
dominio.com.brewww.dominio.com.br(SAN — Subject Alternative Names) - Configura o Nginx para servir o desafio ACME e depois ativa HTTPS com TLS 1.3 + HSTS
- Agenda renovação automática no AWX toda terça-feira às 03:00 UTC
- Quando executado manualmente, pergunta via survey quais tenants renovar
Tecnologias Utilizadas
Ansible é a ferramenta de automação que conecta via SSH nos servidores e aplica as configurações descritas em playbooks YAML. Sem agente instalado, sem daemon. Um comando no control node e todos os servidores ficam no estado desejado. Usado massivamente em times DevOps para garantir que a infraestrutura seja reproduzível e auditável.
Certbot é o cliente oficial do protocolo ACME, mantido pela Electronic Frontier Foundation. Ele faz a comunicação com a CA do Let’s Encrypt: prova que você controla o domínio (através de um arquivo temporário no servidor), recebe o certificado assinado, e salva tudo no /etc/letsencrypt/live/<domínio>/. No lab ele é instalado como snap — a forma recomendada atualmente, que sempre traz a versão mais recente.
Let’s Encrypt é uma Autoridade Certificadora (CA) gratuita e automatizada, mantida por Mozilla, Cisco, EFF e outros. Emite certificados X.509 válidos por 90 dias para qualquer domínio que você consiga provar que controla. A curta validade é intencional — força automação e garante que certificados comprometidos expirem rápido.
ACME v2 é o protocolo que Let’s Encrypt usa para emissão automatizada. No método webroot, o Certbot cria um arquivo temporário em /.well-known/acme-challenge/ no seu servidor web. A CA do Let’s Encrypt faz uma requisição HTTP para esse arquivo e, se conseguir ler, confirma que você controla o domínio e emite o certificado.
Nginx é o servidor web que fica na frente da aplicação. No lab ele tem duas responsabilidades: servir o diretório do desafio ACME durante a validação, e depois servir HTTPS com o certificado emitido. Configuramos TLS 1.2/1.3, HSTS, OCSP Stapling e headers de segurança.
AWX é a versão open source do Ansible Automation Platform (Red Hat Tower). Ele fornece uma interface web e API para executar playbooks Ansible de forma centralizada, com controle de acesso, logs históricos, e — o que mais importa aqui — schedules (cron jobs) para execução automática de jobs.
Arquitetura
┌─────────────────────────────────────────────────────────────┐
│ AWX / Ansible Tower │
│ Job Template: SSL Create Job Template: SSL Renewal │
│ Schedule: RRULE FREQ=DAILY às 03:00 UTC │
└──────────────────────────┬──────────────────────────────────┘
│ Trigger / SSH
▼
┌─────────────────────────────────────────────────────────────┐
│ Ansible Control Node │
│ playbook-ssl-create.yml │
│ playbook-ssl-renewal.yml │
│ roles/ssl_certbot roles/ssl_renewal │
└────────┬──────────────────┬────────────────────┬────────────┘
│ SSH │ SSH │ SSH
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ fxshell1.com │ │ fxshell2.com.br │ │ fxshell3.com.br│
│ Nginx + SSL │ │ Nginx + SSL │ │ Nginx + SSL │
│ 192.168.1.10 │ │ 192.168.1.11 │ │ 192.168.1.12 │
└──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└───────────────────┴──────────────────────┘
│ ACME HTTP Challenge
▼
┌────────────────────────┐
│ Let's Encrypt CA │
│ ACME v2 Protocol │
│ Certificado 90 dias │
└────────────────────────┘
| Componente | Função |
|---|---|
| AWX | Orquestra execução, agenda renovação diária, guarda histórico |
| Ansible Control Node | Executa roles, conecta nos servidores via SSH |
Role ssl_certbot |
Instala Certbot, configura Nginx HTTP, emite certificado, sobe HTTPS |
Role ssl_renewal |
Verifica validade, renova se <30 dias, registra log |
| Nginx | Serve ACME challenge, proxy HTTPS com TLS 1.3 + HSTS |
| Let’s Encrypt CA | Valida domínio via ACME, emite certificado X.509 gratuito |

Como o Fluxo Funciona
O AWX dispara o playbook ssl-create.yml. O Ansible conecta em cada servidor via SSH e executa a role ssl_certbot em sequência:
- Instala o Certbot via snap e cria o symlink em
/usr/bin/certbot - Garante que o Nginx está instalado e rodando
- Faz o deploy do template
nginx-http.conf.j2— configuração HTTP simples que serve o diretório.well-known/acme-challenge/e redireciona todo o resto para HTTPS - Verifica se o certificado já existe em
/etc/letsencrypt/live/<domínio>/ - Se não existir, executa
certbot certonly --webroot— o Certbot cria o arquivo de desafio, a CA do Let’s Encrypt faz o GET HTTP para validar, e o certificado é emitido - Faz o deploy do template
nginx-ssl.conf.j2— configuração HTTPS completa com TLS 1.2/1.3, HSTS, OCSP Stapling e security headers - Verifica a data de expiração do certificado emitido com
openssl x509 -enddate
Para renovação, o job ssl-renewal.yml roda diariamente. Para cada servidor, ele lê a data de expiração do certificado atual, calcula os dias restantes, e só executa certbot renew se faltarem menos de 30 dias. Se renovou, notifica o handler para recarregar o Nginx.
Estrutura do Projeto
ansible-ssl-letsencrypt/
├── ansible.cfg # Config: inventory, become, pipelining
├── playbook-ssl-create.yml # Playbook principal: emite certificados
├── playbook-ssl-renewal.yml # Playbook de renovação: usado pelo AWX
├── awx-job-template.yml # Definição dos Job Templates e Schedule
├── inventory/
│ └── hosts.yml # 3 hosts com site_domain, email, webroot
├── group_vars/
│ └── all.yml # Certbot config, Nginx SSL params globais
├── roles/
│ ├── ssl_certbot/
│ │ ├── tasks/main.yml # Instala, configura Nginx, emite cert
│ │ ├── handlers/main.yml # restart/reload nginx
│ │ └── templates/
│ │ ├── nginx-http.conf.j2 # Config HTTP + ACME challenge
│ │ └── nginx-ssl.conf.j2 # Config HTTPS + TLS 1.3 + HSTS
│ └── ssl_renewal/
│ ├── tasks/main.yml # Verifica validade, renova, loga
│ ├── handlers/main.yml # reload nginx após renovação
│ └── templates/
│ └── renewal-report.j2 # Relatório de renovação
└── diagrama/
├── ansible-ssl-letsencrypt.html # Diagrama animado interativo
├── ansible-ssl-letsencrypt.gif # GIF para blog e LinkedIn
└── gerar_gif.py # Script Pillow que gerou o GIF
Inventário — Os 3 Sites
O inventário define os três servidores. Cada host tem site_domain (nome principal, usado como --cert-name pelo Certbot) e site_domains (lista com raiz e www, usada tanto no certbot certonly quanto nos server_name do Nginx).
# inventory/hosts.yml
all:
children:
webservers:
hosts:
fxshell1_server:
ansible_host: 192.168.1.10
ansible_user: ubuntu
site_domain: fxshell1.com.br # cert-name no Certbot
site_domains: # SAN: raiz + www no mesmo cert
- fxshell1.com.br
- www.fxshell1.com.br
site_email: admin@fxshell1.com.br
site_webroot: /var/www/fxshell
nginx_config_name: fxshell1.com.br
fxshell2_server:
ansible_host: 192.168.1.11
ansible_user: ubuntu
site_domain: fxshell2.com.br
site_domains:
- fxshell2.com.br
- www.fxshell2.com.br
site_email: admin@fxshell2.com.br
site_webroot: /var/www/fxshell2
nginx_config_name: fxshell2.com.br
fxshell3_server:
ansible_host: 192.168.1.12
ansible_user: ubuntu
site_domain: fxshell3.com.br
site_domains:
- fxshell3.com.br
- www.fxshell3.com.br
site_email: admin@fxshell3.com.br
site_webroot: /var/www/fxshell3
nginx_config_name: fxshell3.com.br
O Certbot aceita múltiplos --domain num único comando. O primeiro domínio da lista vira o nome do certificado (/etc/letsencrypt/live/fxshell1.com.br/), e os demais entram como SANs. O resultado é um único arquivo .pem válido para fxshell1.com.br e www.fxshell1.com.br — sem precisar de dois certificados separados.
Role ssl_certbot — O Coração da Emissão
A task mais importante da role verifica se o certificado já existe antes de tentar emitir. Isso garante idempotência — você pode rodar o playbook quantas vezes quiser sem gerar requisições desnecessárias para a CA (o Let’s Encrypt tem rate limits).
O comando usa um loop Jinja2 para gerar múltiplos --domain, cobrindo dominio.com.br e www.dominio.com.br num único certificado SAN:
# roles/ssl_certbot/tasks/main.yml (trecho)
- name: Verificar se certificado já existe
stat:
path: "{{ certbot_certs_dir }}/{{ site_domain }}/fullchain.pem"
register: cert_exists
- name: Obter certificado SSL via certbot (webroot — raiz + www)
command: >
certbot certonly
--webroot
--webroot-path {{ site_webroot }}
{% for d in site_domains %}--domain {{ d }} {% endfor %}
--email {{ site_email }}
--agree-tos
--non-interactive
{% if certbot_staging %}--staging{% endif %}
--rsa-key-size {{ certbot_rsa_key_size }}
--keep-until-expiring
when: not cert_exists.stat.exists
Para fxshell1_server, o comando expandido fica:
certbot certonly --webroot --webroot-path /var/www/fxshell \
--domain fxshell1.com.br --domain www.fxshell1.com.br \
--email admin@fxshell1.com.br --agree-tos --non-interactive \
--rsa-key-size 4096 --keep-until-expiring
O flag --keep-until-expiring garante que, mesmo se o certificado já existir, o Certbot não vai renovar antes de 30 dias. O flag --staging permite testar o fluxo completo sem consumir os rate limits de produção do Let’s Encrypt.
Template Nginx HTTPS
O template nginx-ssl.conf.j2 gera uma config com as melhores práticas de TLS:
server {
listen 443 ssl http2;
server_name {{ site_domain }} www.{{ site_domain }};
ssl_certificate {{ certbot_certs_dir }}/{{ site_domain }}/fullchain.pem;
ssl_certificate_key {{ certbot_certs_dir }}/{{ site_domain }}/privkey.pem;
ssl_trusted_certificate {{ certbot_certs_dir }}/{{ site_domain }}/chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:...;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
# HSTS: força HTTPS por 1 ano em todos os subdomínios
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
}
O OCSP Stapling (ssl_stapling on) reduz a latência das conexões: em vez de o browser consultar a CA para verificar se o certificado foi revogado, o servidor já faz essa consulta periodicamente e inclui a resposta assinada no handshake TLS.
Role ssl_renewal — Lógica de Renovação
A task de renovação calcula os dias restantes com openssl e date, e só chama o certbot renew se a condição for verdadeira:
- name: Calcular dias restantes
shell: |
end_date="{{ cert_end_date.stdout }}"
today=$(date +%s)
expiry=$(date -d "$end_date" +%s)
echo $(( (expiry - today) / 86400 ))
register: days_remaining
- name: Renovar certificado se faltam menos de 30 dias
command: >
certbot renew
--cert-name {{ site_domain }}
--non-interactive
--quiet
when: days_remaining.stdout | int < 30
notify: reload nginx
Cada execução registra uma linha em /var/log/certbot-renewal.log com data, domínio, dias restantes e se foi renovado.
AWX — Job Templates e Schedule
A configuração do AWX está documentada no arquivo awx-job-template.yml. Os dois jobs principais são:
SSL - Criar Certificado: roda sob demanda com survey para o operador escolher domínio e ambiente (staging/produção).
SSL - Renovar Certificados: tem dois modos de uso:
- Schedule automático — toda terça-feira às 03:00 UTC, renova todos os hosts do grupo
webservers - Execução manual com survey — antes de executar, o AWX exibe um formulário para o operador selecionar quais tenants renovar
O survey de seleção de tenants funciona assim: o campo tenants aceita o nome de um ou mais hosts separados por vírgula (ex: fxshell1_server,fxshell2_server) ou o grupo webservers para renovar todos. Esse valor é passado para a diretiva hosts: do playbook, que funciona como um --limit dinâmico.
# O playbook usa o valor do survey como filtro de hosts
- name: Renovar certificados SSL Let's Encrypt
hosts: "{{ tenants | default('webservers') }}"
O schedule usa a sintaxe RFC 5545 (iCalendar RRULE), que é o formato que o AWX aceita:
DTSTART:20240102T030000Z RRULE:FREQ=WEEKLY;BYDAY=TU
# BYDAY=TU = Tuesday (terça-feira)
# FREQ=WEEKLY = toda semana
# Hora: 03:00 UTC
Para configurar manualmente na interface do AWX:
Projects → Criar projeto apontando para o repositório git
Inventories → Importar hosts.yml com os 3 servidores
Credentials → Adicionar chave SSH dos servidores
Job Templates → SSL - Criar Certificado (playbook-ssl-create.yml)
Job Templates → SSL - Renovar (playbook-ssl-renewal.yml)
→ Habilitar Survey com campo "tenants"
→ Habilitar ask_limit_on_launch: true
Schedules → Adicionar no job de renovação:
→ Recurrence: Weekly, Day: Tuesday, Time: 03:00 UTC
→ Default tenants: webservers (todos)
Executando
# 1. Testar conectividade com todos os servidores
ansible all -m ping
# 2. Testar com staging primeiro (não gasta rate limit)
ansible-playbook playbook-ssl-create.yml -e "certbot_staging=true"
# 3. Verificar o que seria feito (dry-run parcial)
ansible-playbook playbook-ssl-create.yml --check
# 4. Emitir certificados reais para todos os sites
ansible-playbook playbook-ssl-create.yml
# 5. Emitir só para um site específico
ansible-playbook playbook-ssl-create.yml -l fxshell1_server
# 6. Forçar renovação manualmente
ansible-playbook playbook-ssl-renewal.yml -e "force_renewal=true"
# 7. Testar HTTPS após emissão
curl -I https://fxshell1.com.br
curl -I https://fxshell2.com.br
curl -I https://fxshell3.com.br
Para verificar os headers de segurança:
curl -sI https://fxshell1.com.br | grep -E "Strict-Transport|X-Frame|X-Content"
Monitoramento e Troubleshooting
| Sintoma | Causa provável | Solução |
|---|---|---|
certbot: command not found |
Snap não instalou ou symlink faltando | Verificar snap list certbot e recriar symlink |
Challenge failed: connection refused |
Nginx não está rodando ou porta 80 bloqueada | systemctl status nginx e verificar firewall |
Too many certificates already issued |
Rate limit do Let’s Encrypt atingido | Usar --staging para testes; aguardar 7 dias |
nginx: configuration file test failed |
Caminho do certificado errado no template | Verificar se /etc/letsencrypt/live/<domínio>/ existe |
SSL certificate has expired |
Renovação automática falhou | Verificar /var/log/certbot-renewal.log e /var/log/letsencrypt/ |
AWX job falha com Permission denied |
Usuário SSH sem sudo configurado | Verificar become: true e sudoers no servidor |
# Verificar logs do certbot
cat /var/log/letsencrypt/letsencrypt.log | tail -50
# Verificar log de renovação customizado
cat /var/log/certbot-renewal.log
# Testar validade do certificado manualmente
openssl s_client -connect fxshell1.com.br:443 -servername fxshell1.com.br < /dev/null 2>/dev/null \
| openssl x509 -noout -enddate
Para Que Serve no Mercado
Times DevOps e SRE que gerenciam dezenas ou centenas de domínios usam exatamente esse padrão. As variações incluem:
- Wildcard certificates com validação DNS ao invés de HTTP (necessário quando os servidores não têm porta 80 pública)
- Integração com Vault para armazenar as credenciais dos provedores DNS e automatizar renovação de wildcards
- Pipeline de CI/CD que dispara o playbook quando um novo site é adicionado ao inventário
- Certificados internos usando a mesma lógica com uma CA própria (CFSSL, Vault PKI) para serviços internos que não precisam de CA pública
O princípio é o mesmo em qualquer escala: infraestrutura declarativa, idempotente, auditável. Um certificado expirado em produção é a prova de que existe um processo manual em algum lugar esperando para falhar.
Conclusão
O que resolve esse lab não é só a automação do certbot — é a combinação de idempotência do Ansible com a orquestração do AWX. Você pode executar esse playbook mil vezes e o resultado é sempre o mesmo: se o certificado existe e está válido, nada muda. Se está para expirar, é renovado. Se não existe, é criado. Esse nível de previsibilidade é o que separa infraestrutura mantida de infraestrutura gerenciada.