AlpinaShop tiene una infraestructura sólida y absolutamente nadie puede usarla. La tienda se sirve por una dirección IP numérica, sin nombre y sin certificado: escribir https://34.120.x.x en un navegador produce una advertencia roja de seguridad, y ningún cliente introduce los datos de su tarjeta después de ver eso. Todo el trabajo de los seis apartados anteriores del módulo depende de la pieza que falta: un nombre y un certificado.

Esta lección cierra el módulo 3 poniendo en marcha lo que el cliente ve realmente. Vas a crear la zona DNS pública de alpinashop.example, apuntar el dominio a la IP global alpinashop-lb-ip, delegar desde el registrador y verificarlo; aprovisionar un certificado gestionado por Google y entender por qué se queda en PROVISIONING cuando falla algo; endurecer la conexión con una política SSL; añadir las cabeceras de seguridad que debe emitir la aplicación Flask; y verificar el resultado de extremo a extremo con curl y openssl. Terminaremos con una lista de comprobación que recorre todo lo construido en el módulo, de la VPC al certificado.

Aviso. La configuración de TLS y de cabeceras de seguridad tiene efectos permanentes difíciles de revertir —HSTS es el ejemplo canónico— e implicaciones de cumplimiento si se procesan pagos. Antes de aplicar esta configuración a un dominio real de producción, revísala con un profesional de seguridad. Y una nota práctica: alpinashop.example usa el dominio .example, reservado para documentación; en un caso real trabajarías con un dominio registrado de verdad.

Contenido

  1. Repaso funcional del DNS: lo justo para operar
  2. Cloud DNS: zonas gestionadas
  3. La zona pública de AlpinaShop, registro a registro
  4. Delegación desde el registrador y verificación con dig
  5. Zonas privadas para resolución interna
  6. DNSSEC: qué protege y qué no
  7. Certificados gestionados por Google
  8. Por qué un certificado se queda en PROVISIONING
  9. Certificate Manager: muchos dominios y comodines
  10. Certificados propios y cuándo tienen sentido
  11. Políticas SSL: versión mínima y conjuntos de cifrado
  12. Cabeceras de seguridad que emite la aplicación
  13. Verificación de extremo a extremo
  14. Lista de comprobación de publicación segura del módulo 3

  1. Repaso funcional del DNS: lo justo para operar

El DNS traduce nombres a direcciones. Su modelo es un árbol invertido en el que cada nivel delega en el siguiente: la raíz delega en .example, que delega en alpinashop.example, cuyos servidores autoritativos responden por todo lo que hay debajo.

Este es el recorrido completo de lo que ocurre cuando un cliente escribe el dominio en el navegador, y conviene tenerlo delante porque en él aparecen todas las piezas de la lección:

sequenceDiagram
    participant N as Navegador
    participant R as Resolutor (8.8.8.8)
    participant RT as Servidores raíz
    participant TLD as Servidores de .example
    participant CD as Cloud DNS<br/>alpinashop-publica
    participant LB as Balanceador<br/>alpinashop-lb-ip

    N->>R: ¿A de alpinashop.example?
    R->>RT: ¿quién sabe de .example?
    RT-->>R: los servidores de .example
    R->>TLD: ¿quién sabe de alpinashop.example?
    TLD-->>R: NS → ns-cloud-b1.googledomains.com (delegación)
    R->>CD: ¿A de alpinashop.example?
    CD-->>R: 34.120.10.5 (+ firma DNSSEC)
    R-->>N: 34.120.10.5, guardada durante el TTL
    N->>LB: TLS ClientHello (SNI: alpinashop.example)
    LB-->>N: certificado alpinashop-cert + política SSL
    N->>LB: GET / sobre HTTPS
    LB-->>N: 200 + HSTS, CSP, X-Content-Type-Options

Fíjate en dos puntos que se corresponden con dos apartados de esta lección: la delegación que devuelven los servidores de .example (apartado 4) y el SNI que envía el navegador, que es lo que permite que un mismo balanceador sirva varios dominios con varios certificados (apartados 7 y 9).

Los tipos de registro que necesitas conocer:

Tipo Para qué Ejemplo
A Nombre → dirección IPv4 alpinashop.example → 34.120.10.5
AAAA Nombre → dirección IPv6 alpinashop.example → 2600:1901::…
CNAME Alias de otro nombre www → alpinashop.example
MX Servidores de correo, con prioridad 10 aspmx.l.google.com.
TXT Texto libre: verificación de dominio, SPF, DKIM, DMARC "v=spf1 include:_spf.google.com ~all"
NS Delegación: quién manda en esta zona Los cuatro servidores de Cloud DNS
CAA Qué autoridades pueden emitir certificados del dominio 0 issue "pki.goog"
SOA Metadatos de la zona Se genera solo

Tres reglas que causan la mayoría de los problemas y conviene tener presentes:

  1. No puede haber un CNAME en el vértice de la zona. alpinashop.example no puede ser un CNAME; tiene que ser un registro A o AAAA. Es una restricción del propio protocolo, porque el vértice ya tiene registros SOA y NS y un CNAME no puede coexistir con otros registros. Por eso reservamos una IP global estática en 03-01: es exactamente lo que se necesita en el vértice.
  2. El TTL manda sobre la propagación. Cuando un resolutor cachea una respuesta, la conserva el tiempo que indique el TTL. Si el TTL es de 24 horas y cambias la IP, hay clientes que seguirán yendo a la vieja durante 24 horas. Antes de una migración, baja el TTL a 60-300 segundos con días de antelación, y súbelo de nuevo cuando todo esté estable. Es el consejo más rentable de todo el apartado.
  3. Los nombres absolutos terminan en punto. alpinashop.example. con punto final es un nombre absoluto; sin él, algunas herramientas le añaden el dominio de búsqueda local. Cloud DNS es estricto: exige el punto.

  1. Cloud DNS: zonas gestionadas

Cloud DNS es el servicio de DNS autoritativo de Google, con anycast global, latencia muy baja y un acuerdo de nivel de servicio del 100 % de disponibilidad. Sus unidades son las zonas gestionadas:

Tipo Visible desde Uso en AlpinaShop
Pública Todo internet alpinashop.example: la tienda
Privada Solo las VPC que autorices interna.alpinashop.example: servicios internos
De reenvío Delega consultas a servidores propios Conectividad híbrida (07-03)
Peering Comparte resolución entre VPC VPC compartida (07-03)

Ojo con la distinción entre registrar un dominio y alojar su DNS: son cosas distintas. El registrador es donde compras el dominio (Cloud Domains, o cualquier otro); Cloud DNS es donde se alojan sus registros. Puedes tener el dominio en un registrador y el DNS en Google sin problema, y de hecho es el caso más habitual.

  1. La zona pública de AlpinaShop, registro a registro

gcloud config set project alpinashop-prod
gcloud services enable dns.googleapis.com

gcloud dns managed-zones create alpinashop-publica \
  --dns-name="alpinashop.example." \
  --description="Zona publica de la tienda AlpinaShop" \
  --visibility=public \
  --dnssec-state=on

# Los servidores de nombres que hay que declarar en el registrador
gcloud dns managed-zones describe alpinashop-publica \
  --format="value(nameServers)"

Ese último comando devuelve algo como ns-cloud-b1.googledomains.com. y tres más. Anótalos: son la pieza que va en el registrador.

Ahora los registros. Recuerda la IP global reservada en 03-01:

IP_LB=$(gcloud compute addresses describe alpinashop-lb-ip --global --format="value(address)")
echo "$IP_LB"

# Vertice de la zona: registro A obligatorio (no puede ser CNAME)
gcloud dns record-sets create alpinashop.example. \
  --zone=alpinashop-publica --type=A --ttl=300 --rrdatas="$IP_LB"

# www: CNAME al vertice. Un solo sitio que cambiar si cambia la IP.
gcloud dns record-sets create www.alpinashop.example. \
  --zone=alpinashop-publica --type=CNAME --ttl=300 --rrdatas="alpinashop.example."

# imagenes: MISMA IP. El mapa de URL de 03-02 ya enruta por Host y ruta.
gcloud dns record-sets create imagenes.alpinashop.example. \
  --zone=alpinashop-publica --type=A --ttl=3600 --rrdatas="$IP_LB"

# Correo corporativo
gcloud dns record-sets create alpinashop.example. \
  --zone=alpinashop-publica --type=MX --ttl=3600 \
  --rrdatas="1 aspmx.l.google.com.,5 alt1.aspmx.l.google.com.,10 alt2.aspmx.l.google.com."

# SPF, para que el correo de la tienda no acabe en spam
gcloud dns record-sets create alpinashop.example. \
  --zone=alpinashop-publica --type=TXT --ttl=3600 \
  --rrdatas='"v=spf1 include:_spf.google.com ~all"'

# CAA: solo Google puede emitir certificados de este dominio
gcloud dns record-sets create alpinashop.example. \
  --zone=alpinashop-publica --type=CAA --ttl=3600 \
  --rrdatas='0 issue "pki.goog"'

Decisiones de diseño que merecen explicación:

  • imagenes.alpinashop.example apunta a la misma IP. No hace falta otro balanceador: el mapa de URL enruta por Host y por ruta. Un solo balanceador, un solo certificado, un solo punto donde aplicar Cloud Armor. Un subdominio no implica infraestructura nueva.
  • www es un CNAME al vértice, de modo que un cambio de IP se hace en un único registro.
  • TTL de 300 segundos en lo que puede cambiar (el vértice, www) y de 3600 en lo estable (MX, TXT). Antes de una migración, bájalo a 60.
  • El registro CAA es un candado barato: impide que otra autoridad de certificación emita un certificado para tu dominio, aunque logre engañar a alguien. Si más adelante usas Let's Encrypt, tendrás que añadir 0 issue "letsencrypt.org".

Para varios cambios atómicos, Cloud DNS ofrece transacciones:

gcloud dns record-sets transaction start --zone=alpinashop-publica
gcloud dns record-sets transaction add "$IP_LB" \
  --name=api.alpinashop.example. --ttl=300 --type=A --zone=alpinashop-publica
gcloud dns record-sets transaction add "$IP_LB" \
  --name=blog.alpinashop.example. --ttl=300 --type=A --zone=alpinashop-publica
gcloud dns record-sets transaction execute --zone=alpinashop-publica

gcloud dns record-sets list --zone=alpinashop-publica \
  --format="table(name, type, ttl, rrdatas.list())"

  1. Delegación desde el registrador y verificación con dig

Crear la zona en Cloud DNS no hace nada por sí solo. Internet sigue preguntando a los servidores que diga el registrador. La delegación consiste en ir al panel del registrador y sustituir los servidores de nombres por los cuatro de Cloud DNS.

Y aquí está el error más frecuente: el cambio de servidores de nombres tarda. La delegación en el nivel superior tiene su propio TTL, típicamente de 24 a 48 horas. Durante ese tiempo, unos resolutores del mundo verán la configuración nueva y otros la vieja, con lo que la tienda funcionará "a veces". Eso no es un fallo: es cómo funciona el DNS.

Verificación, en este orden:

# 1) ¿Que servidores dice el nivel superior que son autoritativos?
dig NS alpinashop.example +short

# 2) Preguntar DIRECTAMENTE a Cloud DNS: descarta la propagacion de la ecuacion
dig @ns-cloud-b1.googledomains.com A alpinashop.example +short

# 3) Lo que ve un resolutor publico cualquiera
dig @8.8.8.8 A alpinashop.example +short
dig @1.1.1.1 A alpinashop.example +short

# 4) La cadena completa, desde la raiz
dig +trace alpinashop.example

# 5) El CNAME de www
dig CNAME www.alpinashop.example +short

La lectura de estos comandos es lo que convierte una hora de frustración en un diagnóstico de un minuto:

  • Si (2) funciona y (3) no, la configuración es correcta y solo falta propagación. Espera.
  • Si (2) no funciona, el registro está mal en Cloud DNS. Revísalo tú.
  • Si (1) devuelve los servidores antiguos, la delegación no se ha hecho o no ha propagado todavía.

  1. Zonas privadas para resolución interna

