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 uso | Qué le da step-ca | Sección |
|---|---|---|
| Que todos los dispositivos confíen en tus certificados | Una raíz que instalar, una vez | Confiar en la raíz |
| HTTPS interno para todo lo que hable ACME | El mismo protocolo que Let’s Encrypt, para nombres internos | HTTPS interno con ACME |
| Servicios sin cliente ACME | Certificados que se renuevan solos con un temporizador | Servicios que no saben ACME |
| Móviles y otros certificados de cliente | Claves que un dispositivo tiene para entrar, y una forma de revocarlas | Su propia entrada: los valores por defecto juegan en contra |
| Inicios de sesión SSH | Certificados en vez de authorized_keys | Su 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.
| Variable | Hace |
|---|---|
DOCKER_STEPCA_INIT_NAME | El nombre del emisor en cada certificado |
DOCKER_STEPCA_INIT_DNS_NAMES | Los nombres en los que responde la CA |
DOCKER_STEPCA_INIT_ACME=true | Añade un aprovisionador ACME para el HTTPS interno |
DOCKER_STEPCA_INIT_SSH=true | Crea 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 ...
| Aprovisionador | Tipo | Firma |
|---|---|---|
admin | JWK | Lo que le pidas con su contraseña |
acme | ACME | Certificados para clientes que demuestran que el nombre es suyo |
sshpop | SSHPOP | Renovaciones 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.
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ña | Desbloquea | Dónde acaba |
|---|---|---|
secrets/password | Las claves de la raíz y del intermedio | En el volumen, usada cada vez que arranca la CA |
| Contraseña del aprovisionador | El aprovisionador admin, que firma cada certificado que pidas | El 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ón | Resultado |
|---|---|
| Desafío | tls-alpn-01, respondido por Caddy en el puerto 443 |
| Emisor | Home CA Intermediate CA |
| Duración | 24 horas, de las 17:31 a las 17:32 del día siguiente |
| Renovación | Trabajo 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 serie | Válido hasta | |
|---|---|---|
| Emitido | 241943140971… | 2026-10-10 17:33:03 |
Tras step ca renew | 206409156462… | 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.
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». ↩︎
Let’s Encrypt — Certificate Transparency logs — «Let’s Encrypt submits all certificates we issue to CT logs.» ↩︎
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». ↩︎ ↩︎smallstep/certificates — docker/entrypoint.sh — genera archivos
passwordyprovisioner_passwordseparados, imprime una vez la contraseña del aprovisionador y la destruye; una contraseña proporcionada se escribe en los dos. ↩︎ ↩︎ ↩︎Smallstep — step certificate install — instala un certificado raíz en el almacén de confianza del sistema. ↩︎
Smallstep — ACME basics — el aprovisionador ACME de step-ca y los tipos de desafío que admite. ↩︎
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»;--execproporciona «certificate reloads on your services». ↩︎