Chapter 32

Workload Hardening

Scope. This chapter is a per-service diff, not a re-teach. Chapters 8–12 and 22–25 own these services and several already carry their own hardening checklists — §8.35, §9.30, §9.31, and §10.26 in particular. Where a checklist exists, this chapter cites it and adds only the settings whose consequences appear once a service is in production. Two services are exceptions: BigQuery and Pub/Sub have no owning chapter in this volume, so §32.6 and §32.7 are the book's only treatment of them and take the room the others give back. Chapter 31 enforces these settings estate-wide. Prerequisites. Chapter 8 (§8.35), Chapter 9 (§9.30, §9.31), Chapter 10 (§10.26), Chapter 12, Chapter 11, Chapters 23–25. Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0; every flag resolved with --help; see sources at end.

A hardened service is one whose defaults have been overridden deliberately, and the interesting question per service is which defaults are wrong. That is a short list, and it is not the same list as "every security setting" — most settings are already correct, and enumerating them buries the four that are not.

This chapter is organized around the settings that need changing and the consequences that only appear in production. A configuration guide written before deployment gets the flags right and misses the operational facts: that uniform bucket-level access is reversible for exactly ninety days, that Cloud SQL CMEK cannot be added later, that a node upgrade will force-evict a pod after an hour of respecting its disruption budget.

Flag names are the other trap, and they are worse than they look. Several settings a reader will confidently recall do not exist under the name they remember. Each section below names the real flag, because a hardening runbook full of plausible-looking commands that fail is worse than none.

32.1 Compute Engine Hardening §

§8.35 is the fleet checklist and §28.12 owns the in-guest convergence. This section is the diff between them: the instance-level settings, their real flag names, and what they cost.

SettingFlagValue
Shielded VM--shielded-secure-boot, --shielded-vtpm, --shielded-integrity-monitoringAll three
No external IPomit --address, or --no-addressAlways
Dedicated identity--service-accountNever the default compute account
Scopes--scopes=cloud-platformIAM governs; scopes are a legacy narrowing
OS Login--metadata=enable-oslogin=TRUEProject-wide preferred
Boot disk CMEK--boot-disk-kms-keyNot --kms-key
Deletion protection--deletion-protectionStateful instances

--kms-key does not exist on gcloud compute instances create. The flags are --boot-disk-kms-key and --instance-kms-key, each with -keyring, -location, and -project companions. This is the single most common wrong command in a Compute hardening runbook.

--confidential-compute is deprecated in favor of --confidential-compute-type, and the two are mutually exclusive (§8.17).

The production consequence is the service account, not the instance. An instance's blast radius is whatever its attached account can do, and the default compute account historically received a basic role at project creation. constraints/iam.automaticIamGrantsForDefaultServiceAccounts (§31.1) closes that going forward and changes nothing about existing projects.

Pitfall. Scopes and IAM both restrict an instance's access, and the effective permission is the intersection. A narrow scope on an instance whose account holds a needed role produces a permission error that names the role — which is present — rather than the scope, which is not. Set cloud-platform and govern with IAM.

32.2 GKE Hardening §

§9.30 and §9.31 own the cluster and node baselines and are already checklists. This section adds only the flag corrections and the two upgrade behaviors that decide whether hardening causes an outage.

Three flags are commonly written wrong:

What people writeThe real flag
--enable-intranode-visibility--enable-intra-node-visibility
--enable-google-groups-rbac--security-group
--disable-legacy-endpointsDoes not exist on any GKE command

--enable-binauthz is deprecated; --binauthz-evaluation-mode takes disabled or project-singleton-policy-enforce (§9.27).

The control-plane access model is --enable-dns-access and --enable-ip-access, with --enable-authorized-networks-on-private-endpoint enforcing authorized networks against the DNS endpoint. The older private-endpoint-only framing is superseded.

Now the production behavior, which no pre-deployment guide covers. Node auto-upgrade is a hardening control — an unpatched node is the vulnerability — and it is also a scheduled disruption:

  • A drain respects a PodDisruptionBudget for up to one hour, then force-evicts. A budget that can never be satisfied does not block an upgrade indefinitely; it delays it by an hour and then the pod dies anyway.
  • Surge upgrade defaults are maxSurge=1 and maxUnavailable=0, so a node pool upgrades one node at a time by adding capacity first. On a large pool that is slow, and raising surge trades cost for time.

