Seguridad

Certificados TLS internos para tu LAN: mkcert y CA privada para servicios que nunca salen a internet

El primer certificado que generé para mi Pi-hole lo hice con OpenSSL a mano: openssl req -x509 -newkey rsa:4096, un certificado autofirmado, y cada dispositivo de la red empezó a mostrarme un aviso rojo de “la conexión no es privada”. Funcionaba, el TLS estaba ahí, pero cada navegador, cada cliente y cada app me pedía saltarse el aviso. Eso no es cifrado, es ruido: entrena al usuario a ignorar los errores de certificado, que es justo el comportamiento que hace que los certificados valgan algo.

El problema de fondo es que Let’s Encrypt está diseñado para el internet público. Necesita un dominio resoluble, un puerto 80 abierto, y te emite un certificado de 90 días que un navegador de cualquier parte del mundo valida contra una cadena de confianza pública. Para un servicio que vive en tu LAN —Pi-hole, Grafana, Home Assistant, un registry de Docker, el panel de Proxmox— nada de eso aplica. Y para un nombre interno como pi.homelab.lan o vault.interno, Let’s Encrypt no tiene nada que emitir.

La solución no es un certificado autofirmado con avisos, sino una autoridad de certificación privada propia: una CA que tú controlas, que los dispositivos de tu red confían, y que emite certificados reales para los nombres que necesitas. mkcert automatiza el 90% de ese trabajo en cinco minutos, y step-ca es la opción cuando necesitas una CA de verdad, con emisión automática vía ACME y renovación sin intervención.

TL;DR — Lo que vas a tener al final

  • Tiempo de setup: 10 minutos con mkcert (incluye crear e instalar la CA local), 45-60 minutos con step-ca si montas la CA como servicio.
  • Requisitos: un equipo Linux con libnss3-tools instalado, y cada dispositivo donde quieras evitar el aviso con la CA raíz instalada en su almacén de confianza.
  • Resultado: certificados válidos para nombres internos (*.homelab.lan, IPs, localhost) que los navegadores y clientes aceptan sin advertencias, con renovación simple cuando caducan.
  • Troubleshooting: Node.js y Python que no confían en la CA del sistema (solución: variables NODE_EXTRA_CA_CERTS y REQUESTS_CA_BUNDLE), certificados que caducan cada dos años (solución: --cert-file con fecha larga o CA de paso 1), y la clave raíz que se debe tratar como el equivalente a una contraseña maestra.

