Por qué llevar tu propia CA

Mira lo que hay ahora en una red doméstica. Un NAS, Proxmox, Home Assistant, una impresora, la página de administración de un router. Todos sirven HTTPS con un certificado autofirmado. Todos los navegadores que los visitan sueltan un aviso. La gente le da a continuar. Luego le da a continuar en el siguiente, y en el de después, hasta que saltarse un aviso de certificado es lo que se hace sin más. Ese es el hábito que necesita un atacante.

Una CA pública no lo arregla. Let’s Encrypt no emite para localhost «because nobody uniquely owns it»1, y lo mismo vale para cualquier nombre que solo existe dentro de casa. Incluso para un dominio real, todo certificado que emite va a los registros públicos de Certificate Transparency2. Pide uno para nas.yourname.uk y el nombre de tu NAS pasa a ser de dominio público.

Tu propia CA arregla las dos cosas. Firma los nombres que uses dentro. No se publica nada en ningún sitio. Una vez que cada dispositivo confía en su raíz, los avisos paran, y un aviso vuelve a ser algo que significa algo.

step-ca es la CA de código abierto de Smallstep, y funciona en un contenedor. Todo lo de aquí se ejecutó con step-ca 0.30.2 en Podman, con Caddy 2.11.7 y nginx 1.30.5 como clientes.

Caso de usoQué le da step-caSección
Que todos los dispositivos confíen en tus certificadosUna raíz que instalar, una vezConfiar en la raíz
HTTPS interno para todo lo que hable ACMEEl mismo protocolo que Let’s Encrypt, para nombres internosHTTPS interno con ACME
Servicios sin cliente ACMECertificados que se renuevan solos con un temporizadorServicios que no saben ACME
Móviles y otros certificados de clienteClaves que un dispositivo tiene para entrar, y una forma de revocarlasSu propia entrada: los valores por defecto juegan en contra
Inicios de sesión SSHCertificados en vez de authorized_keysSu propia entrada: una configuración distinta

Dos de esos tienen su propia entrada, porque cada uno es una configuración aparte. Certificados de cliente de step-ca cubre los móviles y mTLS, donde hay que cambiar cinco valores por defecto. Inicios de sesión SSH firmados por step-ca cubre SSH, que usa otras claves y otros archivos en cada máquina.

Ponerla en marcha

La imagen se inicializa sola en el primer arranque a partir de unas pocas variables de entorno3. Dos de ellas deciden cosas que no se pueden cambiar después sin empezar de nuevo, así que ponlas el primer día.

VariableHace
DOCKER_STEPCA_INIT_NAMEEl nombre del emisor en cada certificado
DOCKER_STEPCA_INIT_DNS_NAMESLos nombres en los que responde la CA
DOCKER_STEPCA_INIT_ACME=trueAñade un aprovisionador ACME para el HTTPS interno
DOCKER_STEPCA_INIT_SSH=trueCrea además las claves de CA SSH de host y de usuario

Los interruptores de ACME y SSH los lee el punto de entrada de la imagen4. Actívalos ya aunque hoy solo quieras uno. Añadir SSH más tarde significa construir una CA nueva.

podman run -d --name step-ca \
  -v step:/home/step \
  -p 9000:9000 \
  -e DOCKER_STEPCA_INIT_NAME="Home CA" \
  -e DOCKER_STEPCA_INIT_DNS_NAMES="localhost,ca.home.example" \
  -e DOCKER_STEPCA_INIT_ACME=true \
  -e DOCKER_STEPCA_INIT_SSH=true \
  docker.io/smallstep/step-ca:0.30.2

Construye una raíz, un intermedio firmado por esa raíz, las claves SSH y tres aprovisionadores, y luego sirve en el puerto 9000:

X.509 Root Fingerprint: cd2f8d4c60af32a0a534781cd63b4e2a9ce362e2c1ae5983b56a370bfa2c2489
SSH Host CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
SSH User CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
Serving HTTPS on :9000 ...
AprovisionadorTipoFirma
adminJWKLo que le pidas con su contraseña
acmeACMECertificados para clientes que demuestran que el nombre es suyo
sshpopSSHPOPRenovaciones de certificados SSH de host