Dentro de alpinashop-vpc, Google ya resuelve automáticamente los nombres internos de las VM. Pero es mucho mejor que la aplicación se conecte a pedidos.interna.alpinashop.example que a 10.10.1.5: el día que la IP cambie —una réplica que se promueve, una migración— no hay que tocar el código.

gcloud dns managed-zones create alpinashop-interna \
  --dns-name="interna.alpinashop.example." \
  --description="Resolucion interna de AlpinaShop" \
  --visibility=private \
  --networks=alpinashop-vpc

IP_SQL=$(gcloud sql instances describe alpinashop-pedidos \
  --format="value(ipAddresses[0].ipAddress)")

gcloud dns record-sets create pedidos.interna.alpinashop.example. \
  --zone=alpinashop-interna --type=A --ttl=60 --rrdatas="$IP_SQL"

gcloud dns record-sets create cache.interna.alpinashop.example. \
  --zone=alpinashop-interna --type=A --ttl=60 --rrdatas="10.10.1.20"

Propiedades de las zonas privadas:

  • Solo resuelven desde las VPC autorizadas. Desde internet, pedidos.interna.alpinashop.example no existe. No filtras nada sobre tu topología interna.
  • TTL bajo (60 s) a propósito: son nombres pensados precisamente para cambiar sin drama.
  • La misma zona puede definirse en varias VPC. Compartirla entre proyectos entra en el terreno de VPC compartida y peering, que se trata en 07-03.

  1. DNSSEC: qué protege y qué no

DNSSEC firma criptográficamente las respuestas DNS, de modo que un resolutor puede verificar que la respuesta es auténtica y no la ha fabricado un atacante. Protege del envenenamiento de caché DNS: que alguien consiga que tu resolutor crea que alpinashop.example está en la IP del atacante.

# Ya lo activamos al crear la zona; si no, se actualiza
gcloud dns managed-zones update alpinashop-publica --dnssec-state=on

# Obtener el registro DS que hay que declarar EN EL REGISTRADOR
gcloud dns dns-keys list --zone=alpinashop-publica \
  --format="table(keyTag, type, algorithm, digests[0].digest)"

# Verificar que la cadena de confianza esta completa
dig +dnssec alpinashop.example | grep -E "RRSIG|ad;"

El paso que casi todo el mundo olvida es el segundo: activar DNSSEC en Cloud DNS no basta. Hay que llevar el registro DS al registrador para que el nivel superior firme la delegación. Sin ese paso, has firmado tu zona pero nadie puede verificar la firma, y no sirve de nada.

Qué no protege DNSSEC, para no confundir capas:

  • No cifra nada: las consultas DNS siguen siendo visibles. Eso lo resuelven DoH y DoT, que son cosa del cliente.
  • No protege el contenido de la web: eso es TLS.
  • No impide que alguien registre alpinashop-tienda.example para hacer phishing.

Y una advertencia operativa seria: DNSSEC mal configurado deja el dominio invisible, no simplemente inseguro. Si el registro DS del registrador no concuerda con las claves de la zona, los resolutores validantes rechazan la respuesta y tu dominio desaparece para una parte de internet. Cámbialo con cuidado y verifica siempre después.

  1. Certificados gestionados por Google

Un certificado TLS acredita que el servidor que responde por alpinashop.example es realmente quien dice ser, y permite cifrar la conexión. Google los emite y los renueva gratis y automáticamente.

gcloud compute ssl-certificates create alpinashop-cert \
  --domains="alpinashop.example,www.alpinashop.example,imagenes.alpinashop.example" \
  --global

# Asociarlo al proxy HTTPS creado en 03-02
gcloud compute target-https-proxies update alpinashop-https-proxy \
  --ssl-certificates=alpinashop-cert --global

# Seguir el aprovisionamiento
gcloud compute ssl-certificates describe alpinashop-cert --global \
  --format="yaml(name, managed.status, managed.domainStatus)"

El campo managed.domainStatus da el estado por dominio, que es lo que necesitas para depurar:

Estado Significado Qué hacer
PROVISIONING En curso Esperar, hasta unos 60 minutos en el caso normal
FAILED_NOT_VISIBLE El dominio no resuelve a la IP del balanceador El caso habitual. Revisar el registro A
FAILED_CAA_CHECKING Hay un registro CAA que no permite a Google emitir Añadir 0 issue "pki.goog"
FAILED_RATE_LIMITED Demasiados intentos Esperar; no borrar y recrear en bucle
ACTIVE Listo Nada

Cómo valida Google el dominio, que es la clave para entender los fallos: comprueba que el nombre resuelve a la IP del balanceador donde está asociado el certificado. Es decir, el orden correcto es DNS primero, certificado después. Si creas el certificado antes de que el registro A propague, se quedará esperando.

Ventajas y límites de los certificados gestionados:

Ventajas Límites
Gratis Hasta 100 dominios por certificado
Renovación automática, sin sobresaltos No admite comodines (*.alpinashop.example)
Sin ficheros de clave privada que custodiar Requiere que el dominio ya apunte al balanceador
Se asocian varios a un mismo proxy Solo para balanceadores de Google

  1. Por qué un certificado se queda en PROVISIONING

Merece apartado propio porque es, con diferencia, el problema más frecuente al publicar en Google Cloud. La lista de causas, ordenada por probabilidad:

  1. El registro A no existe o apunta a otra IP. El error más común con mucha diferencia.
  2. El DNS aún no ha propagado. Correcto pero reciente; hay que esperar.
  3. El balanceador no tiene regla de reenvío en el 443. Google valida a través del propio balanceador; si el 443 no está publicado, no puede.
  4. Un registro CAA bloquea a Google. Si tenías un CAA de otra autoridad, hay que añadir pki.goog.
  5. El certificado no está asociado al proxy HTTPS. Sin asociación, la validación no se dispara.
  6. Se ha borrado y recreado muchas veces, activando un límite de tasa. La paciencia es la solución.

