AlpinaShop's security has been built in pieces. In 03-01 the firewall was closed. In 03-04 IAM was designed with groups, custom roles and keyless service accounts. In 03-05 Cloud Armor was put in front. In 03-06 Secret Manager and customer-managed keys arrived. In 03-07 it was published with TLS. In 06-02 the main branch was protected. In 07-03 a perimeter was raised around the data.

Every piece was decided well at the time. And nobody has ever looked at the whole.

That is the difference between having security controls and having security. An attacker does not attack the firewall: they attack the weakest link in a chain you have never walked end to end. And in a forty-person company with three technical people, that link is almost never the one you imagine — it is not an exotic kernel vulnerability, it is a two-year-old JSON key still sitting on the laptop of an intern who no longer works here.

This lesson is the complete review. It walks the architecture layer by layer, applies the principles that make the layers hold together, reviews the software supply chain — today the fastest-growing attack vector — closes the data lifecycle, sets up detection and response, clarifies which part of compliance is Google's and which is yours, and finishes with two things that rarely appear in courses: a hardening checklist you can actually execute, and the honest list of the security debt AlpinaShop still carries, prioritised.

A warning, and it is meant seriously. This lesson is training, not an audit. The model presented is sound and applicable, but any architecture handling personal, payment or health data must be reviewed by a security and compliance professional before going to production, and as often as the sector requires. No course, no checklist and no automated tool replaces that work.

Contents

  1. Defence in depth applied to AlpinaShop
  2. The six principles that hold up everything else
  3. Zero trust and BeyondCorp, with IAP as the practical implementation
  4. Identity: a complete review of the IAM design
  5. Temporary privilege elevation and federation
  6. The software supply chain
  7. Data: classification, encryption, pseudonymisation and deletion
  8. Detection: Security Command Center and the audit logs
  9. Response: the first two hours of an incident
  10. Compliance: what Google gives you and what remains yours
  11. Hardening checklist
  12. AlpinaShop's security debt, prioritised

  1. Defence in depth applied to AlpinaShop

Defence in depth means no control is the only one. If one fails, another behind it limits the damage. It is not about stacking up tools: it is about designing so that the failure of any single layer is not catastrophic.

flowchart TB
    ATK["Attacker / human error"]

    subgraph L1["LAYER 1 - Edge"]
      A1["Cloud Armor pol-catalogo-web<br/>WAF, geo, rate limit"]
      A2["TLS alpinashop-cert<br/>+ HSTS"]
      A3["Cloud CDN<br/>absorbs volume"]
    end

    subgraph L2["LAYER 2 - Identity"]
      B1["IAM: groups, no basic roles"]
      B2["Mandatory MFA + IAP"]
      B3["Keyless service accounts"]
      B4["Temporary elevation PAM"]
    end

    subgraph L3["LAYER 3 - Network"]
      C1["Shared VPC, central firewall"]
      C2["No public IPs on VMs"]
      C3["Cloud NAT egress only"]
      C4["VPC Service Controls"]
    end

    subgraph L4["LAYER 4 - Workload"]
      D1["Cloud Run without privileges"]
      D2["Minimal service account"]
      D3["ingress from the balancer only"]
      D4["Binary Authorization"]
    end

    subgraph L5["LAYER 5 - Data"]
      E1["CMEK with alpinashop-keyring"]
      E2["Secret Manager"]
      E3["Cloud SQL without public IP"]
      E4["Encryption in transit and at rest"]
    end

    subgraph L6["LAYER 6 - Detection"]
      F1["Security Command Center"]
      F2["Cloud Audit Logs"]
      F3["Cloud Monitoring alerts"]
      F4["Immutable log retention"]
    end

    ATK --> L1 --> L2 --> L3 --> L4 --> L5
    L6 -.->|"watches them all"| L1
    L6 -.-> L3
    L6 -.-> L5

    style L1 fill:#fef7e0,stroke:#fbbc04
    style L5 fill:#fce8e6,stroke:#ea4335
    style L6 fill:#e6f4ea,stroke:#34a853

The way to check whether defence in depth is real is to ask the single-failure question at every layer:

If this fails… What contains it? Enough?
Cloud Armor lets an SQL injection through Parameterised queries in the code; the database account only has permissions on tienda Yes
Somebody steals Dani's session MFA on access; Dani is not owner of production; changes go through review Yes
The key of sa-catalogo-web is leaked That account only reads one secret, one bucket and one database; VPC-SC prevents data leaving Partially: it could read orders
A container has a critical vulnerability No privileges, no node access, minimal account Yes
The catalogue bucket is encrypted by ransomware Object versioning + retention + copy in another region Needs verifying (07-06)
Marta loses her laptop MFA with a physical key; sessions expire Depends on whether there is a key

The rows with a doubtful answer are debt, and they go to section 12. That is the real value of the exercise: not confirming what is fine, but finding what is not.

  1. The six principles that hold up everything else

Tools change; principles do not. These six explain why every decision in the course was the right one.

Least privilege

Every identity has exactly the permissions it needs for its function, and not one more.

Applying it honestly hurts a little: it means that when somebody asks "give me Editor so I can test something", the answer is no and you have to spend fifteen minutes working out which permission they actually need. The shortcut of saying yes is what has filled the world with projects where everybody is an editor.

The tool that makes it bearable is the IAM Recommender (03-04), which compares granted permissions against permissions used over 90 days:

gcloud recommender recommendations list \
  --project=alpinashop-prod \
  --recommender=google.iam.policy.Recommender \
  --location=global \
  --format="table(content.overview.member, content.overview.removedRole, priority)"

Separation of duties

Whoever builds does not deploy alone; whoever deploys does not approve alone; whoever administers does not audit.

At AlpinaShop: Dani writes the code, Cloud Build builds it, Marta approves production, and the audit logs are kept by a project neither of them can write to (07-07). No single person can, on their own, put code into production and erase the evidence.

Deny by default

What is not explicitly allowed is forbidden.

It has been applied systematically: the 03-01 firewall with its implicit deny; Cloud Run with --no-allow-unauthenticated; ingress restricted to the balancer; the VPC-SC perimeter; the organization policies in 07-07. The alternative — allow by default and block the known bad — is a race you always lose.

Limited blast radius

When something is compromised — not if — how far does it reach?

This is the principle that justifies separating into projects, giving each workload a different service account, and never reusing credentials. The operational question: if this credential leaks today, what can whoever holds it do? If the answer is long, the design is wrong.

Do not trust the network

Being inside the VPC is not a credential.

The classic model — hard perimeter, soft interior — fails because the moment somebody gets in, they have everything. It is developed in section 3.

Assume breach

Design as if they were already inside.

It changes the priorities: you invest as much in detecting and limiting as in preventing. It is what justifies immutable audit logs, anomalous-behaviour alerts and rehearsing incident response.

  1. Zero trust and BeyondCorp, with IAP as the practical implementation

BeyondCorp is the security model Google adopted internally after the attack known as Operation Aurora in 2009, and which gave rise to the term zero trust.

Classic perimeter model Zero trust
Premise The internal network is trusted No network is trusted
Access VPN → you are inside → you have access Every request is authorised separately
Signals The source IP Identity + device + context + risk
Remote work VPN mandatory The same from anywhere
Internal compromise Free lateral access Every hop is authorised again

In Google Cloud, the practical implementation is IAP (Identity-Aware Proxy), already used in 03-04 and worth looking at here as an architectural piece.

flowchart LR
    U["Employee<br/>from anywhere"] --> LB["Global load balancer"]
    LB --> IAP{"IAP"}
    IAP -->|"identity OK<br/>+ access level OK"| APP["Internal dashboard<br/>private Cloud Run"]
    IAP -->|"rejection"| X["403"]
    CTX["Access Context Manager<br/>managed device,<br/>encryption, region, time"] -.-> IAP

What IAP does, and why it is worth it:

  • The application never sees unauthenticated traffic. The rejection happens at Google's edge.
  • There is no VPN to maintain, no concentrator that falls over, no client to install.
  • It works the same from the office and from home, which removes the incentive to work around the control.
  • It combines with context-based access levels: verified corporate device, encrypted disk, screen lock, allowed country.

