Chapter 15

Sensitive Data Protection

Scope. This chapter covers Sensitive Data Protection (the service formerly branded Cloud DLP): discovering where sensitive data lives, classifying it, detecting PII inside content, and transforming it — tokenization, masking, and de-identification — plus the residency, sovereignty, retention, and lifecycle controls that depend on that classification. It does not cover Cloud KMS key mechanics (Chapter 14), Cloud Storage retention controls (§11.15§11.16), or Security Command Center (Chapter 16). Prerequisites. §2.21 Security Projects, §2.30 Organization Policy, Chapter 14 Cloud KMS and Cryptographic Controls. Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0, hashicorp/google provider 8.x; see sources at end.

Every other data-handling chapter in this book — object storage, relational databases, logs in transit to a SIEM — eventually asks the same question: is this field sensitive, and if so, what transformation does it require before it can be stored, logged, or shared? Sensitive Data Protection is where that question gets a machine-checkable answer. It supplies the infoType detectors, the likelihood model, and the transformation primitives that the rest of the book cites rather than reinvents; §11.x and §12.x point here for discovery and de-identification instead of describing infoTypes themselves.

The service has an unusual operational shape worth stating once: the installed Google Cloud SDK has no gcloud dlp command group. Every other chapter in this book leans on gcloud as the default CLI surface; this one cannot. Automation against Sensitive Data Protection goes through the REST API (dlp.googleapis.com), a client library, Terraform, or the console — there is no first-class command-line tool. That is not a gap to apologize for; it means every inspect template, de-identify template, and job trigger in this chapter is defined as data (Terraform HCL or a JSON request body) rather than assembled from flags, which is arguably the more auditable pattern anyway.

One naming note: the product is branded Sensitive Data Protection, but the API host, the IAM roles (roles/dlp.*), and every Terraform resource (google_data_loss_prevention_*) still carry the old dlp name. A reader searching the console for "DLP" will find it under Security → Sensitive Data Protection.

15.1 Sensitive Data Discovery §

Discovery is continuous, automatic profiling: point a DiscoveryConfig at an organization, folder, or project, and it inventories BigQuery datasets, Cloud SQL instances, and Cloud Storage buckets on an ongoing schedule, producing a data profile for each table, file store, or database it finds. Only one discovery configuration is permitted per organization, folder, or project scope.

Each profile carries a sensitivity level and a data risk level, both categorical (High, Moderate, Low, Unknown), not numeric scores. Sensitivity level reflects the default sensitivity of each infoType found, any manual overrides, and the likelihood that highly sensitive infoTypes are present. Data risk level layers on top of that: it factors in the calculated sensitivity level plus whether access controls already limit exposure — a highly sensitive table locked behind narrow IAM is lower risk than the same table world-readable.

This is a materially different operation from an on-demand inspection job (§15.4): discovery runs continuously and produces an inventory with a risk rating; an inspection job runs once (or on a trigger) and returns findings for a specific request. Discovery actions include exporting profiles to a BigQuery table, publishing a Pub/Sub notification on new or changed profiles, applying GCP resource tags keyed on sensitivity, and publishing to Dataplex Universal Catalog. Chapters 11 and 12 cite this section rather than re-describing profiling.

resource "google_data_loss_prevention_discovery_config" "bq_profiling" {
  parent       = "projects/rc-saas-prod-data-01/locations/us-central1"
  display_name = "bq-continuous-profiling"
  status       = "RUNNING"

  targets {
    big_query_target {
      filter {
        other_tables {}
      }
    }
  }

  actions {
    export_data {
      profile_table {
        project_id = "rc-saas-prod-data-01"
        dataset_id = "dlp_profiles"
        table_id   = "bigquery_profiles"
      }
    }
  }
}

15.2 Data Classification §

This book's data classification is the data-class value of the five mandatory project labels: public, internal, confidential, or restricted. Discovery's sensitivity and risk levels (§15.1) are the evidence a classification decision is based on; the label itself is the durable, human-set assertion that downstream policy reads.

data-classMeaningTypical transformation before egress
publicCleared for external releaseNone required
internalEmployee-facing, low individual harm if leakedMasking (§15.6) for logs and support tooling
confidentialBusiness-sensitive or regulated at moderate severityTokenization or de-identification (§15.5, §15.7)
restrictedHigh-harm PII, financial, or health dataDe-identification mandatory; access requires justification