El aprovisionador es la idea que tiene la CA de «quién pregunta». Cada uno tiene sus propias reglas, y casi todo lo que sigue es poner esas reglas.

Una CA, cinco usoscontenedor step-caRaíz, intermediouna raíz en la que confiaracmeadminsshpopCRLaprovisionadoresCada dispositivoconfía en la raíz, una vezCaddy, ACME24 h, se renueva soloSin ACMEstep ca renew --daemonMóviles1 año, revocado por CRLSSHsu propia entrada
Una raíz, de confianza una sola vez, y un aprovisionador por cada vía de entrada. Los clientes ACME y los servicios renovados viven con el valor por defecto de 24 horas; los móviles necesitan cambiarlo; SSH tiene su propia entrada.

Fija la versión. latest era 0.30.2 el día que se escribió esto, y una CA es lo último que quieres que te cambie por debajo al hacer un pull.

Dos contraseñas, no una

La guía de Docker de Smallstep dice que una contraseña generada cubre tanto «the encrypted CA keys and the default CA provisioner», guardada en secrets/password3. En esta imagen no es eso lo que pasa. Intenta emitir un certificado con esa contraseña y el aprovisionador la rechaza:

failed to decrypt JWE: invalid password

El propio punto de entrada de la imagen lo explica. Genera dos contraseñas aleatorias, una para las claves de la CA y otra para el aprovisionador. La del aprovisionador se imprime en el registro una vez y luego se destruye4:

echo "👉 Your CA administrative password is: $(< $STEPPATH/provisioner_password )"
...
shred -u $STEPPATH/provisioner_password
ContraseñaDesbloqueaDónde acaba
secrets/passwordLas claves de la raíz y del intermedioEn el volumen, usada cada vez que arranca la CA
Contraseña del aprovisionadorEl aprovisionador admin, que firma cada certificado que pidasEl registro del primer arranque, y en ningún otro sitio

Así que lee el registro en el primer arranque y guarda esa segunda contraseña en un sitio seguro. Piérdela y no puedes emitir otro certificado sin quitar el aprovisionador a mano. Pon DOCKER_STEPCA_INIT_PASSWORD antes del primer arranque si prefieres elegirla tú; el punto de entrada la usa entonces para las dos4.

Confiar en la raíz

Nada confía en tu CA hasta que se lo dices. Una máquina Debian pidiéndole a Caddy una página firmada por step-ca:

$ curl https://nas.home.example/
curl: (60) SSL certificate problem: unable to get local issuer certificate

Copia la raíz y la misma petición pasa:

cp root_ca.crt /usr/local/share/ca-certificates/home-ca.crt
update-ca-certificates
$ curl https://nas.home.example/
internal service, certificate from step-ca

Ese es todo el trabajo. Una vez por dispositivo. step certificate install hace lo mismo en el sistema donde se ejecute5. Solo la raíz va a un almacén de confianza. El intermedio viaja con cada certificado, y un móvil que vaya a presentar un certificado de cliente también lo necesita, lo que se cubre en la entrada de certificados de cliente.

Trata la raíz como la llave de la puerta de casa que ahora es. Cualquier cosa que tenga la clave privada de la raíz puede acuñar un certificado para cualquier nombre, y todos los dispositivos que confían en ella se lo creerán.

HTTPS interno con ACME

ACME es el protocolo que usa Let’s Encrypt, y step-ca lo habla para nombres internos6. Todo lo que tenga un cliente ACME consigue certificados internos igual que consigue los públicos, renovaciones incluidas. Para Caddy son dos líneas globales:

{
	acme_ca https://ca.home.example:9000/acme/acme/directory
	acme_ca_root /etc/caddy/root_ca.crt
}

nas.home.example {
	reverse_proxy 192.0.2.30:5000
}

Lo que registró Caddy en el primer arranque:

"msg":"trying to solve challenge","identifier":"nas.home.example","challenge_type":"tls-alpn-01"
"msg":"certificate obtained successfully","identifier":"nas.home.example"
ComprobaciónResultado
Desafíotls-alpn-01, respondido por Caddy en el puerto 443
EmisorHome CA Intermediate CA
Duración24 horas, de las 17:31 a las 17:32 del día siguiente
RenovaciónTrabajo del propio Caddy, nada que configurar