Diagnóstico en el orden correcto:

# 1) ¿A donde resuelve realmente el dominio?
dig A alpinashop.example +short

# 2) ¿Cual es la IP del balanceador?
gcloud compute addresses describe alpinashop-lb-ip --global --format="value(address)"
#    Los dos valores TIENEN que coincidir.

# 3) ¿Esta el 443 publicado?
gcloud compute forwarding-rules list --global \
  --format="table(name, IPAddress, portRange, target)"

# 4) ¿Esta el certificado asociado al proxy?
gcloud compute target-https-proxies describe alpinashop-https-proxy --global \
  --format="value(sslCertificates)"

# 5) ¿Hay un CAA que estorbe?
dig CAA alpinashop.example +short

Y el consejo que ahorra medio día: cuando cambies el DNS para arreglarlo, no borres el certificado. Google reintenta la validación periódicamente por su cuenta. Borrar y recrear reinicia el reloj y acerca el límite de tasa.

  1. Certificate Manager: muchos dominios y comodines

Cuando los certificados clásicos se quedan cortos —comodines, cientos de dominios, despliegue en varias regiones— la herramienta es Certificate Manager. Su mecanismo de validación es distinto: no requiere que el dominio apunte ya al balanceador, sino que valida mediante un registro DNS de autorización. Eso permite emitir el certificado antes de migrar el tráfico, lo cual es exactamente lo que quieres en una migración real.

gcloud services enable certificatemanager.googleapis.com

# 1) Autorizacion DNS: genera un registro CNAME que hay que crear
gcloud certificate-manager dns-authorizations create auth-alpinashop \
  --domain="alpinashop.example"

gcloud certificate-manager dns-authorizations describe auth-alpinashop \
  --format="value(dnsResourceRecord.name, dnsResourceRecord.data)"

# 2) Crear ese CNAME en la zona
gcloud dns record-sets create "_acme-challenge.alpinashop.example." \
  --zone=alpinashop-publica --type=CNAME --ttl=300 \
  --rrdatas="<valor devuelto por el comando anterior>"

# 3) Certificado con COMODIN: cubre cualquier subdominio
gcloud certificate-manager certificates create cert-alpinashop-comodin \
  --domains="alpinashop.example,*.alpinashop.example" \
  --dns-authorizations=auth-alpinashop

# 4) Mapa de certificados y su entrada
gcloud certificate-manager maps create mapa-alpinashop

gcloud certificate-manager maps entries create entrada-principal \
  --map=mapa-alpinashop \
  --certificates=cert-alpinashop-comodin \
  --hostname="alpinashop.example"

gcloud certificate-manager maps entries create entrada-comodin \
  --map=mapa-alpinashop \
  --certificates=cert-alpinashop-comodin \
  --hostname="*.alpinashop.example"

# 5) Asociar el mapa al proxy (sustituye a --ssl-certificates)
gcloud compute target-https-proxies update alpinashop-https-proxy \
  --certificate-map=mapa-alpinashop --global
Certificado clásico gestionado Certificate Manager
Comodines No
Validación El dominio debe apuntar ya al balanceador Registro DNS de autorización, antes de migrar
Escala Hasta 100 dominios Miles
Complejidad Un comando Autorización + certificado + mapa + entrada
Recomendación AlpinaShop hoy Cuando haga falta un comodín o una migración sin corte

Para AlpinaShop, el certificado clásico con tres nombres es suficiente. Si mañana cada marca asociada tuviera su subdominio, el comodín de Certificate Manager sería la respuesta.

  1. Certificados propios y cuándo tienen sentido

También puedes subir un certificado emitido por otra autoridad:

gcloud compute ssl-certificates create alpinashop-cert-propio \
  --certificate=cadena-completa.pem \
  --private-key=clave-privada.pem \
  --global

Casos legítimos: certificados de validación extendida (EV), certificados emitidos por la autoridad corporativa, o requisitos contractuales concretos. A cambio asumes tres cosas: custodiar la clave privada (Secret Manager, 03-06), renovar antes de que caduque —el incidente clásico de las 3 de la mañana— y avisar antes del vencimiento. Si no hay un motivo específico, el certificado gestionado es mejor decisión, precisamente porque elimina esas tres responsabilidades.

  1. Políticas SSL: versión mínima y conjuntos de cifrado

Por defecto, el balanceador acepta versiones antiguas de TLS para no dejar fuera a nadie. Para una tienda que procesa pagos, eso no es aceptable: PCI DSS exige TLS 1.2 como mínimo.

gcloud compute ssl-policies create pol-ssl-alpinashop \
  --profile=MODERN \
  --min-tls-version=1.2 \
  --description="TLS 1.2 minimo, perfil moderno"

gcloud compute target-https-proxies update alpinashop-https-proxy \
  --ssl-policy=pol-ssl-alpinashop --global

Los perfiles disponibles:

Perfil Conjuntos de cifrado Compatibilidad Cuándo
COMPATIBLE Todos, incluidos los antiguos Máxima Solo si tienes clientes muy viejos
MODERN Los recomendados actualmente Muy buena La elección de AlpinaShop
RESTRICTED Solo los exigidos por normativas estrictas Menor Requisitos de cumplimiento explícitos
CUSTOM Los que enumeres tú La que decidas Casos muy concretos
# Ver que conjuntos habilita cada perfil antes de decidir
gcloud compute ssl-policies list-available-features

Cómo decidir sin romper nada: MODERN con TLS 1.2 mínimo deja fuera navegadores realmente antiguos (Internet Explorer en Windows anterior a 7, Android 4.x). Para una tienda de material de montaña en 2026, ese tráfico es residual y probablemente ni siquiera humano. Si te preocupa, mide antes: el log del balanceador registra la versión de TLS negociada, así que puedes saber exactamente a cuántos clientes reales afectaría.

Dos ajustes más del proxy, ya que estamos:

# HTTP/3 (QUIC): menos latencia de establecimiento, sobre todo en movil
gcloud compute target-https-proxies update alpinashop-https-proxy \
  --quic-override=ENABLE --global