Security posture. The common failure is classifying at the project or bucket level and never re-checking it — a project labeled internal accumulates a restricted table over time because nobody reclassifies after the fact. Discovery configs (§15.1) exist precisely to catch that drift on a schedule instead of relying on a one-time manual label. Least-privilege position: only the platform-security group can change a data-class label, and the change is logged the same as any other IAM-relevant metadata change (§2.12).

15.3 PII Detection §

PII detection is inspection against infoType detectors — named patterns such as EMAIL_ADDRESS, PERSON_NAME, PHONE_NUMBER, CREDIT_CARD_NUMBER, and US_SOCIAL_SECURITY_NUMBER. The current, authoritative list is queried from the API rather than hardcoded, since it grows over time. Google states no total: the infoType reference directs callers to "call the infoTypes.list method of Sensitive Data Protection" for the current set, which is why no count appears in this book.

Every finding carries a likelihood from a six-value enum: LIKELIHOOD_UNSPECIFIED (documented as "Default value; same as POSSIBLE"), VERY_UNLIKELY, UNLIKELY, POSSIBLE (the default), LIKELY, and VERY_LIKELY, with VERY_UNLIKELY carrying the highest chance of a false positive and VERY_LIKELY the lowest. min_likelihood on an inspect configuration is the practical false-positive dial: raising it from POSSIBLE to LIKELY trades recall for fewer false alarms in a high-volume scan. Custom detectors — stored_info_type regex or dictionary definitions — extend the catalog with organization- specific identifiers (an internal customer ID format, say) the built-in detectors don't know.

resource "google_data_loss_prevention_stored_info_type" "internal_customer_id" {
  parent       = "projects/rc-saas-shared-sec-01/locations/us-central1"
  display_name = "internal-customer-id"

  regex {
    pattern = "CUST-[0-9]{9}"
  }
}

15.4 DLP Scanning §

An on-demand inspection is a DlpJob against content.inspect (for inline content) or dlpJobs.create (for storage: Cloud Storage, BigQuery, or Datastore). A job trigger schedules recurring storage inspection with triggers.schedule.recurrence_period_duration or runs on demand with triggers.manual.

For data the service cannot reach directly — a file share behind a firewall, another cloud — hybrid inspection lets a caller stream content to a hybrid job trigger for scanning without the service fetching the source itself. The submission method is hybridInspect, and it exists on both resources — projects.locations.dlpJobs.hybridInspect and projects.locations.jobTriggers.hybridInspect — so a client can post content to a standing trigger or to a job it created directly. A hybrid job does not end on its own: projects.locations.dlpJobs.finish has to be called explicitly, which makes an abandoned hybrid job a quiet source of cost.

