Lab práctico · Semana 8: Cumplimiento normativo e IA en Google Cloud

Inspeccionar y desidentificar datos personales con Sensitive Data Protection

⏱ 45-60 minDificultad: mediaApartados: 3.23.1

Qué vas a construir

Vas a enviar a Sensitive Data Protection (SDP, antes Cloud DLP) un texto de ejemplo con datos personales ficticios de clientes españoles (nombre, correo, teléfono, DNI, IBAN y tarjeta) y a aplicar cuatro tratamientos: inspección, enmascarado, sustitución por el tipo de dato y tokenización reversible con reidentificación posterior.

flowchart LR
  T["Texto con PII ficticia"] --> I["content:inspect → hallazgos"]
  T --> M["content:deidentify → enmascarado"]
  T --> R["content:deidentify → [TIPO]"]
  T --> K["content:deidentify → TOKEN(...)"]
  K --> RI["content:reidentify → valor original"]

Antes de empezar

  • Proyecto con facturación. Todo se hace con curl desde Cloud Shell, sin instalar nada.
  • Todos los datos son inventados. No uses nunca datos reales de clientes para practicar.
export PROJECT_ID=$(gcloud config get-value project)
gcloud services enable dlp.googleapis.com

export DLP="https://dlp.googleapis.com/v2/projects/${PROJECT_ID}/locations/global"
dlp() {  # uso: dlp METODO FICHERO.json
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token)" \
    -H "x-goog-user-project: ${PROJECT_ID}" \
    -H "Content-Type: application/json" \
    "${DLP}/content:$1" -d @"$2"
}

export TEXTO='Cliente: Lucía Martín Pérez, email lucia.martin@example.com, teléfono +34 612 345 678, DNI 12345678Z, IBAN ES91 2100 0418 4502 0005 1332, tarjeta 4111 1111 1111 1111.'

Usamos la ubicación global. SDP permite fijar una región en la ruta (locations/REGIÓN) para que el procesamiento ocurra allí, algo relevante para el RGPD; consulta las ubicaciones admitidas en la documentación de SDP.

El DNI 12345678Z tiene la letra de control correcta, la tarjeta 4111 1111 1111 1111 es el número de prueba clásico (cumple Luhn) y el IBAN es un ejemplo de formato válido: por eso los detectores los reconocerán aunque sean ficticios.

Paso 1: inspeccionar

Pedimos a SDP que busque infoTypes concretos, incluidos los específicos de España, e incluya el texto encontrado en la respuesta (includeQuote).

cat > inspect.json <<EOF
{
  "item": { "value": "${TEXTO}" },
  "inspectConfig": {
    "infoTypes": [
      {"name": "PERSON_NAME"}, {"name": "EMAIL_ADDRESS"}, {"name": "PHONE_NUMBER"},
      {"name": "SPAIN_DNI_NUMBER"}, {"name": "IBAN_CODE"}, {"name": "CREDIT_CARD_NUMBER"}
    ],
    "minLikelihood": "POSSIBLE",
    "includeQuote": true
  }
}
EOF

dlp inspect inspect.json | jq -r '.result.findings[] | "\(.infoType.name)\t\(.likelihood)\t\(.quote)"'

Verás cada hallazgo con su probabilidad (POSSIBLE, LIKELY, VERY_LIKELY). Prueba a cambiar minLikelihood a LIKELY y observa qué desaparece: es el equilibrio entre falsos positivos y falsos negativos que tendrás que ajustar en un proyecto real.

Paso 2: enmascarar la tarjeta y el IBAN

El enmascarado deja visibles solo los últimos caracteres, como en un recibo. charactersToIgnore conserva espacios para que el formato siga siendo legible.

cat > mask.json <<EOF
{
  "item": { "value": "${TEXTO}" },
  "inspectConfig": { "infoTypes": [{"name": "CREDIT_CARD_NUMBER"}, {"name": "IBAN_CODE"}] },
  "deidentifyConfig": {
    "infoTypeTransformations": {
      "transformations": [{
        "infoTypes": [{"name": "CREDIT_CARD_NUMBER"}, {"name": "IBAN_CODE"}],
        "primitiveTransformation": {
          "characterMaskConfig": {
            "maskingCharacter": "*",
            "numberToMask": 12,
            "charactersToIgnore": [{"charactersToSkip": " "}]
          }
        }
      }]
    }
  }
}
EOF

dlp deidentify mask.json | jq -r '.item.value'

Paso 3: sustituir cada dato por su tipo

Útil para analítica de textos libres o para limpiar un prompt antes de enviarlo a un LLM: el modelo sabe que ahí había un correo, pero no cuál.

cat > replace.json <<EOF
{
  "item": { "value": "${TEXTO}" },
  "inspectConfig": {
    "infoTypes": [
      {"name": "PERSON_NAME"}, {"name": "EMAIL_ADDRESS"}, {"name": "PHONE_NUMBER"},
      {"name": "SPAIN_DNI_NUMBER"}, {"name": "IBAN_CODE"}, {"name": "CREDIT_CARD_NUMBER"}
    ]
  },
  "deidentifyConfig": {
    "infoTypeTransformations": {
      "transformations": [{ "primitiveTransformation": { "replaceWithInfoTypeConfig": {} } }]
    }
  }
}
EOF

dlp deidentify replace.json | jq -r '.item.value'

Obtendrás algo como Cliente: [PERSON_NAME], email [EMAIL_ADDRESS], …. Esta transformación no es reversible.

Paso 4: tokenizar el DNI de forma reversible y reidentificar