Y recuerda que la redirección de HTTP a HTTPS ya está hecha en 03-02, con un mapa de URL aparte en el puerto 80 que devuelve un 301 permanente. Sin esa redirección, la cabecera HSTS del apartado siguiente no llegaría nunca a los clientes que escriben el dominio sin protocolo.

  1. Cabeceras de seguridad que emite la aplicación

El balanceador cifra la conexión, pero hay protecciones que solo puede activar la aplicación, porque son instrucciones para el navegador.

# seguridad.py — cabeceras de seguridad de AlpinaShop
from flask import request

CSP = "; ".join([
    "default-src 'self'",
    "img-src 'self' https://imagenes.alpinashop.example data:",
    "script-src 'self' https://www.google.com/recaptcha/",
    "style-src 'self' 'unsafe-inline'",
    "frame-ancestors 'none'",          # equivale a X-Frame-Options: DENY
    "form-action 'self'",
    "base-uri 'self'",
    "upgrade-insecure-requests",
])


def registrar_cabeceras_seguridad(app):
    @app.after_request
    def cabeceras(respuesta):
        # HSTS: obliga al navegador a usar HTTPS durante un ano.
        # 'preload' solicita la inclusion en la lista precargada de los
        # navegadores. CUIDADO: es practicamente irreversible.
        respuesta.headers["Strict-Transport-Security"] = \
            "max-age=31536000; includeSubDomains"

        # Impide que el navegador "adivine" el tipo de contenido,
        # lo que evita que un .txt subido por un usuario se ejecute como script.
        respuesta.headers["X-Content-Type-Options"] = "nosniff"

        # No filtrar la URL completa (que puede llevar un SKU o un id de pedido)
        # a dominios externos.
        respuesta.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"

        # Denegar de entrada APIs del navegador que la tienda no usa.
        respuesta.headers["Permissions-Policy"] = \
            "geolocation=(), microphone=(), camera=(), payment=(self)"

        # Politica de seguridad de contenido: la defensa mas fuerte frente a XSS.
        # Se despliega PRIMERO en modo solo-informe durante semanas.
        if request.path.startswith("/admin"):
            respuesta.headers["Content-Security-Policy"] = CSP
        else:
            respuesta.headers["Content-Security-Policy-Report-Only"] = CSP

        return respuesta
Cabecera Qué evita Trampa
Strict-Transport-Security Degradación a HTTP y ataques de intermediario Casi irreversible: durante max-age, los navegadores se niegan a usar HTTP en tu dominio
Content-Security-Policy XSS, inyección de scripts de terceros Rompe el sitio si se despliega sin medir. Usa Report-Only primero
X-Content-Type-Options Ejecución de contenido subido por usuarios Ninguna. Ponla siempre
Referrer-Policy Fuga de URLs internas a terceros Ninguna
Permissions-Policy Uso de APIs del navegador por scripts de terceros Enumera solo lo que uses

HSTS merece un párrafo aparte. Empieza con max-age=300 durante unos días, súbelo a un día, luego a un año, y solo entonces considera preload. La razón: una vez que un navegador ha visto la cabecera, se negará a conectar por HTTP a tu dominio durante todo el max-age, aunque tú quieras. Y preload va aún más lejos: incorpora tu dominio a una lista compilada dentro de los propios navegadores, de la que salir lleva meses. Si activas includeSubDomains y luego resulta que un subdominio interno no tiene certificado, ese subdominio deja de ser accesible.

De la misma forma, CSP se despliega en Content-Security-Policy-Report-Only primero, se recogen los informes de violación durante semanas y se ajusta la política hasta que no queden falsos positivos. Es exactamente la misma disciplina que el modo vista previa de Cloud Armor en 03-05: medir antes de aplicar.

  1. Verificación de extremo a extremo

# 1) Handshake completo, cadena de certificados y version de TLS
curl -vI https://alpinashop.example/ 2>&1 | \
  grep -E "SSL connection|subject:|issuer:|expire|HTTP/"

# 2) Detalle del certificado: nombres cubiertos y fechas
echo | openssl s_client -connect alpinashop.example:443 \
  -servername alpinashop.example 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates -ext subjectAltName

# 3) ¿Se rechazan de verdad las versiones antiguas de TLS?
openssl s_client -connect alpinashop.example:443 -tls1_1 2>&1 | grep -i "alert\|failure"
#    Debe FALLAR. Si conecta, la politica SSL no esta aplicada.

# 4) ¿Redirige HTTP a HTTPS?
curl -sI http://alpinashop.example/ | grep -E "HTTP/|location"

# 5) ¿Estan las cabeceras de seguridad?
curl -sI https://alpinashop.example/ | \
  grep -iE "strict-transport|content-security|x-content-type|referrer-policy"

# 6) ¿El subdominio de imagenes va al bucket y con caché? (03-03)
curl -sI https://imagenes.alpinashop.example/productos/MOCH-2210/web/frontal-800.a3f9c1.webp | \
  grep -iE "HTTP/|age:|cache-control|via:"

# 7) ¿Bloquea Cloud Armor lo que debe? (03-05, desde fuera de la oficina)
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://alpinashop.example/buscar?q=%27%20OR%201%3D1--"

Qué debe salir de cada uno:

Comprobación Resultado esperado
1 TLSv1.3, emisor Google Trust Services, HTTP/2 200
2 subjectAltName con los tres nombres; caducidad a meses vista
3 Fallo de conexión. Si conecta, revisa la política SSL
4 301 con location: https://alpinashop.example/
5 Las cuatro cabeceras presentes
6 200, age mayor que cero en la segunda petición, via: 1.1 google
7 403

Herramientas externas complementarias, útiles para el informe a dirección: SSL Labs para una nota global de la configuración TLS, y Security Headers para las cabeceras. Ninguna sustituye a comprobarlo tú, pero producen un documento presentable.

  1. Lista de comprobación de publicación segura del módulo 3

Este es el resumen operativo de todo lo construido. Úsalo antes de cada publicación de un servicio nuevo.

