En la lección anterior resolviste la confianza: un certificado X.509 ata la clave pública de portal.medinube.example a su identidad con la firma de una CA. Pero el certificado, en un fichero, no protege ningún byte. Falta el protocolo que lo usa en cada conexión: TLS (Transport Layer Security), la capa que hay debajo de la "s" de HTTPS. Esta lección tiene algo especial: no vas a aprender casi ninguna primitiva nueva. Vas a ver cómo ECDH con X25519 (04-04), los certificados y su cadena (05-01), las firmas (04-03), HKDF (02-04) y los AEAD (02-03) encajan, pieza a pieza, en el handshake de TLS 1.3 — el momento "ya lo sabías todo" del curso. Después bajaremos al barro: inspeccionar servidores con openssl s_client, hacer TLS bien desde Python (y corregir un verify=False heredado en MediNube), configurar nginx como terminador TLS, y entender mTLS y el contraste con SSH.

Contenido

  1. Qué garantiza TLS y dónde vive
  2. El handshake de TLS 1.3, pieza a pieza (ya las conoces todas)
  3. Forward secrecy en TLS
  4. TLS 1.2 vs 1.3 y las versiones muertas
  5. Inspección práctica con openssl s_client
  6. TLS correcto desde Python (y el pecado de verify=False)
  7. El lado servidor: nginx como terminador TLS
  8. mTLS: cuando el cliente también se identifica
  9. SSH, el otro protocolo cotidiano: TOFU vs PKI

Qué garantiza TLS y dónde vive

TLS es un protocolo que convierte un canal inseguro (TCP) en uno que ofrece, de una tacada, tres de los cuatro objetivos del módulo 1:

  • Confidencialidad: nadie que escuche el cable lee los historiales de Ana Pérez.
  • Integridad: nadie modifica los datos en tránsito sin que se detecte (el tag del AEAD, 02-03).
  • Autenticidad del servidor: la Clínica Sol habla con el verdadero portal.medinube.example, no con MalloryClinic (el certificado, 05-01). La del cliente es opcional (mTLS, más abajo).

(El no repudio no está en la lista: TLS usa MACs y claves simétricas compartidas para el tráfico, así que cualquiera de los dos extremos pudo generar cualquier registro. Para no repudio, firmas de aplicación como las recetas de 04-03.)

¿Dónde vive? Entre el transporte y la aplicación:

Capa Protocolo Ejemplo
Aplicación HTTP, SMTP, IMAP... GET /pacientes/ana.perez
Seguridad TLS handshake + registros cifrados
Transporte TCP puertos, retransmisión
Red IP enrutado

HTTPS no es un protocolo distinto de HTTP: es HTTP corriendo sobre TLS (puerto 443 en lugar de 80). Lo mismo vale para SMTPS, IMAPS, etc. La aplicación escribe bytes en claro y TLS los cifra por debajo; por eso añadir TLS no obliga a reescribir la aplicación. Importante para MediNube: TLS protege datos en tránsito; los datos en reposo (la base de datos de historiales) son otro problema, que retomaremos en 06-02.

El handshake de TLS 1.3, pieza a pieza (ya las conoces todas)

Antes de cifrar el primer byte de aplicación, cliente y servidor negocian parámetros, se autentican y acuerdan claves. Es el handshake. En TLS 1.3 (RFC 8446, 2018) cuesta una sola ida y vuelta (1-RTT):

sequenceDiagram
    participant C as Cliente (navegador de Ana Pérez)
    participant S as portal.medinube.example
    Note over C: Genera par efímero X25519 (como en 04-04)
    C->>S: ClientHello<br/>versiones + cifrados soportados +<br/>clave pública efímera X25519 + SNI
    Note over S: Genera SU par efímero X25519<br/>ECDH → secreto compartido<br/>HKDF → claves de sesión (02-04)
    S->>C: ServerHello (clave pública efímera X25519)<br/>🔒 Certificado (cadena de 05-01)<br/>🔒 CertificateVerify (firma, 04-03)<br/>🔒 Finished
    Note over C: ECDH + HKDF → mismas claves<br/>Verifica cadena del certificado (05-01)<br/>Verifica la firma CertificateVerify
    C->>S: 🔒 Finished
    Note over C,S: 🔒 Datos de aplicación con AEAD:<br/>AES-256-GCM o ChaCha20-Poly1305 (02-03)