Un certificado de 24 horas suena alarmante. No lo es. Un cliente ACME renueva mucho antes de que caduque, cada día, sin que intervenga nadie. Una clave robada vale un día. Ese es el valor por defecto alrededor del cual se construyó step-ca, y aquí es justo el correcto.

El desafío sí necesita que la CA llegue al nombre que está firmando. step-ca tiene que resolver nas.home.example y conectarse a él en el puerto 443 para tls-alpn-01, o en el 80 para http-01. Un DNS interno que usen los dos lados es lo primero que hay que tener bien.

Servicios que no saben ACME

Mucho equipo no tiene cliente ACME: la interfaz web de un hipervisor, una impresora, un aparato viejo. Para esos, emite el certificado con el aprovisionador admin y renuévalo con step ca renew:

step ca certificate proxmox.home.example pve.crt pve.key
step ca renew --force pve.crt pve.key
Número de serieVálido hasta
Emitido241943140971…2026-10-10 17:33:03
Tras step ca renew206409156462…2026-10-10 17:33:06

Número de serie nuevo, 24 horas nuevas, y sin pedir contraseña. El certificado actual demuestra quién pregunta, y esa es la idea: la renovación no necesita ningún secreto guardado en la máquina más allá de la clave que ya tiene. Sí necesita que el certificado siga siendo válido, así que una renovación que se salta su ventana significa volver a emitirlo a mano.

Si lo dejas corriendo, se renueva solo. --daemon renueva a los dos tercios de la vida por defecto, y --exec ejecuta un comando tras cada renovación, que es donde va la recarga7:

step ca renew --daemon --exec "systemctl reload nginx" pve.crt pve.key

Ejecuta eso desde una unidad de systemd y la máquina se cuida sola. Olvídalo, y en 24 horas el servicio suelta el mismo aviso del que querías librarte.

La CA es la parte fácil

Poner step-ca en marcha llevó un comando. Que una máquina Debian confiara en ella llevó dos. Caddy necesitó dos líneas y no volvió a pedir nada. Esa es la parte que la gente aplaza durante años, viviendo con un aviso del navegador en cada página interna, y es el trabajo más barato de esta lista.

Lo que cuesta es todo lo que toca un certificado después de emitirse: la raíz en cada dispositivo, las renovaciones que nadie vigila, el único servicio que no sabe ACME y hay que acordarse de él. Nada de eso es difícil. Todo es el tipo de trabajo que solo se hace bien cuando alguien se hace cargo de él.

Una red doméstica es una finca pequeña. Se merece lo mismo que recibe una como es debido: una raíz, guardada con cuidado, de confianza a propósito, y un aviso que significa algo cuando aparece. Como tal, la CA no es la parte lista. La parte lista es no volver a saltarse un aviso nunca.


  1. Let’s Encrypt — Certificates for localhost — no puede emitir para “localhost” «because nobody uniquely owns it, and it’s not rooted in a top level domain». ↩︎

  2. Let’s Encrypt — Certificate Transparency logs — «Let’s Encrypt submits all certificates we issue to CT logs.» ↩︎

  3. Smallstep — Run a private CA in Docker — las variables DOCKER_STEPCA_INIT_*; dice que la contraseña generada es para «the encrypted CA keys and the default CA provisioner». ↩︎ ↩︎

  4. smallstep/certificates — docker/entrypoint.sh — genera archivos password y provisioner_password separados, imprime una vez la contraseña del aprovisionador y la destruye; una contraseña proporcionada se escribe en los dos. ↩︎ ↩︎ ↩︎

  5. Smallstep — step certificate install — instala un certificado raíz en el almacén de confianza del sistema. ↩︎

  6. Smallstep — ACME basics — el aprovisionador ACME de step-ca y los tipos de desafío que admite. ↩︎

  7. Smallstep — step ca renew — con --daemon, «By default, it will renew the certificate before 2/3 of the validity period of the certificate has elapsed»; --exec proporciona «certificate reloads on your services». ↩︎