La seudonimización sustituye el DNI por un token estable (el mismo DNI produce siempre el mismo token, así que puedes unir tablas) que solo quien tenga la llave puede revertir. Para el lab generamos una llave AES de 256 bits y la enviamos sin envolver (unwrapped); en producción la llave iría envuelta con Cloud KMS (kmsWrapped), de modo que reidentificar exija permiso sobre la llave de KMS.

export CLAVE=$(openssl rand -base64 32)

cat > tokenize.json <<EOF
{
  "item": { "value": "${TEXTO}" },
  "inspectConfig": { "infoTypes": [{"name": "SPAIN_DNI_NUMBER"}] },
  "deidentifyConfig": {
    "infoTypeTransformations": {
      "transformations": [{
        "infoTypes": [{"name": "SPAIN_DNI_NUMBER"}],
        "primitiveTransformation": {
          "cryptoDeterministicConfig": {
            "cryptoKey": { "unwrapped": { "key": "${CLAVE}" } },
            "surrogateInfoType": { "name": "DNI_TOKEN" }
          }
        }
      }]
    }
  }
}
EOF

export TOKENIZADO=$(dlp deidentify tokenize.json | jq -r '.item.value')
echo "$TOKENIZADO"

El DNI aparece como DNI_TOKEN(NN):…: el prefijo (surrogate infoType) permite localizar el token después. Ahora reidentificamos con la misma llave:

cat > reidentify.json <<EOF
{
  "item": { "value": "${TOKENIZADO}" },
  "inspectConfig": {
    "customInfoTypes": [{ "infoType": {"name": "DNI_TOKEN"}, "surrogateType": {} }]
  },
  "reidentifyConfig": {
    "infoTypeTransformations": {
      "transformations": [{
        "infoTypes": [{"name": "DNI_TOKEN"}],
        "primitiveTransformation": {
          "cryptoDeterministicConfig": {
            "cryptoKey": { "unwrapped": { "key": "${CLAVE}" } },
            "surrogateInfoType": { "name": "DNI_TOKEN" }
          }
        }
      }]
    }
  }
}
EOF

dlp reidentify reidentify.json | jq -r '.item.value'

Debe reaparecer 12345678Z. Prueba a generar otra CLAVE y repetir la reidentificación: fallará o devolverá basura. Quien controla la llave controla la reidentificación.

Paso 5 (opcional): inspeccionar un bucket con un job

Los métodos de contenido sirven para datos en tránsito (una API, un prompt, un log). Para datos en reposo se usan jobs de inspección o, mejor aún a escala de organización, el descubrimiento continuo con perfiles de datos. En la consola: Security → Sensitive Data Protection → Discovery o Inspection → Create job, eligiendo un bucket con ficheros de prueba. Ten en cuenta que los jobs se facturan por GiB inspeccionado (con el primer GiB gratis al mes).

Comprueba que funciona

  • El paso 1 lista seis hallazgos (nombre, correo, teléfono, DNI, IBAN y tarjeta) con su probabilidad.
  • El paso 2 deja la tarjeta como **** **** **** 1111 o similar, respetando espacios.
  • El paso 3 sustituye cada dato por su [INFO_TYPE].
  • El paso 4 produce un token y la reidentificación devuelve el DNI original.

Si algún detector no encuentra el dato, revisa el texto (tildes, espacios) o baja minLikelihood. Los resultados exactos pueden variar ligeramente según la versión de los detectores.

Limpieza

No se crean recursos en la nube (salvo que hicieras el paso opcional: borra el job y el bucket). Borra los ficheros y la llave de la sesión:

rm -f inspect.json mask.json replace.json tokenize.json reidentify.json
unset CLAVE TOKENIZADO TEXTO

Preguntas para pensar como arquitecto

Para el RGPD, ¿los datos tokenizados del paso 4 siguen siendo datos personales?

Sí. Es seudonimización: con la llave se puede volver al DNI, así que el dato sigue siendo personal y le aplica el RGPD, aunque con menor riesgo. Solo técnicas irreversibles que impidan razonablemente la reidentificación (y un análisis de riesgo, p. ej. k-anonimato) permiten hablar de anonimización.

¿Dónde colocarías SDP en una arquitectura de chatbot con Gemini para atención al cliente?

En dos puntos: antes de enviar el prompt al modelo (desidentificar datos que el modelo no necesita) y antes de guardar las conversaciones en logs o en BigQuery. Hoy lo más directo es usar Model Armor con su filtro de datos sensibles, que se apoya en SDP, y usar el descubrimiento de SDP para vigilar que no aparezca PII en los almacenes analíticos.

Un equipo de analítica necesita unir ventas y soporte por cliente, pero no debe ver los DNI. ¿Qué técnica eliges?

Un token determinista (cryptoDeterministicConfig o cryptoHashConfig) aplicado igual en ambas fuentes: el mismo DNI da el mismo token y se puede hacer el join. Si nunca habrá que volver atrás, el hash es suficiente; si el equipo de fraude debe poder reidentificar casos concretos, cifrado determinista con la llave envuelta en Cloud KMS y permiso de descifrado solo para ese equipo.

¿Por qué en producción la llave debe ir envuelta con Cloud KMS?

Porque así la llave de tokenización nunca viaja ni se guarda en claro: SDP pide a Cloud KMS que la desenvuelva en cada operación, y eso exige permiso IAM sobre la llave KMS, queda auditado y puede revocarse (desactivando la versión). Es separación de funciones aplicada a la reidentificación.


Volver al módulo