StackPractices
beginner Por Mathias Paulenko

Configurar Certificados SSL con Let's Encrypt

Guía paso a paso para obtener certificados TLS gratuitos de Let's Encrypt con Certbot — en Nginx, Apache, standalone, Docker y Kubernetes.

Temas: devops

Descripción General

HTTPS necesita un certificado de una CA de confianza, y Let’s Encrypt los emite gratis mediante el protocolo ACME. Certbot es el cliente oficial: demuestra que el dominio es tuyo, descarga el certificado, lo instala y —la parte que la gente olvida— lo renueva antes de que expire.

Esta receta cubre las tres formas en que Certbot valida la propiedad (plugins del servidor web, standalone y DNS-01 para wildcards), además de la automatización de la renovación, despliegues en Docker y Kubernetes, y los detalles operativos —rate limits, staging, monitorización— que evitan que un certificado se convierta en una caída a las 3 de la madrugada.

Cuándo Usar

Usa esta receta cuando:

  • Configuras HTTPS para una nueva aplicación web pública.
  • Reemplazas certificados expirados o autofirmados por unos de confianza para los navegadores.
  • Automatizas la renovación de certificados para que nunca expiren sin aviso.
  • Configuras TLS en un reverse proxy, balanceador de carga, contenedor Docker o ingress de Kubernetes.
  • Necesitas una alternativa gratuita y automatizada a las CAs de pago.

Prerequisitos

Antes de ejecutar Certbot, asegúrate de que:

  • El registro A de tu dominio (y AAAA si usas IPv6) apunta a la IP pública de este servidor — Let’s Encrypt valida desde internet público.
  • Los puertos 80 y 443 están abiertos en el firewall; la validación HTTP-01 usa el puerto 80, también en cada renovación.
  • Para el modo --nginx / --apache, ya hay un sitio HTTP respondiendo en el puerto 80 — Certbot lee el server block existente.
  • Para todo lo que no sea una prueba puntual, usa --staging primero. El límite de producción de Let’s Encrypt es de 50 certificados por dominio registrado por semana (5 duplicados por semana) — staging te deja iterar sin gastarlo.

Cómo Funciona

Certbot pide un certificado a Let’s Encrypt y la CA responde con un desafío: coloca un token en http://tudominio.com/.well-known/acme-challenge/<token>. Let’s Encrypt descarga esa URL desde internet público — si el token coincide, has demostrado que controlas el dominio y el certificado se emite. El mismo desafío se repite en cada renovación, por eso el puerto 80 debe seguir abierto.

flowchart diagram: Certbot en tu servidor

Cuando el puerto 80 no puede exponerse —redes internas, certificados wildcard— el desafío DNS-01 demuestra la propiedad con un registro TXT.

Implementación Paso a Paso

Instalar Certbot

# Ubuntu / Debian — snap es el método oficialmente recomendado
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

# O por apt (funciona bien, puede ir una versión por detrás)
sudo apt update
sudo apt install -y certbot python3-certbot-nginx

# Los setups con Apache necesitan su propio plugin
sudo apt install -y python3-certbot-apache

# Verificar instalación
certbot --version

Nginx (Plugin Automático)

# Prueba primero contra staging — no gastes los rate limits
sudo certbot --nginx --staging -d example.com -d www.example.com

# Y luego en serio
sudo certbot --nginx -d example.com -d www.example.com

# Probar la renovación automática
sudo certbot renew --dry-run

El plugin --nginx lee tu server block, instala el certificado y configura la redirección a HTTPS. El resultado queda así:

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

http2 on; es la sintaxis actual — la forma antigua listen 443 ssl http2; está deprecated desde nginx 1.25 y emite un warning.

Apache (Plugin Automático)

sudo certbot --apache -d example.com -d www.example.com
# /etc/apache2/sites-available/example.com.conf
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
    SSLHonorCipherOrder on
</VirtualHost>

Standalone (Sin Servidor Web)

# Standalone levanta un servidor temporal en el puerto 80 — nada más puede ocuparlo
sudo certbot certonly --standalone -d example.com

