Procedimiento para habilitar HTTPS (TLS/SSL) en un servidor web Nginx, utilizando un certificado TLS autofirmado en Ubuntu Server. Se aborda la generación del par de claves y el certificado mediante OpenSSL, la configuración del virtual host para escuchar en puerto 443 con las directivas SSL adecuadas. Mostramos las diferencias a nivel de seguridad entre usar certificados autofirmados y certificados de CA de autoridades de confianza.
- Requisitos para pasar servidor web de HTTP a HTTPS (SSL/TLS).
- Generación del certificado autofirmado.
- Configurar servidor Nginx para usar certificado y pasarlo a HTTPS (SSL/TLS).
- Certificados autofirmados vs CA Autorizada (Let’s Encrypt o similar).
Requisitos para pasar servidor web de HTTP a HTTPS (SSL/TLS)
Dispondremos de un servidor web con Nginx. En el siguiente tutorial indicamos cómo desplegar un servicio web con proxy y balanceo de carga en dos nodos:
En este otro tutorial mostramos cómo implementar un servidor web Nginx en Ubuntu Server 22:
Generación del certificado autofirmado
Para generar el certificado autofirmado, desde el shell del equipo Linux con Nginx, ejecutaremos los siguientes comandos. En primer lugar crearemos una carpeta para almacenar los datos del certificado que generaremos:
|
1 |
sudo mkdir -p /etc/nginx/ssl |
Generaremos el certificado con (cambiando los datos «Murcia», «proyectoa.com», «ES», por los que deseemos):
|
1 2 3 4 |
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/sitio.key \ -out /etc/nginx/ssl/sitio.crt \ -subj "/C=ES/ST=Murcia/L=Murcia/O=Organizacion/CN=proyectoa.com" |
Los parámetros del comando anterior y su significado:
- req -x509: genera certificado autofirmado formato X.509.
- -nodes: no cifra la clave privada.
- -days 365: validez del certificado.
- -newkey rsa:2048: genera nueva clave RSA 2048 bits.
- -subj: datos del sujeto (C=país, ST=provincia, L=ciudad, O=organización, CN=nombre común/dominio)
Estableceremos los permisos para los dos ficheros generados:
|
1 2 |
sudo chmod 600 /etc/nginx/ssl/sitio.key sudo chmod 644 /etc/nginx/ssl/sitio.crt |
Configurar servidor Nginx para usar certificado y pasarlo a HTTPS (SSL/TLS)
Configuraremos ahora el fichero de nuestro sitio web Nginx. Si estamos usando un proxy/balanceador, (como en el tutorial que indicamos anteriormente), aplicaremos este proceso en el nodo proxy/balanceador. Editaremos el fichero:
|
1 |
sudo nano /etc/nginx/sites-available/api_balanceador.conf |
(Sustituiremos api_balanceador.conf por el fichero de configuración del site que tengamos en Nginx)
Y aplicaremos el siguiente contenido (en este ejemplo tenemos dos nodos con Nginx 192.168.1.10 y 192.168.1.11, el balanceador conecta con los nodos por HTTP, pero el balanceador conectará con los clientes de Internet por HTTPS, que es lo que queremos):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
upstream backend { least_conn; server 192.168.1.10:80 max_fails=3 fail_timeout=30s; server 192.168.1.11:80 max_fails=3 fail_timeout=30s; keepalive 32; } server { listen 80; listen 8080; server_name tudominio.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name tudominio.com; ssl_certificate /etc/nginx/ssl/sitio.crt; ssl_certificate_key /etc/nginx/ssl/sitio.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; access_log /var/log/nginx/balanceador_access.log; error_log /var/log/nginx/balanceador_error.log; location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 60s; proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; } } |
En el ejemplo anterior hemos añadido un par de redirecciones, tanto las del puerto 80, como las del puerto 8080, ambas serán redireccionadas al HTTPS.
En el caso de un servidor web Nginx «normal» (sin proxy/balanceos), el fichero de configuración del site:
|
1 |
sudo nano /etc/nginx/sites-available/tudominio.conf |
Tendría el siguiente contenido para usar HTTPS, con puertos normales 80 (con redirección a HTTPS) y 443 (HTTPS):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
server { listen 80; server_name tudominio.com www.tudominio.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name tudominio.com www.tudominio.com; root /var/www/tudominio; index index.html index.php; ssl_certificate /etc/nginx/ssl/sitio.crt; ssl_certificate_key /etc/nginx/ssl/sitio.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; access_log /var/log/nginx/tudominio_access.log; error_log /var/log/nginx/tudominio_error.log; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; } location ~ /\. { deny all; } } |
Habilitaremos los sites (si no lo hemos hecho ya), con:
|
1 |
sudo ln -s /etc/nginx/sites-available/tudominio.conf /etc/nginx/sites-enabled/ |
Si existe, conviene eliminar el site por defecto de Nginx, con:
|
1 |
sudo rm -f /etc/nginx/sites-enabled/default |
Comprobaremos la sintaxis de los ficheros de configuración de Nginx, con:
|
1 |
sudo nginx -t |
Y, si todo es correcto, reiniciaremos el servicio de Nginx para aplicar los cambios y pasar el tráfico a HTTS (SSL/TLS):
|
1 |
sudo systemctl reload nginx |
Si tenemos el cortafuegos de Linux habilitado, será conveniente abrir los puertos que condiremos (443, 80, etc.):
|
1 2 3 |
sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable |
Si intentamos acceder desde un navegador al servidor web con HTTPS y certificado autofirmado, el navegador nos mostrará una advertencia:
Su conexión no es privada.
Es posible que los atacantes estén intentando robar tu información de api.proyectoa.com (por ejemplo contraseñas, mensajes o tarjetas de crédito).
net::ERR_CERT_COMMON_NAME_INVALID