And its honest limit: IAP protects access to HTTP applications and to SSH/RDP over a tunnel. It is not a substitute for network segmentation or for IAM: it is one more layer.

Applied to AlpinaShop, it solves the problem from the 07-03 exercise — Lucía working from home — better than any IP list:

# Access level based on the device, not on the IP
gcloud access-context-manager levels create dispositivo_gestionado \
  --title="Verified corporate device" \
  --basic-level-spec=dispositivo.yaml \
  --policy=POLICY_ID
# dispositivo.yaml
- devicePolicy:
    requireScreenlock: true
    requireCorpOwned: true
    allowedEncryptionStatuses:
      - ENCRYPTED
    osConstraints:
      - osType: DESKTOP_MAC
        minimumVersion: "14.0"
      - osType: DESKTOP_WINDOWS
        minimumVersion: "10.0"
  regions:
    - ES
    - FR
    - PT

It reads like this: whoever uses a company-owned machine, encrypted, with a screen lock, with an up-to-date operating system, from Spain, France or Portugal, may come in. The IP has stopped mattering, which is exactly the point.

  1. Identity: a complete review of the IAM design

Identity is the new perimeter. In a well-built cloud, the vast majority of serious incidents start with a credential, not with an open port.

The basic roles: get rid of them

Owner, Editor and Viewer have existed since before IAM had granular roles and should never be used in production:

Basic role What it really includes
Viewer Read access to almost everything, including a lot of data
Editor Modify and delete almost any resource in the project
Owner Editor + manage permissions + link billing

An immediate audit:

for P in alpinashop-prod alpinashop-dev alpinashop-datos alpinashop-cicd alpinashop-red; do
  echo "=== $P ==="
  gcloud projects get-iam-policy "$P" --format=json \
    | jq -r '.bindings[] | select(.role|test("^roles/(owner|editor|viewer)$")) |
             "\(.role): \(.members|join(", "))"'
done

What you have to find, and why:

Finding Risk Action
A person with Owner in production They can delete everything and remove the auditing Replace with specific roles
<number>[email protected] with Editor The default Compute account. Any workload that inherits it has permission for everything Disable it and use your own accounts
<number>@cloudbuild.gserviceaccount.com with Editor The pipeline can modify anything A scoped role
allUsers or allAuthenticatedUsers in any binding Public. It is a leak, not a risk Remove immediately

The default Compute account deserves its own paragraph because it is, by a distance, the most widespread failing in Google Cloud. It exists in every project, historically it came with Editor, and any VM, function or service that does not specify another account inherits it. A vulnerability in your web application goes from "read a database" to "administer the entire project". The organization policy automaticIamGrantsForDefaultServiceAccounts (07-07) prevents it in new projects; in existing ones it has to be fixed by hand.

AlpinaShop's target design

Identity Production Development Data CI/CD Network
gcp-infra@ (Marta) Operational roles; owner nobody editor viewer builds.editor networkAdmin, securityAdmin
gcp-desarrollo@ (Dani) run.viewer, logging.viewer, errorreporting.viewer editor — builds.viewer networkViewer
gcp-datos@ (Lucía) — — bigquery.dataEditor, bigquery.jobUser — networkViewer
gcp-seguridad@ securityReviewer, logging.viewer same same same same
gcp-facturacion@ billing.viewer same same same same
sa-catalogo-web 3 specific permissions — — — —
sa-deploy-prod run.developer + serviceAccountUser — — — —

Nobody is Owner of production. It is the 03-04 decision that generates the most argument and delivers the most value: if nobody can bypass the controls, the controls are real. For emergencies there is the break-glass access procedure in section 5, which is audited and temporary.

Keyless service accounts

The rule, already established in 03-04 and closed out here:

A downloaded service account JSON key is a permanent password with no expiry that anyone can copy.

Alternatives, in order of preference:

Situation Keyless solution
Workload inside GCP Attached identity: the VM, the Cloud Run service or the pod wears it
GKE Workload Identity
CI/CD from GitHub Workload Identity Federation (in use since 06-02)
A person who needs to act as an account Impersonation with short-lived credentials
A local system that cannot federate A key, with automated rotation and a usage alert

Searching for existing keys across the whole organization:

for P in $(gcloud projects list --format='value(projectId)'); do
  for SA in $(gcloud iam service-accounts list --project="$P" --format='value(email)' 2>/dev/null); do
    gcloud iam service-accounts keys list --iam-account="$SA" --project="$P" \
      --managed-by=user --format="value(name,validAfterTime)" 2>/dev/null \
      | sed "s|^|$P $SA |"
  done
done

--managed-by=user is the crux of the command: it filters the downloadable keys and discards the ones Google manages internally, which are harmless and would fill the output with noise.

  1. Temporary privilege elevation and federation

The problem with permanent permissions

Marta needs compute.securityAdmin to touch the firewall. She uses it once a month. The rest of the time, that permission only adds risk: if her session is stolen on any given Tuesday, the attacker can open the firewall.

Privileged Access Manager (PAM) solves this with permissions that are requested, justified, approved and expire by themselves.

gcloud pam entitlements create firewall-emergencia \
  --location=global \
  --project=alpinashop-red \
  --entitlement-file=derecho.yaml
# derecho.yaml
privilegedAccess:
  gcpIamAccess:
    resourceType: cloudresourcemanager.googleapis.com/Project
    resource: projects/alpinashop-red
    roleBindings:
      - role: roles/compute.securityAdmin

maxRequestDuration: 7200s          # maximum 2 hours

eligibleUsers:
  - principals:
      - group:[email protected]

requesterJustificationConfig:
  notMandatory: {}                  # in an emergency no long text is demanded

approvalWorkflow:
  manualApprovals:
    requireApproverJustification: true
    steps:
      - approvers:
          - principals:
              - group:[email protected]

What changes:

Permanent permission Temporary elevation
Exposure window Always 2 hours a month
Traceability One log line among thousands A request with a justification
Approval None Another person
Revocation Somebody has to remember Automatic

And the important nuance for a small business: if the approver is the only person who can approve and they are on holiday, the process blocks an emergency. There has to be a break-glass path: an entitlement with automatic approval, a one-hour duration, and an immediate alert to the security channel when it is used. You do not prevent it; you make it noisy. It is the same philosophy as the Terraform emergency procedure in 06-07: when there can be no preventive control, there has to be a detective one.

Identity federation

Workload Identity Federation lets an external identity — GitHub Actions, a cluster in another cloud, an OIDC provider — obtain short-lived Google Cloud credentials with no key at all. AlpinaShop has been using it since 06-02.

The security point almost everybody configures badly is the attribute condition:

# WRONG: any GitHub repository in the world can obtain credentials
--attribute-condition="assertion.repository_owner=='alpinashop'"

That condition looks restrictive and is less so than it seems: if somebody creates a GitHub organization called alpinashop, they are in. And even if that were not so, any branch or pull request in the repository could deploy to production.

# RIGHT: a specific repository, by numeric ID, and only the main branch
--attribute-condition="assertion.repository_id=='123456789' &&
                       assertion.ref=='refs/heads/main' &&
                       assertion.repository_visibility=='private'"

repository_id is numeric and immutable: it cannot be impersonated by renaming. This is the detail that separates a secure federation from one that only looks secure.

  1. The software supply chain

It is the vector that has grown most in recent years, and the one that gets the least attention in small businesses. There is no need to attack your application if you can attack a library your application installs.

flowchart LR
    DEP["Dependencies<br/>PyPI, npm"] --> CODE["Code in GitHub"]
    CODE --> BUILD["Cloud Build"]
    BUILD --> IMG["Image in<br/>Artifact Registry"]
    IMG --> RUN["Cloud Run"]

    S1["Assured OSS<br/>+ dependency scanning"] -.-> DEP
    S2["Mandatory review<br/>+ commit signing"] -.-> CODE
    S3["Reproducible build<br/>+ SLSA provenance"] -.-> BUILD
    S4["Vulnerability scanning<br/>+ attestation"] -.-> IMG
    S5["Binary Authorization"] -.-> RUN

    style S5 fill:#e6f4ea,stroke:#34a853

Vulnerability scanning in Artifact Registry

gcloud services enable containerscanning.googleapis.com --project=alpinashop-prod