(🔒 = ya cifrado con las claves recién derivadas.) Recorramos cada pieza reconociendo de dónde viene:

  1. ClientHello. El cliente propone versiones y cipher suites, e incluye directamente su clave pública efímera X25519 (en TLS 1.3 no se espera a negociar: se apuesta por las curvas más comunes y se adjunta la key share). También envía el SNI (Server Name Indication): a qué dominio quiere conectarse, imprescindible cuando una IP sirve varios dominios — es el -servername que usaste en 05-01.
  2. ECDH efímero (ECDHE). El servidor genera su propio par efímero, y ambos calculan el secreto compartido. Esto es, literalmente, el intercambio X25519 que programaste en 04-04. La "E" final de ECDHE significa ephemeral: claves de usar y tirar, una pareja nueva por conexión.
  3. Derivación de claves. Del secreto ECDH no se usa ni un byte directamente: se pasa por HKDF (02-04) en un "calendario de claves" (key schedule) que deriva claves distintas para cada dirección (cliente→servidor y servidor→cliente) y cada fase. Regla que ya conocías: nunca uses un secreto DH crudo, deriva.
  4. Certificate. El servidor envía su certificado hoja más las intermedias. El cliente valida la cadena exactamente como en 05-01: firmas hasta una raíz del trust store, fechas, y el dominio del SNI en el SAN. Aquí es donde MalloryClinic muere.
  5. CertificateVerify. Sutileza crucial: el certificado es público — Mallory podría reenviar el certificado legítimo de MediNube. Por eso el servidor además firma con su clave privada una transcripción de todo el handshake (con ECDSA o RSA-PSS, las firmas de 04-03). Solo quien posee la clave privada correspondiente al certificado puede producir esa firma, y como cubre la transcripción (incluidas las key shares), tampoco se puede recortar y pegar en otra conexión.
  6. Finished (ambos). Un MAC (módulo 3) sobre la transcripción con las claves derivadas: confirma que ambos ven el mismo handshake, sin manipulaciones.
  7. Datos de aplicación. Todo el tráfico va con AEAD: AES-256-GCM o ChaCha20-Poly1305 (02-03), con nonces derivados de un contador de registros — irrepetibles por construcción, como exigía la lección 02-02.

Haz el inventario: X25519+ECDH (04-04), HKDF (02-04), certificados y cadena (05-01), firmas RSA-PSS/ECDSA (04-03), MAC (03), AEAD (02-03), CSPRNG para todas las claves efímeras (01). TLS 1.3 es tu curso entero, ensamblado por ingenieros de protocolos. No hay magia: hay composición cuidadosa de piezas que ya dominas.

Forward secrecy en TLS

En 04-04 viste que usar claves DH efímeras da forward secrecy: comprometer las claves de largo plazo no descifra el tráfico pasado. En TLS 1.3 esto no es opcional: todas las suites usan ECDHE. La clave privada del certificado de portal.medinube.example solo se usa para firmar (CertificateVerify), nunca para cifrar tráfico ni transportar claves de sesión.

Consecuencia práctica que debe entender todo el equipo de MediNube: si mañana se filtra la clave privada del certificado (spoiler: en 05-03 simularemos exactamente ese incidente), el atacante podrá suplantar al portal hasta que se revoque el certificado, pero no podrá descifrar ni un byte del tráfico ya capturado. En TLS 1.2 antiguo con suites RSA de transporte de clave (sin DHE/ECDHE), sí podría: grababa tráfico hoy y lo descifraba el día que consiguiera la clave. Es una de las grandes razones de la tabla siguiente.

TLS 1.2 vs 1.3 y las versiones muertas