Por lo que, para sitios web de producción que vayan a usarse desde navegadores, no es conveniente usar certificados autofirmados, emplearemos certificados de tipo Let’s Encrypt o certificados de una CA reconocida. Así evitaremos el mensaje de advertencia.
Los certificados autofirmados son útiles para entornos de prueba, de desarrollo y para servicios web específicos (tipo API Rest) que no utilizarán navegadores de Internet y que puedan asumir algunos de los problemas de seguridad que se derivan de su uso, como indicamos en el siguiente punto.
Certificados autofirmados vs CA Autorizada (Let’s Encrypt o similar)
Al configurar TLS (SSL, HTTPS) en Nginx para un servicio API REST, si la comunicación se realiza exclusivamente entre una app y el servidor, sin un navegador que muestre advertencias al usuario, ¿tiene sentido usar un certificado de una Autoridad de Certificación (CA) como Let’s Encrypt, o un certificado autofirmado ofrece el mismo nivel de seguridad?
La respuesta es que, a nivel de cifrado, son idénticos, pero a nivel de autenticación del servidor, la diferencia es muy grande y determina la seguridad real del sistema.
Ambos cifran igual de bien
Lo primero que debemos aclarar es que el protocolo TLS (Transport Layer Security) y sus algoritmos criptográficos son independientes del emisor del certificado. Tanto un certificado firmado por Let’s Encrypt como uno autofirmado, generado con openssl, pueden negociar exactamente los mismos conjuntos de cifrado (ciphersuites): AES-256-GCM, ChaCha20-Poly1305, etc.
Cuando un cliente (como por ejemplo una app) inicia una conexión TLS, el servidor Nginx envía su certificado con la clave pública. El cliente y el servidor realizan el handshake TLS y derivan una clave de sesión simétrica que usan para cifrar todo el tráfico subsiguiente. Este túnel cifrado es matemáticamente igual de robusto en ambos casos. Un atacante que capture paquetes de red solo verá un flujo de bytes indescifrable, sin importar si el certificado del servidor fue generado por una CA de confianza o por nosotros mismos.
La seguridad diferencial, por tanto, no está en cómo se cifra, sino en quién garantiza la identidad del que está al otro lado del túnel.
La autenticación en certificado autofirmado vs certificado de CA autorizada
Aquí reside la diferencia fundamental. Un certificado de CA autorizada, como los emitidos por Let’s Encrypt (o cualquier otra CA raíz presente en los almacenes de confianza de los sistemas operativos), proporciona un mecanismo de autenticación del servidor automático y verificable. La CA ha realizado un proceso de validación (generalmente de control del dominio) y ha estampado su firma digital sobre el certificado del servidor. El cliente, al conectarse, puede seguir la cadena de confianza hasta una raíz que ya conoce y en la que confía previamente, confirmando de forma fiable que está hablando con el servidor legítimo.