# Query the vulnerabilities of the deployed image
gcloud artifacts docker images list-vulnerabilities \
  europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:a3f91c2 \
  --format="table(vulnerability.severity, vulnerability.packageIssue[0].affectedPackage,
                  vulnerability.packageIssue[0].fixedVersion)"

And the step that turns scanning into a control: failing the build when there are critical vulnerabilities with a patch available.

# Step in cloudbuild.yaml
- id: comprobar-vulnerabilidades
  name: gcr.io/google.com/cloudsdktool/cloud-sdk
  entrypoint: bash
  args:
    - -c
    - |
      set -e
      sleep 60   # give the scan time
      CRITICAS=$(gcloud artifacts docker images list-vulnerabilities \
        "${_IMAGEN}:$COMMIT_SHA" --format=json \
        | jq '[.[] | select(.vulnerability.severity=="CRITICAL")
                   | select(.vulnerability.packageIssue[0].fixedVersion.fullName != "")] | length')
      echo "Critical vulnerabilities WITH A PATCH: $$CRITICAS"
      [ "$$CRITICAS" -eq 0 ] || { echo "BUILD BLOCKED"; exit 1; }

The fixedVersion nuance is not a technicality: blocking on vulnerabilities with no patch available paralyses development without improving security, because there is nothing to be done except change dependency. You block what can be fixed; the rest is recorded, the risk is assessed and a decision is taken.

Minimal, reproducible base images

Base image Typical size Packages Surface
ubuntu:22.04 ~78 MB ~100 High: shell, package manager, utilities
python:3.12 ~1 GB ~400 Very high
python:3.12-slim ~130 MB ~120 Medium
gcr.io/distroless/python3 ~50 MB Minimal Low: no shell
scratch (static binaries) The size of the binary 0 None

With no shell, an attacker who achieves code execution cannot launch sh, or curl, or wget. It is not invulnerable, but most post-exploitation tooling stops working.

And reproducibility, which is what makes all of the above verifiable:

# WRONG: "latest" changes under your feet. Today's image is not yesterday's
FROM python:3.12-slim

# RIGHT: immutable digest. This image is exactly this one, always
FROM python:3.12-slim@sha256:2d3f4a1b9c8e7d6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f

WORKDIR /app
COPY requirements.txt .
# Install with verified hashes: if a dependency changes, it fails
RUN pip install --no-cache-dir --require-hashes -r requirements.txt
COPY . .
USER 1000:1000                      # NEVER root
CMD exec gunicorn --bind :$PORT --workers 4 --threads 20 main:app

The three lines that matter: a digest instead of a tag, --require-hashes so that a tampered dependency makes the install fail, and USER 1000 so as not to run as root.

SLSA and provenance

SLSA (Supply-chain Levels for Software Artifacts) is a framework with assurance levels about how an artefact was built. Cloud Build generates verifiable provenance natively:

gcloud artifacts docker images describe \
  europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:a3f91c2 \
  --show-provenance

Provenance answers, with a cryptographic signature: which exact commit, which build instructions, which machine, which moment. It serves what during an incident is a question worth hours: was this image running in production built from our code, or did somebody publish it by hand?

SLSA level What it guarantees How it is reached in GCP
1 Provenance exists Cloud Build generates it on its own
2 Provenance signed by a build service Managed Cloud Build
3 Isolated, non-falsifiable build Cloud Build with isolated workers

Binary Authorization: closing the circle

It was already introduced in 07-01. Here is where it fits: it is the control that stops an image which did not go through the process reaching production.

gcloud container binauthz policy import - <<'EOF'
defaultAdmissionRule:
  evaluationMode: REQUIRE_ATTESTATION
  enforcementMode: DRYRUN_AUDIT_LOG_ONLY      # ALWAYS start here
  requireAttestationsBy:
    - projects/alpinashop-cicd/attestors/escaneo-superado
    - projects/alpinashop-cicd/attestors/aprobacion-marta
EOF

And it applies to Cloud Run, not just to GKE, which makes it directly relevant to AlpinaShop after 07-02:

gcloud run services update alpinashop-web --region=europe-west1 \
  --binary-authorization=default

Dependencies: Assured Open Source

Assured Open Source Software is a service through which Google distributes the open source packages it uses itself: it builds them on its own infrastructure, scans them, signs them and gives them SLSA provenance.

Its value is bounded and it is worth saying so: it covers a catalogue of popular Java and Python packages, not everything you install. For AlpinaShop, with a dozen common dependencies, most would be covered. It is not a silver bullet; it is reducing the surface where you have to trust blindly.

  1. Data: classification, encryption, pseudonymisation and deletion

Classify before protecting

You cannot protect what you do not know the nature of. AlpinaShop's classification:

Level What it includes Required controls
Public Catalogue, prices, photos Nothing special
Internal Sales metrics, inventory IAM by group
Confidential Orders, addresses, email addresses IAM + CMEK + data access auditing
Restricted Payment data, identity documents Not stored (the gateway holds them)

The fourth row is the best security decision in the whole course, and it is not technical: data you do not keep cannot leak. AlpinaShop does not store card numbers because the gateway returns a token. That removes the entire scope of PCI-DSS from its infrastructure at a stroke.

Discovery with Sensitive Data Protection

In 04-07 DLP — today Sensitive Data Protection — was introduced for data governance. Here it is used as a security control: finding personal data where it should not be.

gcloud dlp jobs create inspect \
  --project=alpinashop-datos \
  --inspect-job-file=inspeccion.json
{
  "inspectJob": {
    "storageConfig": {
      "bigQueryOptions": {
        "tableReference": {
          "projectId": "alpinashop-datos",
          "datasetId": "alpinashop_analitica",
          "tableId": "eventos_web"
        },
        "sampleMethod": "RANDOM_START"
      }
    },
    "inspectConfig": {
      "infoTypes": [
        {"name": "EMAIL_ADDRESS"},
        {"name": "PHONE_NUMBER"},
        {"name": "CREDIT_CARD_NUMBER"},
        {"name": "SPAIN_DNI_NUMBER"},
        {"name": "IBAN_CODE"}
      ],
      "minLikelihood": "LIKELY",
      "includeQuote": false
    },
    "actions": [
      {"saveFindings": {"outputConfig": {"table": {
        "projectId": "alpinashop-datos", "datasetId": "seguridad", "tableId": "hallazgos_dlp"}}}}
    ]
  }
}

Two decisions in that file:

  • includeQuote: false: do not store the fragment that was found. Storing the detected card number in the findings table would create a second copy of the problem. A real and frequent mistake.
  • SPAIN_DNI_NUMBER: there are country-specific detectors. Using only the generic ones leaves out what matters most in Spain.

What always turns up, and AlpinaShop was no exception: email addresses in free-text comment fields, phone numbers in the "order notes" field, and full addresses in application logs from months ago. None of it was intentional. All three are personal data for GDPR purposes.

Encryption and CMEK

Google encrypts everything at rest by default, with nothing to configure. CMEK (03-06) adds that you control the key yourself in Cloud KMS:

Default encryption CMEK
Who controls the key Google You, in alpinashop-keyring
Rotation Google's, automatic You define the period
Revoking access to the data Not possible By disabling the key
Record of key usage No Yes, in the KMS logs
Cost Included KMS cost + operations
Complexity None Lifecycle management

The "red button" CMEK gives you. If an intrusion is detected in progress, disabling the key makes the data encrypted with it unreadable within seconds, even for somebody holding read permissions. It is a containment capability that does not exist without CMEK.

With its counterpart, which has to be said plainly: if you lose the key, you lose the data. Permanently. That is why KMS prevents immediate deletion and imposes a scheduled destruction period.

Pseudonymisation

For Lucía to analyse purchasing behaviour she does not need to know who each customer is:

-- Pseudonymised view: analytics without identifying anybody
CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.pedidos_seudonimos` AS
SELECT
  TO_HEX(SHA256(CONCAT(cliente_email, @sal_secreta))) AS cliente_id,
  DATE(fecha_pedido)                                   AS fecha,
  SUBSTR(codigo_postal, 1, 2)                          AS provincia,
  categoria_producto,
  importe_total,
  canal
FROM `alpinashop-datos.alpinashop_analitica.pedidos`;

Three techniques in five lines: a salted hash so that the same customer is the same cliente_id without it being reversible; generalisation of the postcode to its first two digits, which gives the province without pinpointing the neighbourhood; and omission of name, address and phone number, which add nothing to the analysis.

And the honest warning: pseudonymisation is not anonymisation. With the salt, the data is still re-identifiable and therefore still personal data for GDPR purposes. It reduces the risk; it does not remove the obligation.

Retention and deletion

The GDPR requires you not to keep personal data longer than necessary, and to be able to delete it at the data subject's request.

# Data lake lifecycle
resource "google_storage_bucket" "datalake" {
  name     = "alpinashop-datalake"
  location = "EUROPE-WEST1"

  lifecycle_rule {
    condition { age = 90 }
    action { type = "SetStorageClass" storage_class = "NEARLINE" }
  }
  lifecycle_rule {
    condition { age = 365 }
    action { type = "SetStorageClass" storage_class = "COLDLINE" }
  }
  lifecycle_rule {
    condition { age = 2555 }        # 7 years: tax obligation
    action { type = "Delete" }
  }

  versioning { enabled = true }     # protection against accidental deletion and ransomware
}
-- Automatic partition expiry in BigQuery
ALTER TABLE `alpinashop-datos.alpinashop_analitica.eventos_web`
SET OPTIONS (partition_expiration_days = 400);

And deletion on request, which is where most companies discover that their architecture never accounted for it:

-- Right to erasure procedure
CREATE OR REPLACE PROCEDURE `alpinashop-datos.alpinashop_analitica.borrar_cliente`(email STRING)
BEGIN
  -- 1. Anonymise the orders: they are kept for tax reasons, without identification
  UPDATE `alpinashop-datos.alpinashop_analitica.pedidos`
  SET cliente_email = 'DELETED', cliente_nombre = 'DELETED',
      direccion = 'DELETED', telefono = NULL
  WHERE cliente_email = email;

  -- 2. Delete from the tables where there is no retention obligation
  DELETE FROM `alpinashop-datos.alpinashop_analitica.eventos_web` WHERE usuario_email = email;
  DELETE FROM `alpinashop-datos.alpinashop_analitica.carritos`    WHERE cliente_email = email;

  -- 3. Record the exercise of the right (without the deleted data)
  INSERT INTO `alpinashop-datos.cumplimiento.solicitudes_rgpd`
  VALUES (TO_HEX(SHA256(email)), CURRENT_TIMESTAMP(), 'ERASURE', SESSION_USER());
END;

Note point 1: the orders are not deleted, they are anonymised, because tax regulations require invoices to be kept. The right to erasure is not absolute and it collides with other legal obligations; resolving that collision is not a technical decision and must be documented with legal advice.

  1. Detection: Security Command Center and the audit logs

Security Command Center

SCC is Google Cloud's security centre. It detects insecure configurations, vulnerabilities and active threats.

Tier What it includes Cost
Standard Basic Security Health Analytics, configuration findings, inventory Free
Premium Event Threat Detection, Container Threat Detection, VM Threat Detection, compliance (CIS, PCI, ISO), attack path simulation Paid, enterprise-oriented
Enterprise Premium + multicloud + case management + Mandiant intelligence Paid

What the free tier really detects, which is the one a small business will use:

Finding Severity How often it turns up
Publicly accessible bucket Critical Constantly
Service account with an administrator role High Very frequently
Firewall rule allowing 0.0.0.0/0 to an administration port Critical Frequently
Cloud SQL instance with a public IP and no SSL High Frequently
Service account keys older than 90 days Medium Almost always
MFA not enabled on administrator accounts Critical Frequently
Audit logging disabled High Occasionally

Premium adds Event Threat Detection, which analyses the logs in real time looking for behaviour — not configuration: cryptocurrency mining on a VM, data exfiltration from BigQuery, credential use from an impossible geography, a service account being created and immediately followed by a permission grant.

The honest criterion for AlpinaShop: start with the free tier and actually deal with its findings, which is already more than most people do. Premium is justified when there is something to watch actively, a compliance requirement that demands it, or somebody with time to respond to what it detects. Buying detection nobody looks at is worse than not buying it, because it creates the feeling of being protected.

gcloud scc findings list ORGANIZATION_ID \
  --filter='state="ACTIVE" AND severity="CRITICAL"' \
  --format="table(category, resourceName, eventTime)"

The audit logs as the source of truth

They are covered in depth in 07-07. Here, the bare minimum:

Type What it records Enabled by default?
Admin activity Configuration and permission changes Yes, and it cannot be disabled
Data access Data reads and writes No. It has to be enabled and it costs
System events Google's actions on your resources Yes
Policy denials Requests rejected by policies Yes

The fact that "data access" is disabled by default has a consequence that must be accepted consciously: without enabling it, you cannot know who read your customers' data. For AlpinaShop, enabling it in alpinashop-datos and in the buckets holding personal data is not optional if it wants to be able to answer a breach.

Three queries to have written before you need them:

-- 1. IAM permission changes in the last 7 days
SELECT timestamp,
       protopayload_auditlog.authenticationInfo.principalEmail AS who,
       protopayload_auditlog.methodName                        AS action,
       protopayload_auditlog.resourceName                      AS resource
FROM `alpinashop-prod.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.methodName LIKE '%setIamPolicy%'
  AND DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
ORDER BY timestamp DESC
-- 2. Credential use from outside Spain
SELECT DISTINCT
       protopayload_auditlog.authenticationInfo.principalEmail AS who,
       protopayload_auditlog.requestMetadata.callerIp          AS ip,
       protopayload_auditlog.requestMetadata.callerSuppliedUserAgent AS agent,
       COUNT(*) AS requests
FROM `alpinashop-prod.auditoria.cloudaudit_googleapis_com_activity`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
  AND NET.IP_TO_STRING(NET.IP_FROM_STRING(
        protopayload_auditlog.requestMetadata.callerIp)) NOT LIKE '88.20.145.%'
GROUP BY who, ip, agent
HAVING requests > 10
ORDER BY requests DESC
-- 3. Who has created or downloaded service account keys
SELECT timestamp,
       protopayload_auditlog.authenticationInfo.principalEmail AS who,
       protopayload_auditlog.resourceName                      AS account