Aspecto TLS 1.2 (2008) TLS 1.3 (2018)
Idas y vueltas del handshake 2-RTT 1-RTT (más rápido; existe 0-RTT opcional, con matices de replay)
Forward secrecy Opcional (según suite) Obligatoria (siempre ECDHE)
Cifrados admitidos Decenas de suites, incluidas malas: CBC con MAC-then-encrypt, RC4, 3DES, exportables Solo 5 suites, todas AEAD (AES-GCM, ChaCha20-Poly1305)
RSA como transporte de clave Permitido Eliminado
Cuánto handshake va cifrado Casi nada (certificado en claro) Todo desde el ServerHello (el certificado viaja cifrado)
Algoritmos rotos (MD5, SHA-1, grupos DH débiles) Presentes en el ecosistema Purgados
Renegociación y compresión (fuentes de CVEs históricos) Presentes Eliminadas

TLS 1.3 es, sobre todo, un ejercicio de poda: quitar todo lo que dos décadas de ataques (BEAST, POODLE, Lucky13, FREAK, Logjam...) demostraron frágil. Menos opciones = menos combinaciones inseguras que un administrador pueda elegir por error. Esto también es criptoagilidad (regla nº 8): el protocolo negocia versión y suite, y por eso migrar fue posible.

Versiones muertas — no negociables en 2026: SSLv2 y SSLv3 (POODLE, 2014), TLS 1.0 y TLS 1.1 (deprecadas formalmente por RFC 8996 en 2021; los navegadores las retiraron en 2020). La política de MediNube, que aplicaremos en nginx más abajo: mínimo TLS 1.2, preferido TLS 1.3. TLS 1.2 bien configurado (solo suites ECDHE+AEAD) sigue siendo aceptable; todo lo anterior, no.

Inspección práctica con openssl s_client

openssl s_client es el navegador de los pobres y la herramienta de diagnóstico de los profesionales: abre una conexión TLS y te cuenta todo lo que negoció.

# Conexión básica; -servername envía el SNI (casi siempre necesario);
# -brief resume; sin él tendrás el volcado completo.
openssl s_client -connect portal.medinube.example:443 \
    -servername portal.medinube.example </dev/null

En la salida (completa) aprende a localizar estos bloques:

CONNECTED(00000003)
depth=2 C=US, O=..., CN=Raíz Confiable          ← la cadena, de la raíz (depth=2)
depth=1 C=US, O=..., CN=CA Intermedia R3        ← a la intermedia (depth=1)
depth=0 CN=portal.medinube.example              ← y la hoja (depth=0)
verify return:1                                  ← 1 = ese eslabón valida
---
Certificate chain
 0 s:CN=portal.medinube.example                 ← s: subject de cada eslabón
   i:C=US, O=..., CN=CA Intermedia R3           ← i: su issuer (¡debe encadenar!)
 1 s:C=US, O=..., CN=CA Intermedia R3
   i:C=US, O=..., CN=Raíz Confiable
---
SSL handshake has read 4321 bytes ...
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384  ← protocolo y suite negociados
Server public key is 256 bit                     ← clave del certificado (P-256)
Verify return code: 0 (ok)                       ← EL veredicto final
---

Las tres líneas de oro: Verify return code: 0 (ok) (la cadena valida contra el trust store; cualquier otro código es un problema — 18 = autofirmado, 10 = caducado, 20 = falta la intermedia...), New, TLSv1.3, Cipher is ... (qué versión y suite se negoció) y la sección Certificate chain (qué envió el servidor; un clásico de producción es olvidar configurar la intermedia y que los clientes estrictos fallen).

Trucos de diagnóstico habituales:

# ¿Acepta el servidor versiones viejas? (debería FALLAR):
openssl s_client -connect portal.medinube.example:443 -tls1_1 </dev/null

# ¿Soporta TLS 1.3?
openssl s_client -connect portal.medinube.example:443 -tls1_3 </dev/null

# Probar TU laboratorio de 05-01 validando contra TU raíz:
openssl s_client -connect localhost:8443 -servername portal.medinube.example \
    -CAfile ~/medinube-lab/pki/ca.crt </dev/null

# Ver fechas del certificado remoto de un tirón (patrón de 05-01):
openssl s_client -connect portal.medinube.example:443 \
    -servername portal.medinube.example </dev/null 2>/dev/null \
  | openssl x509 -noout -dates

TLS correcto desde Python (y el pecado de verify=False)