Maintenance exclusions are the control for both, and they are the setting a team reaches for during a freeze and has not configured in advance.

Pitfall. Turning auto-upgrade off to avoid disruption converts a scheduled, budgeted disruption into an unpatched control plane and an unsupported version. The version support window will force the upgrade eventually, at a time nobody chose.

32.3 Cloud Run Hardening §

§10.26 is already a five-layer table with owners. The diff is two flag facts and one that changes how you remediate an existing service.

SettingFlagHardened value
Ingress--ingressinternal-and-cloud-load-balancing
Identity--service-accountsa-run-<service>, never the default
Egress--network, --subnet, --vpc-egressDirect VPC egress; all-traffic
CMEK--keyNot --encryption-key
Deploy gate--binary-authorization§25.10

--no-allow-unauthenticated exists on gcloud run deploy and not on gcloud run services update. That matters operationally: you cannot harden an existing public service by re-running an update with that flag. The remediation is to remove the allUsers invoker binding from the service's IAM policy directly, which is a different command and a different mental model.

--vpc-egress=all is deprecated; the value is all-traffic.

The production consequence is --min-instances against secret fetching. A service that reads a secret at startup pays that latency on every cold start, and a service with --min-instances=0 cold-starts constantly. §10.22 owns the related trap that latest resolves at revision start, so a rotated secret does not reach a running revision.

Pitfall. Cloud Run domain mappings are not production-ready; the production path for a custom domain is a global external Application Load Balancer with a serverless NEG. A hardening exercise that adds Cloud Armor (Chapter 19) requires that load balancer anyway, so the two decisions are the same decision.

32.4 Cloud SQL Hardening §

Chapter 12 owns Cloud SQL. Two settings here are creation-time only, which makes this the service where hardening late is genuinely impossible.

SettingFlagHardened value
Public IP--no-assign-ipAlways
Private path--networkA PSA range (§5.x)
TLS--ssl-modeTRUSTED_CLIENT_CERTIFICATE_REQUIRED
Connector enforcement--connector-enforcementRequired
CMEK--disk-encryption-keyCreation-time only
Backups--backup, --enable-point-in-time-recovery, --retained-backups-countEnabled
Deletion protection--deletion-protectionAlways
IAM database auth--database-flags=cloudsql.iam_authentication=onPostgreSQL and MySQL only

--ssl-mode has three values and the names are not intuitive: ALLOW_UNENCRYPTED_AND_ENCRYPTED, ENCRYPTED_ONLY, and TRUSTED_CLIENT_CERTIFICATE_REQUIRED. Only the third verifies the client.

--require-ssl is described as legacy, not deprecated, by both Google and the SDK. Use --ssl-mode; do not call the old flag deprecated in a runbook, because it is not.

CMEK cannot be retrofitted. An instance created without a customer-managed key can never have one, so the remediation is a migration to a new instance — which for a production database is a maintenance window, not a configuration change. §31.9's constraint prevents new instances from having the problem and cannot fix existing ones.

IAM database authentication is PostgreSQL and MySQL only, never SQL Server, which is the kind of fact that turns a standard into an exception halfway through a rollout.

Pitfall. Removing a public IP from an instance that has one changes its connectivity for every client at once. The order is: establish private connectivity, migrate clients, verify, then remove the address — and --connector-enforcement is what stops a client quietly reverting to the old path.

32.5 Cloud Storage Hardening §

Chapter 11 owns Cloud Storage. The diff is that most hardening flags live on update rather than create, and one important setting has a ninety-day fuse.

Create with these: --uniform-bucket-level-access, --public-access-prevention, --default-encryption-key, --soft-delete-duration, --location.

Then update with these, because they do not exist on create: --versioning, --retention-period, --lock-retention-period, --log-bucket.

Uniform bucket-level access is reversible for exactly ninety consecutive days. After ninety days the bucket is permanently uniform. That is a good property and it means the decision has a deadline: a bucket enabled and left alone becomes irreversible without anyone acting.

--public-access-prevention and uniform access do different jobs. Uniform access removes per-object ACLs so bucket IAM is the whole story; public access prevention blocks public principals regardless. A bucket needs both, and §33.1 is what happens with neither.

Soft delete defaults to seven days and it is billed. It is a recovery control worth having and it is also storage you are paying for on every deleted object, which surprises teams whose workload writes and deletes at volume.

