Google Cloud Storage (GCS) Integration Guide
Connection Configuration
BurnLedger connects to GCS using a service account key with read-only permissions.
Configuration fields:
| Field | Required | Description |
|---|---|---|
bucket |
Yes | The GCS bucket name. |
credentials_json |
Yes, unless endpoint is set |
The service account JSON key, as raw JSON — not base64. The connector passes the string straight to the Google SDK; a base64 blob is rejected as malformed credentials. |
endpoint |
No | Custom endpoint for an emulator (fake-gcs-server) or a GCS-compatible host. Mutually exclusive with credentials_json: endpoint mode authenticates with WithoutAuthentication, and the SDK errors if credentials are supplied alongside it. |
read_only |
Conditional | Your assertion that the credential is read-only. Required whenever the bucket IAM TestPermissions call cannot run — notably in endpoint mode, or when the principal cannot query bucket IAM. See Service Account Setup. |
soft_delete_clear |
Conditional | Your assertion that the bucket holds no soft-deleted copies. Required when the bucket's soft-delete policy is disabled: GCS refuses to enumerate the soft-deleted class then, while copies from a previously-enabled window remain restorable until their own hard-delete dates — so absence cannot be proven by API. Assert it only for a bucket whose soft delete was never enabled, or whose last enabled window has fully expired. With soft delete enabled, deletion certification is only possible after the retention window of the deleted objects elapses — recoverable copies are, correctly, a refusal. |
There is no project_id field — the project is taken from the service account key. These are the exact JSON keys the connector reads; they are snake_case in both SDKs — do not write credentialsJson or readOnly, since the connection object is sent to the API verbatim and unknown keys are ignored.
TypeScript
const system = await bl.registerSystem({
name: "user-uploads-gcs",
connectorType: "gcs",
connectionConfig: {
bucket: "myapp-user-uploads",
// Raw service account JSON, e.g. fs.readFileSync("burnledger-key.json", "utf8")
credentials_json: process.env.GCS_CREDENTIALS_JSON,
},
subjectQuery: "users/{identifier}/",
hashScope: "existence",
});
Python
system = bl.register_system(
name="user-uploads-gcs",
connector_type="gcs",
connection_config={
"bucket": "myapp-user-uploads",
# Raw service account JSON, e.g. open("burnledger-key.json").read()
"credentials_json": os.environ["GCS_CREDENTIALS_JSON"],
},
subject_query="users/{identifier}/",
hash_scope="existence",
)
Object Prefix Pattern
Same syntax as S3. Use {identifier} as the subject placeholder. The expanded
string is used verbatim as the object-name prefix — there are no wildcards,
and * is passed through as a literal character that will match nothing. A
template with no {identifier} is rejected with INVALID_QUERY_TEMPLATE.
users/{identifier}/
uploads/{identifier}-
{identifier}/documents/
Service Account Setup
BurnLedger verifies that the credential cannot write to the bucket and rejects connections with write access — without ever writing. It calls Bucket.IAM().TestPermissions for storage.objects.create and storage.objects.delete, which evaluates the principal's IAM bindings without performing either action. If the principal would be granted a write permission, construction is refused.
This IAM test only runs against real GCS. On fake-gcs-server or other non-GCP S3/GCS-compatible endpoints there is no IAM to query, so verification cannot run — in that case set "read_only": true in the connector config to assert the credential is read-only (BurnLedger only ever reads regardless). Without a successful IAM test and without the assertion, construction is refused (fails closed).
Create a custom role
gcloud iam roles create burnledger_readonly \
--project=myproject \
--title="BurnLedger Read-Only" \
--permissions=storage.objects.get,storage.objects.list,storage.buckets.get
Create a service account
# Create
gcloud iam service-accounts create burnledger-ro \
--display-name="BurnLedger Read-Only"
# Bind the custom role to the bucket
gsutil iam ch \
serviceAccount:burnledger-ro@myproject.iam.gserviceaccount.com:projects/myproject/roles/burnledger_readonly \
gs://myapp-user-uploads
# Generate the key
gcloud iam service-accounts keys create burnledger-key.json \
--iam-account=burnledger-ro@myproject.iam.gserviceaccount.com
Pass the contents of burnledger-key.json as credentials_json verbatim.
Do not base64-encode it — unlike the BigQuery connector, the GCS connector does
no decoding and hands the string straight to the Google SDK.
Verify
# Should succeed
gsutil ls gs://myapp-user-uploads/users/test/
# Should fail with "AccessDeniedException: 403"
gsutil cp /dev/null gs://myapp-user-uploads/test-write
Retained copies and soft delete
A deleted object that is still recoverable has not been deleted, so BurnLedger will not certify zero over it. Retention state is probed at construction:
- Retention state cannot be determined (the credential lacks permission to read it, or the API call fails) → construction is refused. It is not assumed to be "not retained": assuming the safe-looking answer is exactly how a verification record gets signed over data that is still recoverable.
- Recoverable copies exist for the subject → attestation fails rather than reporting zero. Purge them, then re-run.
Soft delete is ON BY DEFAULT on this store, so a bucket or container nobody has explicitly configured will hit this. That is the control working — the data really is still recoverable — but it is a change in behaviour: previously only live objects were counted, and a subject with soft-deleted blobs would have been certified as having none.
Hash Scope Options
| Scope | Behavior |
|---|---|
existence (default) |
Hashes five canonical fields — bucket, content_length, key, last_modified (UTC, microsecond precision), and md5 (hex-encoded). Does not download content. CRC32C and the object generation number are not part of the hash. |
full |
Downloads and hashes complete object content, bounded by the system's max_bytes. Use only when content-level proof is required. |
Composite and customer-managed-encryption objects may have no MD5 in their metadata; the
md5field is then the empty string, and the hash rests on the remaining four fields.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
CONNECTION_FAILED |
Invalid or expired credentials | Regenerate the service account key. Also check you passed the key as raw JSON, not base64 — a base64 blob fails here. |
CONNECTION_FAILED with "bucket not found" |
Wrong bucket name or project | Verify bucket name and ensure the service account has access. |
CONNECTION_FAILED with "cannot verify the credential is read-only" |
Bucket IAM TestPermissions could not run (emulator/endpoint mode, or the principal cannot query bucket IAM) |
Confirm the credential is read-only yourself, then set "read_only": true in the connector config. |
CONNECTION_FAILED mentioning credentials with endpoint set |
credentials_json was supplied together with endpoint |
Endpoint mode uses WithoutAuthentication; remove credentials_json. |
S3_VERSIONING_CONFLICT naming a soft-deleted or noncurrent copy |
The store still holds a recoverable copy of the subject's data — deletion is not complete until it expires or is purged | With soft delete enabled, wait out the retention window of the deleted objects. With it disabled, see soft_delete_clear. |
INVALID_QUERY_TEMPLATE |
Prefix pattern contains no {identifier} |
Add the {identifier} placeholder. A template without it would match nothing and certify an empty result, so it is refused. |
WRITE_ACCESS_DETECTED |
Service account has storage.objects.create or storage.objects.delete |
Use the custom read-only role above. Remove Storage Object Admin or Storage Object Creator roles. |