Skip to content

feat(security): Vercel and GCP audit events go to the audit trail, not to logs - #2530

Open
Makisuo wants to merge 3 commits into
audit-trail/pagefrom
audit-trail/vercel-gcp
Open

Makisuo wants to merge 3 commits into
audit-trail/pagefrom
audit-trail/vercel-gcp

Conversation

@Makisuo

@Makisuo Makisuo commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Third layer of the unified audit log, on top of #2520: Vercel and Google Cloud audit events stop being logs.

Why

An audit event names a person, their email and their IP address. Stored as a log it was readable by every org member, reachable from raw SQL, dashboards and the agent, and writable by anything holding an ingest key: the Vercel panel selected rows purely by service.name = vercel-audit-log. The gateway now writes these to audit_trail_events, which only audit_log:read can read and only provider adapters can write.

What changed

Gateway (apps/ingest)

  • New audit_trail module turns Vercel audit drain events and GCP Admin Activity and Policy Denied entries into rows. The key derivation matches the TypeScript writer and both sides assert the same two vectors.
  • Off by default. INGEST_AUDIT_TRAIL_ENABLED turns it on. Off, or on a gateway that only forwards (self-hosted), both feeds stay logs exactly as before. Non-production stages set it; production sets it in its environment (see "Before merging").
  • Who may write. The Vercel audit drain has its own handler. It accepts only the drain Maple creates: the org's private key plus an x-maple-source-id naming one of the org's integration sources. The public key, a source token, a private key naming no source, and a key naming another org's source are refused with 401. Existing drains already send both, so they keep delivering.
  • Limits. Audit rows are not metered, so they are bounded instead:
    • a delivery is inflated and parsed on the blocking pool inside the shared decode memory budget, and may not exceed 4 MiB decoded;
    • an org gets 10 Vercel deliveries and 100 GCP entries a second, then 429 (the sender retries, so a burst is delayed and not lost);
    • every column is capped (256 bytes, or 1024 for free text), an over-long event id is hashed, and a payload over 32 KB is not kept;
    • a timestamp outside the table's 400 days, or more than a day ahead, is replaced by the time of receipt, so a sender cannot pick the partition or the expiry.
  • What stays a log. GCP System Event entries (Google's own actions on a workload) and Kubernetes API writes (k8s.io, mostly a cluster's own controllers, thousands an hour). Data Access entries were already filtered out by the sink.
  • A paused Vercel source is acked and dropped, as its logs are. A GCP audit entry is stored ahead of the plan check on logs and of the org's ingest rules.
  • Categories are read off the words of the action name, in an order that settles the double readings: a member's role change is permission, password protection is configuration, environment is not env. GCP reads the call itself (setIamPermissions, SetOrgPolicy, sink changes). A federated workload with only a principalSubject is a named service. Vercel rows fill the target from the action and its payload.

Web

  • The audit log page gets its platform filter, column and detail row back, with Vercel and Google Cloud named. Target shows from 1680 px up, so Outcome stays on screen beside the rail; it is always in the row's detail.
  • /infra/vercel no longer reads audit events from logs. When the drain is on, its rail points at the Security audit log. The query, endpoint and row schema behind the old panel are removed.
  • The Vercel and GCP setup copy and the public docs say where these events land.

Before merging

  1. Turn it on after the table exists. Production needs INGEST_AUDIT_TRAIL_ENABLED=true in the gateway's environment, set once audit_trail_events is in Tinybird (the rollout step on feat(security): audit_trail_events table, list endpoint and Maple's own events #2510) and this build is the one running. The gateway can deploy at any time before that: off, it behaves as today.
  2. Rolling back. Turn the switch off; do not roll the gateway back to a build before this one while it is on. Audit rows use a new WAL frame tag, and an older build that recovers a segment holding one drops the rest of that segment, other orgs' rows included. This is why the switch defaults to off.
  3. The gap between deploy and switch-on. The /infra/vercel audit panel is gone as soon as web deploys, and Vercel audit events are still logs until the switch is on. They stay readable in the logs explorer in between.
  4. Existing rules and dashboards. Anything filtering logs on vercel.audit.*, gcp.audit.* or the vercel-audit-log service stops matching new events. I could not check other orgs' alert rules and dashboards from here. That needs a look at production.
  5. Old entries. Audit events already stored as logs stay there, readable by every member, until logs retention removes them. Nothing copies them into the table and nothing purges them.

Not in this PR: verifying Vercel's x-vercel-signature with a per-drain secret. The sender check above stops every credential except the org's private key; a holder of that key who also knows the source id can still post an event. Closing that needs a stored drain secret.

Screenshots

Audit log, 1440 wide platforms
Audit log, 1760 wide (Target shows) wide
An event opened detail
Phone width

/infra/vercel rail, from the lab. The old group listed the three newest audit events to every member.

Before After
before after

Verified

  • cargo test --locked passes: 376 library tests and 213 binary tests. The gateway tests read the row that reaches a fake Tinybird: a Vercel delivery and a GCP entry each land in audit_trail_events with their org, event id and actor, and the GCP entry does not reach the org's logs. Also covered: the four refused senders, a paused source acked and dropped, a gzip delivery that inflates past the limit (413), the per-org rate, column caps, the timestamp window, the category tables, and k8s.io staying a log. Clippy is clean on the new module.
  • Typecheck passes for domain, query-engine-integrations, web-data, backend, api and web. oxlint -c .oxlintrc.effect.json is clean on the 33 changed TypeScript files.
  • Tests pass for the Vercel and GCP integration pages, the audit log page (25 scene tests), the lab, the routes, query-engine-integrations, the domain HTTP contracts and the API's internal routes.
  • ui:index, ui:patterns:check and the UI kit baseline are unchanged.

