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.
| Setting | Flag | Value |
|---|---|---|
| Shielded VM | --shielded-secure-boot, --shielded-vtpm, --shielded-integrity-monitoring | All three |
| No external IP | omit --address, or --no-address | Always |
| Dedicated identity | --service-account | Never the default compute account |
| Scopes | --scopes=cloud-platform | IAM governs; scopes are a legacy narrowing |
| OS Login | --metadata=enable-oslogin=TRUE | Project-wide preferred |
| Boot disk CMEK | --boot-disk-kms-key | Not --kms-key |
| Deletion protection | --deletion-protection | Stateful 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 write | The real flag |
|---|---|
--enable-intranode-visibility | --enable-intra-node-visibility |
--enable-google-groups-rbac | --security-group |
--disable-legacy-endpoints | Does 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=1andmaxUnavailable=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.
| Setting | Flag | Hardened value |
|---|---|---|
| Ingress | --ingress | internal-and-cloud-load-balancing |
| Identity | --service-account | sa-run-<service>, never the default |
| Egress | --network, --subnet, --vpc-egress | Direct VPC egress; all-traffic |
| CMEK | --key | Not --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.
| Setting | Flag | Hardened value |
|---|---|---|
| Public IP | --no-assign-ip | Always |
| Private path | --network | A PSA range (§5.x) |
| TLS | --ssl-mode | TRUSTED_CLIENT_CERTIFICATE_REQUIRED |
| Connector enforcement | --connector-enforcement | Required |
| CMEK | --disk-encryption-key | Creation-time only |
| Backups | --backup, --enable-point-in-time-recovery, --retained-backups-count | Enabled |
| Deletion protection | --deletion-protection | Always |
| IAM database auth | --database-flags=cloudsql.iam_authentication=on | PostgreSQL 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:
| Layer | What it controls |
|---|---|
| Project and dataset IAM | Who can query at all |
| Authorized views | A view that reads a table the caller cannot |
| Authorized datasets | The same, for every view in a dataset |
| Column-level and row-level security | Which 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.
| Setting | Flag | Hardened value |
|---|---|---|
| CMEK | --topic-encryption-key | A key in the security project |
| Data locality | --message-storage-policy-allowed-regions | Matches the residency constraint (§31.2) |
| Retention | --message-retention-duration | As short as the consumer allows |
| Dead letter | --dead-letter-topic, --max-delivery-attempts | Set, with a monitored dead-letter topic |
| Push authentication | --push-auth-service-account, --push-auth-token-audience | Always, for push subscriptions |
| Delivery | --enable-exactly-once-delivery | Where 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.
| Setting | Hardened value | Owned by |
|---|---|---|
| Source host connection | Secure Source Manager or a connected external host | §22.x |
| Branch protection | Required reviews, dismissed on push, tags protected | §22.x |
| Fork-originated builds | No credentials, isolated, gating nothing | §22.x |
| Pre-build scanning | Secret, static, dependency, and IaC scanning | §22.12 |
| Build identity | sa-build-<app>, user-specified | §23.x |
| Deploy identity | sa-deploy-<env>, never the build identity | §25.13 |
| Approval | --require-approval on the trigger | §23.x |
| Plan/apply separation | Apply the approved plan artifact | §26.24 |
| Deploy-time gate | Binary Authorization, enforced not dry-run | §25.10 |
| Image reference | Digest, 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.
| Setting | Flag | Notes |
|---|---|---|
| Repository CMEK | --kms-key | Creation-time |
| Immutable tags | --immutable-tags | Docker format only |
| Repository mode | --mode | Lowercase values |
| Cleanup policies | --cleanup-policy on set-cleanup-policies | Not on repositories create |
--mode takes lowercase-hyphenated values — standard-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-keydoes not exist ongcloud compute instances create— the flags are--boot-disk-kms-keyand--instance-kms-key.- Scopes and IAM intersect, and a narrow scope produces an error naming a role that is present; set
cloud-platformand govern with IAM. - GKE's real flags are
--enable-intra-node-visibilityand--security-group;--disable-legacy-endpointsdoes not exist and--enable-binauthzis 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=allis deprecated in favor ofall-traffic. --no-allow-unauthenticatedexists onrun deployand not onrun services update— remediate an existing public service by removing the invoker binding.- Cloud SQL
--ssl-modehas three values and onlyTRUSTED_CLIENT_CERTIFICATE_REQUIREDverifies the client;--require-sslis 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, notcreate:--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 GAgcloud bqgroup contains onlymigration-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-accountis 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-accountrather than assuming. - Private build pools use
--no-public-egress, not--no-external-ip, and need an egress path configured first. - Artifact Registry
--modetakes lowercase values,--immutable-tagsis Docker-only,--cleanup-policyis not onrepositories 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 §
| Control | Why it matters | How to verify (CLI + Console) |
|---|---|---|
Instances use --boot-disk-kms-key, not the flag people expect | --kms-key does not exist on this command | gcloud compute instances describe NAME --zone=ZONE --format='value(disks.diskEncryptionKey)' |
| No instance uses the default compute service account | Its historical basic role is the blast radius | gcloud compute instances list --format='value(name,serviceAccounts.email)' |
| GKE node auto-upgrade on, with maintenance exclusions configured | Off means unpatched; on without exclusions means surprise disruption | gcloud container node-pools describe POOL --cluster=CLUSTER --region=REGION |
| PodDisruptionBudgets are satisfiable | An unsatisfiable budget delays an upgrade by an hour, then loses the pod | kubectl get pdb -A |
| Cloud Run services not publicly invokable | The update flag does not exist; check the binding | gcloud run services get-iam-policy SERVICE --region=REGION |
Cloud SQL created with --disk-encryption-key | CMEK can never be added later | gcloud sql instances describe INSTANCE --format='value(diskEncryptionConfiguration)' |
Cloud SQL --ssl-mode verifies the client | The other two values do not | gcloud sql instances describe INSTANCE --format='value(settings.ipConfiguration.sslMode)' |
| Buckets have both uniform access and public access prevention | They do different jobs | gcloud storage buckets describe gs://BUCKET |
| Uniform access decision made inside ninety days | After that it is permanent | Bucket uniformBucketLevelAccess.lockedTime |
| BigQuery consumers granted views, not tables | The view's SQL is the boundary | bq show --format=prettyjson DATASET |
| BigQuery datasets carry a default table expiration | Data that no longer exists cannot be exfiltrated | bq show --format=prettyjson DATASET |
Every push subscription sets --push-auth-service-account | Otherwise the endpoint is an unauthenticated webhook | gcloud pubsub subscriptions describe SUB --format='value(pushConfig.oidcToken)' |
| Dead-letter topics have a consumer and a backlog alert | Otherwise dead-lettering is silent data loss | gcloud pubsub subscriptions list; alert policy (§18.15) |
| Build and deploy identities differ | A compromised build must not be able to ship | Trigger --service-account versus target executionConfigs |
| Cloud Build default identity resolved explicitly | It varies by organization setting | gcloud builds get-default-service-account --region=REGION |
Private pools use --no-public-egress with an egress path | The flag name is not what people expect, and NAT must exist | gcloud builds worker-pools describe POOL --region=REGION |
Docker repositories set --immutable-tags | Non-Docker formats cannot, so pin by digest there | gcloud artifacts repositories describe REPO --location=REGION |
| Cleanup policies keep the rollback target | A days-based rule outlives no incident | gcloud artifacts repositories describe REPO --location=REGION |
| Artifact hardening audited, not policy-enforced | No organization policy constraint exists for it | Asset inventory query (§29.6) |
Sources §
- https://cloud.google.com/compute/shielded-vm/docs/shielded-vm — Shielded VM options and their instance flags (last validated 2026-09-04)
- https://cloud.google.com/kubernetes-engine/docs/concepts/node-pool-upgrade-strategies — surge settings and the disruption-budget behavior during a drain (last validated 2026-09-04)
- https://cloud.google.com/kubernetes-engine/docs/concepts/maintenance-windows-and-exclusions — maintenance windows and exclusions (last validated 2026-09-04)
- https://cloud.google.com/container-optimized-os/docs/concepts/security — the node OS security model behind §9.31 (last validated 2026-09-04)
- https://cloud.google.com/sql/docs/postgres/configure-ssl-instance — Cloud SQL SSL modes and the legacy flag (last validated 2026-09-04)
- https://cloud.google.com/sql/docs/postgres/configure-cmek — Cloud SQL CMEK and its creation-time restriction (last validated 2026-09-04)
- https://cloud.google.com/storage/docs/uniform-bucket-level-access — uniform bucket-level access and its ninety-day reversibility window (last validated 2026-09-04)
- https://cloud.google.com/storage/docs/soft-delete — soft delete defaults and its billing behavior (last validated 2026-09-04)
- https://cloud.google.com/bigquery/docs/authorized-views — authorized views as the sharing boundary (last validated 2026-09-04)
- https://cloud.google.com/bigquery/docs/column-level-security-intro — column-level security and policy tags (last validated 2026-09-04)
- https://cloud.google.com/bigquery/docs/row-level-security-intro — row-level security (last validated 2026-09-04)
- https://cloud.google.com/dataplex/docs/transition-to-dataplex-catalog — the catalog product rename and Data Catalog's deprecation (last validated 2026-09-04)
- https://cloud.google.com/pubsub/docs/authenticate-push-subscriptions — push subscription OIDC authentication (last validated 2026-09-04)
- https://cloud.google.com/artifact-registry/docs/repositories/cleanup-policy — cleanup policies and Keep-over-Delete precedence (last validated 2026-09-04)