Pitfall. A retention policy and a lock are different actions, and locking is irreversible in the strong sense: the retention period can then only be increased, and objects cannot be deleted before it elapses by anyone. That is exactly what §29.2 wants for evidence and exactly wrong for a bucket holding anything with a lifecycle.

32.6 BigQuery Hardening §

BigQuery has no owning chapter in this volume, so this section is the book's treatment of it — and the tooling situation is the first thing to know.

BigQuery's administrative surface is bq, not gcloud. The generally available gcloud bq command group contains exactly one subgroup, migration-workflows. Anything else is bq mk, bq update, and bq show, or the API.

The access model has four layers, and they are additive rather than alternative:

LayerWhat it controls
Project and dataset IAMWho can query at all
Authorized viewsA view that reads a table the caller cannot
Authorized datasetsThe same, for every view in a dataset
Column-level and row-level securityWhich columns and rows a permitted caller sees

Authorized views are the mechanism that makes BigQuery shareable. A consumer is granted access to the view's dataset and not to the underlying table, so the view's SQL defines exactly what leaves — which is a far better boundary than copying a filtered table.

Column-level security uses policy tags, and the product that manages them has been renamed: as of 2026-04-10 Dataplex Universal Catalog is called Knowledge Catalog. Data Catalog is deprecated. Policy tags themselves are not.

Set CMEK with --destination_kms_key and a default table expiration on every dataset. An expiration is a security control as much as a cost one: data that no longer exists cannot be exfiltrated, and a scratch dataset with no expiration becomes a permanent copy of production.

Data Access audit logs are on by default for BigQuery — the only service where that is true (§33.11). That is a genuine advantage and it is worth knowing so you do not spend effort enabling what is already there.

Pitfall. Query results land in a destination table, and an anonymous cached result table inherits the dataset's access rather than the source's. A user who can query a restricted table through an authorized view and write results elsewhere has copied the data past the control — which is why the write side needs its own restriction, not just the read side.

32.7 Pub/Sub Hardening §

Pub/Sub also has no owning chapter here, and its security surface is smaller than most but has one control that is routinely missed.

SettingFlagHardened value
CMEK--topic-encryption-keyA key in the security project
Data locality--message-storage-policy-allowed-regionsMatches the residency constraint (§31.2)
Retention--message-retention-durationAs short as the consumer allows
Dead letter--dead-letter-topic, --max-delivery-attemptsSet, with a monitored dead-letter topic
Push authentication--push-auth-service-account, --push-auth-token-audienceAlways, for push subscriptions
Delivery--enable-exactly-once-deliveryWhere the consumer is not idempotent

Push endpoint authentication is the control that gets missed. A push subscription posts to an HTTPS endpoint, and without --push-auth-service-account the endpoint has no way to know the request came from Pub/Sub. With it, Pub/Sub attaches an OIDC token the receiver validates, and --push-auth-token-audience pins what that token is valid for.

Without it, the endpoint is an unauthenticated webhook, and anyone who learns the URL can inject messages the consumer will treat as legitimate. This is not theoretical — the URL appears in configuration, in logs, and in code.

--message-storage-policy-allowed-regions is the residency control and it is per topic. constraints/gcp.resourceLocations (§31.2) governs where the topic resource is created; the storage policy governs where its messages rest.

Schemas are an integrity control. Attaching a schema with --schema and --message-encoding rejects malformed messages at publish rather than at the consumer, which turns a parsing failure into a publish failure with an attributable publisher.

Pitfall. A dead-letter topic that nothing consumes is a silent data-loss sink: messages that fail delivery accumulate there and expire on the retention period. Configuring dead-lettering without an alert on the dead-letter topic's backlog (§18.15) converts a loud failure into a quiet one.

32.8 CI/CD Hardening §

Chapters 22–25 own the delivery chain and were written immediately before this Part. This section is the settings table, with the owner of each.

SettingHardened valueOwned by
Source host connectionSecure Source Manager or a connected external host§22.x
Branch protectionRequired reviews, dismissed on push, tags protected§22.x
Fork-originated buildsNo credentials, isolated, gating nothing§22.x
Pre-build scanningSecret, static, dependency, and IaC scanning§22.12
Build identitysa-build-<app>, user-specified§23.x
Deploy identitysa-deploy-<env>, never the build identity§25.13
Approval--require-approval on the trigger§23.x
Plan/apply separationApply the approved plan artifact§26.24
Deploy-time gateBinary Authorization, enforced not dry-run§25.10
Image referenceDigest, never a tag§25.12, §24.5