Known flake, not from this change: grpc_export_takes_gzip_within_the_http_body_limits failed in 2 of 20 local runs of the gateway's binary tests. It does not touch this code.

Not verified: against a real Vercel audit drain or a real GCP sink. preview:web has no ingest gateway, so this layer needs a full preview to exercise end to end. The category for a Vercel action is read off its name, because Vercel publishes no closed list; unrecognised actions are filed as resource changes.

…t to logs

An audit event names a person, their email and their IP address. As a log it was
readable by every member and writable by anything holding an ingest key. The
gateway now writes these to audit_trail_events, behind audit_log:read.

Gateway (apps/ingest):
- New audit_trail module: Vercel audit drain events and GCP Admin Activity and
  Policy Denied entries as rows, with the same key derivation as the TypeScript
  writer. GCP System Event entries are Google's own actions and stay logs.
- The Vercel audit drain has its own handler. It refuses the public ingest key,
  which ships in browser bundles, and acks and drops while the integration's
  source is paused.
- A GCP audit entry is stored ahead of the plan check on logs and is not metered.
- A gateway that only forwards has no such table: there both feeds stay logs.

Web:
- The audit log page gets its platform filter, column and detail row back, with
  Vercel and Google Cloud named. Target shows on wide screens only, so Outcome
  stays on screen beside the rail.
- /infra/vercel no longer reads audit events from logs. Its rail points at the
  Security audit log when the drain is on. The query, endpoint and row schema
  behind the old panel are removed.
- The Vercel and GCP setup copy and the public docs say where these events land.
@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3ae1e965-f297-4de0-85fd-b836f25b455a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@maple-review-bot

This comment has been minimized.

…ded and off by default

Review of the Vercel and GCP layer:

- The Vercel audit drain accepted any private key or source token. It now
  takes only the drain Maple creates: the private key naming one of the org's
  integration sources.
- A delivery is decoded inside the shared decode memory budget and capped at
  4 MiB; a small gzip could otherwise parse into gigabytes.
- Audit rows are unmetered, so each org has a delivery rate, every column a
  cap, and a timestamp outside the table's retention is replaced by the time
  of receipt.
- Kubernetes API writes stay logs. Categories are matched on whole words, a
  federated workload is a named service, and Vercel rows carry a target.
- INGEST_AUDIT_TRAIL_ENABLED (default off) turns the trail on, so the gateway
  can deploy before the table exists and is switched off rather than rolled
  back past the new WAL frame tag.
- The handler records request metrics like the other ingest routes.
- Tests read the stored row instead of the returned count.
@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 inline note from Maple's review. The score and summary are in the review comment above.

Comment thread apps/ingest/src/main.rs Resolved
… to retry

verify_source_claims left a claim unverified when the store could not be
asked, and the audit handler read that as an unknown sender (401). The drain
then looked misconfigured during a Postgres blip. The lookup now reports the
failure and the handler answers 503, as the key lookup does.
@maple-review-bot

maple-review-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge. Both audit parsers and every refusal path of the new drain handler are pinned by tests; only the prd flag ordering has to be honoured at deploy.
no findings · tests covered · risk high · 3/3 new units observable

Vercel audit-drain and GCP Admin Activity/Policy Denied entries now land in audit_trail_events behind INGEST_AUDIT_TRAIL_ENABLED instead of as logs, and the web pages follow: the audit log regains its platform column and filter, /infra/vercel links to it and loses the old log query. The refusal path is tight and tested; safe to merge once the flag is set after the table exists.

  • audit_trail turns Vercel drain and GCP audit entries into warehouse rows, bounded by column caps, a 4 MiB inflate limit and per-org rates
  • New POST /v1/vercel/audit-log handler accepts only the org's private key plus one of its own source ids
  • Audit log page regains the platform filter, Platform column and detail rows; Target moves to 1680 px
  • Vercel audit-log log query, endpoint and row schema removed; the overview rail links to the audit log instead

Before merge

Fixed since the last review

  • ✅ F2 · A claim-store error refuses Vercel audit deliveries with 401, not 503

Production impact

Open errors in the changed files (2)
Issue Service Occurrences File
@maple/mcp/errors/McpQueryError (8 issues) maple-ai 33 packages/backend/src/queries/registry.ts
@maple/mcp/errors/McpQueryError (2 issues) maple-chat 9 packages/backend/src/queries/registry.ts

After this merges, Maple checks whether they stop.

10153a2 · What was checked, coverage and telemetry details are on the Maple / review check. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo Makisuo added the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Nov 12, 2026
@Makisuo
Makisuo deployed to preview-tinybird November 12, 2026 14:50 — with GitHub Actions Active
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

🍁 Maple PR preview

Warning

Preview cleanup could not be confirmed. The Alchemy teardown outcome was failure.

Final commit 10153a2 · View workflow run

@Makisuo Makisuo removed the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Nov 12, 2026
@Makisuo
Makisuo deployed to preview-tinybird November 12, 2026 15:01 — with GitHub Actions Active

This branch had an error being deployed

1 failed and 1 active deployments
preview-tinybird — 10153a2d Deployed Nov 12, 2026 by Makisuo via branch #1225
pr-preview — 10153a2d Deployed Nov 12, 2026 by Makisuo via deploy-pr-preview #11656
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant