This check flags GCP projects and organizations that have no log sink exporting audit logs to durable storage. Without one, your audit trail is capped at Cloud Logging's default retention and can be tampered with. Fix it by creating an aggregated sink that routes all audit logs to a locked Cloud Storage bucket or BigQuery dataset.
Audit logs are the record of who did what in your cloud environment. In GCP, Cloud Logging captures Admin Activity and Data Access logs automatically, but those logs sit in the Logging service with default retention windows and are subject to the same access controls as everything else in the project. If you want a durable, tamper-resistant copy that outlives those defaults and survives a project compromise, you need a log sink.
The logging_nosinks check looks for exactly this gap: a project or organization where no sink is configured to export audit logs to an external destination. When it fires, you have audit data with a shelf life and nowhere to fall back to during an investigation.
What this check detects
The check inspects your GCP logging configuration and confirms whether at least one log sink exists that captures audit logs. A log sink is a routing rule: it matches log entries against a filter and forwards them to a destination such as Cloud Storage, BigQuery, Pub/Sub, or another logging bucket.
Specifically, logging_nosinks flags the resource when:
- No sink is configured at the project or organization level, or
- Existing sinks have filters that exclude audit logs, leaving the audit trail unexported
Note: GCP enables Admin Activity audit logs by default and they cannot be turned off. The problem this check addresses is not whether logs are generated, but whether they are exported somewhere durable. The default _Required bucket retains Admin Activity logs for 400 days, but you do not control that retention and you cannot extend it.
Why it matters
Audit logs only help you if they exist when you need them. There are three concrete ways the absence of a sink hurts you.
1. Attackers delete their tracks
If an attacker gains a role like roles/logging.admin or broad project owner access, they can modify retention, disable user-defined log buckets, or remove sinks. Without a sink exporting to a separate, access-restricted destination, there is no copy outside their reach. A locked Cloud Storage bucket with retention policies gives you logs that even a project owner cannot quietly erase.
Warning: The default _Default and _Required log buckets live inside the same project. An identity with sufficient privileges in that project can affect them. Exporting to a sink that targets a dedicated logging project, owned by a separate team, breaks that single point of control.
2. Default retention is too short for real investigations
The _Default bucket retains logs for 30 days unless you extend it. Many breaches go undetected for months. The 2023 industry averages still put mean time to identify a breach well over 200 days. If your audit logs expired four months before you noticed anything, the investigation starts with a blank page.
3. Compliance frameworks require durable, tamper-evident logging
PCI DSS, SOC 2, HIPAA, and CIS Benchmarks all expect audit logs to be retained for a defined period and protected from modification. CIS GCP Benchmark control 2.2 explicitly calls for a sink that exports all log entries. Failing this check is often a direct audit finding.
How to fix it
The recommended fix is an aggregated sink at the organization level that captures audit logs across all projects and routes them to a destination outside the source projects. Below are the steps using the gcloud CLI, the Console, and Terraform.
Step 1: Create a destination bucket
Use a dedicated logging project so the destination is isolated from the projects being logged.
gsutil mb -p my-logging-project -l us-central1 gs://org-audit-log-archive/
Then apply a retention policy and lock it so the logs become immutable for the retention period.
# Retain for 365 days
gsutil retention set 365d gs://org-audit-log-archive/
# Lock the policy (irreversible)
gsutil retention lock gs://org-audit-log-archive/
Danger: gsutil retention lock is permanent. Once locked, the retention policy cannot be shortened or removed, and objects cannot be deleted before their retention expires. Confirm the retention period meets your compliance requirements before locking, because there is no undo.
Step 2: Create the aggregated sink
This sink captures all logs across the organization. The empty --log-filter means no filtering, so everything including all audit logs is exported.
gcloud logging sinks create org-audit-sink \
storage.googleapis.com/org-audit-log-archive \
--organization=123456789012 \
--include-children \
--log-filter='logName:"cloudaudit.googleapis.com"'
The filter above narrows the export to audit logs specifically, which keeps storage costs down. If you want every log entry, drop the --log-filter flag entirely.
Warning: An unfiltered sink exports everything, including high-volume application and data access logs. This can run up significant Cloud Storage and egress costs. Start with the audit log filter above and widen only if you have a reason to.
Step 3: Grant the sink's service account write access
Creating a sink generates a unique writer identity. That identity needs permission to write to the destination.
# Get the writer identity
gcloud logging sinks describe org-audit-sink \
--organization=123456789012 \
--format='value(writerIdentity)'
# Grant it object creator on the bucket
gsutil iam ch \
serviceAccount:[email protected]:roles/storage.objectCreator \
gs://org-audit-log-archive/
Console steps
- Go to Logging → Log Router in the GCP Console.
- Click Create sink.
- Name the sink and set the destination to your Cloud Storage bucket, BigQuery dataset, or Pub/Sub topic.
- Under Choose logs to include in sink, enter the filter
logName:"cloudaudit.googleapis.com"for audit logs only, or leave it empty to capture all logs. - Click Create sink. GCP will display the writer identity and prompt you to grant it access.
Terraform
If you manage infrastructure as code, define the sink and the IAM binding together so the writer identity is always granted access.
resource "google_logging_organization_sink" "audit_sink" {
name = "org-audit-sink"
org_id = "123456789012"
destination = "storage.googleapis.com/${google_storage_bucket.audit_archive.name}"
filter = "logName:\"cloudaudit.googleapis.com\""
include_children = true
}
resource "google_storage_bucket" "audit_archive" {
name = "org-audit-log-archive"
project = "my-logging-project"
location = "US-CENTRAL1"
uniform_bucket_level_access = true
retention_policy {
retention_period = 31536000 # 365 days in seconds
is_locked = true
}
}
resource "google_storage_bucket_iam_member" "sink_writer" {
bucket = google_storage_bucket.audit_archive.name
role = "roles/storage.objectCreator"
member = google_logging_organization_sink.audit_sink.writer_identity
}
Tip: Use include_children = true on the organization sink so any new project created under the org is covered automatically. Without it, you would need to add a sink to every project by hand, and new projects would slip through.
How to prevent it from happening again
A one-time fix is not enough. New organizations, new folders, and drift can all reintroduce the gap. Bake the requirement into your platform.
Enforce with Organization Policy and IaC
Manage the org-level sink in your Terraform root module and apply it through CI/CD. Anyone who deletes the sink will see it reappear on the next apply. Pair this with a policy that prevents the sink from being modified outside of the pipeline.
Add a policy-as-code gate
Use OPA/Conftest or a Terraform Sentinel policy to block any plan that does not include an organization-level audit sink. A simple Rego check can assert the resource exists:
package gcp.logging
deny[msg] {
not input.resource.google_logging_organization_sink
msg := "No organization-level log sink defined. Audit logs must be exported."
}
Run the check on a schedule
Continuous monitoring catches drift that a CI gate misses, for example a sink deleted directly in the Console during an incident. Running logging_nosinks as part of a scheduled Lensix scan means you find out within minutes rather than during your next audit.
Best practices
- Centralize logs in a dedicated project. Keep the logging project under tight IAM control, separate from the workloads it monitors, so a compromise in one project does not reach the archive.
- Use locked retention policies. Immutable buckets give you logs that survive a malicious or accidental deletion attempt.
- Filter for cost, but never exclude audit logs. Export the full
cloudaudit.googleapis.comstream. You can trim noisy application logs without touching the audit trail. - Route to BigQuery for active investigation. Cloud Storage is cheap cold storage. A parallel sink to BigQuery makes logs queryable with SQL during an incident.
- Alert on sink changes. Create a log-based metric and alert when
google.logging.v2.ConfigServiceV2.DeleteSinkorUpdateSinkappears in audit logs. If someone disables your export, you want to know immediately. - Test recovery. Periodically confirm you can actually retrieve and read exported logs. An export that no one has verified is a false sense of security.
Tip: Set up the alert and the sink in the same Terraform module. That way the monitoring for the sink ships alongside the sink itself, and neither can exist without the other.
The fix here is small, a single aggregated sink and a locked bucket, but the payoff is large. When an incident happens, the difference between a complete audit trail and an empty one is often the difference between a contained event and a guessing game.