Red (03-01)

  • [ ] VPC en modo personalizado, con subredes planificadas y sin solapes de CIDR.
  • [ ] Ninguna VM con IP pública salvo justificación explícita; salida por Cloud NAT.
  • [ ] SSH solo desde el rango de IAP 35.235.240.0/20; nunca 0.0.0.0/0 en el 22.
  • [ ] Regla que permite 130.211.0.0/22 y 35.191.0.0/16 hacia el puerto de la aplicación.
  • [ ] Denegación explícita al final, con registro activado.
  • [ ] Cloud SQL en IP privada, mediante acceso privado a servicios.
  • [ ] Acceso privado de Google activado en las subredes sin salida a internet.

Balanceo (03-02)

  • [ ] IP global estática reservada.
  • [ ] Comprobación de estado superficial, que no consulta la base de datos.
  • [ ] Puerto con nombre declarado en el grupo de instancias.
  • [ ] Drenaje de conexiones configurado.
  • [ ] max-rate-per-instance medido con una prueba de carga, no elegido a ojo.
  • [ ] Registro del balanceador activado.
  • [ ] Redirección de HTTP a HTTPS.

Caché (03-03)

  • [ ] CDN activado en el backend de bucket y en el servicio de backend.
  • [ ] Modo de caché adecuado; nunca FORCE_CACHE_ALL sobre contenido de usuario.
  • [ ] Parámetros utm_*, gclid y fbclid excluidos de la clave de caché.
  • [ ] Nombres de objeto inmutables con TTL largo; HTML con TTL corto.
  • [ ] Caché negativa con los 5xx a 0.
  • [ ] serve-while-stale activado.

Identidad (03-04)

  • [ ] Ningún rol básico (Owner, Editor) en producción.
  • [ ] Permisos concedidos a grupos, no a personas.
  • [ ] Una cuenta de servicio por carga de trabajo; nunca la de Compute por defecto.
  • [ ] Cero claves JSON descargadas.
  • [ ] Accesos excepcionales con condición de caducidad.
  • [ ] IAP con tunnelResourceAccessor para SSH y httpsResourceAccessor para lo interno.

WAF (03-05)

  • [ ] Política de seguridad adjunta al servicio de backend.
  • [ ] Exención de la IP de la oficina en la prioridad más baja.
  • [ ] Reglas OWASP con sensibilidad 1, tras al menos una semana en vista previa.
  • [ ] Limitación de tasa en el formulario de acceso y en el buscador.
  • [ ] Registro VERBOSE y alerta sobre el volumen de peticiones denegadas.

Secretos y cifrado (03-06)

  • [ ] Ningún secreto en metadatos, en Git, en imágenes de contenedor ni en logs.
  • [ ] Secretos en Secret Manager, con permiso a nivel de secreto.
  • [ ] Procedimiento de rotación escrito; deshabilitar antes de destruir.
  • [ ] CMEK donde lo exija el cumplimiento, con permiso al agente de servicio.

DNS y TLS (03-07)

  • [ ] Zona pública en Cloud DNS, con delegación verificada con dig.
  • [ ] Registro A del vértice apuntando a la IP del balanceador.
  • [ ] DNSSEC activado y registro DS declarado en el registrador.
  • [ ] Registro CAA restringiendo quién puede emitir certificados.
  • [ ] Certificado gestionado en estado ACTIVE, asociado al proxy HTTPS.
  • [ ] Política SSL con TLS 1.2 mínimo y perfil MODERN.
  • [ ] HSTS, CSP, X-Content-Type-Options y Referrer-Policy en las respuestas.
  • [ ] Verificación de extremo a extremo con curl y openssl superada.

Esta lista es un punto de partida sólido, no un certificado de conformidad. Antes de publicar un servicio que trate datos personales o pagos, la revisión final la hace un profesional de seguridad y cumplimiento. Las mejores prácticas de seguridad se retoman de forma transversal en 07-04 y el gobierno a escala en 07-07.

Errores Comunes y Consejos

  • Intentar un CNAME en el vértice de la zona. El protocolo no lo permite. Registro A a la IP global estática.
  • No bajar el TTL antes de una migración. Con TTL de 24 horas, el cambio tarda un día en verse. Bájalo a 60 con antelación.
  • Olvidar el punto final en los nombres de Cloud DNS.
  • Crear el certificado antes que el registro DNS. Se queda en PROVISIONING. Primero el DNS, luego el certificado.
  • Borrar y recrear el certificado por impaciencia. Reinicia el reloj y acerca el límite de tasa. Google reintenta solo.
  • Activar DNSSEC sin declarar el registro DS en el registrador. No sirve de nada; y si el DS no concuerda, el dominio desaparece.
  • Poner un CAA sin incluir pki.goog. El certificado gestionado nunca se emitirá.
  • Esperar un comodín de un certificado gestionado clásico. No los admite: eso es Certificate Manager.
  • Activar HSTS con preload desde el primer día. Es prácticamente irreversible. Sube el max-age por fases.
  • Desplegar CSP sin Report-Only. Rompe el sitio. Misma disciplina que el modo vista previa de Cloud Armor.
  • Probar solo desde el navegador. Cachea DNS y certificados de forma agresiva. Usa dig y openssl s_client.
  • Subir un certificado propio y olvidar la renovación. Es el incidente clásico de madrugada. Si no hay un motivo real, usa el gestionado.
  • Consejo: usa un subdominio de pruebas (pre.alpinashop.example) apuntando a un balanceador de preproducción. Ensayar la publicación completa ahí cuesta poco y evita sorpresas.
  • Consejo: alerta de caducidad de certificado en Cloud Monitoring (06-04), aunque sean gestionados. Una renovación puede fallar si el DNS cambió.
  • Consejo: gestiona la zona DNS como código con Terraform (06-07). Un registro añadido a mano y no documentado es un misterio garantizado.

Ejercicios

Ejercicio 1 — Publicar un subdominio nuevo de extremo a extremo

AlpinaShop lanza un blog en blog.alpinashop.example, servido por un servicio de backend nuevo bs-blog que ya existe. Escribe todos los pasos necesarios, en orden, desde el DNS hasta la verificación, incluyendo qué hay que tocar de las lecciones anteriores del módulo.

