Lab práctico · Semana 7: Seguridad en Google Cloud: identidad, perímetros, cifrado y cadena de suministro

SSH por IAP sin IP pública e impersonación de cuentas de servicio

⏱ 60-75 minDificultad: mediaApartados: 3.15.2

Qué vas a construir

Una VM sin IP pública, a la que entrarás por SSH a través de IAP TCP forwarding, que corre con una cuenta de servicio dedicada y con permisos mínimos. Después operarás como esa cuenta desde Cloud Shell mediante impersonación, sin descargar ninguna clave.

flowchart LR
  CS["Cloud Shell (tu usuario)"] -->|"gcloud compute ssh --tunnel-through-iap"| IAP["IAP (35.235.240.0/20)"]
  IAP -->|"TCP 22"| VM["vm-lab15 sin IP pública"]
  VM -->|"token del servidor de metadatos"| API["Cloud Storage (solo lectura)"]
  CS -.->|"--impersonate-service-account"| SA["sa-lab15"]

Antes de empezar

  • Proyecto dedicado con facturación y la red default (o una VPC con una subred en Madrid; ver lab-06-vpc-firewall-nat).
  • Cloud Shell abierto.
export PROJECT_ID=$(gcloud config get-value project)
export REGION=europe-southwest1
export ZONE=europe-southwest1-a
export SA=sa-lab15@${PROJECT_ID}.iam.gserviceaccount.com
export BUCKET=gs://${PROJECT_ID}-lab15
export YO=$(gcloud config get-value account)

gcloud services enable compute.googleapis.com iap.googleapis.com iamcredentials.googleapis.com

Si tu red no se llama default, sustituye --network=default en los comandos.

Paso 1: cuenta de servicio con mínimo privilegio

Creamos una cuenta de servicio para la VM y un bucket de prueba sobre el que solo podrá leer.

gcloud iam service-accounts create sa-lab15 --display-name="VM del lab 15"

gcloud storage buckets create $BUCKET --location=$REGION --uniform-bucket-level-access
echo "hola desde el bucket" > saludo.txt
gcloud storage cp saludo.txt $BUCKET/

gcloud storage buckets add-iam-policy-binding $BUCKET \
  --member="serviceAccount:$SA" --role="roles/storage.objectViewer"

Fíjate en que el rol se concede en el bucket, no en el proyecto. Comprueba que la cuenta no tiene claves gestionadas por el usuario (y que así seguirá):

gcloud iam service-accounts keys list --iam-account=$SA --managed-by=user

La lista debe estar vacía.

Paso 2: VM sin IP pública que corre como esa cuenta

Una VM sin IP externa no puede llegar a las APIs de Google (Cloud Storage, por ejemplo) salvo que la subred tenga Private Google Access. Lo activamos primero en la subred de Madrid:

gcloud compute networks subnets update default --region=$REGION --enable-private-ip-google-access

--no-address impide la IP externa. --service-account y --scopes=cloud-platform hacen que los permisos los decida solo IAM (la forma recomendada; los scopes heredados limitaban por API). Activamos también OS Login para que el acceso SSH dependa de IAM y no de claves en metadatos.

gcloud compute instances create vm-lab15 \
  --zone=$ZONE --machine-type=e2-micro \
  --image-family=debian-12 --image-project=debian-cloud \
  --network=default --no-address \
  --service-account=$SA --scopes=cloud-platform \
  --metadata=enable-oslogin=TRUE \
  --shielded-secure-boot

Para crear una VM que corre como sa-lab15, necesitas iam.serviceAccounts.actAs sobre ella (rol roles/iam.serviceAccountUser); como Owner ya lo tienes, pero en un equipo real es el permiso que controla quién puede «prestar» una identidad a una VM.

Paso 3: permitir IAP en el firewall y conceder el acceso

IAP llega a tus VMs desde el rango 35.235.240.0/20. Abrimos solo el puerto 22 y solo desde ese rango. Si existe la regla default-allow-ssh (abierta a 0.0.0.0/0), bórrala: con IAP ya no hace falta.

gcloud compute firewall-rules create allow-ssh-from-iap \
  --network=default --direction=INGRESS --action=allow \
  --rules=tcp:22 --source-ranges=35.235.240.0/20

gcloud compute firewall-rules delete default-allow-ssh --quiet 2>/dev/null || true

Permisos necesarios para el usuario (como Owner ya los tienes; en producción se concederían a un grupo de administradores):

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$YO" --role="roles/iap.tunnelResourceAccessor"
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$YO" --role="roles/compute.osAdminLogin"

En la consola: Security → Identity-Aware Proxy → SSH and TCP resources muestra las VMs y quién puede crear túneles hacia ellas.

Paso 4: conectarte por SSH a través de IAP

gcloud compute ssh vm-lab15 --zone=$ZONE --tunnel-through-iap

Ya dentro de la VM, comprueba la identidad que usa y que el servidor de metadatos le da tokens sin ninguna clave en disco:

curl -s -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email; echo

gcloud storage cat gs://PROYECTO-lab15/saludo.txt        # sustituye PROYECTO: funciona (objectViewer)
echo x | gcloud storage cp - gs://PROYECTO-lab15/x.txt   # falla con 403: no puede escribir
exit