The two rows that carry the most weight are the identity separation and the approval. A build account that can also deploy makes a compromised build a compromised production; an approval that survives new commits approves code nobody read.

Cloud Build's default identity depends on organization settings and may be the legacy build account or the Compute Engine default. The legacy account cannot generate ID tokens, and Google recommends a user-specified account. Resolve which one you actually have with gcloud builds get-default-service-account rather than assuming.

Pitfall. Hardening the pipeline while leaving the repository's CI configuration file in the repository means a pull request can change the build. §37.1 owns that gap; the control is a trigger whose build configuration is not caller-modifiable, plus --comment-control on fork-originated builds.

32.9 Build Environment Hardening §

The build environment is a machine that holds credentials and produces artifacts, and its two settings are about isolation.

Private pools remove public egress:

gcloud builds worker-pools create pool-us-central1-prod \
  --region=us-central1 \
  --no-public-egress \
  --peered-network=projects/rc-saas-shared-net-01/global/networks/rc-saas-vpc

The flag is --no-public-egress, not --no-external-ip. A worker with no public egress reaches the internet only through the peered network's controlled path, which is what makes dependency fetching auditable (§37.2).

--peered-network is what lets a build reach private resources — a private GKE control plane, a Cloud SQL instance on a private address, an internal artifact mirror — without any of them being publicly reachable.

§20.10 owns running a pipeline inside a service perimeter, which is the stronger form of the same argument and is where a build that touches restricted data belongs.

The build's own identity is §32.8's row, and the isolation here is what makes that identity's credentials harder to exfiltrate: a worker with no public egress cannot easily post a token anywhere.

Pitfall. A private pool without a configured egress path fails to fetch dependencies, and the failure looks like a package repository outage. Cloud NAT or a private mirror has to exist first — the same ordering trap as §31.3's, in a different service.

32.10 Artifact Hardening §

Chapter 24 owns Artifact Registry. Three settings, one of which does not exist where people look for it, and one absence worth stating.

SettingFlagNotes
Repository CMEK--kms-keyCreation-time
Immutable tags--immutable-tagsDocker format only
Repository mode--modeLowercase values
Cleanup policies--cleanup-policy on set-cleanup-policiesNot on repositories create

--mode takes lowercase-hyphenated valuesstandard-repository, remote-repository, virtual-repository — not the SCREAMING_SNAKE form the API uses. The same CLI-versus-API casing split appears in §29.6.

Immutable tags are Docker-format only, so a Maven or npm repository cannot have them and the digest-pinning discipline (§25.12) has to carry the whole weight there.

In a cleanup policy, a Keep rule always beats a Delete rule, and policy changes take about a day to take effect. Both facts matter when a cleanup policy is deleting something you need: the fix is a Keep rule, and it is not immediate.

Artifact Registry has no organization policy constraints at all. The constraint reference lists none for the service, so artifact hardening cannot be enforced estate-wide through Chapter 31 the way storage or Cloud SQL can. It has to be enforced by the module that creates repositories (§26.34) and audited afterward (§29.6).

Pitfall. A cleanup policy can delete the image a rollback needs. §25.x's rollback target is a previous release's artifact, and a retention rule expressed in days rather than in versions will eventually be shorter than the interval between a deployment and the incident that requires reverting it.

