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

# Asymptote with Your Own Cloud Storage Bucket

> Keep Beacon telemetry in a Google Cloud Storage bucket you own and give Asymptote read access to it.

Use this guide when your Macs' Beacon telemetry should land in your own Google Cloud project first, with Asymptote reading a copy for your dashboard. It covers the whole setup in order: the bucket and access, the details to send us, the rollout to your Macs, and how to confirm it works.

## What you'll set up

```mermaid theme={null}
flowchart TB
  subgraph Macs["Your Macs"]
    Beacon["AI agents and Beacon"]
  end
  subgraph Project["Your Google Cloud project"]
    Bucket[("Cloud Storage bucket")]
    PubSub["Pub/Sub subscriptions"]
  end
  subgraph Cloud["Asymptote"]
    Dashboard["Dashboard"]
  end
  Beacon -->|"uploads with create-only key"| Bucket
  Bucket -->|"new-object notice"| PubSub
  PubSub -->|"which objects are new"| Dashboard
  Bucket -->|"reads with Asymptote's own account"| Dashboard
```

Beacon on each Mac records agent activity and uploads it to your bucket with a key that can only create objects. For each new object, the bucket posts a notice to Pub/Sub. Asymptote reads the subscriptions to learn which objects are new, fetches them with its own service account, and loads them into your dashboard. No Asymptote key is involved, and the bucket stays yours and keeps every object.

In your Google Cloud project:

* **A new Cloud Storage bucket just for Beacon**, with uniform bucket-level access, public access prevention, and versioning.
* **`beacon-endpoint-writer`**, a service account your Macs upload with. It can create objects below your Beacon prefix and nothing else.
* **Read access for Asymptote's reader**, `beacon-clickpipes-reader@asymptote-labs.iam.gserviceaccount.com`, on the bucket.
* **A Pub/Sub topic and two subscriptions**, one for agent activity (`runtime`) and one for inventory. They let Asymptote load objects in any order, so an upload that arrives late is never skipped.

<Note>
  Asymptote receives the telemetry your Macs upload in full, after Beacon's local redaction of secrets: prompts, the commands agents ran and their output, tool inputs and outputs, and file paths and diffs.
</Note>

## Before you start

| You need | Why |
| - | - |
| A Google Cloud project, and an identity that can enable APIs, create buckets, service accounts and Pub/Sub resources, and set IAM | Step 1 |
| Terraform 1.5 or later with the Google provider 5.0 or later, or gcloud | Step 1 |
| Permission to create service-account keys | Beacon's forwarder authenticates with a JSON key. If your organization policy blocks keys, it can't run on your Macs |
| An Asymptote organization | Asymptote connects your bucket to it in step 3 |
| An Apple Silicon Mac to try it on, and Jamf Pro or Rippling to roll it out | Step 4 |

## 1. Create the bucket and access

Pick a root prefix such as `beacon-prod`. Beacon adds `runtime/` and `inventory/` below it, so don't put either in the prefix.

<Tabs>
  <Tab title="Terraform">
    Create a `main.tf` in a new directory. Change the project, location, bucket name, and prefix:

    ```hcl title="main.tf" theme={null}
    provider "google" {
      project = "acme-security"
    }

    # The bucket and the create-only service account your Macs upload with.
    module "beacon_gcs" {
      source = "github.com/asymptote-labs/agent-beacon//terraform/gcs?ref=v1.3.38"

      project     = "acme-security"
      location    = "us-west1"
      bucket_name = "acme-beacon-telemetry"
      prefix      = "beacon-prod"
    }

    # Read access for Asymptote, with the new-object subscriptions.
    module "asymptote_reader" {
      source = "github.com/asymptote-labs/agent-beacon//terraform/gcs-asymptote-reader?ref=v1.3.38"

      bucket = module.beacon_gcs.bucket
    }

    output "bucket_name" {
      value = module.beacon_gcs.bucket_name
    }

    output "prefix" {
      value = module.beacon_gcs.prefix
    }

    output "writer_email" {
      value = module.beacon_gcs.writer_email
    }

    output "asymptote_subscriptions" {
      value = module.asymptote_reader.subscriptions
    }
    ```

    Apply it:

    ```bash theme={null}
    terraform init
    terraform apply
    ```

    Review the plan and type `yes`. It's working when the apply ends with `Apply complete!` and prints the outputs. Keep them for step 3. Your own Cloud KMS key, object expiry, and other options are in the base module's [`variables.tf`](https://github.com/asymptote-labs/agent-beacon/blob/main/terraform/gcs/variables.tf).
  </Tab>

  <Tab title="gcloud">
    Create the bucket and the writer by following [step 1 of Claude with Jamf and GCS](/guides/jamf-gcs-mdm#1-prepare-google-cloud), gcloud tab. Then give Asymptote read access and create the subscriptions:

    ```bash theme={null}
    export GCP_PROJECT="<project-id>"
    export BEACON_GCS_BUCKET="<bucket>"
    export BEACON_GCS_PREFIX="beacon-prod"
    export ASYMPTOTE_READER="serviceAccount:beacon-clickpipes-reader@asymptote-labs.iam.gserviceaccount.com"
    export ASYMPTOTE_TOPIC="asymptote-${BEACON_GCS_BUCKET}-objects"

    # Let Asymptote read the bucket.
    gcloud storage buckets add-iam-policy-binding "gs://${BEACON_GCS_BUCKET}" \
      --member "$ASYMPTOTE_READER" --role roles/storage.objectViewer

    # Publish every new object below the prefix to a topic.
    gcloud pubsub topics create "$ASYMPTOTE_TOPIC" --project "$GCP_PROJECT"
    gcloud pubsub topics add-iam-policy-binding "$ASYMPTOTE_TOPIC" --project "$GCP_PROJECT" \
      --member "serviceAccount:$(gcloud storage service-agent --project "$GCP_PROJECT" | tr -d '[:space:]')" \
      --role roles/pubsub.publisher
    gcloud storage buckets notifications create "gs://${BEACON_GCS_BUCKET}" \
      --topic "projects/${GCP_PROJECT}/topics/${ASYMPTOTE_TOPIC}" \
      --event-types OBJECT_FINALIZE \
      --object-prefix "${BEACON_GCS_PREFIX}/" \
      --payload-format json

    # One subscription per stream that Asymptote may consume. They never expire.
    for stream in runtime inventory; do
      gcloud pubsub subscriptions create "asymptote-${BEACON_GCS_BUCKET}-${stream}" \
        --project "$GCP_PROJECT" \
        --topic "$ASYMPTOTE_TOPIC" \
        --message-filter "attributes.eventType = \"OBJECT_FINALIZE\" AND hasPrefix(attributes.objectId, \"${BEACON_GCS_PREFIX}/${stream}/\")" \
        --ack-deadline 60 \
        --message-retention-duration 7d \
        --expiration-period never
      for role in roles/pubsub.subscriber roles/pubsub.viewer; do
        gcloud pubsub subscriptions add-iam-policy-binding "asymptote-${BEACON_GCS_BUCKET}-${stream}" \
          --project "$GCP_PROJECT" --member "$ASYMPTOTE_READER" --role "$role"
      done
    done
    ```
  </Tab>
</Tabs>

## 2. Create the writer's key

Create the key with gcloud, so the private key never enters Terraform state. Use the `writer_email` output, or the writer's email from the gcloud steps:

```bash theme={null}
umask 077
gcloud iam service-accounts keys create ./beacon-endpoint-writer.json \
  --iam-account "beacon-endpoint-writer@<project-id>.iam.gserviceaccount.com"
```

Keep the file for step 4. On one Mac you install it directly; with an MDM, you deliver it to each Mac with the MDM's secret or file delivery, to a root-owned path outside Beacon's directories with mode `0600`, and then delete your local copy. Wait about a minute before a Mac first uses a brand-new key; Google rejects it for the first few seconds.

## 3. Send Asymptote the details

Send your Asymptote contact:

* the bucket name and prefix;
* the two subscriptions: `asymptote_subscriptions`, or `projects/<project-id>/subscriptions/asymptote-<bucket>-runtime` and `-inventory`.

No key is involved. Asymptote connects the bucket to your organization and tells you when it's done.

## 4. Roll out Beacon to your Macs

Try it on one Mac first, or go straight to your MDM.

<Tabs>
  <Tab title="One Mac (no MDM)">
    **Install Beacon.** On an Apple Silicon Mac, in Terminal while logged in at the Mac:

    ```bash title="Download, verify, and install" theme={null}
    VERSION="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/asymptote-labs/agent-beacon/releases/latest)"; VERSION="${VERSION##*/v}"
    PKG="BeaconEndpointAgent-${VERSION}-arm64.pkg"
    BASE="https://github.com/asymptote-labs/agent-beacon/releases/download/v${VERSION}"
    curl -fsSLO "${BASE}/${PKG}" && curl -fsSLO "${BASE}/${PKG}.sha256"
    test "$(awk '{print $1}' "${PKG}.sha256")" = "$(shasum -a 256 "${PKG}" | awk '{print $1}')" \
      && pkgutil --check-signature "${PKG}" \
      && sudo installer -pkg "${PKG}" -target /
    ```

    The block checks the package against its published SHA-256 and its Apple signature before installing. `pkgutil` should print `Status: signed by a developer certificate issued by Apple for distribution`. The package sets up Claude Code and Codex for the logged-in user; [Try Beacon on One Mac](/mdm/try-on-one-mac) covers what it installs and how to add other agents.

    **Put the writer's key where the forwarder can use it**, in a root-only location outside Beacon's directories:

    ```bash theme={null}
    export GOOGLE_APPLICATION_CREDENTIALS="/Library/Application Support/Your Organization/Secrets/beacon-gcs-writer.json"

    sudo install -d -m 0700 -o root -g wheel "$(dirname "$GOOGLE_APPLICATION_CREDENTIALS")"
    sudo install -m 0600 -o root -g wheel ./beacon-endpoint-writer.json "$GOOGLE_APPLICATION_CREDENTIALS"
    rm ./beacon-endpoint-writer.json
    ```

    **Start the forwarder** with your values from step 1:

    ```bash theme={null}
    sudo env \
      BEACON_GCS_BUCKET="<bucket>" \
      BEACON_GCS_PREFIX="<prefix>" \
      BEACON_GCS_STORAGE_CLASS="STANDARD" \
      GOOGLE_APPLICATION_CREDENTIALS="$GOOGLE_APPLICATION_CREDENTIALS" \
      /opt/beacon/jamf/claude/gcs/install-forwarder.sh
    ```

    It's working when the output shows the forwarder's `state = running`. When you roll out with an MDM later, this Mac gets the same setup again.
  </Tab>

  <Tab title="Jamf or Rippling">
    | Value | Jamf script parameter | `install-forwarder.sh` |
    | - | - | - |
    | Bucket name | 4 | `BEACON_GCS_BUCKET` |
    | Prefix | 5 | `BEACON_GCS_PREFIX` |
    | Storage class (`STANDARD`) | 6 | `BEACON_GCS_STORAGE_CLASS` |
    | Path of the key file on the Mac | 8 | `GOOGLE_APPLICATION_CREDENTIALS` |

    Then follow your MDM's guide from the step after its Google Cloud setup, which you've already done:

    | Rollout | Continue at |
    | - | - |
    | Jamf Pro | [Claude with Jamf and GCS, step 2](/guides/jamf-gcs-mdm#2-deliver-the-writer-credential) |
    | Rippling | [Claude with Rippling and GCS, step 2](/guides/rippling-gcs-mdm#2-create-and-deliver-the-writer-credential), from "Use this endpoint path" |
  </Tab>
</Tabs>

## 5. Confirm it works

On a Mac with Beacon installed, write a test event:

```bash theme={null}
sudo /opt/beacon/bin/beacon endpoint gcs validate --system
```

The forwarder uploads in batches, so allow up to five minutes. Then list the objects with your own identity, because the writer can't list the bucket:

```bash theme={null}
gcloud storage ls "gs://<bucket>/<prefix>/runtime/**"
```

Asymptote loads each new object within a few minutes of it landing. Your Asymptote contact confirms when the first events arrive.

## Operate

### Rotate the writer's key

Create a new key, deliver it to every Mac, restart the forwarder, confirm a new object arrives, and then delete the old key. Your MDM guide has the exact commands under credential rotation.

### Remove

1. Ask Asymptote to disconnect the bucket.
2. Remove the forwarder from your Macs. Uninstalling Beacon removes it.
3. Run `terraform destroy`. Deleting the writer service account also deletes its keys. Destroy stops at a bucket that still holds objects. To delete the telemetry too, add `force_destroy = true` to the `beacon_gcs` module, run `terraform apply`, and then destroy.

## Troubleshooting

<AccordionGroup>
  <Accordion title="The forwarder setup fails right after creating the key">
    A brand-new service-account key is rejected for the first few seconds (`Invalid JWT Signature`). Wait a minute and run the setup again.
  </Accordion>

  <Accordion title="Objects arrive in the bucket but not in Asymptote">
    Check that the subscriptions exist and that Asymptote's reader is a subscriber on both: `gcloud pubsub subscriptions get-iam-policy <subscription>`. Then send your Asymptote contact the subscription names.
  </Accordion>

  <Accordion title="The bucket name is already taken">
    Someone else, in any project, already has a bucket with that name. Choose another bucket name.
  </Accordion>
</AccordionGroup>

## Related

<Columns cols={2}>
  <Card title="AWS S3" icon="bucket" href="/deployment/self-hosted-s3">
    The same setup with an S3 bucket.
  </Card>

  <Card title="Claude with Jamf and GCS" icon="bucket" href="/guides/jamf-gcs-mdm">
    Roll out Beacon with Jamf Pro.
  </Card>

  <Card title="Google Cloud Storage with Terraform" icon="cloud" href="/log-forwarding/gcs-terraform">
    The bucket and writer on their own, without Asymptote reading them.
  </Card>

  <Card title="Hosting options" icon="server" href="/deployment/hosting-options">
    Compare Managed, Open Source, and Private Deployment.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.