Primer día revisando integraciones en MediNube y aparece esta joya en el código heredado que llama a la API del laboratorio externo:

import requests

def llamar_api_MAL(url_laboratorio: str, datos: dict) -> dict:
    # ⚠️ CÓDIGO HEREDADO — NO COPIAR ⚠️
    # "Fix" de 2023: daba SSLError: certificate verify failed,
    # y con esto 'se arregló'. (Narrador: no se arregló.)
    respuesta = requests.post(
        url_laboratorio,
        json=datos,
        verify=False,   # ← EL PECADO CAPITAL
        timeout=10,
    )
    return respuesta.json()

¿Qué hace verify=False? Desactiva por completo la validación del certificado: se acepta cualquier certificado, de cualquiera, para cualquier nombre. El tráfico sigue cifrado, y esa es la trampa psicológica — "sale por HTTPS, ¿qué problema hay?". El problema es que está cifrado hacia no se sabe quién: MalloryClinic puede presentar su certificado casero del ejercicio 3 de 05-01 y este código lo abraza. Es, exactamente, deshacer todo el módulo 5 con un argumento de teclado. El SSLError original era el sistema funcionando: probablemente faltaba la intermedia en el servidor del laboratorio, o era un certificado interno cuya CA había que distribuir. La versión correcta:

import requests

def llamar_api(url_laboratorio: str, datos: dict) -> dict:
    """Llama a la API del laboratorio con TLS verificado.

    verify=True es el valor por defecto de requests: valida cadena,
    fechas y nombre (SAN) contra el bundle de CAs (certifi).
    """
    respuesta = requests.post(url_laboratorio, json=datos, timeout=10)
    respuesta.raise_for_status()
    return respuesta.json()


def llamar_api_interna(url: str, datos: dict) -> dict:
    """Variante para servicios internos firmados por la CA del laboratorio
    de MediNube (05-01): verify= puede apuntar a UNA CA concreta.
    Así confiamos en nuestra raíz interna SOLO para esta llamada,
    sin tocar el trust store del sistema.
    """
    respuesta = requests.post(
        url, json=datos, timeout=10,
        verify="/etc/medinube/pki/ca.crt",   # la raíz del lab de 05-01
    )
    respuesta.raise_for_status()
    return respuesta.json()

Reglas: nunca verify=False (ni siquiera "temporalmente": los temporales de 2023 siguen en producción en 2026); si el certificado es de una CA interna, pasa la ruta de esa CA en verify=; si falla la validación contra un tercero, arregla la causa (avisa al proveedor de que su cadena está incompleta).

Un escalón por debajo de requests, la stdlib:

import socket
import ssl

# create_default_context() es LA forma correcta: activa validación de
# certificado Y de nombre, carga las CAs del sistema y fija versiones
# mínimas seguras. No construyas SSLContext() a pelo.
contexto = ssl.create_default_context()

# Para servicios internos del laboratorio: añade tu raíz al contexto.
# contexto.load_verify_locations("/etc/medinube/pki/ca.crt")

with socket.create_connection(("portal.medinube.example", 443)) as tcp:
    # server_hostname hace doble servicio: envía el SNI y activa
    # la comprobación del nombre contra el SAN.
    with contexto.wrap_socket(tcp, server_hostname="portal.medinube.example") as tls:
        print("Protocolo:", tls.version())            # p. ej. 'TLSv1.3'
        print("Suite:", tls.cipher()[0])              # p. ej. 'TLS_AES_256_GCM_SHA384'
        cert = tls.getpeercert()                      # dict con subject, SAN, fechas
        print("SAN:", cert.get("subjectAltName"))

Los equivalentes del pecado capital en este nivel, que encontrarás en más código heredado: ssl._create_unverified_context(), context.check_hostname = False, context.verify_mode = ssl.CERT_NONE. Todos son verify=False con otro disfraz; todos deben morir en revisión de código.

El lado servidor: nginx como terminador TLS

La aplicación Python de MediNube no habla TLS directamente: delante se pone nginx como terminador TLS (recibe HTTPS del mundo, descifra, y pasa HTTP a la aplicación por red interna). Ventajas: la configuración TLS vive en un solo sitio, mantenida por gente que solo piensa en eso, y la aplicación queda simple. Configuración esencial comentada:

# /etc/nginx/sites-available/portal.medinube.example

# Bloque 1: el puerto 80 SOLO redirige a HTTPS. Nada de servir contenido
# en claro "porque es solo la portada".
server {
    listen 80;
    server_name portal.medinube.example;
    return 301 https://$host$request_uri;
}

# Bloque 2: el servicio real, solo por TLS.
server {
    listen 443 ssl;
    http2 on;
    server_name portal.medinube.example;

    # Certificado: fullchain = hoja + intermedias (¡recuerda el error
    # clásico visto en s_client!). privkey = la clave privada, permisos 600.
    ssl_certificate     /etc/medinube/tls/fullchain.pem;
    ssl_certificate_key /etc/medinube/tls/privkey.pem;

    # Política de versiones de MediNube: 1.2 mínimo, 1.3 preferido.
    ssl_protocols TLSv1.2 TLSv1.3;
    # Para TLS 1.2, solo suites ECDHE+AEAD (en 1.3 no hace falta: todas lo son).
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;

    # HSTS: le dice al navegador "durante 2 años, háblame SOLO por HTTPS".
    # Cierra la ventana del primer acceso por HTTP. Mención breve: actívalo
    # cuando estés seguro; con 'preload' es casi irreversible.
    add_header Strict-Transport-Security "max-age=63072000" always;

    location / {
        proxy_pass http://127.0.0.1:8000;   # la app Python, en red interna
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
    }
}

No memorices suites: usa el generador de configuraciones de Mozilla (perfil "intermediate") y las herramientas de test (SSL Labs, testssl.sh) para verificar el resultado. Lo importante es el qué: redirección 80→443, cadena completa, versiones mínimas, HSTS. Cómo consigue MediNube ese fullchain.pem y cómo se renueva solo, en la próxima lección (05-03).

mTLS: cuando el cliente también se identifica

Hasta ahora, TLS autentica al servidor; el cliente (Ana Pérez) se autentica después, dentro de la aplicación, con su contraseña Argon2id del módulo 3. Pero para las integraciones máquina a máquina — el servidor de la Clínica Sol o del Centro Médico Luna llamando a la API de MediNube — hay una opción mejor: mTLS (TLS mutuo), donde también el cliente presenta un certificado y firma su CertificateVerify. El handshake es el mismo con un paso extra: el servidor solicita certificado (CertificateRequest) y el cliente responde con el suyo.

¿Cuándo usarlo? Servicio-a-servicio con pocas partes conocidas: integraciones B2B como las clínicas, microservicios internos, agentes de monitorización. ¿Cuándo no? Usuarios humanos finales (gestionar certificados en el navegador de cada paciente es inviable; ahí, contraseñas+MFA o passkeys).

Diseño de MediNube: los certificados de cliente de las clínicas los emite la CA interna del laboratorio de 05-01, con SAN clinica-sol.clientes.medinube.example y centro-luna.clientes.medinube.example. En nginx, el lado servidor se activa así:

    # Dentro del server 443 de la API de integraciones:
    ssl_client_certificate /etc/medinube/pki/ca.crt;  # CA que emite los certs de clínica
    ssl_verify_client on;                             # exige certificado de cliente
    # La app recibe la identidad verificada, sin gestionar contraseñas de API:
    proxy_set_header X-Clinica-DN $ssl_client_s_dn;

Y el cliente, desde Python: requests.post(url, cert=("/etc/clinica-sol/tls/cliente.crt", "/etc/clinica-sol/tls/cliente.key"), verify="/etc/clinica-sol/tls/medinube-ca.crt"). Fíjate en la simetría: cada lado valida al otro contra una CA; MalloryClinic no puede suplantar a ninguno de los dos extremos. El precio es operativo: emitir, renovar y sobre todo revocar certificados de clínica cuando una se da de baja — exactamente el ciclo de vida de 05-03.

SSH, el otro protocolo cotidiano: TOFU vs PKI

