> ## Documentation Index
> Fetch the complete documentation index at: https://braintrust.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# BTQL summary queries fail with signBlob permission error

export const plans_0 = "Plans: Enterprise"

export const deployments_0 = "Deployments: Self-hosted"

export const data_plane_version_0 = undefined

export const use_case_0 = "Use case - Self-hosted GKE Standard deployments where BTQL shape=summary queries or /attachment requests fail due to missing IAM signBlob permissions"

<Note>
  **Applies to:**

  * Plan - {plans_0}
  * Deployment - {deployments_0}
  * {data_plane_version_0}
  * {use_case_0}
</Note>

## Summary

**Issue:** BTQL queries using `shape => 'summary'` fail with `500` (surfaced as `502` at the GCE load balancer) with `SigningError: Permission 'iam.serviceAccounts.signBlob' denied`. Queries without `shape => 'summary'` succeed.

**Cause:** The `shape => 'summary'` code path uses `@google-cloud/storage` to generate V4 signed GCS URLs when result sets exceed 4 MB. This requires `iam.serviceAccounts.signBlob`, which is implicitly available on GKE Autopilot but must be explicitly granted on GKE Standard with Workload Identity.

**Resolution:** Grant `roles/iam.serviceAccountTokenCreator` to the Braintrust service account and apply the two related bucket IAM bindings.

***

## Resolution steps

### Step 1: Add the missing IAM bindings via Terraform

Apply three resources matching [upstream PR #13](https://github.com/braintrustdata/terraform-google-braintrust-data-plane/pull/13):

```hcl theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
# Allows the Braintrust SA to call signBlob on itself
resource "google_service_account_iam_member" "braintrust_token_creator" {
  service_account_id = google_service_account.braintrust.name
  role               = "roles/iam.serviceAccountTokenCreator"
  member             = "serviceAccount:${google_service_account.braintrust.email}"
}

# Brainstore SA: object admin on the API bucket (required for topics feature)
resource "google_storage_bucket_iam_member" "brainstore_api_bucket_gcs_object_admin" {
  bucket = google_storage_bucket.api.name
  role   = "roles/storage.objectAdmin"
  member = "serviceAccount:${google_service_account.brainstore.email}"
}

# Brainstore SA: legacy bucket reader on the API bucket
resource "google_storage_bucket_iam_member" "brainstore_api_bucket_gcs_reader" {
  bucket = google_storage_bucket.api.name
  role   = "roles/storage.legacyBucketReader"
  member = "serviceAccount:${google_service_account.brainstore.email}"
}
```

No pod restarts are required. IAM changes take effect immediately.

### Step 2: Verify the fix

Run a query with `shape => 'summary'` against a large result set and confirm a `200` response:

```sql theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SELECT COUNT(*)
FROM project_logs('your-project-id', shape => 'summary')
WHERE created >= '2026-03-10' AND created < '2026-03-11'
```

***

## Background

### Why only large result sets fail

Results under 4 MB are returned inline — no signed URL is generated and `signBlob` is never called. Results over 4 MB are materialized to GCS and returned as a signed URL, which requires `iam.serviceAccounts.signBlob`. This is why some `shape => 'summary'` queries succeed while aggregation queries on large result sets always fail.

The threshold is configurable via the `RESPONSE_BUCKET_OVERFLOW_THRESHOLD` environment variable on the API pod.

### GKE Autopilot vs. GKE Standard

On GKE Autopilot, the default node service account has broad enough permissions to cover `signBlob` implicitly. On GKE Standard with Workload Identity, the pod service account only has explicitly granted roles. The `roles/iam.serviceAccountTokenCreator` binding must be added manually if your fork predates the upstream Terraform change.