Índice

  1. [El problema: por qué un autofirmado no es una solución](#el-problema-por-qué-un-autofirmado-no-es-una-solución)
  2. [Qué es una CA privada y en qué se diferencia](#qué-es-una-ca-privada-y-en-qué-se-diferencia)
  3. [mkcert: la CA local en cinco minutos](#mkcert-la-ca-local-en-cinco-minutos)
  4. [Usar los certificados de mkcert en Nginx](#usar-los-certificados-de-mkcert-en-nginx)
  5. [Propagar la confianza a toda la red](#propagar-la-confianza-a-toda-la-red)
  6. [Los lenguajes que no usan el almacén del sistema](#los-lenguajes-que-no-usan-el-almacén-del-sistema)
  7. [Renovar sin drama: el ciclo de vida real](#renovar-sin-drama-el-ciclo-de-vida-real)
  8. [step-ca: cuando mkcert no alcanza](#step-ca-cuando-mkcert-no-alcanza)
  9. [mTLS: certificados entre servicios](#mtls-certificados-entre-servicios)
  10. [Conclusión](#conclusión)

El problema: por qué un autofirmado no es una solución

Un certificado autofirmado es un certificado que se valida a sí mismo. El navegador no lo rechaza por Cryptography defectuosa —lo rechaza porque no hay una cadena de confianza que lo ancle a una raíz que el sistema considere confiable.

El resultado operativo es el que ya conoces: una advertencia que exige hacer clic en “Avanzado” o “Aceptar el riesgo”. En un navegador de escritorio es un clic. En un móvil es más clicks. En una API, en un cliente de Docker, en un health check o en un agente de monitoreo es un error de validación que hay que resolver con verify=False o con una lista de CA extra, y eso a su vez es deuda de seguridad que nadie documenta.

Peor aún: el aviso entrena al usuario a ignorar errores de TLS. Cuando el día que nada de eso importe tengas un certificado realmente expirado o un ataque de intermediario, la advertencia se ve idéntica. Ese es el costo real del autofirmado.

Las tres salidas reales son:

  • Una CA privada instalada en los dispositivos: el navegador confía porque la raíz está en su almacén. Esto es lo que hace mkcert y step-ca. Funciona tanto en el navegador como en cualquier cliente del sistema operativo.
  • Let’s Encrypt con un DNS público: si tu servicio interno es accesible desde internet con un dominio real, esta es la opción y ya la cubrimos en la guía de certbot.
  • Confiar en el certificado pero sin criptografía decente: aceptar el aviso y seguir. Funciona, no es lo que quieres.

Esta guía es sobre la primera.


Qué es una CA privada y en qué se diferencia

Una autoridad de certificación es simplemente una programa que firma certificados públicos y su propia raíz. La diferencia entre la CA de Let’s Encrypt y la tuya es que la de Let’s Encrypt es pública, auditada y su raíz está preinstalada en cada sistema operativo del mundo; la tuya es local, nadie más la conoce, y tienes que distribuirla tú.

Esa distribución es el punto clave del diseño. La cadena de confianza funciona así:

CA raiz (tu maquina)
   |
   +---> CA intermedia (opcional, para rotar sin tocar todos los equipos)
             |
             +---> certificado de pi.homelab.lan
             +---> certificado de vault.homelab.lan
             +---> certificado de *.homelab.lan

Cada sistema que debe confiar en tus certificados necesita la CA raíz en su almacén. Los certificados intermedios van embebidos en el fullchain que sirve el servidor, así que el cliente los descarga sola en el handshake.

Dos decisiones de diseño que conviene tomar antes de empezar:

¿Distribución manual o automática? Con mkcert, la CA raíz se instala a mano en cada máquina (o con un script de tu MDM o tu Ansible). Con step-ca, puedes montar un endpoint de enrollment por HTTP o un profile de Apple que hace la distribución sola. En un homelab de 5-10 máquinas, la instalación manual con un script es perfectamente manejable.

¿Una CA raíz directamente o con intermedia? Una intermedia te permite rotar la raíz sin tocar cada máquina. Es la práctica recomendada en cualquier entorno donde la raíz se distribuya a muchos equipos. step-ca la soporta de forma nativa; mkcert no, porque está pensado para desarrollo y su CA raíz es de vida corta por diseño.


mkcert: la CA local en cinco minutos

mkcert es una herramienta de la EFF y de FiloSottile que hace exactamente una cosa: crea una CA local, la instala en los almacenes de confianza del sistema y del navegador, y emite certificados firmados por ella. Sin configuración, sin archivos de OpenSSL, sin CSR.

Instalación en Debian, Ubuntu y RHEL:

# Debian / Ubuntu: certutil es necesario para el almacen NSS de Firefox
sudo apt install libnss3-tools
# RHEL / Rocky / AlmaLinux / Fedora
sudo dnf install nss-tools
# Instalar mkcert (binario oficial)
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
sudo install -m 0755 mkcert /usr/local/bin/mkcert
mkcert --version
# mkcert 1.4.4

Crea la CA local e instálala:

mkcert -install
# Created a new local CA
# The local CA is now installed in the system trust store!
# The local CA is now installed in the Firefox trust store (requires browser restart)!

Ese -install hace tres cosas: genera la CA raíz, la instala en el almacén de confianza del sistema (que en Linux es /usr/local/share/ca-certificates/ más un update-ca-certificates) y en el almacén NSS que usa Firefox. Si tienes Chromium o Chrome, usa lasame almacén del sistema, así que también queda cubierta.

El aviso de reiniciar Firefox es real: el almacén NSS se carga en memoria al arrancar el proceso.

Ubicación de los archivos de la CA. mkcert te dice dónde están, y necesitas saberlo:

mkcert -CAROOT
# /home/tu-usuario/.local/share/mkcert
ls -la "$(mkcert -CAROOT)"
# -rw-r--r-- 1 usuario usuario  2419 ... rootCA-key.pem
# -rw-r--r-- 1 usuario usuario  2140 ... rootCA.pem

Emite el primer certificado:

mkcert pi.homelab.lan "*.homelab.lan" vault.homelab.lan localhost 127.0.0.1 ::1
# Created a new certificate valid for the following names
#  - "pi.homelab.lan"
#  - "*.homelab.lan"
#  - "vault.homelab.lan"
#  - "localhost"
#  - "127.0.0.1"
#  - "::1"
# The certificate is at "./pi.homelab.lan+4.pem" and the key at "./pi.homelab.lan+4-key.pem"

Un detalle importante del wildcard: *.homelab.lan no cubre pi.homelab.lan por sí solo… en realidad sí lo cubre, pero no cubre profund.pi.homelab.lan (segundo nivel). Por eso el comando de arriba lista los nombres que necesitas de forma explícita. Si quieres cubrir un segundo nivel, añade "*.pi.homelab.lan".

Emite en archivos con nombre fijo, que es lo que necesitas para un servidor web:

mkcert -key-file /etc/ssl/private/homelab.key -cert-file /etc/ssl/certs/homelab.pem 
  pi.homelab.lan "*.homelab.lan" vault.homelab.lan
sudo chmod 600 /etc/ssl/private/homelab.key

El mkcert también acepta las variables de entorno CAROOT para gestionar CAs separadas, lo que es útil si tienes un entorno de desarrollo y uno de pruebas y no quieres mezclar la confianza entre ambos.


Usar los certificados de mkcert en Nginx

La parte importante: no sirvas solo el certificado. Sirve el certificado más tu CA raíz. Si sirves únicamente el certificado firmado, el cliente no tiene la raíz para validarlo y vuelve el aviso. Necesitas el archivo fullchain que incluya el rootCA.pem.

# Concatena el certificado de servicio con la CA raiz
cat homelab.pem "$(mkcert -CAROOT)/rootCA.pem" > /etc/ssl/certs/homelab-fullchain.pem
sudo tee /etc/nginx/sites-available/pihole.homelab.lan > /dev/null
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name pi.homelab.lan;
    ssl_certificate     /etc/ssl/certs/homelab-fullchain.pem;
    ssl_certificate_key /etc/ssl/private/homelab.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo nginx -t && sudo systemctl reload nginx
echo | openssl s_client -connect pi.homelab.lan:443 -servername pi.homelab.lan 2>/dev/null | openssl x509 -noout -subject -issuer
# subject=CN = pi.homelab.lan
# issuer=CN = mkcert

El issuer=CN = mkcert te dice que la cadena está bien montada: el certificado fue firmado por tu CA local. Si en su lugar vieras issuer=CN = pi.homelab.lan, estarías sirviendo un certificado autofirmado y el aviso volvería.

Y la prueba final, desde un navegador de la red:

curl -sv https://pi.homelab.lan 2>&1 | grep -E "SSL certificate verify|subject:|issuer:"
# SSL certificate verify ok
# subject: CN = pi.homelab.lan
# issuer: CN = mkcert

SSL certificate verify ok es la línea que querías ver. Si aparece unable to get local issuer certificate, falta la raíz en el almacén del cliente que está haciendo la prueba.


Propagar la confianza a toda la red

Aquí es donde la instalación manual se vuelve laboriosa si tienes más de unas pocas máquinas. Estas son las formas de resolverlo, de la más simple a la más sofisticada.

Opción 1: copiar la CA raíz a mano. Funciona para un homelab pequeño:

# En la maquina que emitio los certificados
scp "$(mkcert -CAROOT)/rootCA.pem" usuario@maquina-destino:/tmp/
# En la maquina destino (Debian / Ubuntu)
sudo cp /tmp/rootCA.pem /usr/local/share/ca-certificates/homelab-root.crt
sudo update-ca-certificates
# 1 added
# En la maquina destino (RHEL / Rocky)
sudo cp /tmp/rootCA.pem /etc/pki/ca-trust/source/anchors/homelab-root.crt
sudo update-ca-trust
# 1 added

Opción 2: Ansible, que ya tienes si administras el homelab con automatización:

# En tu playbook
- name: Instalar la CA raiz del homelab en los clientes
  ansible.builtin.copy:
    src: files/rootCA.pem
    dest: /usr/local/share/ca-certificates/homelab-root.crt
    owner: root
    group: root
    mode: "0644"
  notify: update-ca-certificates
- name: Actualizar el almacen de confianza
  ansible.builtin.command: update-ca-certificates
  changed_when: true

Opción 3: un certificado de una CA real, firmado por Let’s Encrypt. Es la opción más elegante para la red local y la que mucha gente no conoce: existe un programa llamado step-ca que puede actuar como Registration Authority de una CA pública. Tu step-ca recibe el challenge DNS-01 de Let’s Encrypt y lo reenvía; Let’s Encrypt valida tu control del dominio y firma un certificado para tu step-ca; tu step-ca usa esa firma para emitir el certificado de tu servicio interno, y los clientes lo validan contra la cadena pública de Let’s Encrypt. El resultado es un certificado de una CA pública para un nombre que solo existe en tu LAN.

Es la solución “correcta” pero es la más compleja de montar, y para un homelab honestamente es sobreingeniería. La opción 1 o 2 resuelve el 95% de los casos.


Los lenguajes que no usan el almacén del sistema

Este es el gotcha que más tiempo consume: algunos runtimes no leen el almacén de confianza del sistema operativo y traen su propio. Por eso el navegador dice que todo está bien y tu aplicación dice que no.

Node.js no usa el almacén del sistema. Tienes que apuntarlo explícitamente a la CA:

export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
node app.js
# O en un systemd unit:
# Environment=NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/homelab-root.crt

Python con requests usa certifi, no el almacén del sistema:

export REQUESTS_CA_BUNDLE=/usr/local/share/ca-certificates/homelab-root.crt
# O en Python:
#   import certifi, os
#   os.environ['REQUESTS_CA_BUNDLE'] = '/ruta/a/rootCA.pem'

Java usa su propio almacén en $JAVA_HOME/lib/security/cacerts. Para añadir tu CA:

sudo keytool -importcert -trustcacerts 
  -alias homelab-root 
  -file /usr/local/share/ca-certificates/homelab-root.crt 
  -keystore "$JAVA_HOME/lib/security/cacerts" 
  -storepass changeit -noprompt

Docker es un caso aparte. Los contenedores no heredan el almacén del host, así que la CA hay que montarla dentro:

docker run --rm 
  -v /usr/local/share/ca-certificates/homelab-root.crt:/usr/local/share/ca-certificates/homelab-root.crt:ro 
  -v /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro 
  mi-imagen

En Docker Compose es lo mismo, en el servicio que consume la API:

services:
  mi-app:
    image: mi-imagen
    volumes:
      - /usr/local/share/ca-certificates/homelab-root.crt:/usr/local/share/ca-certificates/homelab-root.crt:ro
      - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro

Y en kubectl, si usas Kubernetes, la forma limpia es un ConfigMap con la CA y un initContainer que la copia al almacén del contenedor.

curl sí usa el almacén del sistema en Linux, pero por si acaso puedes forzar el bundle:

curl --cacert /usr/local/share/ca-certificates/homelab-root.crt https://pi.homelab.lan

Renovar sin drama: el ciclo de vida real

Los certificados de mkcert no tienen fecha de expiración corta por defecto, pero sí expiran: el OpenSSL que usa genera por defecto un certificado válido por dos años. Ese es un ciclo cómodo para desarrollo, pero en un servicio queCorre en serio vas a querer automatizar la renovación antes de que se te pase la fecha.

Reemitir el certificado con un script:

sudo tee /usr/local/bin/renovar-homelab-cert > /dev/null
#!/bin/bash
set -euo pipefail
DOMINIOS=(pi.homelab.lan "*.homelab.lan" vault.homelab.lan)
CERT=/etc/ssl/certs/homelab.pem
KEY=/etc/ssl/private/homelab.key
FULLCHAIN=/etc/ssl/certs/homelab-fullchain.pem
mkcert -key-file "$KEY" -cert-file "$CERT" "${DOMINIOS[@]}"
cat "$CERT" "$(mkcert -CAROOT)/rootCA.pem" > "$FULLCHAIN"
chmod 600 "$KEY"
systemctl reload nginx
echo "Certificado renovado: $(date)"

Programar la renovación con un timer de systemd. Los timers son más robustos que cron porque sobreviven a reinicios, registran el estado y se pueden consultar con systemctl list-timers:

sudo tee /etc/systemd/system/renovar-homelab-cert.service > /dev/null
[Unit]
Description=Reemitir certificados del homelab con mkcert
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/renovar-homelab-cert
sudo tee /etc/systemd/system/renovar-homelab-cert.timer > /dev/null
[Unit]
Description=Renovar certificados del homelab mensualmente
[Timer]
OnCalendar=monthly
Persistent=true
RandomizedDelaySec=3600
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now renovar-homelab-cert.timer
systemctl list-timers | grep renovar-homelab
# Próxima ejecución: dom 25 nov 2026 03:42:18

Un timer mensual con un certificado válido por dos años te da margen de sobra. Si quieresRenewal más frecuente por política de seguridad, ponle OnCalendar=weekly — reemitir un certificado local es instantáneo y no consume recursos.

Y una regla de seguridad que no es negociable: la clave raíz de la CA (rootCA-key.pem) da control total sobre cualquier tráfico TLS que confíe en ella. Quien la tenga puede interceptar todas las conexiones HTTPS de todos los dispositivos de tu red que instalaron la raíz, sin que aparezca ningún aviso. La documentación de mkcert lo dice de forma explícita: “no la compartas”.

En la práctica, en un homelab:

  • Mantenla con permisos 600 en la máquina que emite, y solo en esa.
  • No la subas a un repositorio Git, ni la pongas en un contenedor, ni la copies a un backup en la nube sin cifrar.
  • Si la máquina que emite los certificados se va a formatear o a reinstalar, genera una CA nueva. Los certificados emitidos con la CA vieja siguen funcionando, pero los dispositivos que solo confían en la nueva raíz dejarán de validar los viejos.
  • Ten un plan para reemitir y redistribuir si sospechas que se filtró. Con mkcert es «crear CA nueva, reinstalar en todos, reemitir todos los certificados».

step-ca: cuando mkcert no alcanza

mkcert es perfecto para desarrollo y para homelabs pequeños. Se queda corto cuando necesitas:

  • Emisión automática: que cualquier servidor de tu red pida su certificado sin intervención humana, con renovación transparente.
  • Una CA como servicio centralizado, disponible para todos los nodos, en lugar de una CA por máquina.
  • Certificados de vida corta, rotados constantemente, que se revocan sin necesidad de CRL.
  • mTLS a escala: autenticación mutua entre servicios, con identidad verificable de cada cliente.

Ahí entra step-ca, la autoridad de certificación de Smallstep, que es open source y habla ACME. Los clientes hablan ACME, así que certbot y step funcionan contra ella sin adaptadores.

Instalación del servidor CA:

curl -fsSL https://dl.smallstep.com/step-ca/latest/step-ca_linux_0.27.4_amd64.tar.gz | tar -xz -C /usr/local/bin
step-ca version

Inicializar la CA (esto genera la raíz y el intermedio):

sudo step ca init
# Your CA configuration file: /root/.step/config/ca.json
# Configuration written to /root/.step/config/ca.json
# CA directory: /root/.step
# Intermediate certificate: /root/.step/certs/intermediate_ca.crt
# Root certificate: /root/.step/certs/root_ca.crt
# Root private key: /root/.step/secrets/root_ca_key
# Intermediate private key: /root/.step/secrets/intermediate_ca_key

La estructura de la CA es exactamente la que se describía antes: raíz fuera de línea (en /root/.step/secrets/root_ca_key), un intermedio que firma, y los certificados de servicio emitidos por ese intermedio. La raíz nunca se usa para firmar directamente, así que comprometer el servidor de step-ca no equivale a comprometer la raíz.

Emitir un certificado como cliente, hablando ACME contra tu CA local:

step ca certificate pi.homelab.lan vault.homelab.lan 
  --provisioner [email protected] 
  --password-file /root/.step/secrets/admin-password 
  --not-after 720h 
  --ca-url https://ca.homelab.lan:9000 
  --root /root/.step/certs/root_ca.crt 
  --out /etc/ssl/certs/homelab-fullchain.pem 
  --key /etc/ssl/private/homelab.key

El parámetro --not-after 720h es el que cambia la juego: 30 días de validez. Con certificados tan cortos, la revocación deja de ser un problema real —el certificado expira solo en 30 días— y puedes emitir uno nuevo sin que nadie tenga que distribuir una lista de revocación.

Los clientes también pueden usar certbot contra step-ca, porque speaks ACME:

sudo certbot certonly 
  --server https://ca.homelab.lan:9000/acme/acme/directory 
  --ca-cert /root/.step/certs/root_ca.crt 
  --standalone 
  -d pi.homelab.lan 
  --email [email protected] 
  --agree-tos 
  --non-interactive

Y el cliente step puede pedir certificados de forma nativa y, con --force y un token de renovación, hacerlo sin interacción:

step ca renew --force /etc/ssl/certs/homelab-fullchain.pem 
  --ca-url https://ca.homelab.lan:9000 
  --root /root/.step/certs/root_ca.crt

Los certificates de step-ca se pueden distribuir a los clientes de dos formas: copias la raíz con step ca certificate --root o sirves el root_ca.crt por HTTP para que los dispositivos lo descarguen. La segunda es la que usan las configuraciones de enrollment automático de Apple y Windows, y es lo que hace que step-ca escale a un entorno real.


mTLS: certificados entre servicios

Hasta aquí todo era “el cliente confía en mi servidor”. La variante bidireccional es mTLS: los dos extremos presentan certificado, y ambos verifican al otro. El servidor no solo comprueba que el cliente conozca la clave pública del servidor — también exige un certificado firmado por tu CA, y el nombre del cliente queda en el Subject o en un SAN.

Esto es lo que permite que un servicio interno solo acepte peticiones de los servicios autorizados y rechace todo lo demás, sin contraseñas ni tokens.

En Nginx, exigir el certificado del cliente:

server {
    listen 443 ssl;
    server_name api.homelab.lan;
    ssl_certificate     /etc/ssl/certs/homelab-fullchain.pem;
    ssl_certificate_key /etc/ssl/private/homelab.key;
    # Verificar el certificado que presenta el cliente
    ssl_client_certificate /etc/ssl/certs/homelab-fullchain.pem;
    ssl_verify_client on;
    # Y opcionalmente exigir que el SAN sea el esperado
    ssl_verify_depth 2;
    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Client-Verify $ssl_client_verify;
        proxy_set_header X-Client-DN $ssl_client_s_dn;
    }
}

La directiva ssl_verify_client on es la que exige el certificado. Con optional el servidor acepta conexiones sin certificado y valida el que venga; con off no verifica nada. Y las dos variables X-Client-Verify y X-Client-DN te dan el resultado de la verificación y la identidad del cliente, que es lo que usas para autorizar.

Para autorizar por nombre de cliente, el módulo authz con nginx hace el trabajo:

location / {
    auth_ssl on;
    auth_ssl_client_certificate /etc/ssl/certs/homelab-fullchain.pem;
    auth_ssl_verify_client on;
    auth_ssl_name homeassistant.homelab.lan;
    auth_basic "API interna";
    auth_basic_user_file /etc/nginx/.htpasswd;
    proxy_pass http://127.0.0.1:9000;
}

El resultado: un dispositivo de la LAN sin el certificado correcto recibe un 403 aunque tenga la contraseña correcta, y un dispositivo con el certificado correcto entra sin necesidad de contraseña.

El cliente presenta su certificado con las opciones de curl:

curl --cert /etc/ssl/certs/cliente.pem 
     --key /etc/ssl/private/cliente.key 
     --cacert /usr/local/share/ca-certificates/homelab-root.crt 
     https://api.homelab.lan

Conclusión

El error conceptual más común con los certificados internos es pensar que “sin internet no hay HTTPS”. Hay HTTPS todo el tiempo en las redes locales, lo que cambia es quién firma el certificado y cómo se distribuye esa confianza. Una CA propia te da exactamente lo que te da Let’s Encrypt en el internet —certificados válidos, sin advertencias, validables por cualquier cliente— pero con la ventaja de que puedes emitirlos para *.homelab.lan, para direcciones IP internas y para nombres que nunca existirán en el DNS público.

Mi recomendación práctica: empieza con mkcert si tienes menos de una decena de máquinas. Cinco minutos de instalación y un script para copiar la raíz te resuelven el 90% de los casos. Sube a step-ca cuando la emisión manual se vuelva un estorbo o cuando necesites mTLS entre servicios: es el momento en que la automatización de la CA empieza a pagar su costo de complejidad.

Y una cosa que no puedes delegar en ninguna herramienta: la clave raíz es el activo más sensible de tu red. Trátala como una contraseña maestra, no como un archivo de configuración.


Preguntas frecuentes

¿Puedo usar el mismo certificado de mkcert en varios servidores?

Sí, técnicamente funciona, pero no es lo recomendado. Si el mismo certificado está en varios servidores, la clave privada está en todos ellos y revocar uno implica revocar todos. Con step-ca emites un certificado por servidor y puedes revocarlos individualmente. Con mkcert, genera un certificado distinto por host aunque uses la misma CA, así que la clave de cada servicio vive solo en su máquina.

¿Por qué mi navegador acepta el certificado pero curl no?

Porque curl y el navegador no siempre leen el mismo almacén de confianza. En Debian y Ubuntu ambos deberían usar /etc/ssl/certs/ca-certificates.crt, así que si el navegador funciona y curl no, revisa que update-ca-certificates se haya ejecutado y que el archivo fullchain que sirves incluya el rootCA.pem. Si estás dentro de un contenedor Docker, esa es casi seguro la causa: el contenedor no ve el almacén del host.

¿Cómo hago para que un contenedor Docker confíe en mi CA?

Monta la raíz dentro del contenedor como archivo y actualiza el almacén allí, o monta directamente el bundle del sistema. En el compose: -v /usr/local/share/ca-certificates/homelab-root.crt:/usr/local/share/ca-certificates/homelab-root.crt:ro y -v /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro. Para imágenes que no ejecutan update-ca-certificates al arrancar, la variable SSL_CERT_FILE apuntando a la ruta de la CA dentro del contenedor es la alternativa más fiable.

¿Qué diferencia hay entre mkcert y step-ca en la práctica?

mkcert es una CA local de un solo equipo: tú generas los certificados a mano y copias la raíz a cada máquina. step-ca es un servicio centralizado que cualquiera de tus nodos puede consultar vía ACME para emitir y renovar certificados automáticamente, con vida corta y revocación sin CRL. Si tienes cinco máquinas y desarrollo local, mkcert sobra. Si tienes veinte servicios que rotan credenciales, necesitas step-ca.

¿Qué pasa si pierdo la clave raíz de mi CA?

Si pierdes rootCA-key.pem de mkcert, no puedes emitir más certificados nuevos ni revocar los existentes. Los certificados ya emitidos siguen funcionando hasta que expiren, porque la raíz pública sigue instalada en los dispositivos. La solución es crear una CA nueva con mkcert -install, reinstalar la raíz nueva en todos los clientes y reemitir todos los certificados. Con step-ca tienes la raíz fuera de línea separada del servidor, lo que hace que perder la clave raíz sea un incidente de recuperación y no una pérdida total.


Lee también

Alfonso Moreno | Sysadmin Senior

Soy Alfonso, sysadmin senior con RHCE y más de 10 años administrando sistemas Linux para empresas y proyectos personales. Me especializo en infraestructura, redes, virtualización, monitoring, automation y self-hosting. Sobre SoyLinux.com SoyLinux.com es un blog técnico en español sobre Linux, software libre y self-hosting. Lo mantengo en mi tiempo libre. Áreas de expertise - Linux server administration: RHEL, CentOS, Rocky, Alma, Fedora, Debian, Ubuntu - Network services: Postfix, BIND, Samba, NFS, Squid - Virtualización y containers: KVM, Proxmox, Docker, Podman - Monitoring: Prometheus, Grafana, Zabbix - Automation: Ansible, Bash scripting - Self-hosting: Vaultwarden, Nextcloud, Jellyfin, Immich, Homelab Contacto - Email: [email protected]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *