step-ca: PKI Interna com ACME para PME

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