FROM `alpinashop-prod.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.methodName =
      'google.iam.admin.v1.CreateServiceAccountKey'
ORDER BY timestamp DESC

The third deserves a permanent alert: in an organization that federates identities and uses impersonation, creating a JSON key is an exceptional event that must always be looked at.

  1. Response: the first two hours of an incident

Here is what almost no course includes and what really makes the difference. A realistic script for a team of three.

Scenario: it is 23:40 on a Thursday in October, in the middle of the campaign. An SCC alert arrives: "Anomalous IAM grant: the account sa-informes-nocturnos has granted roles/owner to an external account".

Minutes 0-10: assess, do not act

Nothing is touched yet. The first impulse — delete the account, cut off access — destroys evidence and sometimes warns the attacker. The first thing is to answer three questions:

  1. Is it real or a false positive? Is there a scheduled change tonight? Is somebody working?
  2. Is it ongoing? Is there still activity from that identity right now?
  3. What is the scope? What can that credential touch?
# Activity from that identity in the last hour
gcloud logging read '
  protoPayload.authenticationInfo.principalEmail="[email protected]"
  AND timestamp>="'$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)'"' \
  --project=alpinashop-prod --limit=200 \
  --format="table(timestamp, protoPayload.methodName, protoPayload.requestMetadata.callerIp)"

Minutes 10-30: contain

If it is real, cut without destroying. The order matters:

# 1. Remove the permission that was improperly granted
gcloud projects remove-iam-policy-binding alpinashop-prod \
  --member="user:[email protected]" --role="roles/owner"

# 2. DISABLE the compromised account (do not delete it: the evidence is lost)
gcloud iam service-accounts disable \
  [email protected]

# 3. Revoke the live tokens: without this, tokens already issued remain valid
#    for up to an hour even though the account is disabled
gcloud iam service-accounts keys list \
  --iam-account=sa-informes-nocturnos@alpinashop-prod.iam.gserviceaccount.com

# 4. Freeze the evidence: copy the relevant logs to a bucket with retention
gcloud logging read 'timestamp>="'$INICIO'"' --project=alpinashop-prod \
  --format=json > /tmp/incidente-$(date +%F).json
gcloud storage cp /tmp/incidente-*.json gs://alpinashop-evidencia-forense/

Step 3 is the one that is always forgotten. Disabling an account does not invalidate the access tokens already issued, which live for up to an hour. During that hour the attacker is still inside even though the console says the account is disabled.

Minutes 30-60: assess the scope

The questions, in this order:

Question How it is answered
What did that identity do in the last 30 days? Admin activity logs
Did it access personal data? Data access logs (if they were enabled)
Did information leave? Flow Logs, VPC-SC logs, Cloud Storage transfers
Did it create persistence? Look for new accounts, new keys, new firewall rules, new functions
How did it get in? The origin of the credentials: a leaked key? a public repository? a laptop?

The search for persistence is the most neglected part and the one that makes an incident repeat itself the following week:

# Resources created in the last 48 hours across the whole organization
gcloud asset search-all-resources --scope=organizations/ORG_ID \
  --query="createTime>$(date -u -d '48 hours ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --format="table(name, assetType, createTime)"

Hour 1-2: communicate

Recipient When What is said
Internal team Immediately What has happened and who does what
Management As soon as it is confirmed Business impact, in business language
Legal adviser / DPO Within 24 h If personal data is involved
AEPD Within 72 h of becoming aware If there is a risk to people's rights
Affected customers Subject to legal assessment If the risk is high
Insurer As per the policy Deadlines are usually strict

The GDPR's 72 hours for notifying the supervisory authority are not negotiable and the clock starts when you become aware, not when you finish the investigation. It is the number one reason to call the legal adviser within the first hour, even if the scope is not yet known.

What has to be ready BEFORE

An incident is no time to improvise. The folder that has to be written today:

  • Phone numbers: internal, management, legal adviser, insurer, Google Cloud support.
  • The containment command already written, tested and with permissions verified.
  • An evidence bucket with locked retention, created outside the project that may be compromised.
  • An alternative communication channel — if corporate email is compromised, you cannot coordinate by email.
  • Emergency access tested: that Marta can get in even if her usual account is locked.
  • One drill a year. Half an hour in a room, over a written scenario. It uncovers more holes than any configuration audit.

  1. Compliance: what Google gives you and what remains yours

The split from 01-05, revisited now with the full context:

Aspect Google You
Physical security of the data centres ✅ —
Hardware, physical network, hypervisor ✅ —
Encryption at rest by default ✅ —
Operating system patching ✅ in managed services ❌ on Compute Engine
IAM configuration — ❌ Yours
Firewall rules — ❌ Yours
Security of your application code — ❌ Yours
Data classification — ❌ Yours
Managing employee access — ❌ Yours
Backups and testing them — ❌ Yours (07-06)
Incident response in your layer — ❌ Yours

Google's certifications — ISO 27001, ISO 27017, ISO 27018, SOC 1/2/3, PCI-DSS as a provider, ENS in Spain, sector-specific schemes — accredit its infrastructure. They are downloaded from the Compliance Reports Manager.

And here is the most expensive misunderstanding about compliance in the cloud:

Google being certified to ISO 27001 does not certify your company. Its infrastructure being fit for PCI-DSS does not make your shop compliant. What you get is that the layer underneath is already audited, which reduces your scope considerably — but your configuration, your code, your processes and your people are still yours and are still auditable.

For AlpinaShop, as a Spanish online shop:

Obligation Situation Outstanding
GDPR and LOPDGDD Fully applicable Record of processing activities; processor agreement with Google (the Cloud Data Processing Addendum); analysis of international transfers
PCI-DSS Minimal scope: no cards stored Keep it that way; SAQ-A questionnaire
LSSI-CE Legal notice, cookies Periodic review
Digital Services Act Depending on size Review with advisers
National Security Scheme (ENS) Does not apply (not public sector) —

The documentation you have to have, and which an inspection asks for before any technical configuration:

  1. Record of processing activities (GDPR art. 30).
  2. Data processor agreement with Google Cloud.
  3. Impact assessment (DPIA) if there is high-risk processing.
  4. Information security policy, even if it is four pages.
  5. Breach response procedure, with the deadlines from section 9.
  6. Record of access to personal data — which is exactly what the data access logs give you.
  7. Analysis of international transfers and the safeguards applied.

It bears repeating because it matters: this table is indicative and educational. A compliance professional must validate what applies to your specific case, especially regarding the GDPR, international transfers and PCI-DSS scope.

  1. Hardening checklist

Actionable over everything built in the course. The status column is AlpinaShop's today.

Identity and access

# Control Status
1 Mandatory MFA on all accounts, with a physical key for administrators ⚠️ MFA yes, keys no
2 No basic role (owner/editor/viewer) in production ⚠️ Editor on cloudbuild
3 Permissions to groups only, never to people ✅
4 Zero service account JSON keys ⚠️ One outstanding
5 Default Compute account disabled or without permissions ❌
6 Identity federation with a condition on repository_id and branch ✅
7 Temporary elevation (PAM) for administrative permissions ❌
8 Documented quarterly access review ❌
9 Employee offboarding process that revokes within 24 h ⚠️ Informal

Network

# Control Status
10 No public IPs on VMs; egress via Cloud NAT ✅
11 Firewall with deny by default and rules by tag ✅
12 Cloud SQL with a private IP only ✅
13 Shared VPC with permissions per subnet ✅ (07-03)
14 VPC Service Controls around the data ⚠️ In dry-run mode
15 Flow Logs enabled with sampling ✅
16 Cloud Armor with managed rules and rate limiting ✅
17 Administrative access via IAP only, no public SSH ✅

Workload

# Control Status
18 Containers without root and without privileges ✅
19 A dedicated, minimal service account per service ✅
20 ingress restricted to the balancer ✅ (07-02)
21 Minimal base images pinned by digest ⚠️ Slim, by tag
22 Vulnerability scanning that blocks the build ❌ Scans, does not block
23 Binary Authorization in dry-run or enforced mode ❌
24 No secrets in environment variables or in the code ✅

Data

# Control Status
25 Documented data classification ✅
26 CMEK on the confidential data ✅
27 No card data stored ✅
28 Periodic scanning with Sensitive Data Protection ⚠️ Once
29 Automated retention and deletion policies ⚠️ Partial
30 Right to erasure procedure tested ❌
31 Versioning and deletion protection on critical buckets ✅
32 Backups with a tested restore ❌ (07-06)

Detection and response

# Control Status
33 Security Command Center active and its findings dealt with ⚠️ Active, no process
34 Data access logs enabled on the sensitive data ❌
35 Audit logs exported to a separate, immutable project ❌ (07-07)
36 Alerts on IAM changes and key creation ❌
37 Written response plan, with phone numbers and commands ❌
38 Annual incident drill ❌
39 Forensic evidence bucket with locked retention ❌

Governance

# Control Status
40 Organization policies applied ❌ (07-07)
41 All infrastructure in Terraform and reviewed ✅
42 Record of processing activities ⚠️ Out of date
43 Processor agreement with Google signed ✅

Result: 20 ✅, 11 ⚠️, 12 ❌. Not bad for a forty-person company. And not enough.

  1. AlpinaShop's security debt, prioritised

No company is at 100 %. What distinguishes a mature organization is not having no debt, but knowing which debt it has, having prioritised it and paying it off. Prioritisation by risk (probability × impact) against effort.

Priority 1 — This week

# Debt Risk Effort
1 The outstanding JSON key. There is a key for sa-dataflow-pedidos created 14 months ago for a test. Nobody knows where it is. It can read the entire data lake Critical: a lost permanent credential 2 h
2 Default Compute account with Editor. Any workload that does not specify an account inherits it Critical: trivial privilege elevation 4 h
3 Cloud Build with Editor in production. A compromise of the pipeline is a total compromise High 4 h
4 Alerts on IAM changes and key creation. Today nobody would find out High: without detection there is no response 2 h

The four together add up to less than two days of work and are what reduces risk the most. Starting here is not a matter of opinion.

Priority 2 — This month

# Debt Risk Effort
5 Data access logs disabled. Faced with a breach, AlpinaShop cannot say who read what. It is a legal problem, not just a technical one High 1 day + recurring cost
6 No written response plan. Section 9 is theory until the folder exists High 1 day
7 VPC-SC in dry-run mode for weeks. A half-finished control does not protect Medium-high 2 days
8 Backup restore never tested. Covered in 07-06 Critical if it happens 1 day
9 Scanning that does not block. Images with patchable critical vulnerabilities are deployed Medium 4 h

Priority 3 — This quarter

# Debt Risk Effort
10 Organization policies not applied (07-07) Medium 3 days
11 No periodic access review Medium Process
12 Physical keys for administrators Medium 1 day + purchase
13 Temporary elevation with PAM Medium 2 days
14 Binary Authorization Low-medium 3 days
15 Images by digest and --require-hashes Low-medium 1 day
16 Incident drill Medium 4 h
17 Updated record of processing Legal With advisers

Consciously accepted debt

And this is part of maturity too: deciding not to do something, in writing, with a reason.

Decision Reason
Not buying SCC Premium There is nobody who could deal with what it detects. To be revisited once there are 10 technical employees
Not adopting Cloud Service Mesh for mTLS between services There is one service (DA-004). Internal traffic is already encrypted by Google's network
Not encrypting at application level on top of CMEK The threat model does not include a Google administrator. It would be revisited with health data
Not running external penetration tests this year Cost against current maturity. Priority 1 and 2 have to be paid off first

An accepted, documented risk is not a security failure: it is a management decision. A risk nobody has looked at is.

Common Mistakes and Tips

  • Confusing having tools with having security. SCC enabled and nobody looking at the findings is worse than not having it: it creates false confidence.
  • Leaving the default Compute account with Editor. It is the most widespread failing in Google Cloud and it turns any vulnerability into a total compromise of the project.
  • Creating JSON keys "just to test". They are never deleted. Hunt for them periodically with --managed-by=user.
  • Federating identity without a strict attribute condition. Without repository_id and ref, any branch or repository with the same organization name can deploy to production.
  • Deleting the compromised account during an incident. It destroys evidence. You disable it.
  • Forgetting that live tokens survive disabling for up to an hour.
  • Storing the fragment detected by DLP. Creating a table with the card numbers you found duplicates the problem, it does not solve it.
  • Believing that pseudonymisation is anonymisation. With the salt, it is still personal data for GDPR purposes.
  • Thinking Google's certifications certify your company. They certify the layer underneath. Your configuration is yours.
  • Blocking builds on vulnerabilities with no patch available. It paralyses the team without improving anything. You block what can be fixed.
  • Enabling all the controls at once. VPC-SC, Binary Authorization and organization policies on the same day is a guaranteed outage. One at a time, in dry-run mode first.
  • Leaving the response plan until it is needed. The incident is the worst moment to write it.
  • Tip: ask the single-failure question at every layer. It takes half an hour and finds more than any scanner.
  • Tip: the best security measure is the data you do not keep. Before protecting a piece of data, ask whether it needs storing at all.
  • Tip: a half-hour drill once a year uncovers more real holes than a configuration audit.
  • Tip: write down the accepted debt. It turns a hole into a decision, and it makes sure it gets revisited.

Exercises

Exercise 1 — Identity audit

Write an audit script that, across AlpinaShop's five projects, detects and reports:

  1. Any binding with basic roles (owner, editor, viewer).
  2. Bindings with allUsers or allAuthenticatedUsers on projects and on buckets.
  3. Service accounts with user-managed keys, and their age.
  4. Service accounts unused for 90 days.
  5. Service accounts with administrator roles.

For each finding, state the severity and the recommended action. Also explain why point 4 matters more than it looks.

Exercise 2 — Designing the response to a specific incident

Scenario. On a Monday morning, an external developer posts on a public forum that they have found an AlpinaShop GitHub repository with a credenciales.json file in the commit history. The file is a key for sa-dataflow-pedidos, deleted from the repository eight months ago but present in the history. That account has roles/bigquery.dataViewer on alpinashop_analitica and roles/storage.objectAdmin on alpinashop-datalake.

Write the complete response plan with timestamps: what is done in the first 10 minutes, in the first 30, in the first hour and in the first 24 hours. Include the commands, the communication decisions (with whom and when), how you determine whether there was improper access, and what structural changes are made afterwards so that it does not happen again.

Exercise 3 — Prioritising on a limited budget

AlpinaShop's management approves 10 days of Marta's time and €3,000 for security this quarter. There is no more.

From the debt list in section 12, choose what gets done and what does not, justifying each decision on risk against effort. Draw up a plan by weeks and, most importantly, write what you tell management about what is left undone, in language a managing director understands.

Solutions

Solution 1 — Identity audit

#!/usr/bin/env bash
# auditoria-identidad.sh — AlpinaShop identity review
set -uo pipefail

PROYECTOS=(alpinashop-prod alpinashop-dev alpinashop-datos alpinashop-cicd alpinashop-red)
HOY=$(date +%s)

echo "===== 1. BASIC ROLES ====="
for P in "${PROYECTOS[@]}"; do
  gcloud projects get-iam-policy "$P" --format=json 2>/dev/null \
    | jq -r --arg p "$P" '
        .bindings[]
        | select(.role | test("^roles/(owner|editor|viewer)$"))
        | .role as $r
        | .members[]
        | "\($p) | \($r) | \(.)"'
done | while IFS='|' read -r PROY ROL MIEMBRO; do
  GRAV="MEDIUM"
  [[ "$ROL" == *owner*  ]] && GRAV="CRITICAL"
  [[ "$ROL" == *editor* ]] && GRAV="HIGH"
  [[ "$PROY" == *prod*  ]] && GRAV="CRITICAL"
  echo "[$GRAV] $PROY $ROL -> $MIEMBRO"
done

echo
echo "===== 2. PUBLIC ACCESS ====="
for P in "${PROYECTOS[@]}"; do
  gcloud projects get-iam-policy "$P" --format=json 2>/dev/null \
    | jq -r --arg p "$P" '.bindings[] | select(.members[]? | test("^all(Users|AuthenticatedUsers)$"))
                          | "[CRITICAL] \($p) PUBLIC PROJECT: \(.role)"'
done
for B in $(gcloud storage buckets list --format='value(name)' 2>/dev/null); do
  gcloud storage buckets get-iam-policy "gs://$B" --format=json 2>/dev/null \
    | jq -r --arg b "$B" '.bindings[]? | select(.members[]? | test("^all(Users|AuthenticatedUsers)$"))
                          | "[CRITICAL] PUBLIC BUCKET gs://\($b): \(.role)"'
done

echo
echo "===== 3. JSON KEYS ====="
for P in "${PROYECTOS[@]}"; do
  for SA in $(gcloud iam service-accounts list --project="$P" --format='value(email)' 2>/dev/null); do
    gcloud iam service-accounts keys list --iam-account="$SA" --project="$P" \
      --managed-by=user --format='value(name,validAfterTime)' 2>/dev/null \
    | while read -r NOMBRE FECHA; do
        [ -z "$NOMBRE" ] && continue
        DIAS=$(( (HOY - $(date -d "$FECHA" +%s)) / 86400 ))
        GRAV="HIGH"; [ "$DIAS" -gt 90 ] && GRAV="CRITICAL"
        echo "[$GRAV] $P $SA key aged $DIAS days"
      done
  done
done

echo
echo "===== 4. UNUSED SERVICE ACCOUNTS (90 days) ====="
for P in "${PROYECTOS[@]}"; do
  gcloud recommender insights list --project="$P" --location=global \
    --insight-type=google.iam.serviceAccount.Insight \
    --filter='insightSubtype=SERVICE_ACCOUNT_USAGE' \
    --format="value(description)" 2>/dev/null | sed "s|^|[MEDIUM] $P |"
done

echo
echo "===== 5. SERVICE ACCOUNTS WITH ADMINISTRATOR ROLES ====="
for P in "${PROYECTOS[@]}"; do
  gcloud projects get-iam-policy "$P" --format=json 2>/dev/null \
    | jq -r --arg p "$P" '
        .bindings[]
        | select(.role | test("[Aa]dmin$|^roles/owner$|^roles/iam\\.securityAdmin$"))
        | .role as $r | .members[]
        | select(startswith("serviceAccount:"))
        | "[HIGH] \($p) \($r) -> \(.)"'
done

Table of findings, severity and action:

Finding Severity Action
owner in production granted to a person Critical Replace with specific roles; use PAM for the exceptional case
editor granted to cloudbuild in production Critical Scope down to run.developer + serviceAccountUser
allUsers anywhere Critical Remove there and then and review the access logs
JSON key older than 90 days Critical Migrate to federation or impersonation and delete the key
Service account unused for 90 days Medium Disable for 30 days; if nothing breaks, delete
Service account with an *Admin role High A custom role with the permissions actually used

Why point 4 matters more than it looks. An unused service account is a silent target: nobody watches its activity, nobody would notice anomalous use, and because it is not used, nobody remembers why it exists or what permissions it has. Beyond removing attack surface, the clean-up brings something more valuable: every account removed reduces the noise in the audit, and a list of twenty accounts of which all twenty are understood is infinitely safer than one of sixty of which twenty are understood.

The correct technique is to disable before deleting (gcloud iam service-accounts disable): if something breaks, it is reactivated in a second; if it was deleted, you have to recreate it and rebuild all its permissions. Thirty days of waiting is a reasonable period, because it captures the monthly processes.

Solution 2 — Designing the response to a specific incident

T+0 to T+10 min — Assess without acting.

The first decision: this is already public. Unlike an internal detection, here the potential attacker does not merely exist: an entire forum knows about the key. Urgency is at a maximum and stealth no longer buys anything, which changes the calculation compared with section 9.

# 1. Confirm the key exists and is still active
gcloud iam service-accounts keys list \
  --iam-account=sa-dataflow-pedidos@alpinashop-datos.iam.gserviceaccount.com \
  --managed-by=user --format="table(name, validAfterTime, validBeforeTime)"

# 2. Activity from that account in the last 48 hours
gcloud logging read '
  protoPayload.authenticationInfo.principalEmail="[email protected]"
  AND timestamp>="'$(date -u -d '48 hours ago' +%Y-%m-%dT%H:%M:%SZ)'"' \
  --project=alpinashop-datos --limit=500 \
  --format="table(timestamp, protoPayload.methodName, protoPayload.requestMetadata.callerIp)"

You are looking for one thing only: requests from IPs that are not Dataflow's. If any appear, the incident goes from "exposed credential" to "confirmed access", with immediate legal implications.

T+10 to T+30 min — Contain.

# 1. Delete the key (here it IS deleted: it is the exposed credential, not the evidence)
gcloud iam service-accounts keys delete KEY_ID \
  --iam-account=sa-dataflow-pedidos@alpinashop-datos.iam.gserviceaccount.com

# 2. Disable the account until the scope is understood
#    (it breaks the Dataflow pipeline: accepted consciously)
gcloud iam service-accounts disable \
  [email protected]

# 3. Freeze the evidence OUTSIDE the affected project
gcloud logging read 'timestamp>="'$(date -u -d '9 months ago' +%Y-%m-%dT%H:%M:%SZ)'"
  AND protoPayload.authenticationInfo.principalEmail=~"sa-dataflow-pedidos"' \
  --project=alpinashop-datos --format=json > /tmp/evidencia.json
gcloud storage cp /tmp/evidencia.json gs://alpinashop-evidencia-forense/incidente-clave/

# 4. Switch VPC-SC to enforced mode over alpinashop-datos
#    Even if they stole more credentials, the data can no longer leave

The hard decision in these twenty minutes: disabling the account breaks the orders pipeline into BigQuery. You do it anyway. Faced with a public credential that has access to the data lake, the analytics can wait a few hours. And it is exactly the decision you should have thought through beforehand, not in the heat of the moment.

Point 4 deserves attention: had the perimeter been enforced instead of in dry-run mode (debt #7), the leaked key could not have taken a single byte out of the organization. The incident illustrates why a half-finished control is not half a control.

T+30 min to T+1 h — Determine whether there was access.

-- All use of the account since the key was created, with IP and method
SELECT
  DATE(timestamp) AS day,
  protopayload_auditlog.requestMetadata.callerIp AS ip,
  protopayload_auditlog.methodName               AS method,
  COUNT(*) AS n
FROM `alpinashop-datos.auditoria.cloudaudit_googleapis_com_data_access`
WHERE protopayload_auditlog.authenticationInfo.principalEmail
      = '[email protected]'
GROUP BY day, ip, method
ORDER BY day DESC

And here appears the problem that makes this exercise the best demonstration of why debt #5 matters: the data access logs are disabled. AlpinaShop has the admin activity logs — it will know if somebody changed permissions — but it cannot know whether somebody read the orders table.

The practical consequence, and it is serious: being unable to demonstrate that there was no access, legally the case has to be treated as if there could have been. You go from "exposed credential with no evidence of use" to "possible personal data breach whose scope cannot be bounded". That completely changes the notification to the AEPD.

Alternative avenues of evidence, all worse:

Source What it gives you Limitation
Billing logs Anomalous BigQuery queries by bytes processed Only if the volume was large
Cloud Storage metrics Download spikes from the data lake Coarse granularity
Repository history When it was pushed and when it became public Says nothing about who used it
Admin activity Whether persistence was created Does not cover data reads

T+1 to T+2 h — Communicate.

Moment To whom What
T+35 min Management "A credential with access to customer data has been publicly exposed. Contained. Investigating the scope. There may be an obligation to notify the AEPD."
T+45 min Legal adviser / DPO The facts, and the limitation of the evidence
T+1 h The team Split: Marta contains, Dani reviews the repository, Lucía validates what data was there
T+2 h Google Cloud support Open a case: they can contribute data and help with the analysis

T+2 to T+24 h — Scope and eradication.

# 1. Look for OTHER credentials in the history of ALL the repositories
#    There is almost never just one
for REPO in alpinashop-catalogo alpinashop-infra alpinashop-datos alpinashop-ml; do
  git clone --mirror "[email protected]:alpinashop/$REPO.git" "/tmp/$REPO"
  trufflehog git "file:///tmp/$REPO" --json > "/tmp/hallazgos-$REPO.json"
done

# 2. Look for persistence created in the last 8 months
gcloud asset search-all-iam-policies --scope=organizations/ORG_ID \
  --query="policy:serviceAccount" --format=json > /tmp/todas-politicas.json

# 3. Rotate EVERYTHING that was in the same repository
gcloud secrets versions add db-password-catalogo --data-file=- <<< "$NUEVA"
gcloud secrets versions add api-key-pasarela-pago --data-file=- <<< "$NUEVA_API"

T+24 to T+72 h — Notification. If the legal adviser determines there is a risk to data subjects' rights, notification to the AEPD within 72 hours. Since access cannot be ruled out, notification is most likely required. The notification describes: what happened, what data could be affected, what measures have been taken, and why the scope cannot be bounded — which, said plainly, is in itself an unfavourable finding about the organization.

Structural changes afterwards:

Change Prevents
Enabling data access logs on alpinashop-datos Being unable to bound the scope. The most important one of all
Secret scanning on every push (GitHub Secret Scanning + a pre-commit hook) A credential being pushed again
Removing all JSON keys and migrating to federation and impersonation The very existence of permanent credentials
VPC-SC in enforced mode A leaked credential being able to take data out
An alert on CreateServiceAccountKey A key being created without anybody finding out
Rewriting the repository history (BFG) or rotating everything they contain The history remaining exploitable
A blameless post-mortem (07-06) The lesson being lost

The final reflection of the exercise. The key was pushed eight months ago, it was deleted from the repository, and everybody thought the problem was solved. Deleting a file from Git does not delete its history: anybody with access to the repository — or to the public repository, or to a fork — could recover it with git log -p. It is such a widespread misunderstanding that it deserves a rule of its own:

A secret that has been in a repository, even for a minute, is compromised. The only correct answer is to rotate it. Deleting it from the history is tidying up, not remediation.

Solution 3 — Prioritising on a limited budget

Constraints: 10 days of Marta's time, €3,000, one quarter. And a premise that has to be respected: Marta also has to operate the platform, so 10 days of security are 10 real days, not elastic ones.

What gets done:

Week Task Days € Why
1 Locate and remove the JSON key; migrate to federation 0.5 0 A lost permanent credential. Maximum risk, minimum cost
1 Default Compute account: remove Editor and disable it 0.5 0 Trivial elevation. Half a day
1 Cloud Build: scope down to specific roles 0.5 0 Pipeline compromise = total compromise
1 Alerts on IAM changes and key creation 0.5 0 Without detection there is no response. Two hours
2 Enable data access logs on alpinashop-datos and sensitive buckets 1 ~600/year A de facto legal requirement. Without it a breach cannot be bounded
2-3 Written incident response plan, tested in a drill 1.5 0 It turns theory into capability
3-4 Restore drill for alpinashop-pedidos 1.5 ~100 An untested backup does not exist. Existential risk
5 VPC-SC from dry-run to enforced 2 0 The work is already half done; finishing it is cheap and very effective
6 Blocking vulnerability scanning in the pipeline 0.5 0 Half a day
7 Physical keys for the 3 technical people 0.5 ~200 It eliminates credential phishing, which is the number one vector
8 Access review + documented quarterly process 1 0 A process, not a tool
— Total 10 ~€900

That leaves €2,100 spare. And here comes the proposal that delivers the most value per euro:

Buy a half-day external security review (~€1,500-2,000) at the end of the quarter, over the already-hardened architecture.

The reasoning: Marta built this platform and has the blind spots of the person who built it. An outside look at an already tidy system finds different things from what it would find on a messy one — and that is why it is bought after paying off the priority 1 and 2 debt, not before. It also brings something you cannot manufacture internally: a report signed by a third party, which is what a corporate customer, an insurer or an inspection asks for.

What is NOT done, and why:

Not done Reason
SCC Premium ~€1,000/month and nobody can deal with what it detects. Buying detection without response capability is spending on peace of mind, not on security
Binary Authorization 3 days for a low-medium risk with the supply chain already controlled by other means
PAM 2 days. Valuable, but with 3 people and nobody being owner of production, the exposure is smaller
Organization policies They come in 07-07 with the landing zone. Doing it twice is wasting days
External penetration tests €5,000-15,000 and it would find what we already know. It is done once the known debt is paid off, not before

What management is told — and this is the part that costs the most effort and matters the most:

"With these ten days we close the four holes that expose us most today: a permanent password we lost more than a year ago and that still works, an internal account that grants permission to delete anything, an automated system that can touch everything, and the absence of any warning when somebody changes permissions.

On top of that we do two things we do not have today and that are either a legal obligation or common sense: knowing who accesses our customers' data — today we could not answer that question if the Spanish Data Protection Agency asked, and not being able to answer it is itself a problem — and checking that our backups can really be restored, which we have never verified. And we rehearse what we would do if something happens tomorrow, because the day of the incident is the worst day to improvise.

What is left undone, and I want it on the record: we are not going to have active threat monitoring — if somebody gets in quietly, we would find out late; we are not going to technically prevent unapproved software from being deployed; administrative permissions will remain permanent rather than temporary; and we are not going to buy a professional simulated attack. Each of those four is a budget decision, not an oversight, and we review them next quarter.

With what we are doing we move from a risk I would call high to a medium one. Getting to low requires a person dedicated to security or a managed service, and that is a different conversation, best had when we are sixty people or when a large customer requires it of us by contract."

The three elements that make this communication good, and that can be reused in any company: it talks about business risks and not about product names; it says explicitly what is left uncovered instead of letting management assume everything is solved; and it sets when it gets reviewed, which turns a budget cut into a conscious decision with a date instead of into silence.

Conclusion

AlpinaShop has gone from having security controls to having reviewed security, and — more usefully — to knowing exactly what it is missing.

You have applied defence in depth layer by layer — edge, identity, network, workload, data, detection — and above all you have learnt the single-failure question, which is the cheapest and most effective tool in the whole lesson: for each layer, if this fails, what contains it? The doubtful answers are the debt.

You have the six principles that explain every decision in the course: least privilege with the Recommender that makes it bearable, separation of duties — Dani writes, Cloud Build builds, Marta approves, and nobody can deploy and erase the evidence — deny by default, limited blast radius with its operational question, do not trust the network, and assume breach.

You understand zero trust and BeyondCorp with IAP as the practical implementation, and you have seen that an access level based on the device — corporate, encrypted, locked, up to date, from an allowed region — solves remote working far better than any IP list, because it makes the IP stop mattering.

You have reviewed identity thoroughly: removing the basic roles, the script that finds them, and the three findings that always turn up — a person with Owner, Cloud Build with Editor and, above all, the default Compute account with Editor, which is the most widespread failing in Google Cloud and the one that turns any application vulnerability into a total compromise. You know the keyless alternatives for each situation and the command with --managed-by=user that finds the ones still around. You know how to set up temporary elevation with PAM — with the break-glass path that does not prevent but makes noise — and identity federation with the attribute condition on repository_id and ref that separates a secure federation from one that only looks secure.

You have mastered the supply chain: scanning that blocks only what has a patch, minimal base images — with the table that runs from python:3.12 to distroless — pinned by digest and with --require-hashes, USER 1000 instead of root, SLSA provenance that answers "did this image come out of our code?", Binary Authorization — which also applies to Cloud Run — and Assured Open Source with its honest scope.

You have closed the data lifecycle: classification into four levels with the best security decision in the course — not storing cards, because data you do not keep does not leak — discovery with Sensitive Data Protection and Spanish detectors, includeQuote: false so as not to duplicate the problem, CMEK with its "red button" of disabling the key and its irreversible counterpart, pseudonymisation with a salted hash and generalisation — which is not anonymisation — and retention with the erasure procedure that anonymises the orders instead of deleting them because the tax obligation rules.

You know how to detect: Security Command Center with its three tiers, what the free one really finds, and the honest criterion that buying detection nobody looks at is worse than not buying it. And you have the four types of audit log with the fact that changes everything — data access is disabled by default, and without it you cannot know who read your customers' data — along with the three queries to have written before you need them.

You have a realistic response script: ten minutes to assess without touching anything, twenty to contain without destroying evidence — disable, do not delete, and revoke the live tokens that survive for an hour — half an hour for the scope including the search for persistence, and communication with the GDPR's 72 hours counting from awareness and not from conclusion. Plus the folder that has to be prepared today, including the alternative communication channel and the annual drill.

You know what compliance Google gives you and what remains yours, with the most expensive misunderstanding cleared up: its certifications accredit the layer underneath, not your company. And you have the list of seven documents that are asked for before any technical configuration.

And you take away two things worth more than any theoretical section: the checklist of 43 controls with AlpinaShop's real status — 20 ✅, 11 ⚠️, 12 ❌ — and the prioritised debt in three blocks, with four priority 1 tasks that add up to less than two days and remove the most risk. Plus the list of consciously accepted debt, because a documented risk is a management decision and a risk nobody has looked at is a hole.

The leaked key exercise has left the most uncomfortable lesson of all, and it is worth taking away verbatim: a secret that has been in a repository, even for a minute, is compromised; deleting it from the history is tidying up, not remediation.

And the budget exercise has left the other one: a small business's security is not decided with a list of the ideal, but by choosing ten well-spent days and telling management, in writing and in their language, what is left uncovered.

Two threads from this lesson are left explicitly open. One points to 07-06: the backup restore has never been tested, and it appears as a critical risk both in the checklist and in the prioritisation. The other points to 07-07: organization policies, immutable audit logs in a separate project and periodic access review are governance, and that is where they are resolved.

But first there is a conversation that has been deferred for seven modules. Every lesson has added services; none has closed the bill. This very lesson has proposed enabling data access logs — which cost money — has ruled out SCC Premium on price, and has allocated €3,000 as if it were a lot of money, which for a forty-person company it is. None of those three decisions can be taken well without understanding where every euro of AlpinaShop's bill comes from. The next lesson opens the whole bill, line by line, and explains it.

Google Cloud Platform (GCP) Course

Module 1: Introduction to Google Cloud Platform

Module 2: Core GCP Services

Module 3: Networking and Security

Module 4: Data and Analytics

Module 5: Machine Learning and AI

Module 6: DevOps and Monitoring

Module 7: Advanced GCP Topics

Module 8: Final Project

© Copyright 2026. All rights reserved