La VM no tiene IP pública ni Cloud NAT: llega a las APIs de Google gracias a Private Google Access, que activaste en la subred en el paso 2. Sin él, gcloud storage se quedaría esperando hasta agotar el tiempo.

Paso 5: impersonar la cuenta de servicio desde Cloud Shell

Para probar exactamente lo que puede hacer la cuenta, sin claves, date permiso para generar tokens en su nombre:

gcloud iam service-accounts add-iam-policy-binding $SA \
  --member="user:$YO" --role="roles/iam.serviceAccountTokenCreator"

Ahora ejecuta órdenes como sa-lab15 (espera uno o dos minutos si IAM aún no ha propagado):

gcloud storage cat $BUCKET/saludo.txt --impersonate-service-account=$SA     # funciona
gcloud compute instances list --impersonate-service-account=$SA            # falla: sin permisos de Compute
gcloud auth print-access-token --impersonate-service-account=$SA | cut -c1-20; echo "..."

El token impreso es de corta duración (por defecto una hora) y lo emite iamcredentials.googleapis.com. Cada emisión queda registrada en los audit logs de IAM, así que sabes qué persona actuó como la cuenta de servicio, algo imposible de saber con una clave JSON compartida.

También puedes impersonar para toda la sesión y volver atrás:

gcloud config set auth/impersonate_service_account $SA
gcloud storage ls $BUCKET
gcloud config unset auth/impersonate_service_account

Comprueba que funciona

  • gcloud compute instances describe vm-lab15 --zone=$ZONE --format="value(networkInterfaces[0].accessConfigs)" devuelve vacío: no hay IP pública.
  • El SSH por IAP funciona. Si borras la regla allow-ssh-from-iap, deja de funcionar: el único camino de entrada es el rango de IAP. (Nota: las versiones recientes de gcloud compute ssh usan IAP automáticamente cuando la VM no tiene IP externa, pero conviene indicar --tunnel-through-iap de forma explícita.)
  • Dentro de la VM y por impersonación, la cuenta lee el bucket pero no escribe ni lista VMs.
  • gcloud iam service-accounts keys list --iam-account=$SA --managed-by=user sigue vacío.
  • En Logging → Logs Explorer, busca protoPayload.methodName="GenerateAccessToken" para ver las impersonaciones (son Data Access logs de IAM; si no aparecen, activa los audit logs de lectura de datos para Identity and Access Management API en IAM → Audit Logs).

Limpieza

gcloud compute instances delete vm-lab15 --zone=$ZONE --quiet
gcloud compute firewall-rules delete allow-ssh-from-iap --quiet
gcloud storage rm --recursive $BUCKET
gcloud iam service-accounts delete $SA --quiet
gcloud compute networks subnets update default --region=$REGION --no-enable-private-ip-google-access   # solo si lo activaste para este lab
gcloud projects remove-iam-policy-binding $PROJECT_ID \
  --member="user:$YO" --role="roles/iap.tunnelResourceAccessor"
gcloud projects remove-iam-policy-binding $PROJECT_ID \
  --member="user:$YO" --role="roles/compute.osAdminLogin"
rm -f saludo.txt

Si borraste default-allow-ssh y otros labs la necesitan, vuelve a crearla solo si de verdad hace falta (mejor usa IAP también en ellos).

Preguntas para pensar como arquitecto

¿Por qué IAP TCP forwarding es mejor que un bastión con IP pública?

No expone ningún puerto a Internet (solo al rango de IAP), la autorización es IAM por usuario o grupo (iap.tunnelResourceAccessor), puede exigir niveles de acceso de contexto (dispositivo, IP, país), cada conexión queda auditada y no hay una VM bastión que parchear y vigilar. Combinado con OS Login, tampoco hay claves SSH repartidas en metadatos.

Un script de un administrador usa hoy una clave JSON de una cuenta de servicio con rol Editor. ¿Qué propones?

Eliminar la clave, conceder al grupo de administradores roles/iam.serviceAccountTokenCreator sobre una cuenta de servicio con roles mínimos (no Editor) y ejecutar el script con --impersonate-service-account o con ADC impersonada. Después, aplicar la política de organización que impide crear claves. Si el script corre fuera de Google Cloud de forma desatendida, usar Workload Identity Federation.

¿Qué diferencia hay entre los roles que usaste en el paso 2 y en el paso 5?

En el paso 2 hace falta roles/iam.serviceAccountUser (permiso actAs) para adjuntar la cuenta a un recurso: la VM obtiene los tokens. En el paso 5, roles/iam.serviceAccountTokenCreator permite generar tokens directamente y actuar como la cuenta. Ambos son sensibles: quien los tiene hereda de facto los permisos de la cuenta de servicio.

¿Cómo garantizarías en toda la organización que ninguna VM nueva tenga IP pública?

Con la política de organización constraints/compute.vmExternalIpAccess en modo denegar todo (o con una lista mínima de excepciones), aplicada en la organización o en la carpeta de producción. IAM no sirve para esto, porque no controla la configuración del recurso sino quién puede crearlo.


Volver al módulo