Cierra el mapa mental con el otro protocolo seguro que usas a diario: SSH. Criptográficamente es un primo hermano de TLS (intercambio de claves con Curve25519, AEAD, firmas Ed25519 — ¡tus piezas otra vez!), pero resuelve la confianza con un modelo distinto:

TLS (Web PKI) SSH (típico)
Modelo de confianza Terceros de confianza (CAs) TOFU: Trust On First Use
Primera conexión Cadena validable desde el minuto cero Te muestra la huella de la clave del servidor y te pregunta ("Are you sure you want to continue connecting?")
Conexiones siguientes Se revalida la cadena cada vez Compara contra ~/.ssh/known_hosts; si la clave cambió, alarma
Punto débil La peor CA del trust store La primera conexión: si Mallory está en medio ese día, aceptaste su clave
Escala Millones de servidores desconocidos Puñado de servidores que administras

TOFU es razonable cuando conectas pocas veces a máquinas que conoces (y puedes verificar la huella por otro canal); no escala a "cualquier navegador contra cualquier web". Curiosidad para dejar el hilo tendido: SSH también admite certificados con CAs propias para flotas grandes de servidores — cuando una organización crece, siempre acaba reinventando una PKI.

Errores Comunes y Consejos

  • verify=False y su familia (CERT_NONE, check_hostname=False, curl -k). Cifrar sin autenticar es cifrar hacia Mallory. Ante un error de certificado: diagnostica con openssl s_client, arregla la causa, jamás silencies.
  • Olvidar las intermedias en el servidor. Tu navegador quizá lo tolere (cachea intermedias), pero requests u otro backend fallará. Sirve siempre el fullchain, y verifica con s_client que la sección Certificate chain está completa.
  • Olvidar el SNI en las pruebas. Sin -servername (o sin server_hostname en Python), muchos servidores devuelven el certificado por defecto y creerás que está roto. Si s_client te da un certificado "raro", revisa el SNI antes de nada.
  • Creer que "HTTPS" implica seguro todo el sistema. TLS protege el tránsito. La base de datos, los logs, los backups (06-02) y la aplicación siguen siendo tu problema — regla de oro nº 9.
  • Fijar versiones o suites exóticas a mano "para más seguridad". Configuraciones creativas rompen clientes y a veces bajan la seguridad. Perfil Mozilla intermediate + test automatizado.
  • Terminar TLS en nginx y olvidar la red interna. El proxy_pass http:// en claro es aceptable en localhost o red estrictamente controlada; entre hosts o zonas de confianza distintas, también TLS (o mTLS) interno. Lo retomamos en 06-02.

Ejercicios

Ejercicio 1 — Lectura de handshake. Conéctate con openssl s_client (con -servername) a dos sitios públicos que uses y responde para cada uno: (a) ¿versión y cipher suite negociadas? (b) ¿cuántos eslabones envió el servidor y quién es la intermedia? (c) ¿Verify return code? (d) ¿acepta TLS 1.1? Justifica por qué el resultado de (d) es el esperable en 2026.

Ejercicio 2 — Caza del verify=False. Este código heredado de MediNube consulta el estado de las recetas en la API de Farmacia Robles, que usa un certificado emitido por la CA interna compartida del consorcio de farmacias (/etc/medinube/pki/farmacias-ca.crt). Identifica todos los problemas y reescríbelo correctamente:

import requests, urllib3
urllib3.disable_warnings()

def estado_receta_MAL(id_receta):
    r = requests.get(
        "https://api.farmacia-robles.example/recetas/" + id_receta,
        verify=False)
    return r.json()

Ejercicio 3 — Tu laboratorio, servido por TLS. Con el portal.crt/portal.key de la mini-PKI de 05-01, levanta un servidor HTTPS local en el puerto 8443 usando http.server + ssl.SSLContext de la stdlib, y verifícalo con openssl s_client ... -CAfile ~/medinube-lab/pki/ca.crt, comprobando Verify return code: 0 (ok) y la versión negociada. (Pista: ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(...) + ctx.wrap_socket(httpd.socket, server_side=True); añade 127.0.0.1 portal.medinube.example a /etc/hosts o usa -servername.)

Soluciones

Solución 1. Ejemplo de sesión y lectura:

openssl s_client -connect ejemplo.com:443 -servername ejemplo.com </dev/null 2>/dev/null | grep -E "New,|Verify"
# → New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384   (a)
# → Verify return code: 0 (ok)                        (c)
openssl s_client -connect ejemplo.com:443 -servername ejemplo.com </dev/null 2>/dev/null | grep -A6 "Certificate chain"   # (b): cuenta los pares s:/i:
openssl s_client -connect ejemplo.com:443 -tls1_1 </dev/null
# → normalmente error tipo "no protocols available" o alerta del servidor  (d)

(d) Debe fallar: TLS 1.0/1.1 están deprecadas por RFC 8996 y retiradas de clientes desde 2020; además tu propio binario de openssl moderno puede negarse a intentarlo. Que un servidor las acepte hoy es un hallazgo de auditoría.

Solución 2. Problemas: (1) verify=False: acepta cualquier certificado — MalloryClinic puede responder con recetas falsas o capturar los identificadores; (2) urllib3.disable_warnings(): silencia deliberadamente el aviso que delataba el problema — el "fix" que oculta el incendio; (3) sin timeout: una API colgada bloquea el hilo para siempre; (4) sin control de errores HTTP; (5) el error de fondo era simplemente que la CA del consorcio no es pública: la solución es confiar en esa CA explícitamente, no en todas ni en ninguna. Reescritura:

import requests

CA_FARMACIAS = "/etc/medinube/pki/farmacias-ca.crt"

def estado_receta(id_receta: str) -> dict:
    r = requests.get(
        f"https://api.farmacia-robles.example/recetas/{id_receta}",
        verify=CA_FARMACIAS,   # confiamos SOLO en la CA del consorcio
        timeout=10,
    )
    r.raise_for_status()
    return r.json()

Solución 3.

import http.server
import ssl

httpd = http.server.HTTPServer(("0.0.0.0", 8443),
                               http.server.SimpleHTTPRequestHandler)

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)   # rol servidor, versiones seguras
ctx.load_cert_chain(certfile="/home/tu_usuario/medinube-lab/pki/portal.crt",
                    keyfile="/home/tu_usuario/medinube-lab/pki/portal.key")
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
print("Sirviendo en https://localhost:8443 ...")
httpd.serve_forever()

Verificación (el SNI/-servername hace que el nombre pedido case con el SAN del certificado):

openssl s_client -connect localhost:8443 -servername portal.medinube.example \
    -CAfile ~/medinube-lab/pki/ca.crt </dev/null 2>/dev/null | grep -E "New,|Verify"
# → New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
# → Verify return code: 0 (ok)

Sin -CAfile obtendrías código 19/20 ("self-signed certificate in certificate chain" / "unable to get local issuer"): tu raíz de laboratorio no está en el trust store del sistema — y así debe seguir; se confía en ella explícitamente, llamada a llamada.

Conclusión

TLS era el examen sorpresa que ya llevabas aprobado: ECDHE con X25519 para acordar secretos con forward secrecy (04-04), HKDF para derivar las claves de sesión (02-04), el certificado X.509 y su cadena para saber con quién hablas (05-01), una firma CertificateVerify que demuestra posesión de la clave privada (04-03) y AEAD para todo el tráfico (02-03) — empaquetado en un handshake de una sola ida y vuelta. Por el camino has aprendido la parte que no sale en los libros de teoría: leer openssl s_client como un profesional, desterrar verify=False y sus disfraces del código de MediNube, configurar nginx como terminador TLS decente (1.2 mínimo, 80→443, HSTS), reservar mTLS para las integraciones clínica↔MediNube y situar el modelo TOFU de SSH frente a la PKI.

Queda la última milla, la que distingue un despliegue de fin de semana de un servicio serio: los certificados caducan. ¿Quién emite el de portal.medinube.example, quién lo renueva a las 4 de la mañana, quién se entera antes de que expire, y qué hacemos el día que la clave privada aparezca donde no debe? Todo eso es operación, tiene protocolo propio (ACME) y hasta runbook de incidente. Nos vemos en 05-03: Gestión del Ciclo de Vida de Certificados, la lección que cierra el módulo.

© Copyright 2026. Todos los derechos reservados