Chapter 29
Building for Compliance
Scope. This chapter is about evidence. Every control an auditor asks about is already owned by an earlier chapter, and this chapter never re-teaches one. Each section answers a single question: what artifact proves this control ran, which command produces that artifact, and how does it reach an auditor without being edited on the way. §29.6 is the book's full treatment of Cloud Asset Inventory. Chapter 30 owns which framework demands what, and §30.11 owns mapping an artifact to a control. Prerequisites. Chapter 17 (logging and audit), Chapter 16 (Security Command Center), Chapter 14 (Cloud KMS), Chapter 3 (IAM), Chapter 11 (Cloud Storage). Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0; see sources at end.
Compliance work fails in a predictable way: the controls are in place, the engineers know they are in place, and nobody can produce a document that says so. The gap is not a security gap. It is that the estate's state lives in APIs, the APIs answer questions about now, and an auditor is asking about a period that ended three months ago.
So this chapter is about turning a control into an artifact. An artifact is a file, produced by a named command, at a recorded time, stored somewhere it cannot be quietly changed. That is a different engineering problem from the one Chapters 1–25 solved, and it needs its own design: a schedule, a destination, a retention period, and a chain of custody.
Two properties separate evidence from a screenshot. It must be point-in-time — an assertion about a specific moment, not a live query someone ran during the audit — and it must be tamper-evident, so the party producing it is not also able to revise it. Nearly everything in this chapter follows from those two requirements.
The chapter cites controls by section number and does not explain them. Where a section says "prove CMEK is in force", the explanation of CMEK is §14.8 and the explanation of why it matters is Chapter 14. What is here is the command that produces the proof.
29.1 Compliance vs. Security §
Security is a property of the system; compliance is a property of what you can demonstrate about it. They diverge in both directions, and the divergences are where the work is.
Four cases, and only one is comfortable:
| Secure | Compliant | The situation |
|---|---|---|
| Yes | Yes | The control exists and produces evidence |
| Yes | No | The control exists and nothing records it — this chapter |
| No | Yes | The control was evidenced once and has since drifted |
| No | No | Honest, at least |
The third row is the dangerous one. A control evidenced at a point in time and never re-evidenced is a compliance artifact describing a system that no longer exists. It is why §29.2's schedule matters more than its format: annual evidence describes one day a year.
Google's shared responsibility model draws the line, and its stronger framing is shared fate — Google supplies the certified infrastructure, the customer configures it, and the customer's configuration is the part every framework in Chapter 30 actually assesses. Google's own certifications are downloadable from Compliance Reports Manager at no additional cost, and they answer questions about Google's controls, never about yours.
Assured Workloads is the one product that enforces rather than evidences. gcloud assured workloads create --compliance-regime=... creates a folder whose resources are constrained to a regime's requirements — personnel, data location, support access. Its enum is sovereignty- and government-oriented (fedramp-high, il4, itar, cjis, irs-1075); there is no PCI, SOC 2, or ISO regime, and expecting one is a common misreading.
Pitfall. Treating a Google certification as your own is the most common compliance error in cloud programs. Google's ISO 27001 certificate covers Google's infrastructure. An auditor asking whether your access reviews happen will not accept it, and §29.4 is the answer.
29.2 Evidence Collection §
This section owns the production of the artifact. §30.11 owns mapping an artifact to a control, and the split is deliberate: producing evidence is an engineering problem, mapping it is a framework problem.
The pipeline has four parts and each is a design decision:
- A schedule, so the artifact exists for every period rather than for the day someone remembered.
- A command that writes a durable object rather than printing to a terminal.
- A destination bucket that the producing identity can write and cannot alter.
- A retention lock, so the evidence outlives the incident it might describe.
Build the evidence bucket first, and lock it:
gcloud storage buckets create gs://rc-saas-shared-evidence-01 \
--project=rc-saas-shared-sec-01 \
--location=us-central1 \
--uniform-bucket-level-access \
--public-access-prevention \
--default-encryption-key=$KMS_KEY
gcloud storage buckets update gs://rc-saas-shared-evidence-01 \
--retention-period=P7Y \
--lock-retention-period
--lock-retention-period is irreversible and that is the point. Once locked, the retention period cannot be shortened and objects cannot be deleted before it elapses — by anyone, including the project owner. That is what makes the producing party unable to revise the record, which is the whole claim an auditor is testing.
Grant the collector roles/storage.objectCreator and not roles/storage.objectAdmin. Create-only means the collector can write today's evidence and cannot alter yesterday's, which is chain of custody expressed as IAM rather than as a policy document.
Schedule it with Cloud Scheduler invoking a Cloud Run job, authenticated with --oidc-service-account-email so the trigger carries its own identity. A cron job on someone's VM is not evidence anyone will accept.
Pitfall. A retention lock applies to the bucket for its whole life. Locking a seven-year retention on a bucket that also receives daily debug exports means seven years of storage cost for artifacts nobody will read. Keep the evidence bucket for evidence.
29.3 Audit Trails §
Chapter 17 owns Cloud Logging. This section owns producing the bounded, exportable extract an auditor asks for.
The four audit log types and their exact log IDs, which is what a filter needs:
| Type | Log ID | Default |
|---|---|---|
| Admin Activity | cloudaudit.googleapis.com%2Factivity | Always on, cannot be disabled |
| Data Access | cloudaudit.googleapis.com%2Fdata_access | Off, except BigQuery |
| System Event | cloudaudit.googleapis.com%2Fsystem_event | Always on |
| Policy Denied | cloudaudit.googleapis.com%2Fpolicy | On when a policy denies |
The URL-encoded %2F is required — the log name contains a literal slash, and an unencoded filter silently matches nothing.
gcloud logging read \
'logName="projects/PROJECT_ID/logs/cloudaudit.googleapis.com%2Factivity"
AND timestamp>="2026-06-01T00:00:00Z"
AND timestamp<"2026-07-01T00:00:00Z"' \
--project=PROJECT_ID --order=asc --format=json
For anything larger than an ad-hoc question, the export is the evidence, not the read. §17.15's sink to BigQuery and §17.13's log bucket retention are the durable record; logging read is how you extract a period from it.
Data Access logs are the ones every framework wants and the ones that are off. §33.11 owns that failure; the point here is that an auditor asking who read a dataset last quarter cannot be answered retroactively. The logs either existed then or they did not.
Pitfall. --freshness and a timestamp filter are mutually exclusive — --freshness works only with descending order and a filter carrying no timestamp. That bites exactly the bounded-window query an auditor asks for, so use explicit timestamp>= and timestamp< bounds and drop --freshness entirely.
29.4 Access Reviews §
The artifact is a point-in-time answer to "who could do what", which is a harder question than "who is bound to what".
A policy dump is not an access review. get-iam-policy returns bindings at one node; effective access is the union of organization, folder, and project bindings, minus deny policies (§3.17), qualified by conditions. Cloud Asset Inventory answers the real question:
gcloud asset analyze-iam-policy-longrunning \
--organization=123456789012 \
--gcs-output-path=gs://rc-saas-shared-evidence-01/access/2026-Q2.json
The long-running form is the evidence form, because it writes a durable object to a destination you control. The synchronous analyze-iam-policy prints to a terminal, which makes it an inspection tool.
Review privileged access separately, because it is time-bounded. Privileged Access Manager grants are the record of who elevated, when, and with whose approval:
gcloud pam entitlements export ENTITLEMENT \
--location=global --organization=123456789012 --destination=ent.yaml
gcloud pam grants search --caller-relationship=had-approved
A grant history is stronger evidence than a role binding. It shows access that was requested, approved, used, and expired — which is what a reviewer wants and what a standing binding can never demonstrate.
Recommender supplies the "is this excessive" half. The IAM recommender surfaces over-granted roles from observed usage; §33.3 owns it as a failure and §29.4 uses its output as the evidence that a review considered least privilege rather than merely listing bindings.
Pitfall. An access review that produces a list nobody acted on is evidence of a process that ran, which auditors accept and attackers do not. Record the decisions — revoked, retained with justification — alongside the list, or the artifact proves only that a report was generated.
29.5 Change Records §
Three independent sources record change, and a framework generally wants the intersection: what changed, who changed it, and what it was before.
| Source | Answers | Window |
|---|---|---|
| Admin Activity audit logs | Who, when, which API method | Log retention (§17.13) |
| Cloud Asset Inventory history | What the resource looked like before | 35 days |
| Terraform plan and state | What was intended and approved | Repository history |
The 35-day limit is stated by the tool itself. gcloud asset get-history --start-time documents that it "Must be after the current time minus 35 days":
gcloud asset get-history \
--organization=123456789012 \
--asset-names=//cloudresourcemanager.googleapis.com/projects/PROJECT_ID \
--content-type=iam-policy \
--start-time=2026-08-01T00:00:00Z
That window is shorter than every audit period in Chapter 30, which is the operational consequence: prior-state evidence must be captured on a schedule (§29.2) or it is simply gone. A quarterly audit asking what an IAM policy looked like in month one cannot be served by get-history at all.
The Terraform record is the strongest of the three for anything provisioned that way, because it carries intent and approval, not just effect (§26.23). It is also the only one that shows a change that was proposed and rejected.
Pitfall. Change records prove that a change happened, not that it was authorized. The authorization lives in the pull request approval and the pipeline's own logs (§26.24). An auditor asking about change management wants both halves, and only one of them is in Google Cloud.
29.6 Asset Inventory §
Cloud Asset Inventory is the estate's ledger, and this section is the book's full treatment of it — §16.2 defers here, and Security Command Center's own asset commands are deprecated.
gcloud asset export is the evidence command because it writes a durable, timestamped object; gcloud asset list prints to a terminal and is the inspection command.
--content-type is the load-bearing enum, and the CLI values are lowercase-hyphenated:
| Value | What it exports |
|---|---|
resource | Resource metadata — configuration of every asset |
iam-policy | The IAM policy on each asset |
org-policy | Organization policies set on child assets |
access-policy | Access Context Manager policies |
os-inventory | OS inventory of VM instances |
relationship | Relationships between assets |
The REST enum is SCREAMING_SNAKE (IAM_POLICY) and the CLI enum is not. Mixing them is a silent failure in a script and a wrong command in a document.
gcloud asset export \
--organization=123456789012 \
--content-type=resource \
--snapshot-time=2026-07-01T00:00:00Z \
--output-path=gs://rc-saas-shared-evidence-01/assets/2026-07-01-resource.json
--snapshot-time accepts a past time, which is what makes an inventory an assertion about a period rather than about the moment the auditor asked. It is the single most useful flag in the chapter.
Export to BigQuery for anything you need to query — --bigquery-dataset with --bigquery-table, plus --partition-key taking read-time or request-time. A quarterly snapshot table makes "what existed on this date" a SQL question.
Feeds turn inventory into detection. gcloud asset feeds create publishes asset changes to Pub/Sub, which is how a control becomes continuous rather than periodic (§34.16).
Pitfall. Google documents a volatile window: "due to delays in resource data collection and indexing, there is a volatile window during which running the same query at different times may return different results". Two exports with the same --snapshot-time can differ. Take the export once, store it, and cite the stored artifact — do not regenerate it for the auditor.
29.7 Vulnerability Records §
The artifact differs by where the software runs, and a framework generally wants both.
For container images, Artifact Analysis is the record:
gcloud artifacts vulnerabilities list \
us-central1-docker.pkg.dev/rc-saas-shared-art-01/prod-docker/billing@sha256:DIGEST
Reference the image by digest, never by tag. A vulnerability record attached to a tag describes whatever that tag pointed at when the scan ran, and §24.5 exists because tags move.
For virtual machines, VM Manager's vulnerability reports are the record:
gcloud compute os-config vulnerability-reports list \
--location=us-central1-a --project=rc-saas-prod-app-01
These require the OS Config agent, which means a report's absence is ambiguous — it may mean no vulnerabilities or no agent. An evidence process must therefore reconcile the report list against the instance list, and the gap is itself a finding.
--additional-package-types on gcloud artifacts docker images scan is deprecated. Scanning covers all package types by default, and --skip-package-types only narrows coverage — a flag that can only make evidence weaker.
What a framework actually asks for is the remediation record, not the finding list: which findings were fixed, which were accepted, and on what timeline. Neither command above produces that; it comes from the ticketing system, and the two have to be joined.
Pitfall. A vulnerability scan is a point-in-time statement about a static artifact, and new vulnerabilities are discovered in unchanged images. Evidence that an image was clean at build time says nothing about today, which is why §37.9's continuous re-evaluation matters more to an auditor than a build-time scan.
29.8 Encryption Evidence §
The claim is usually "customer data is encrypted at rest with keys we control", and the hard half is proving the scope — that no resource escaped.
Google-managed encryption at rest needs no evidence from you; it is Google's certification (§29.1). The customer's claim is about CMEK, and Cloud KMS Inventory is the command that proves it:
RING=projects/rc-saas-shared-sec-01/locations/us-central1/keyRings/kr-us-central1-data
gcloud kms inventory search-protected-resources \
--scope=organizations/123456789012 \
--keyname="$RING/cryptoKeys/k-storage-uploads"
This answers the question from the key's side, listing every resource a key protects. That is the direction an auditor rarely asks in and the one that catches the resource nobody configured — because the complementary query, resource by resource, only ever returns what you thought to check.
Cloud Asset Inventory answers it from the resource side. A --content-type=resource export contains each asset's key reference, so the two together give both directions: which resources a key protects, and which resources have no key at all.
Key rotation is its own evidence. gcloud kms keys versions list shows version history and creation times, which is what demonstrates a rotation schedule was honored rather than configured.
Encryption in transit is mostly a configuration assertion, evidenced from the resource export: TLS policies, constraints/gcp.restrictTLSVersion (§31.9), and load balancer certificate configuration.
Pitfall. CMEK evidence proves the key is configured, not that the key is controlled. The stronger claim — that Google cannot decrypt without you — depends on where the key lives and who holds roles/cloudkms.admin on it (§14.8, §2.21). An auditor who asks the second question is asking about IAM, and the answer is §29.4's artifact.
29.9 Backup Evidence §
Every framework asks about backup and almost none accept a backup policy as evidence. What they want is proof that backups exist, are immutable, and have been restored.
Backup vaults are the immutability mechanism, and the retention is enforced by the service rather than by policy:
gcloud backup-dr backup-vaults create bv-prod-7y \
--location=us-central1 \
--backup-min-enforced-retention="p1y" \
--access-restriction=within-org
--backup-min-enforced-retention is required on create, which is a good default: a vault without enforced retention is a bucket with a nicer name. --effective-time sets when the enforcement begins, and --access-restriction defaults to within-org.
The three artifacts an auditor wants, in ascending order of how rarely they exist:
- A backup inventory — what is protected, from
gcloud backup-dr backup-plan-associations list. Common. - A coverage reconciliation — the resource list minus the protected list. Rare, and it is where unprotected production data is found.
- A restore test record — a restore that was performed, timed, and verified. Almost never.
The second is produced by joining §29.6's asset export against the association list, and it is the artifact worth building because it is the one that finds the gap rather than describing the plan.
Backups in the same project and region as the source are not backups for the failure modes that matter — a compromised project, a mistaken deletion with sufficient privilege. §33.19 owns that failure.
Pitfall. An untested restore is a plan, not a capability. The evidence is a dated record of a restore into an isolated project with a verification step, and producing it on a schedule is the only way it exists when asked for.
29.10 Incident Records §
Chapter 34 owns responding to an incident. This section owns the record it leaves behind.
Security Command Center findings carry the lifecycle — a finding's state and mute fields, plus its history, are the record that something was detected, triaged, and resolved. The durable export is to BigQuery:
gcloud scc findings export-to-bigquery organizations/123456789012 \
--dataset=projects/rc-saas-shared-sec-01/datasets/scc_findings
A muted finding is a decision and must be evidenced as one. Muting removes a finding from the default view and does not resolve it; a mute with no recorded justification is indistinguishable from a suppressed problem, and §33.20 treats it as security theater.
The finding is only part of the record. A framework asks about the incident process: detection time, acknowledgement time, containment, notification, and closure. Those live in the ticketing and paging systems, and the evidence artifact is the join between an SCC finding ID and the incident record that references it. Requiring that reference is the cheapest way to make the join possible later.
Notification obligations are the part with legal deadlines. Several frameworks in Chapter 30 impose them, and the evidence is a timestamped record of when the determination was made, not when the email went out.
Pitfall. Findings are exported forward, so an export configured after an incident does not contain it. The export has to exist before it is needed, which is the same argument as §29.3's about Data Access logs and §29.5's about the 35-day history window — evidence infrastructure is retrospective only if it was prospective.
29.11 Policy Evidence §
The claim is that a control was in force, across the estate, for a period. A policy definition proves it was written down.
gcloud asset analyze-org-policies answers the estate-wide question:
gcloud asset analyze-org-policies \
--scope=organizations/123456789012 \
--constraint=constraints/compute.vmExternalIpAccess
It reports where the constraint applies and where it is overridden, which is the part a policy document cannot supply. An exception at one folder is the finding; the policy's existence is not.
gcloud org-policies list --show-unset gives the complement — which constraints are not set at all. For an estate whose baseline is Chapter 31, the unset list is the gap analysis.
Effective policy is the only meaningful reading. A constraint set at the organization can be overridden below it (§2.31), so evidence must come from the effective evaluation at the resource, not from the definition at the root.
For anything enforced outside organization policy, the evidence is the converged configuration: §28.12's assertion tasks produce a per-host verification, and §26.22's drift detection produces a periodic statement that infrastructure still matches its definition. Both are evidence that a control held over time rather than at one moment.
Pitfall. A dry-run policy produces audit log entries and enforces nothing (§2.30). Evidence collected from a policy left in dry-run shows a control that was evaluated and never applied — and because the artifacts look almost identical, this survives review more often than it should. Check dryRunSpec before treating any policy as a control.
Chapter Summary §
- Compliance is what you can demonstrate; the common failure is a correct estate with no artifact describing it.
- Evidence must be point-in-time and tamper-evident, and nearly every design decision in this chapter follows from those two properties.
- Google's certifications, downloadable from Compliance Reports Manager, cover Google's infrastructure and never your configuration.
- Assured Workloads enforces rather than evidences, and its regimes are sovereignty- and government-oriented — there is no PCI, SOC 2, or ISO regime.
- Lock the evidence bucket's retention (
--lock-retention-period); it is irreversible, and irreversibility is what makes the producer unable to revise the record. - Grant the collector create-only object access, so it can write today's evidence and cannot alter yesterday's.
- Audit log IDs need the URL-encoded
%2F; an unencoded filter silently matches nothing. --freshnessis mutually exclusive with a timestamp filter, which is exactly the bounded-window query an audit needs — use explicit bounds.- Data Access audit logs are off by default except BigQuery, and cannot be obtained retroactively.
gcloud asset analyze-iam-policy-longrunningwith--gcs-output-pathis the evidence form of an access review; the synchronous command is an inspection tool.- Privileged Access Manager grant history is stronger evidence than a standing role binding, because it shows request, approval, use, and expiry.
- Cloud Asset Inventory history is limited to 35 days — shorter than every audit period in Chapter 30 — so prior-state evidence must be captured on a schedule.
gcloud asset export --content-typetakes lowercase-hyphenated values (iam-policy); the REST enum is SCREAMING_SNAKE. Do not mix them.--snapshot-timeaccepts a past time, which is what makes an inventory an assertion about a period.- Google documents a volatile window in which the same asset query returns different results; store the export and cite the stored artifact.
- Reference scanned images by digest; a vulnerability record attached to a tag describes whatever the tag pointed at.
- A missing VM vulnerability report may mean no findings or no agent — reconcile against the instance list.
gcloud kms inventory search-protected-resourcesproves encryption scope from the key's side, which catches the resource nobody thought to check.- Backup evidence has three tiers, and the two that matter — coverage reconciliation and a dated restore test — are the two that usually do not exist.
- A muted SCC finding is a decision and needs a recorded justification, or it is indistinguishable from a suppressed problem.
- Evidence infrastructure must exist before it is needed: exports are forward-looking, and a dry-run policy evidences a control that never applied.
Security Checklist §
| Control | Why it matters | How to verify (CLI + Console) |
|---|---|---|
| Evidence bucket retention locked | Makes the producing party unable to revise the record | gcloud storage buckets describe gs://BUCKET --format='value(retentionPolicy)' |
| Collector holds create-only object access | Chain of custody expressed as IAM | gcloud storage buckets get-iam-policy gs://BUCKET |
| Evidence bucket uses CMEK and has Data Access logs | Evidence is itself sensitive and its reads should be recorded | Bucket description; project auditConfigs |
| Collection runs on a schedule with its own identity | Annual evidence describes one day a year | gcloud scheduler jobs describe JOB --location=REGION |
| Data Access audit logs enabled before the audit period | They cannot be obtained retroactively | gcloud projects get-iam-policy PROJECT_ID --format='yaml(auditConfigs)' |
| Asset export scheduled at least monthly | CAI history covers only 35 days | Evidence bucket object listing by date |
Asset exports use an explicit --snapshot-time | Makes the artifact an assertion about a period | The stored command and its output |
| Access review output written to Cloud Storage | The synchronous command leaves no artifact | gcloud asset analyze-iam-policy-longrunning --gcs-output-path in the job |
| Access review records decisions, not just bindings | Otherwise it proves a report ran | The stored review document |
| Privileged access grants exported | Time-bounded access is stronger evidence than a binding | gcloud pam grants search --caller-relationship=had-approved |
| Images referenced by digest in vulnerability records | A tag can be repointed after the scan | The stored scan output |
| VM vulnerability reports reconciled against instances | A missing report is ambiguous | Report list joined to gcloud compute instances list |
| CMEK scope proven from the key's side | Catches the resource nobody configured | gcloud kms inventory search-protected-resources --scope=... |
| Backup coverage reconciled against the asset inventory | Finds unprotected production data | Asset export minus backup plan associations |
| A dated restore test exists | An untested restore is a plan, not a capability | The restore test record and its verification step |
| Backup vault has enforced retention | Without it a vault is a bucket with a nicer name | gcloud backup-dr backup-vaults describe VAULT --location=REGION |
| SCC findings exported to BigQuery before an incident | Exports are forward-looking | gcloud scc findings export-to-bigquery configured |
| Every mute carries a recorded justification | Otherwise it is a suppressed problem | Mute config descriptions |
| Effective org policy evaluated, not definitions | A constraint can be overridden below the root | gcloud asset analyze-org-policies --constraint=... --scope=... |
| No control evidenced from a dry-run policy | Dry-run logs and enforces nothing | Policy dryRunSpec versus spec |
Sources §
- https://cloud.google.com/asset-inventory/docs/overview — Cloud Asset Inventory concepts, content types, and the volatile-window caveat (last validated 2026-09-04)
- https://cloud.google.com/logging/docs/audit — the four audit log types, their log IDs, and which are enabled by default (last validated 2026-09-04)
- https://cloud.google.com/kms/docs/view-key-usage — Cloud KMS Inventory and protected-resource search (last validated 2026-09-04)
- https://cloud.google.com/storage/docs/bucket-lock — retention policies and the irreversibility of locking (last validated 2026-09-04)
- https://cloud.google.com/architecture/framework/security/shared-responsibility-shared-fate — the shared responsibility and shared fate model (last validated 2026-09-04)
- https://cloud.google.com/policy-intelligence/docs/role-recommendations-overview — IAM role recommendations as least-privilege evidence (last validated 2026-09-04)
- https://cloud.google.com/security/compliance/compliance-reports-manager — where Google's own certifications and audit reports are obtained (last validated 2026-09-04)