Static write-only telemetry endpoints are an attack runway for agentic systems. Replace them with an ephemeral JWT gate in front of your collector.
This is Part 1 of Hardened Agentic Stack — a trust-boundary series for high-privilege agents. We start where many teams leave the door open: telemetry ingest.
What Happened at Nutrient
In June 2026, Nutrient disclosed a security incident that nearly turned their observability stack into an attack runway. An attacker found a public Sentry DSN in the JavaScript bundle, submitted a fake bug report containing a malicious “runbook” with an npx command designed to exfiltrate secrets, and relied on an AI coding agent that read the Sentry feed as part of automated incident response.
No credentials. No repo access. Just a static public write endpoint and an agent that treated attacker-controlled text as instructions.
The serverless observability post covers the full LGTMP replacement stack. This post extracts the ingest lesson and turns it into the first control of a hardened agentic deployment.
The Problem: Static DSNs Are Write-Only Trust
A classic DSN looks harmless:
https://[email protected]/789
It’s intentionally public. Browser SDKs can’t hold server secrets, so vendors made ingest write-only and accepted that anyone who can read your bundle can also write events.
For years the blast radius was mostly spam or reconnaissance. Agents change the economics. Observability feeds become instruction channels. Fake issues, fake logs, and fake traces become prompt-injection payloads. “Write-only” stops meaning “low risk” once something autonomous reads and acts on what gets written there.
The architectural failure isn’t “Sentry exists.” Sentry behaved as documented. The failure is treating unauthenticated, attacker-writable telemetry as trusted input for tools that can execute.
Ingest that anyone can poison is a primary attack surface.
ClawQL Context: Why Ingest Is Step Zero
ClawQL agents routinely read issues, traces, and logs while diagnosing failures; call tools that can change real systems; and carry memory across sessions. A poisoned observability event isn’t a dashboard inconvenience — it’s a potential tool-invocation precursor.
Before you argue about sandboxes, eBPF kill-switches, or prompt scrubbing (later posts in this series), close the cheap path: nobody outside your auth boundary should be able to write into the feeds your agents read.
The Infrastructure Fix: Ephemeral JWT Gate
Replace the permanent writable URL with a token-gated edge proxy.
| Flow | Path | Who can write |
|---|---|---|
| Legacy | Client → Collector (static DSN) | Anyone who scrapes the bundle |
| Hardened | Client → Auth → short-lived JWT → Worker gate → private collector | Only holders of a live, scoped JWT |
The Worker (Cloudflare or equivalent) is a zero-trust chokepoint: validate, rate-limit, optionally sanitize, then forward — or drop.
JWT shape (issue from a backend that already knows the user/session):
{
"sub": "session_abc123",
"project": "frontend-prod",
"origin": "https://app.example.com",
"scope": ["telemetry:write"],
"iat": 1717600000,
"exp": 1717603600
}
Anyone can decode the payload. Nobody can forge a valid signature without the private signing key. Expiry is the control DSN never had: compromise windows shrink from “until we rotate the global secret” to “until this token dies.”
Recommended defaults for browser RUM / SDK ingest: TTL 5–60 minutes, bind origin / project claims and enforce them in the Worker, rotate signing keys on a schedule with overlapping verify keys during rollout, prefer asymmetric verify in the Worker so the edge never holds the minting key.
Request path: page/session start → backend mints a JWT after authenticating the session → SDK points at the Worker URL with Authorization: Bearer <jwt> → capture → Worker validates signature, exp, claims, content-type, body size, rate limits → valid traffic goes to a private collector endpoint; failures return a boring 204 with no error detail attackers can iterate on.
Gate sketch:
export default {
async fetch(req: Request, env: Env): Promise<Response> {
if (req.method !== 'POST') return new Response(null, { status: 204 });
const token = bearer(req);
if (!token) return new Response(null, { status: 204 });
let claims: Claims;
try {
claims = await verifyJwt(token, env.JWT_PUBLIC_JWKS);
} catch {
return new Response(null, { status: 204 });
}
if (claims.exp * 1000 < Date.now()) return new Response(null, { status: 204 });
if (!allowOrigin(req.headers.get('Origin'), claims.origin)) {
return new Response(null, { status: 204 });
}
if (!(await rateLimit(env, claims.sub))) return new Response(null, { status: 204 });
const body = await req.arrayBuffer();
if (body.byteLength > env.MAX_BODY_BYTES) return new Response(null, { status: 204 });
const cleaned = scrubControlSequences(body);
await fetch(env.PRIVATE_COLLECTOR_URL, {
method: 'POST',
headers: {
'content-type': req.headers.get('content-type') ?? 'application/json',
'x-project': claims.project,
'x-session': claims.sub,
},
body: cleaned,
});
return new Response(null, { status: 204 });
},
};
Sanitization at the Gate
Closing unauthenticated write is necessary but not sufficient. Payloads that clear auth can still carry control text that later modules scrub more deeply. At ingest, keep it cheap and deterministic: reject unexpected content types and oversized bodies; strip common instruction-marker patterns from string fields destined for agent-readable stores; prefer fail closed to the collector rather than “best effort keep the event.”
Deeper dual-model scrubbing and ATR/Panguard enforcement come later in the series. Here the job is: no anonymous poison into the pipe.
The Architecture Pattern: Ephemeral Proxy
Ephemeral Proxy: mint short-lived, scoped credentials → validate at an edge gate → forward only into private infrastructure.
Properties that matter: no ambient write trust (knowing the URL is not enough); scoped claims (a frontend token doesn’t grant agent-admin telemetry scopes); boring failures (don’t teach attackers your validation order); composable (the same gate idea applies to OTLP, Faro, custom webhooks, and agent-side diagnostic uploads).
This is also why a serverless edge function is the right shape for the gate: ephemeral, globally distributed, paid per invocation, and stateless. The gate doesn’t need to be a control plane — it needs to be a chokepoint.
What This Covers and What It Doesn’t
| Threat | Outcome with Ephemeral Proxy |
|---|---|
| Stranger scraped a DSN and injects fake issues | Blocked — no valid JWT |
| Stolen long-lived ingest key reused for months | Dramatically reduced — keys are short-lived session tickets |
| Malicious logged-in user copies their own JWT | Still possible until expiry — detect via residual signals |
| Prompt injection via a legitimate but hostile document the agent opens | Not solved here — later posts |
Residual authenticated abuse needs visibility: unmatched client errors vs server traces, volume spikes, and agent tool-use anomalies (Parts 8–10). Part 1 removes the free anonymous runway.
Getting Started
You’re in a healthier place when: browser and public clients cannot reach Alloy / Sentry / OTLP without a live JWT; minting requires an authenticated session or equivalent machine identity; the Worker verifies signature, expiry, and binding claims on every write; private collector endpoints are network-private (VPC, tunnel, or mTLS) — not “security through an obscure hostname”; agents that read telemetry assume the feed can still contain hostile content from authenticated writers.
Part 2 moves from “how telemetry gets in” to “how agents hold secrets at all”: secret rotation patterns, bootstrap tickets, Vault/Cloudflare exchange, and why long-lived .env values don’t belong on a high-privilege agent host.
Companion implementation: DevSecOps-boilerplate. Docs: Authentication and scoped tokens · Gateway hardening · Input validation.
