Skip to content

Stream to Google Cloud Storage

Stream to Google Cloud Storage

Objects use the same layout as S3: {prefix}/{yyyy}/{mm}/{dd}/{hh}/{batch-id}.ndjson.gz (UTC hour of the write, fresh ULID per object, gzip unless gzip => false).

1. A service account that can only create objects

gcloud iam service-accounts create siem-writer --project=acme-audit

gcloud storage buckets add-iam-policy-binding gs://acme-audit-logs \
  --member="serviceAccount:[email protected]" \
  --role="roles/storage.objectCreator"

gcloud iam service-accounts keys create siem-writer-key.json \
  [email protected]

roles/storage.objectCreator on the bucket allows creating objects and nothing else — no read, list, overwrite or delete. Grant it on the bucket, not the project.

2. Register the stream

app(LogStreams::class)->create(
    name: 'gcs-archive',
    destination: Destination::Gcs,
    endpointUrl: '',                                       // https://storage.googleapis.com
    secret: file_get_contents('siem-writer-key.json'),    // stored encrypted
    options: ['bucket' => 'acme-audit-logs', 'prefix' => 'cbox/audit'],
);

The key is validated at registration (a service_account key with a usable private key) and never echoed in an error.

How it authenticates

The sink signs a one-hour JWT (iss = the service account, aud = the token endpoint, scope devstorage.read_write) with the key's private key — RS256 through PHP's OpenSSL — and exchanges it for an access token (the RFC 7523 JWT-bearer grant). The token is cached, encrypted with the app key, until a minute before it expires. IAM, not the scope, is what limits the account to creating objects.

The token endpoint is always siem.gcs.token_uri (Google's by default). A key file's own token_uri is ignored, so a crafted key cannot choose where the platform sends a signed request.

Failures

Response Meaning Handling
token exchange invalid_grant / invalid_client key deleted, disabled, wrong project circuit opens, action_required
upload 401 token rejected token dropped, circuit opens
upload 403 account lacks storage.objects.create circuit opens, action_required
upload 404 no such bucket circuit opens, action_required
429, 5xx, timeouts transient retried with backoff