# Los certificados quedan en /etc/letsencrypt/live/example.com/
# Apunta tu aplicación a:
#   /etc/letsencrypt/live/example.com/fullchain.pem
#   /etc/letsencrypt/live/example.com/privkey.pem

Desafío DNS para Certificados Wildcard

Los certificados wildcard (*.example.com) solo se pueden validar por DNS-01:

# Validación DNS manual — añades el registro TXT tú mismo
sudo certbot certonly --manual --preferred-challenges dns \
  -d "*.example.com" -d example.com \
  --agree-tos --email admin@example.com

# Automatizada con Cloudflare
pip install certbot-dns-cloudflare
sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
  -d "*.example.com" -d example.com
# ~/.secrets/cloudflare.ini — restríngelo: sudo chmod 600 ~/.secrets/cloudflare.ini
dns_cloudflare_api_token = your-api-token-here

El archivo de credenciales guarda un API token en texto plano — déjalo en 600 y trátalo como una contraseña, la misma regla que aplicarías a cualquier secreto.

Docker / Certbot

# docker-compose.yml
services:
  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
      - ./certbot/log:/var/log/letsencrypt
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
# nginx.conf
server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://app:3000;
    }
}

Ingress de Kubernetes con cert-manager

# cert-manager.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - example.com
        - www.example.com
      secretName: example-tls
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app
                port:
                  number: 80

Mantén un segundo ClusterIssuer apuntando al endpoint de staging (https://acme-staging-v02.api.letsencrypt.org/directory) y cámbiate a él mientras depuras — la misma lógica de rate limits que con --staging.

Archivos complementarios: el repo companion de setup-ssl-certificates tiene el stack de docker-compose, el nginx.conf, los manifiestos de cert-manager y una plantilla de credenciales listos para ejecutar.

Renovación Automática

# Certbot instala un systemd timer en la mayoría de sistemas — verifica que está activo
sudo systemctl status certbot.timer

# Fallback con cron, con un hook que recarga el servidor web
sudo crontab -e
# Agregar:
# 0 3 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

El --deploy-hook importa más de lo que parece: certbot renew sustituye los archivos del certificado en disco, pero nginx sigue sirviendo el viejo hasta que recargas. Para un runbook operativo estructurado sobre esto, mira la plantilla de renovación de certificados.

Variantes

OpciónCostoValidaciónIdeal para
Let’s Encrypt + CertbotGratisHTTP-01 / DNS-01Sitios públicos, la mayoría de casos
ZeroSSL (ACME)Tier gratisHTTP / DNSMismo protocolo, CA alternativa
Cloudflare Origin CAGratisGestionado por CloudflareSitios totalmente detrás de Cloudflare
HTTPS gestionado por la plataformaIncluidoAutomáticoVercel/Netlify/LB cloud — sin Certbot
CA de pago (DigiCert, etc.)$$$Validación extendidaRequisitos de compliance, plazos largos
AutofirmadoGratisNingunaSolo interno/dev — los navegadores avisan

Headers de Seguridad

# Agregar al server block HTTPS
server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Headers de seguridad
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Reanudación de sesión — evita el handshake completo en clientes recurrentes
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
}

Una nota sobre OCSP stapling: verás ssl_stapling on; recomendado en tutoriales antiguos, pero Let’s Encrypt retiró su servicio OCSP en 2025. Con certificados de LE la directiva ya no hace nada — sáltala. Sigue aplicando a CAs que mantienen sus propios respondedores OCSP.

Lo que funciona

  • Usa el plugin del servidor web (--nginx, --apache) siempre que puedas — edita la configuración por ti y elimina toda una clase de errores.
  • Configura la renovación automática desde el día uno. Los certificados duran 90 días; un cron olvidado se convierte en una caída en producción.
  • Incluye el dominio y el subdominio www en la solicitud para evitar advertencias en cualquiera de las URLs.
  • Redirige HTTP a HTTPS en el servidor web, no en el código de aplicación — es más rápido y una cosa menos que mantener.
  • Usa fullchain.pem, no cert.pem. La cadena completa incluye los intermedios que los navegadores necesitan para validar la confianza.