Un certificado autofirmado, por definición, carece de esta cadena de confianza externa. Es el propio servidor quien se autentica a sí mismo. Esto tiene dos implicaciones críticas en el contexto de una app consumiendo un API:
- Suplantación trivial (Man-in-the-Middle): Si un atacante consigue posicionarse en la ruta de red (algo posible en redes Wi-Fi públicas, corporativas o incluso domésticas comprometidas), puede generar su propio par de claves y certificado autofirmado para tu dominio en tiempo real. Como no hay una tercera parte que valide la autenticidad, el atacante puede presentar su certificado fraudulento a tu app. Si la app no tiene un mecanismo de validación muy estricto, completará el handshake TLS con el atacante, creyendo que es tu servidor. A partir de ese momento, el atacante descifra el tráfico, lo lee (tokens API, credenciales, datos sensibles) y lo reenvía opcionalmente al servidor real, todo ello de forma transparente para el cliente y el servidor.
- Ausencia de revocación práctica: Los certificados de CA tienen mecanismos de revocación (CRL y OCSP) que permiten invalidarlos si su clave privada se ve comprometida. Un autofirmado, especialmente si se configura con una validez de décadas por comodidad, carece de esta red de seguridad. Si la clave privada del servidor se filtra, un atacante puede usar ese mismo certificado para suplantar tu servicio durante años sin que los clientes que lo aceptan tengan forma de saber que ya no es válido.
Deshabilitar la validación en el cliente
Muchos desarrolladores, al enfrentarse a un certificado autofirmado durante el desarrollo, optan por la solución más rápida pero menos segura: deshabilitar la validación del certificado en el código del cliente HTTP. Fragmentos como verify=False en Python Requests, o configurar OkHttp para que «confíe en todos los certificados» en Android, generan un gran problema de seguridad. Esto no solo acepta tu certificado autofirmado, sino cualquier certificado presentado por cualquiera, anulando completamente la protección contra ataques MitM.
Este patrón puede pasar a producción por las presiones en los tiempos de entrega y por comodidad. Y es la razón principal por la que un certificado autofirmado se convierte en una vulnerabilidad crítica: porque la alternativa fácil que el desarrollador encuentra para que «funcione» es abrir un agujero de seguridad.
El enfoque seguro para autofirmados: Certificate Pinning
¿Significa esto que los autofirmados son inherentemente inseguros? No, si se implementan con la técnica correcta: Certificate Pinning. Esta estrategia, común en apps de alta seguridad o en comunicaciones M2M muy controladas, consiste en incrustar en el propio binario de la app una copia del certificado del servidor o, mejor aún, el hash de su clave pública (Subject Public Key Info – SPKI).
De esta forma, la app no delega la confianza en una CA externa, sino que confía exclusivamente en esa clave preestablecida. Durante el handshake TLS, valida que el certificado presentado coincida con el pin almacenado. Si un atacante intenta el MitM, su certificado no coincidirá y la conexión será rechazada.
Este enfoque es seguro, pero introduce una complejidad operativa muy alta:
- Rotación difícil: Cada vez que renuevas o cambias el certificado del servidor, debes actualizar la app en todos los clientes. Un fallo de coordinación deja la app inutilizada.
- Requiere un pin de respaldo: Para evitar caídas de servicio, se debe incluir el hash de la clave de un segundo par de claves (offline) que permita la transición.
- Rigidez: Complica el despliegue en entornos multi-servidor o con balanceadores de carga donde las claves pueden variar.
La recomendación para un servicio API REST en Nginx
Para el escenario descrito —un API REST PHP en Nginx consumida desde una app—, la opción clara, más segura en la práctica y mantenible es usar un certificado de una CA autorizada, y Let’s Encrypt es la elección perfecta.
Razones:
- Seguridad por defecto: Las librerías HTTP de cualquier sistema operativo (iOS, Android, Flutter, React Native, etc.) verifican la cadena de confianza sin que escribas una línea de código extra. No hay riesgo de deshabilitar la validación por accidente en producción.
- Protección real contra MitM: Un atacante no puede obtener un certificado válido para tu dominio firmado por una CA de confianza sin comprometer antes la propia CA o tu mecanismo de validación del dominio, algo órdenes de magnitud más difícil que un simple MitM en una cafetería.
- Coste cero y automatización: Let’s Encrypt es gratuito y su renovación se automatiza fácilmente con herramientas como Certbot, eliminando la supuesta ventaja de «gratuidad y rapidez» del autofirmado.
- Simplicidad operativa: Te olvidas del pinning y sus pesadillas de despliegue. La responsabilidad de la confianza recae donde debe estar: en la infraestructura de clave pública (PKI) global, que está diseñada exactamente para esto.
En resumen, un certificado autofirmado puede ser criptográficamente igual de fuerte, pero en un escenario de cliente que sale a redes no controladas (Internet), es una fuente de vulnerabilidades graves. El uso de una CA es la forma correcta de extender la cadena de confianza hasta el endpoint, garantizando no solo la confidencialidad e integridad de los datos, sino la autenticidad del servidor al que la app se conecta. Para una API de producción, la elección es clara: Let’s Encrypt o equivalente.