Completion actions on a job trigger include saveFindings (write to Cloud Storage or BigQuery), pubSub (notify a topic), jobNotificationEmails, deidentify (write a de-identified copy), publishFindingsToDataplexCatalog, and publishToStackdriver (emits the dlp.googleapis.com/finding_count metric). publishFindingsToCloudDataCatalog is deprecated in favor of the Dataplex action. publishSummaryToCscc (publish a summary to Chapter 16's Security Command Center) is Preview and must not be a production dependency.

resource "google_data_loss_prevention_job_trigger" "gcs_weekly_scan" {
  parent       = "projects/rc-saas-shared-sec-01/locations/us-central1"
  display_name = "gcs-weekly-pii-scan"

  triggers {
    schedule {
      recurrence_period_duration = "604800s"
    }
  }

  inspect_job {
    storage_config {
      cloud_storage_options {
        file_set {
          url = "gs://rc-saas-prod-uploads-01/*"
        }
      }
    }
    inspect_template_name = google_data_loss_prevention_inspect_template.default.name
    actions {
      save_findings {
        output_config {
          table {
            project_id = "rc-saas-shared-sec-01"
            dataset_id = "dlp_findings"
          }
        }
      }
    }
  }
}

Grant the scanning identity narrowly: a dedicated service account sa-dlp-scanner holding roles/dlp.user in the scanning project plus roles/storage.objectViewer on the specific bucket, never roles/dlp.admin for a job that only runs scans.

15.5 Tokenization §

Tokenization replaces an identifier with a surrogate using cryptoReplaceFfxFpeConfig (format-preserving encryption, FFX mode) or cryptoDeterministicConfig (deterministic encryption). Both are reversible by a principal holding the key — that is the entire point of tokenization over masking, and it is also the risk: a compromised key or an over-broad grant reverses every token it produced.

The production CryptoKey option is kmsWrapped — a wrapped-key reference into Cloud KMS. The other two options, transient (generated per call, not persisted) and unwrapped (plaintext in the request), are unsuitable for anything but disposable testing; never use unwrapped outside a scratch environment. The caller needs roles/cloudkms.cryptoKeyDecrypter on the wrapped key's path to unwrap it at call time. Key ring layout, protection levels, and rotation are Chapter 14's territory (§14.8); this section shows only the wiring.

The request body nests this under primitiveTransformation.cryptoDeterministicConfig.cryptoKey, with surrogateInfoType.name set to TOKENIZED_CUSTOMER_ID; the key reference itself is:

{
  "kmsWrapped": {
    "wrappedKey": "BASE64_WRAPPED_KEY",
    "cryptoKeyName": "KEY_RESOURCE_NAME"
  }
}

KEY_RESOURCE_NAME is the full key path — for the running estate, projects/rc-saas-shared-sec-01/locations/us-central1/keyRings/kr-us-central1-dlp/cryptoKeys/k-tok-cust — and the caller needs roles/cloudkms.cryptoKeyDecrypter on that key alone (§14.8).

The surrogate is annotated by prefixing it with the custom infoType name and character count — TOKENIZED_CUSTOMER_ID(12):a1b2c3d4e5f6 — which is what makes content.reidentify able to find and reverse it later; reidentification is scoped to callers holding the same key and the dlp.user role, and should be logged and alerted the same way any encryption/decryption event is (§14.16 envelope encryption covers the underlying pattern). cryptoDeterministicConfig preserves equality, so tokenized values still function as a join key across tables; cryptoReplaceFfxFpeConfig additionally preserves the value's shape (radix and length) so a legacy schema expecting a 16-digit number still validates — at a documented latency cost the API reference calls out explicitly, which is why deterministic encryption is the default recommendation unless format preservation is a hard requirement.

15.6 Masking §

characterMaskConfig replaces characters with a fixed maskingCharacter, controlling how many to mask (numberToMask), which end to start from (reverseOrder), and which to skip (charactersToIgnore, typically punctuation).

{
  "primitiveTransformation": {
    "characterMaskConfig": {
      "maskingCharacter": "*",
      "numberToMask": 12,
      "reverseOrder": false
    }
  }
}

Masking is irreversible, and that is precisely why it is the right choice for a log line or a support console: there is no reidentify path, so nobody downstream can recover the original even if that system is fully compromised. Tokenization (§15.5) protects the value; masking removes it.

That same irreversibility makes masking useless as a join key or for any workflow needing the original back. Use deterministic encryption (§15.5) where records must still join, and masking where they must not.

Pitfall. Masking preserves length and position, so a partially masked value can remain identifying: the last four digits of a card, plus a masked pattern revealing the issuer's digit count, plus an account holder's name is often enough to re-identify a person. Mask from the end and leave fewer characters than feels comfortable — and treat "masked" as reduced exposure, not as de-identified.

15.7 De-Identification §

De-identification is the general mechanism: a DeidentifyTemplate applies one or more primitiveTransformation rules to matched infoTypes or named fields, via content.deidentify or as a job trigger's deidentify completion action.

PrimitiveEffectReversible?
redactConfigRemoves the value entirelyNo
replaceConfigSubstitutes a fixed valueNo
replaceWithInfoTypeConfigSubstitutes the infoType name, e.g. [EMAIL_ADDRESS]No
characterMaskConfig (§15.6)Masks characters with a fixed characterNo
cryptoHashConfigOne-way hash; collision-resistantNo
cryptoDeterministicConfig (§15.5)Deterministic encryption; equality preservedYes, with the key
cryptoReplaceFfxFpeConfig (§15.5)Format-preserving encryptionYes, with the key
dateShiftConfigShifts dates by a consistent offset, preserving intervalsPartially
bucketingConfig, fixedSizeBucketingConfigReplaces a number with its rangeNo
timePartConfigKeeps only a component, such as the yearNo
replaceDictionaryConfigSubstitutes from a supplied dictionaryNo

Two template shapes, and choosing the wrong one is the common structural mistake: infoTypeTransformations keys rules by infoType across unstructured content, while recordTransformations keys them by field name for structured data such as a BigQuery row or CSV. Use recordTransformations when you already know which column holds the identifier — it is deterministic, whereas infoType detection is probabilistic (§15.3) and will occasionally miss a value in a column you already knew about. Image content has its own imageTransformations.transforms.

resource "google_data_loss_prevention_deidentify_template" "support_logs" {
  parent       = "projects/rc-saas-shared-sec-01/locations/us-central1"
  display_name = "support-log-deid"

  deidentify_config {
    info_type_transformations {
      transformations {
        info_types {
          name = "EMAIL_ADDRESS"
        }
        primitive_transformation {
          replace_with_info_type_config {}
        }
      }
      transformations {
        info_types {
          name = "CREDIT_CARD_NUMBER"
        }
        primitive_transformation {
          character_mask_config {
            masking_character = "#"
            number_to_mask    = 12
          }
        }
      }
    }
  }
}

Per §2.21, this template — like every inspect template, de-identify template, and job trigger in this chapter — is created in rc-saas-shared-sec-01, not in the project holding the data it protects; that separation is what keeps the transformation logic administratively independent of the workload team that produces the raw data.

Pitfall. A de-identify template with no rule for a given infoType passes that value through unchanged — de-identification is opt-in per infoType, not a default-deny scrub. Test a template against a sample containing every infoType you expect, not just the ones you remembered to configure.

15.8 Data Residency §

Sensitive Data Protection processes requests at whichever location you specify in the API call — either locations/REGION in the request path, or a regional endpoint hostname of the form dlp.REGION.rep.googleapis.com. Google's documentation states plainly that data attached to such a request "remains in the specified region while in transit, in use, and at rest," with one caveat: if you inspect a storage resource that lives in a different location than the one you specify, processing can be split between the two locations, so the request's location and the data's location should match.

The organization-policy enforcement of this is constraints/gcp.resourceLocations, owned by §2.30; it restricts which locations any resource-creating API call, including Sensitive Data Protection's, may target. There is no DLP-specific residency constraint — resourceLocations is the mechanism, applied the same way as for every other service.

15.9 Data Sovereignty §

Sovereignty is a narrower claim than residency: not just where data sits, but who can access it and under what legal authority. Assured Workloads layers sovereign controls — restricted personnel access, allowed-location enforcement, and support-access controls — on top of a folder, and Sensitive Data Protection inherits whatever regional and access restrictions the enclosing Assured Workloads folder imposes; it does not have its own separate sovereignty mode. Key Access Justifications, which surfaces the reason code for every access to a customer-managed key, is owned and described in §14.14 — cite it rather than re-explaining the justification-code mechanism here.

Be precise about what a customer can and cannot control. Assured Workloads and Key Access Justifications constrain and make visible where processing happens and why a key was accessed; they do not give the customer unilateral technical prevention of Google-side personnel access in every configuration, and specific compliance-regime claims (sovereign cloud, data embassy, and similar) vary by control profile and region. Treat vendor sovereignty claims as something to verify against the current Assured Workloads control documentation for the specific regime in question rather than something this book asserts generally. The supported-products documentation divides control packages into regulatory boundaries (FedRAMP, ITAR, IRS 1075, CJIS, the Impact Levels and similar) and regional data boundaries named for a country or region, and both sets change; this book names the two categories and no individual package, because a package list has a shelf life and a compliance claim made against a stale one is worse than no claim at all.

15.10 Data Retention §

Sensitive Data Protection does not own retention — the store the data lives in enforces it. Cloud Storage retention policies and Bucket Lock (§11.15, §11.16) set an immutable minimum hold on objects; BigQuery table and partition expiration set a maximum age after which rows are deleted; Cloud Logging bucket retention and locked buckets (§17.13) do the same for log entries. This section's job is the piece none of those own: deriving a retention schedule from the classification in §15.2, and the fact that a minimum and a maximum are different controls that can directly conflict.

A minimum and a maximum are different controls, and they collide. A restricted-classified record might carry a maximum of 90 days under a data-minimization policy — delete it once it's no longer needed. The same record might simultaneously carry a regulatory minimum of seven years if it is also a financial or audit record. Bucket Lock and a table's expiration_time both take a single duration; if you configure retention using only the classification's data-minimization instinct without checking whether a longer regulatory minimum also applies to that data, you either delete records a regulator requires you to keep, or keep records past the minimization commitment you made to a customer. Resolve the conflict explicitly per classification and document which requirement won, before configuring either control.

15.11 Data Lifecycle Security §

Classification (§15.2) is the input to two decisions across a record's life: what transformation applies at rest and in transit (§15.5§15.7), and what happens when the record must become permanently unrecoverable. For a restricted record protected by envelope encryption under a customer-managed key, the fastest and most complete way to make it unrecoverable is crypto-shredding — destroying the key rather than locating and deleting every copy of the ciphertext. §14.17 owns the mechanism and the guarantee it provides; this section's contribution is the trigger: a classification change to restricted should attach the record to a dedicated key (or key path) precisely so it can be crypto-shredded independently of every other record that happens to share storage with it. Key destruction mechanics and the destruction delay are §14.17's territory, not restated here.

Chapter Summary §

  • The installed Cloud SDK has no gcloud dlp command group; automation goes through the REST API (dlp.googleapis.com), client libraries, or Terraform (google_data_loss_ prevention_*, provider ~> 8.0).
  • The product is Sensitive Data Protection; the API host, IAM roles, and Terraform resources still carry the dlp name.
  • Discovery (§15.1) is continuous, scope-limited profiling producing a sensitivity level and a data risk level (both categorical, not numeric) per resource; it differs from an on-demand inspection job (§15.4), which runs once or on a trigger and returns findings.
  • This book's classification is the data-class label value: public, internal, confidential, restricted.
  • min_likelihood on an inspect configuration is the false-positive dial; the enum runs VERY_UNLIKELY through VERY_LIKELY.
  • Job trigger completion actions include saveFindings, pubSub, deidentify, and publishFindingsToDataplexCatalog; publishSummaryToCscc is Preview and unsuited to production dependence.
  • Tokenization (cryptoDeterministicConfig, cryptoReplaceFfxFpeConfig) is reversible by a key holder; masking (characterMaskConfig) is irreversible. Choose based on whether the value must ever come back.
  • Format-preserving encryption preserves shape for legacy schema compatibility at a documented latency cost; deterministic encryption is the default unless format preservation is required.
  • Production crypto keys for tokenization use kmsWrapped; unwrapped is test-only.
  • Inspection templates, de-identify templates, and job triggers live in rc-saas-shared-sec-01, per §2.21, separate from the data they act on.
  • constraints/gcp.resourceLocations (§2.30) is the residency enforcement mechanism; no DLP-specific residency constraint exists.
  • Assured Workloads and Key Access Justifications (§14.14) bound and expose sovereignty controls; they are not a blanket guarantee against every form of access, and specific regime claims need verification against current documentation.
  • Retention has two independent controls that can conflict: a minimum (Bucket Lock, regulatory hold) and a maximum (lifecycle deletion, table expiration); resolve the conflict per classification rather than configuring either in isolation.
  • Crypto-shredding (§14.17) is the lifecycle-security payoff of tying a classification to a dedicated key: destroying the key destroys recoverability without locating every copy.

Security Checklist §

ControlWhy it mattersHow to verify
Discovery config running at the scope that covers all production dataUndiscovered sensitive data cannot be classified or protecteddiscoveryConfigs.list on the org/folder/project; console: Security → Sensitive Data Protection → Discovery
Every project carries a current data-class labelClassification is the input to every downstream transformation choicegcloud resource-manager tags and label audit against §2.12 process; review discovery profile sensitivity level against the label
Templates and job triggers live in rc-saas-shared-sec-01, not the data projectKeeps transformation logic administratively separate from the workload (§2.21)inspectTemplates.list / deidentifyTemplates.list scoped to the security project only
sa-dlp-scanner holds roles/dlp.user, not roles/dlp.adminLeast privilege for a scanning identitygcloud projects get-iam-policy rc-saas-shared-sec-01 --format=json
No unwrapped CryptoKey outside a disposable testPlaintext key material in the request defeats tokenization's purposeGrep de-identify template JSON/HCL for unwrapped_crypto_key
roles/cloudkms.cryptoKeyDecrypter scoped to the specific key path, not the key ringLimits blast radius of a compromised reidentification callergcloud kms keys get-iam-policy on the specific key
constraints/gcp.resourceLocations applied where residency is requiredEnforces processing location at the API layer regardless of caller intentgcloud org-policies describe constraints/gcp.resourceLocations --organization=ORG_ID --effective
Retention minimum and maximum reconciled per classification before configuring eitherPrevents premature deletion of a regulated record or indefinite retention of a minimized oneManual review: compare Bucket Lock/table-expiration configuration against the classification's documented retention schedule
publishSummaryToCscc not relied on as the sole detection pathIt is a Preview action, unsuited to production dependenceConfirm an independent saveFindings or pubSub action exists alongside it
restricted-classified data bound to a dedicated crypto-shreddable keyEnables destruction-based deletion without locating every ciphertext copyReview key-to-classification mapping against §14.17

Sources §