Chapter Summary §

  • Hardening is about the defaults that are wrong, not about every security setting; enumerating everything buries the few that matter.
  • --kms-key does not exist on gcloud compute instances create — the flags are --boot-disk-kms-key and --instance-kms-key.
  • Scopes and IAM intersect, and a narrow scope produces an error naming a role that is present; set cloud-platform and govern with IAM.
  • GKE's real flags are --enable-intra-node-visibility and --security-group; --disable-legacy-endpoints does not exist and --enable-binauthz is deprecated.
  • A GKE drain respects a PodDisruptionBudget for up to one hour, then force-evicts; surge upgrade defaults are maxSurge=1, maxUnavailable=0.
  • Disabling node auto-upgrade converts a scheduled disruption into an unpatched control plane and a forced upgrade later.
  • Cloud Run CMEK is --key; --vpc-egress=all is deprecated in favor of all-traffic.
  • --no-allow-unauthenticated exists on run deploy and not on run services update — remediate an existing public service by removing the invoker binding.
  • Cloud SQL --ssl-mode has three values and only TRUSTED_CLIENT_CERTIFICATE_REQUIRED verifies the client; --require-ssl is legacy, not deprecated.
  • Cloud SQL CMEK is creation-time only and can never be retrofitted; remediation is a migration.
  • IAM database authentication is PostgreSQL and MySQL only, never SQL Server.
  • Most Cloud Storage hardening flags are on update, not create: --versioning, --retention-period, --lock-retention-period, --log-bucket.
  • Uniform bucket-level access is reversible for exactly ninety consecutive days, then permanent.
  • Soft delete defaults to seven days and is billed.
  • BigQuery's admin surface is bq; the GA gcloud bq group contains only migration-workflows.
  • Authorized views are BigQuery's real sharing boundary, and policy tags are now managed by Knowledge Catalog (renamed from Dataplex Universal Catalog on 2026-04-10). Data Catalog is deprecated.
  • BigQuery is the one service with Data Access audit logs on by default.
  • A Pub/Sub push subscription without --push-auth-service-account is an unauthenticated webhook.
  • A dead-letter topic with no consumer and no alert is a silent data-loss sink.
  • Cloud Build's default identity depends on organization settings; resolve it with gcloud builds get-default-service-account rather than assuming.
  • Private build pools use --no-public-egress, not --no-external-ip, and need an egress path configured first.
  • Artifact Registry --mode takes lowercase values, --immutable-tags is Docker-only, --cleanup-policy is not on repositories create, and in a cleanup policy a Keep rule beats a Delete rule.
  • Artifact Registry has no organization policy constraints at all, so artifact hardening is enforced by the creating module and audited, never enforced estate-wide.

Security Checklist §

ControlWhy it mattersHow to verify (CLI + Console)
Instances use --boot-disk-kms-key, not the flag people expect--kms-key does not exist on this commandgcloud compute instances describe NAME --zone=ZONE --format='value(disks.diskEncryptionKey)'
No instance uses the default compute service accountIts historical basic role is the blast radiusgcloud compute instances list --format='value(name,serviceAccounts.email)'
GKE node auto-upgrade on, with maintenance exclusions configuredOff means unpatched; on without exclusions means surprise disruptiongcloud container node-pools describe POOL --cluster=CLUSTER --region=REGION
PodDisruptionBudgets are satisfiableAn unsatisfiable budget delays an upgrade by an hour, then loses the podkubectl get pdb -A
Cloud Run services not publicly invokableThe update flag does not exist; check the bindinggcloud run services get-iam-policy SERVICE --region=REGION
Cloud SQL created with --disk-encryption-keyCMEK can never be added latergcloud sql instances describe INSTANCE --format='value(diskEncryptionConfiguration)'
Cloud SQL --ssl-mode verifies the clientThe other two values do notgcloud sql instances describe INSTANCE --format='value(settings.ipConfiguration.sslMode)'
Buckets have both uniform access and public access preventionThey do different jobsgcloud storage buckets describe gs://BUCKET
Uniform access decision made inside ninety daysAfter that it is permanentBucket uniformBucketLevelAccess.lockedTime
BigQuery consumers granted views, not tablesThe view's SQL is the boundarybq show --format=prettyjson DATASET
BigQuery datasets carry a default table expirationData that no longer exists cannot be exfiltratedbq show --format=prettyjson DATASET
Every push subscription sets --push-auth-service-accountOtherwise the endpoint is an unauthenticated webhookgcloud pubsub subscriptions describe SUB --format='value(pushConfig.oidcToken)'
Dead-letter topics have a consumer and a backlog alertOtherwise dead-lettering is silent data lossgcloud pubsub subscriptions list; alert policy (§18.15)
Build and deploy identities differA compromised build must not be able to shipTrigger --service-account versus target executionConfigs
Cloud Build default identity resolved explicitlyIt varies by organization settinggcloud builds get-default-service-account --region=REGION
Private pools use --no-public-egress with an egress pathThe flag name is not what people expect, and NAT must existgcloud builds worker-pools describe POOL --region=REGION
Docker repositories set --immutable-tagsNon-Docker formats cannot, so pin by digest theregcloud artifacts repositories describe REPO --location=REGION
Cleanup policies keep the rollback targetA days-based rule outlives no incidentgcloud artifacts repositories describe REPO --location=REGION
Artifact hardening audited, not policy-enforcedNo organization policy constraint exists for itAsset inventory query (§29.6)

Sources §