Ejercicio 2 — Migrar el dominio desde un proveedor antiguo

alpinashop.example está hoy en un hosting tradicional, con el DNS en el registrador y TTL de 86 400 segundos. Hay que migrarlo a Google Cloud sin que la tienda deje de funcionar ni un minuto y sin que el correo se pierda. Escribe el plan con su cronología.

Ejercicio 3 — Diagnosticar una publicación rota

Marta ha seguido la lección y https://alpinashop.example da ERR_SSL_PROTOCOL_ERROR en el navegador. Recoge estos datos:

  • dig A alpinashop.example +short34.120.10.5.
  • gcloud compute addresses describe alpinashop-lb-ip --global34.120.10.5.
  • gcloud compute ssl-certificates describe alpinashop-cert --globalmanaged.status: ACTIVE.
  • curl -I http://alpinashop.example/301 correcto hacia HTTPS.
  • gcloud compute forwarding-rules list --global muestra una sola regla, en el puerto 80.

Identifica la causa y escribe los comandos que la corrigen.


Soluciones

Solución 1

# 1) DNS: registro A del subdominio a la MISMA IP global
IP_LB=$(gcloud compute addresses describe alpinashop-lb-ip --global --format="value(address)")
gcloud dns record-sets create blog.alpinashop.example. \
  --zone=alpinashop-publica --type=A --ttl=300 --rrdatas="$IP_LB"

# 2) Verificar que resuelve ANTES de tocar el certificado
dig @8.8.8.8 A blog.alpinashop.example +short

# 3) Certificado NUEVO que incluya el subdominio (no se edita el existente)
gcloud compute ssl-certificates create alpinashop-cert-v2 \
  --domains="alpinashop.example,www.alpinashop.example,imagenes.alpinashop.example,blog.alpinashop.example" \
  --global

# 4) Asociar AMBOS al proxy: el viejo sigue sirviendo mientras el nuevo se aprovisiona
gcloud compute target-https-proxies update alpinashop-https-proxy \
  --ssl-certificates=alpinashop-cert,alpinashop-cert-v2 --global

# 5) Esperar a ACTIVE
watch -n 60 'gcloud compute ssl-certificates describe alpinashop-cert-v2 \
  --global --format="value(managed.status)"'

# 6) Mapa de URL: enrutar el nuevo Host (03-02)
gcloud compute url-maps add-path-matcher alpinashop-url-map \
  --path-matcher-name=matcher-blog \
  --default-service=bs-blog \
  --new-hosts=blog.alpinashop.example

# 7) Quitar el certificado antiguo cuando el nuevo este activo
gcloud compute target-https-proxies update alpinashop-https-proxy \
  --ssl-certificates=alpinashop-cert-v2 --global
gcloud compute ssl-certificates delete alpinashop-cert --global --quiet

Qué más hay que tocar de las lecciones anteriores:

  • 03-05: la regla 2300 rechaza peticiones con un Host que no esté en la lista. Hay que añadir blog.alpinashop.example a esa expresión, o el blog devolverá 404 desde el WAF y nadie sabrá por qué.
  • 03-03: decidir la política de caché de bs-blog. Un blog es contenido casi estático: CACHE_ALL_STATIC con s-maxage de varios minutos es una gran mejora.
  • 03-01: si bs-blog apunta a instancias nuevas, necesitan la etiqueta de red que permite las comprobaciones de estado.
  • 03-04: quien publica en el blog necesita permisos, y esos permisos van a un grupo.

El paso clave es el 4: asociar ambos certificados al proxy simultáneamente. El balanceador admite varios y elige por SNI, así que el sitio sigue funcionando mientras el nuevo se valida. Sustituir directamente el certificado dejaría la tienda sin certificado válido durante el aprovisionamiento.

Solución 2

T-7 días — Preparar y bajar el TTL.

# En el proveedor ACTUAL: bajar el TTL de todos los registros a 300 s.
# Este paso tarda hasta 86 400 s en surtir efecto: por eso va una semana antes.

Se inventarían todos los registros existentes: A, MX, TXT (SPF, DKIM, DMARC, verificaciones de servicios de terceros), CNAME. Perder un TXT de verificación rompe servicios de forma silenciosa; perder los MX pierde correo, que es irrecuperable.

T-3 días — Construir la zona en Cloud DNS.

gcloud dns managed-zones create alpinashop-publica \
  --dns-name="alpinashop.example." --visibility=public
# Replicar TODOS los registros del inventario, con TTL de 300.

# Verificar contra Cloud DNS directamente, sin delegar todavia
NS=$(gcloud dns managed-zones describe alpinashop-publica --format="value(nameServers[0])")
dig @"$NS" A alpinashop.example +short
dig @"$NS" MX alpinashop.example +short
dig @"$NS" TXT alpinashop.example +short

En este punto la zona nueva es una copia fiel, pero nadie la usa aún. El registro A sigue apuntando al hosting antiguo: eso es deliberado.

T-1 día — Certificado por adelantado. Con Certificate Manager y autorización DNS se puede emitir el certificado antes de que el dominio apunte a Google. Es exactamente el escenario para el que existe esa validación, y evita la ventana en la que el dominio ya apunta al balanceador pero aún no hay certificado.

T-0 — Delegar. Cambiar los servidores de nombres en el registrador a los de Cloud DNS. Durante 24-48 horas convivirán las dos configuraciones; como son idénticas salvo por lo que se cambie después, el usuario no nota nada. Mantener el hosting antiguo encendido todo ese tiempo.

T+2 días — Cambiar el destino. Con la delegación ya propagada y el TTL en 300 segundos, se actualiza el registro A a la IP del balanceador. El cambio real de tráfico ocurre ahora, con una ventana de cinco minutos y con posibilidad de deshacerlo en cinco minutos.

gcloud dns record-sets update alpinashop.example. \
  --zone=alpinashop-publica --type=A --ttl=300 --rrdatas="$IP_LB"

