step-ca: PKI Interna com ACME para PME
Neste artigo
O problema dos certificados self-signed
Numa PME típica, os serviços internos — Grafana, Nextcloud, Gitea — correm em HTTPS com certificados auto-assinados. O resultado é conhecido: navegadores mostram avisos vermelhos, os utilizadores ignoram, e as automações precisam de curl -k, desactivando toda a validação TLS.
O step-ca é uma CA open-source em Go que resolve isto: cria uma PKI interna completa e expõe um endpoint ACME (RFC 8555) — o mesmo protocolo do Let’s Encrypt. Qualquer cliente ACME obtém certificados válidos assinados pela sua CA interna, sem avisos do navegador.
ℹ Porquê step-ca e não mkcert?
O mkcert gera certificados estáticos, não corre como serviço, não suporta ACME nem mTLS. O step-ca é uma CA operacional: corre como daemon, tem API, suporta provisionadores (JWK, ACME, OIDC) e emite certificados de curta duração por design.
A ideia central: em vez de um certificado válido 1 ano que se esquece de renovar, tem certificados de 24h renovados via ACME. Se um certificado for comprometido, a janela de exposição é de horas, não de meses.
Instalar o step-ca
Dois binários: step (cliente) e step-ca (servidor). Em Debian/Ubuntu:
wget https://dl.step.sm/gh-release/cli/docs-ca-install/v0.27.0/step-cli_0.27.0_amd64.deb
sudo dpkg -i step-cli_0.27.0_amd64.deb
wget https://dl.step.sm/gh-release/certificates/docs-ca-install/v0.27.0/step-ca_0.27.0_amd64.deb
sudo dpkg -i step-ca_0.27.0_amd64.deb
Inicializar a CA com raiz e intermediário:
step ca init --name "PME Internal CA" \
--dns "ca.empresa.lan" --address ":9000" \
--provisioner [email protected] \
--password-file /etc/step-ca/password.txt
Distribuir o certificado raiz para todos os clientes. Em Linux: sudo cp ~/.step/certs/root_ca.crt /usr/local/share/ca-certificates/step-ca-root.crt && sudo update-ca-certificates. Em Windows, importar via GPO para “Autoridades de Certificação de Raiz Fidedigna” em todas as máquinas do domínio.
⚠ A chave raiz é o activo mais crítico
Se a chave raiz for comprometida, toda a PKI está comprometida. Guardar root_ca_key offline (USB, cofre) ou num HSM. Nunca no mesmo servidor que corre o step-ca em produção.
Arrancar via systemd com ExecStart=/usr/bin/step-ca /home/step/.step/config/ca.json --password-file /etc/step-ca/password.txt e systemctl enable --now step-ca.
Configurar ACME interno
Adicionar um provisionador ACME e reiniciar:
step ca provisioner add acme --type ACME
sudo systemctl restart step-ca
O endpoint ACME fica em https://ca.empresa.lan:9000/acme/acme/directory. Com acme.sh:
acme.sh --register-account --server https://ca.empresa.lan:9000/acme/acme/directory
acme.sh --issue -d nextcloud.empresa.lan --standalone \
--server https://ca.empresa.lan:9000/acme/acme/directory
acme.sh --install-cert -d nextcloud.empresa.lan \
--key-file /etc/ssl/private/nextcloud.key \
--fullchain-file /etc/ssl/certs/nextcloud.crt \
--reloadcmd "systemctl reload nginx"
ℹ Validação DNS-01 em redes internas
O desafio HTTP-01 exige porta 80 acessível. Em redes com NAT ou firewalls restritivos, usar DNS-01 (--dns dns_powerdns) ou TLS-ALPN-01. O step-ca suporta todos os tipos de desafio da RFC 8555.
Definir duração dos certificados em ca.json → authority.claims: defaultTLSCertDuration: 24h, maxTLSCertDuration: 720h, minTLSCertDuration: 5m.
mTLS e rotação automática
O mTLS (mutual TLS) é a killer feature do step-ca: o servidor também valida o cliente via certificado. Cada serviço interno obtém o seu certificado de cliente e apresenta-o na handshake. Sem certificado válido, a ligação é recusada.
step ca certificate "svc-backup" svc-backup.crt svc-backup.key \
--san backup.empresa.lan --not-after 24h
Nginx a exigir mTLS com ssl_client_certificate /usr/local/share/ca-certificates/step-ca-root.crt, ssl_verify_client on e ssl_verify_depth 2.
Renovação automática via cron com step ca renew (renova se dentro da janela de 80% da validade):
# /etc/cron.d/step-ca-renew
0 * * * * step step ca renew --force \
/etc/ssl/certs/api.crt /etc/ssl/private/api.key \
--exec "systemctl reload nginx"
ℹ Rotação sem downtime
O step ca renew escreve o novo certificado em ficheiros temporários, substitui atomicamente via rename(2), e só depois executa o --exec. O Nginx recarrega sem drops de ligação.
Erros comuns e soluções
| Erro | Causa | Solução |
|---|---|---|
| x509: signed by unknown authority | Raiz não instalada no cliente | Copiar root_ca.crt e correr update-ca-certificates |
| certbot: Connection refused | Porta 80 bloqueada | Usar desafio DNS-01 ou abrir firewall |
| step: bad password | Password com newline extra | Usar printf em vez de echo |
| 400: no provisioner found | Provisionador ACME ausente | Verificar com step ca provisioner list |
| Nginx: SSL_do_handshake failed | Certificado de cliente expirado | Verificar cron de renovação |
| acme.sh: challenge failed | DNS não resolve ou propagação lenta | Aumentar --dnssleep ou usar HTTP-01 |
Artigos relacionados no kbase.pt