Errores Comunes

  • Cerrar el puerto 80 tras la configuración. La validación HTTP-01 se repite en cada renovación — no solo en la primera emisión.
  • Pedir certificados para dominios internos (app.local). Let’s Encrypt solo firma nombres públicamente resolvibles.
  • Probar contra producción. Unas pocas pruebas con --staging primero te mantienen lejos del límite de 50 certs por semana.
  • Servir cert.pem en vez de fullchain.pem — los intermedios que faltan causan errores de “certificado no confiable”.
  • Renovar sin deploy hook. Los archivos cambian en disco; el servidor web sigue sirviendo el certificado viejo hasta recargar.
  • No probar con --dry-run antes de la ventana de renovación — una primera emisión exitosa no demuestra nada sobre la renovación.

Monitorización de la Expiración de Certificados

# Comprobar la expiración de forma remota
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -dates

Para flotas, alerta con el blackbox exporter de Prometheus en lugar de comprobarlo a mano:

# Alertar si el certificado expira en menos de 7 días
- alert: SslCertificateExpiringSoon
  expr: probe_ssl_earliest_cert_expiry - time() < 7 * 24 * 3600
  for: 10m
  labels:
    severity: warning

Tips de Rendimiento

  1. Usa reanudación de sesión — una caché de sesiones compartida evita el handshake TLS completo en clientes que vuelven:
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
  1. Prefiere ECDSA sobre RSA — claves más pequeñas implican handshakes más rápidos y menos CPU:
sudo certbot certonly --key-type ecdsa --elliptic-curve secp256r1 -d example.com
  1. Mantén HTTP/2 activado — http2 on; multiplexa peticiones sobre la misma conexión TLS, que es donde está gran parte de la mejora real de velocidad.

Solución de Problemas

  • “Connection refused” durante la emisión: el puerto 80 no es alcanzable desde internet — revisa firewall, security groups y que el DNS resuelve a este servidor.
  • “Too many failed authorizations”: tocaste un rate limit de Let’s Encrypt — reintenta con --staging mientras depuras y espera a que pase la ventana de producción (normalmente una hora para validaciones fallidas, una semana para certificados).
  • La renovación funciona pero sigue sirviendo el certificado viejo: falta el --deploy-hook — los archivos se renovaron pero nginx nunca recargó.
  • certbot renew falla en Docker: la ruta del desafío /.well-known/acme-challenge/ no se está sirviendo — verifica que el location de nginx mapea al volumen compartido certbot/www.

Lectura Recomendada

Preguntas frecuentes

¿Cuál es la diferencia entre un certificado autofirmado y uno firmado por CA?

Un certificado autofirmado es uno que generaste tú mismo — nada lo avala, así que los navegadores avisan. Uno firmado por CA lleva la acreditación de una autoridad confiable de que controlas el dominio, así que los navegadores lo aceptan por defecto.

¿Cómo valida Let's Encrypt la propiedad del dominio?

Mediante desafíos ACME — HTTP-01 coloca un token en /.well-known/acme-challenge/ que la CA descarga públicamente, mientras que DNS-01 exige un registro TXT. Los wildcard necesitan DNS-01.

¿Qué pasa si falla la renovación de un certificado?

El certificado actual sigue funcionando hasta su expiración a los 90 días — Certbot registra el error y reintenta en la próxima ejecución del timer. La monitorización (openssl o un probe blackbox) es lo que convierte un fallo silencioso en una alerta.

¿Por qué usar --staging al probar comandos de Certbot?

Staging emite certificados no confiables pero con rate limits mucho más laxos — te permite depurar tu setup sin consumir la cuota de producción de 50 certificados por dominio por semana.

¿Cómo revoco o borro un certificado?

certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem lo revoca públicamente; certbot delete --cert-name example.com elimina los archivos locales y la configuración de renovación.