T+3 días — Estabilizar. Verificar que no queda tráfico llegando al hosting antiguo (sus logs lo dicen), subir los TTL a valores normales, activar DNSSEC con su registro DS, y solo entonces dar de baja el hosting.

La idea que hay que retener: se separa el cambio de quién resuelve (T-0) del cambio de a dónde apunta (T+2). Si se hacen a la vez y algo falla, no se sabe cuál de los dos lo causó y deshacerlo tarda 48 horas.

Solución 3

La causa está en el último dato: solo hay una regla de reenvío, en el puerto 80. Falta la regla del 443. El certificado está ACTIVE (Google validó por HTTP), el DNS es correcto y la redirección funciona —de ahí el 301—, pero cuando el navegador sigue esa redirección a https://, no hay nada escuchando en el 443. El error ERR_SSL_PROTOCOL_ERROR es exactamente eso: no hay diálogo TLS al otro lado.

# 1) Confirmar que el proxy HTTPS existe
gcloud compute target-https-proxies list --global \
  --format="table(name, urlMap, sslCertificates.list())"

# 2) Crear la regla de reenvio que falta
gcloud compute forwarding-rules create alpinashop-fr-https \
  --global \
  --address=alpinashop-lb-ip \
  --target-https-proxy=alpinashop-https-proxy \
  --ports=443 \
  --load-balancing-scheme=EXTERNAL_MANAGED

# 3) Esperar 5-10 minutos a la propagacion y verificar
curl -vI https://alpinashop.example/ 2>&1 | grep -E "SSL connection|HTTP/"

Si el paso 1 revelase que tampoco existe el proxy HTTPS, habría que crearlo antes:

gcloud compute target-https-proxies create alpinashop-https-proxy \
  --url-map=alpinashop-url-map \
  --ssl-certificates=alpinashop-cert \
  --ssl-policy=pol-ssl-alpinashop \
  --global

La lección de diagnóstico: un certificado en ACTIVE demuestra que Google pudo validar el dominio, no que el sitio sirva HTTPS. Son dos cosas distintas y es fácil confundirlas. Ante un fallo de publicación, recorre la cadena completa de 03-02 de fuera hacia dentro: regla de reenvío → proxy → certificado → mapa de URL → servicio de backend → backend → comprobación de estado. El eslabón roto aparece siempre.

Conclusión

AlpinaShop está publicada. Los clientes escriben alpinashop.example en el navegador, ven el candado y compran. Detrás de ese gesto trivial hay siete lecciones de trabajo.

En esta última has repasado el DNS con la profundidad justa para operarlo: los tipos de registro, la imposibilidad de un CNAME en el vértice —que es la razón de ser de la IP global estática—, el papel del TTL en cualquier migración y la costumbre de bajarlo con antelación. Has creado la zona pública alpinashop-publica con el registro A del vértice apuntando a alpinashop-lb-ip, el CNAME de www, el registro de imagenes a la misma IP —porque un subdominio no implica infraestructura nueva cuando el mapa de URL enruta por Host—, los MX, el SPF y un registro CAA que restringe quién puede emitir certificados. Has delegado desde el registrador y sabes diagnosticar con dig si el problema es tuyo o es propagación. Has creado la zona privada alpinashop-interna para que la aplicación deje de conocer direcciones IP de memoria, y has activado DNSSEC sin olvidar el paso que casi todo el mundo olvida: el registro DS en el registrador.

Has aprovisionado un certificado gestionado por Google y, sobre todo, sabes por qué se queda en PROVISIONING y en qué orden mirar las seis causas posibles, empezando por la única que explica la mayoría: el registro A. Conoces los límites de los certificados gestionados y sabes que los comodines y las migraciones sin corte son territorio de Certificate Manager, con su validación por autorización DNS que permite emitir antes de mover el tráfico. Has endurecido la conexión con la política pol-ssl-alpinashop (perfil MODERN, TLS 1.2 mínimo) y has añadido las cabeceras que solo puede emitir la aplicación —HSTS, CSP, X-Content-Type-Options, Referrer-Policy, Permissions-Policy—, con la misma disciplina que ya aplicaste al WAF: Report-Only primero, medir, y solo después aplicar. Y has verificado el resultado de extremo a extremo con curl y openssl s_client, comprobando no solo que HTTPS funciona, sino que TLS 1.1 falla, que el subdominio de imágenes devuelve Age y que Cloud Armor sigue bloqueando lo que debe.

Con la lista de comprobación final, el módulo 3 queda cerrado como una unidad: red segmentada sin IP públicas, balanceador global con comprobaciones de estado sanas, caché en el borde con la clave bien calculada, permisos mínimos por grupos y sin claves descargadas, WAF calibrado con vista previa, secretos fuera del código y claves gestionadas donde hace falta, y por fin nombre y certificado. AlpinaShop está en internet y está protegida.

Y ahora empieza otra historia. Esa tienda que funciona genera datos cada segundo: pedidos que entran en alpinashop-pedidos, visitas que quedan en los registros del balanceador, clics en fichas de producto, búsquedas que no encuentran nada, imágenes que se sirven desde el CDN, carritos que se abandonan en Firestore. Ahora mismo, todo eso se acumula sin que nadie lo mire. Marta sabe cuántas instancias hay levantadas, pero nadie sabe qué mochila se vende más en Cataluña, ni si la campaña de otoño ha funcionado, ni por qué el 30 % de los carritos se quedan a medias en el paso de envío. Lucía, que hasta ahora solo ha aparecido para pedir permisos, pasa a ser la protagonista.

En el módulo 4, Datos y análisis, construiremos la plataforma que responde a esas preguntas. Empezaremos por 04-01, BigQuery, creando el dataset alpinashop_analitica en alpinashop-datos y cargando en él el histórico de pedidos, para descubrir por qué un almacén analítico no es una base de datos más grande, cómo se paga por lo que se consulta y por qué una consulta mal escrita puede costar más que una instancia entera. Después llegarán Pub/Sub para capturar los eventos en el momento en que ocurren, Dataflow para transformarlos, y el resto de las piezas hasta llegar a cuadros de mando que Lucía pueda usar sin pedirle nada a nadie.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados