Supply‑Chain Security for Agentic Browsers: Sigstore‑Signed Auto‑Agent AI Browser Builds, Verified Agent Switchers, and Verifiable “What Is My Browser Agent” Telemetry
Agentic browsers — autonomous or semi‑autonomous agents operating a real browser via Playwright, Puppeteer, WebDriver, or CDP — are crossing from research to production. They do data extraction, testing, accessibility audits, RPA, and LLM‑driven research. The operational benefit is clear; the supply‑chain risk is higher than many teams realize: a compromised agent build or extension can leak credentials, exfiltrate session tokens, falsely report capabilities, or turn an enterprise crawler into an internal DDoS cannon.
This article is a practical, opinionated blueprint for shipping trustworthy agent browsers end‑to‑end:
- Reproducible builds for browsers, extensions, and CDP proxies
- Provenance with SLSA, in‑toto layouts, and Sigstore (Fulcio, Rekor, Cosign)
- SBOM generation and policy checks for agent extensions and CDP proxies
- Locked User‑Agent and Client Hints configurations (and who should own them)
- Verifiable “what is my browser agent” telemetry with signed proofs
The audience is technical. Expect specifics, code, and trade‑offs you can implement next week.
1) Why agentic browsers need a supply‑chain threat model
Agentic browsers are more than a binary; they’re a chain:
- The browser runtime (Chromium/Firefox/WebKit, often containerized)
- An automation layer (Playwright/Puppeteer or WebDriver/CDP)
- Extensions that add scraping, auth, or augmentation
- A CDP/WebSocket proxy that brokers commands/policy (optional but recommended)
- A controller process (LLM orchestrator, scheduler, retriever)
- Tooling and dependencies pulled from public registries (npm, PyPI, crates, Go modules)
Key risks:
- Build pipeline compromise or dependency poisoning inserts backdoors into headless images
- Malicious extensions or patched CRX bundles steal cookies and password data
- CDP/WebSocket MITM injects or exfiltrates sensitive method calls (e.g., Network.getCookies)
- UA/client‑hint drift or spoofing creates non‑determinism and bypasses policy
- Telemetry that’s not cryptographically bound to the build can be faked by malware
Opinion: If your agent can click “Sign in,” it deserves the same supply‑chain rigor as production mobile apps or CI runners. Treat the browser+agent as a deployable artifact with verifiable provenance.
2) Design goals: ship trustworthy agents end‑to‑end
- Determinism: Bit‑for‑bit reproducible builds where feasible; otherwise, deterministic metadata and content‑addressed artifacts
- Verifiable provenance: SLSA provenance attestations anchored in in‑toto layouts, signed with Sigstore and recorded in a transparency log (Rekor)
- SBOM and policy: Generate SPDX/CycloneDX for the browser image, extension set, and CDP proxy; enforce policy via OPA/Rego or similar
- UA/Client Hints stability: Lock down UA and UA‑CH to a vetted profile; expose a documented switcher policy; reject runtime drift
- Verifiable telemetry: A nonce‑based signed report (“what is my browser agent”) that proves identity, build digest, and configuration state
- Privacy by design: Proofs reveal the minimum required; support enterprise modes and local‑only operation
3) Reproducible builds for agent browsers
Absolute bit‑for‑bit reproducibility for whole browsers is challenging, but you can get close:
- Pin everything: container base image digests, compiler versions, Node/Go/Python lockfiles, Playwright channel versions
- Hermetic builds: Use Nix, Bazel, or apko/melange to lock toolchains and strip non‑deterministic metadata
- Normalize timestamps and ordering in archives; use SOURCE_DATE_EPOCH
- Verify with diffoscope across two independently built artifacts
A minimal container approach using distroless/apko and pinned layers:
Dockerfile# syntax=docker/dockerfile:1.6 FROM --platform=linux/amd64 gcr.io/distroless/cc-debian12@sha256:3c7f... as base # Install Chromium with a fixed version (example uses Debian snapshot mirror) FROM debian:12@sha256:0b57... RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates wget gnupg \ && echo "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20240901/ bookworm main" > /etc/apt/sources.list \ && apt-get update \ && apt-get install -y chromium=116.0.5845.96-1~deb12u1 \ && rm -rf /var/lib/apt/lists/* # Copy a prebuilt, reproducible Playwright agent binary and static extension bundle COPY --chown=nonroot:nonroot agent /usr/local/bin/agent COPY --chown=nonroot:nonroot extensions.crxbundle /opt/agent/extensions.crxbundle # Normalize timestamps ENV SOURCE_DATE_EPOCH=1725148800 # Strip symbol tables, normalize permissions, etc. RUN chmod 0555 /usr/local/bin/agent && chmod 0444 /opt/agent/extensions.crxbundle USER nonroot:nonroot ENTRYPOINT ["/usr/local/bin/agent"]
For Node‑based agents, lock npm via package-lock.json and use npm ci --ignore-scripts to blunt post‑install surprises. For Python, pin hashes in requirements.txt and use pip --require-hashes.
Nix can raise the bar further. A simplified flake for Playwright:
nix{ description = "Reproducible agentic browser"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/23.11"; outputs = { self, nixpkgs }: let pkgs = import nixpkgs { system = "x86_64-linux"; }; in { packages.x86_64-linux.agent = pkgs.stdenv.mkDerivation { name = "agent"; src = ./.; buildInputs = [ pkgs.nodejs_20 pkgs.playwright-driver.chromium ]; installPhase = '' mkdir -p $out/bin cp dist/agent $out/bin/agent ''; }; }; }
Reproducibility is a spectrum. Even if you can’t fix Chromium’s exact bytes, you can make your agent binary, CDP proxy, and extension bundle reproducible and content‑addressed. That enables meaningful provenance and verification later.
4) Supply‑chain provenance with SLSA, in‑toto, and Sigstore
SLSA (Supply‑chain Levels for Software Artifacts) defines provenance; in‑toto defines a layout of trusted steps; Sigstore provides easy signing with OIDC identities and a transparency log.
A minimal in‑toto layout that constrains who can build and how:
json{ "_type": "layout", "expires": "2030-01-01T00:00:00Z", "keys": { "builder": {"keyid_hash_algorithms": ["sha256"], "keyid": "abc...", "keyval": {"public": "-----BEGIN PUBLIC KEY-----..."}} }, "steps": [ { "name": "build", "expected_materials": ["ALLOW *"], "expected_products": ["ALLOW agent@sha256:*", "ALLOW extensions.crxbundle@sha256:*"], "pubkeys": ["builder"], "expected_command": ["bazel", "build", "//:agent"], "threshold": 1 } ] }
SLSA provenance v1 attestation (DSSE‑wrapped) can be attached to each build. Using Cosign keyless signing with GitHub Actions (OIDC):
yamlname: build-and-attest on: [push] jobs: build: runs-on: ubuntu-latest permissions: id-token: write # for keyless signing contents: read steps: - uses: actions/checkout@v4 - name: Set up Cosign uses: sigstore/cosign-installer@v3.5.0 - name: Build agent run: | ./scripts/build_agent.sh # outputs ./dist/agent sha256sum dist/agent > dist/agent.sha256 - name: Attach SLSA provenance run: | cosign attest --type slsaprovenance \ --predicate ./slsa-provenance.json \ --keyless \ dist/agent - name: Sign artifact digest and upload to Rekor run: | cosign sign-blob --keyless --output-signature dist/agent.sig dist/agent
A skeleton SLSA predicate (trimmed for space):
json{ "_type": "https://slsa.dev/provenance/v1", "subject": [{"name": "agent", "digest": {"sha256": "<digest>"}}], "predicateType": "https://slsa.dev/provenance/v1", "predicate": { "buildType": "https://slsa.dev/build-type/container", "builder": {"id": "https://github.com/yourorg/yourrepo/.github/workflows/build.yml@refs/heads/main"}, "invocation": { "configSource": { "uri": "https://github.com/yourorg/yourrepo", "digest": {"sha1": "<commit>"} }, "parameters": {"target": "agent"} }, "materials": [ {"uri": "pkg:npm/playwright@1.41.2", "digest": {"sha256": "..."}}, {"uri": "pkg:deb/debian/chromium@116.0.5845.96?distro=debian-12", "digest": {"sha256": "..."}} ] } }
Verification downstream:
bashcosign verify-blob \ --signature dist/agent.sig \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp "https://github.com/yourorg/yourrepo/.github/workflows/build" cosign verify-attestation \ --type slsaprovenance dist/agent
Thanks to keyless signing, your build pipeline identity (OIDC) is bound into a short‑lived certificate (Fulcio) and recorded in Rekor. Consumers can enforce “only accept artifacts built by our GitHub Action on the main branch.”
References:
- Sigstore (Cosign, Fulcio, Rekor): https://sigstore.dev/
- SLSA: https://slsa.dev/
- in‑toto: https://in-toto.io/
5) Extension and CDP proxy SBOM checks
Extensions and CDP proxies wield powerful privileges — they deserve first‑class SBOMs and policies.
- Generate SPDX or CycloneDX from each component (agent binary, extension bundle, CDP proxy)
- Sign SBOMs and attach as attestations to the corresponding artifact digest
- Enforce allow‑lists for extension IDs, versions, and cryptographic publisher identities
- Validate extension CRX signatures offline and pin the publisher key when possible
Generate SBOMs with Syft and scan with Grype:
bashsyft packages dir:./dist -o spdx-json > dist/sbom.spdx.json cosign attest --type spdxjson --predicate dist/sbom.spdx.json --keyless dist/agent grype sbom:dist/sbom.spdx.json --fail-on high
For Chrome extensions:
- Prefer CRX v3 packages downloaded from the Chrome Web Store
- Record the extension ID, version, and the CRX public key hash
- Verify CRX signature at install time; reject unpacked sources in production
Bundle a curated set of extensions into a deterministic archive (e.g., extensions.crxbundle) with a manifest listing IDs and publisher keys:
json{ "bundleVersion": 1, "extensions": [ { "id": "aabbccddeeffgghhiijjkkllmmnnoopp", "version": "1.2.3", "publisherKeySha256": "f0c3...", "sha256": "e4b7..." } ] }
Attach an SBOM and sign the bundle as an artifact. Your agent should only load extensions from this bundle after signature verification.
CDP proxy as a policy and audit control point
A CDP/WebSocket proxy between the agent controller and the browser can:
- Allowlist sensitive methods (e.g., block
Page.addScriptToEvaluateOnNewDocumentunless signed) - Redact/deny cookies and credentials movement
- Produce an immutable audit log (hash‑chained) for compliance
- Expose a metrics/trace surface for “what is my browser agent” proofs
A minimal Go proxy that filters methods:
gopackage main import ( "encoding/json" "log" "net/http" "net/url" "time" "github.com/gorilla/websocket" ) type CDPMsg struct { ID int64 `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params"` } var allowed = map[string]bool{ "Page.navigate": true, "Input.dispatchKeyEvent": true, "Input.dispatchMouseEvent": true, "Runtime.evaluate": false, // require explicit opt-in } func main() { http.HandleFunc("/", handle) log.Fatal(http.ListenAndServe(":9223", nil)) } func handle(w http.ResponseWriter, r *http.Request) { // Upstream CDP endpoint (Chromium) upstream, _ := url.Parse("ws://127.0.0.1:9222/devtools/browser/<id>") c, _, err := websocket.DefaultDialer.Dial(upstream.String(), nil) if err != nil { http.Error(w, err.Error(), 502); return } up := websocket.Upgrader{ CheckOrigin: func(*http.Request) bool { return true } } client, err := up.Upgrade(w, r, nil) if err != nil { return } // client -> browser go func() { for { _, msg, err := client.ReadMessage() if err != nil { return } var m CDPMsg if err := json.Unmarshal(msg, &m); err == nil { if ok, exists := allowed[m.Method]; !exists || !ok { log.Printf("deny method=%s id=%d", m.Method, m.ID) // Optionally return an error response to client continue } } c.WriteMessage(websocket.TextMessage, msg) } }() // browser -> client for { _, msg, err := c.ReadMessage() if err != nil { return } // hash-chain log could be appended here with timestamp _ = time.Now() client.WriteMessage(websocket.TextMessage, msg) } }
Attach an SBOM and provenance to the proxy binary as well. You now have a verifiable control point, not just a convenience shim.
6) Lock UA and Client Hints (and verify they’re locked)
UA strings and Client Hints determine server behavior and often gate bot detection. For trustable agents, drifting UAs are a liability. Define a stable profile and enforce it at multiple layers:
- Launch‑time flags:
--user-agent,--accept-ch, and language/locale switches - CDP overrides: Network conditions and UA override methods
- JavaScript hardening: Freeze
navigator.userAgent,navigator.platform, etc. - HTTP headers: Explicit
Sec-CH-UA,Sec-CH-UA-Platform,Sec-CH-UA-Model, etc.
Playwright example that sets UA and CH deterministically:
tsimport { chromium, devices } from 'playwright'; const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'; const chHeaders = { 'Sec-CH-UA': '"Chromium";v="116", "Not.A/Brand";v="8", "Google Chrome";v="116"', 'Sec-CH-UA-Platform': '"Windows"', 'Sec-CH-UA-Mobile': '?0', 'Sec-CH-UA-Arch': '"x86"' }; (async () => { const browser = await chromium.launch({ args: [ `--user-agent=${UA}`, '--lang=en-US,en', ]}); const context = await browser.newContext({ userAgent: UA, locale: 'en-US', extraHTTPHeaders: chHeaders, }); // Freeze navigator to prevent drift await context.addInitScript(() => { const freeze = (obj: any, prop: string, value: any) => { Object.defineProperty(obj, prop, { value, configurable: false, writable: false }); }; freeze(navigator, 'userAgent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...'); freeze(navigator, 'platform', 'Win32'); freeze(navigator, 'language', 'en-US'); freeze(navigator, 'languages', ['en-US','en']); }); const page = await context.newPage(); await page.goto('https://example.com'); })();
At the proxy layer, reject attempts to change UA outside authorized flows (e.g., calls to Emulation.setUserAgentOverride). Document an allowed set of UA switches (“switchers”) and sign them via policy.
OPA/Rego policy sketch to enforce UA consistency in the proxy:
regopackage cdp.authz default allow = false # Allowed UA values uas := { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" } allow { input.method == "Emulation.setUserAgentOverride" uas[input.params.userAgent] # Optionally require a signed ticket in input.meta.proof } allow { input.method == "Page.navigate" }
Opinion: Resist the temptation to “randomize UA” in production. Stability plus verifiable change control beats feigned stealth, especially for enterprise integrations and site relationships. If you must switch, define versioned, signed switchers and publish them as policy changes.
7) Verifiable “what is my browser agent” proof telemetry
Classic “what is my browser” pages echo headers and JS properties. For agents, we want cryptographic, replay‑resistant proofs:
- A server issues a nonce and expected claim schema
- The agent measures its state (artifact digests, UA/CH, extension list, proxy fingerprint)
- The agent signs a statement with Sigstore keyless (OIDC identity) or a short‑lived SPIFFE/SPIRE identity
- The server verifies the signature and Rekor inclusion, checks attestation references, and returns a verdict/report
Protocol sketch:
- Client requests a challenge
httpPOST /api/agent/prove HTTP/1.1 Host: telemetry.example.com Content-Type: application/json {"wanted": ["ua", "ua_ch", "extensions", "artifact_digests"], "aud": "web"}
Server returns a nonce and policy version:
json{"nonce":"ce9e1b1a-...","policy":"v2024.09","exp": 1725600000}
- Agent collects and signs claims. Example JWS payload:
json{ "iss": "https://github.com/yourorg/yourrepo/actions", "aud": "web", "nonce": "ce9e1b1a-...", "ts": 1725598800, "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...", "ua_ch": { "Sec-CH-UA": "\"Chromium\";v=\"116\", \"Not.A/Brand\";v=\"8\", \"Google Chrome\";v=\"116\"", "Sec-CH-UA-Platform": "\"Windows\"", "Sec-CH-UA-Mobile": "?0" }, "extensions": [ {"id":"aabbcc...","version":"1.2.3","sha256":"e4b7...","publisherKeySha256":"f0c3..."} ], "artifacts": { "agent_sha256": "abc123...", "proxy_sha256": "def456...", "extensions_bundle_sha256": "e4b7..." }, "attestations": [ {"type":"slsaprovenance","rekorLogIndex":123456}, {"type":"spdxjson","rekorLogIndex":123457} ], "proxy": {"policyHash":"9f1c...","allowlistVer":"2024.09"} }
- Sign with Sigstore (Node/TypeScript using
@sigstore/sign):
tsimport { sign } from '@sigstore/sign'; async function signProof(payload: object) { const data = Buffer.from(JSON.stringify(payload)); const bundle = await sign({ payload: data }); // keyless with OIDC return bundle; // DSSE/JWS bundle with cert chain }
- Verify server‑side:
tsimport { verify } from '@sigstore/verify'; async function verifyProof(bundle: any, expected: {aud: string, nonce: string}) { const result = await verify(bundle, { certificateIssuer: 'https://token.actions.githubusercontent.com', signers: ['https://github.com/yourorg/yourrepo/.github/workflows/build.yaml@refs/heads/main'], }); // Extract payload, check nonce, aud, exp // Lookup Rekor entries for referenced attestations and ensure subjects match artifact digests return result; }
Binding runtime to identity: On servers you control, you can go further by using TPM‑based attestation (tpm2‑tools) to bind the host measurement to the proof. For general desktop agents, rely on Sigstore OIDC and strong artifact/attestation linkage.
Privacy: keep proofs minimal. Don’t include unnecessary host identifiers; prefer artifact digests, policy hashes, and extension publisher keys.
Outcome: site operators and enterprise gateways can require a valid proof before unlocking higher rate limits or sensitive operations. This flips the usual cat‑and‑mouse script: trust through transparency, not obfuscation.
8) Policy aggregator: evaluate risk and enforce
Define a policy engine that consumes:
- Sigstore verification result and Rekor inclusion
- SLSA provenance checks (builder identity, branch/ref, materials)
- SBOM scans and allowlists/denylists (licenses, CVEs)
- UA/CH policy (must be in org‑approved set)
- CDP proxy policy hash match
Then emit a verdict: allow, restrict, or deny. Example outcomes:
- Allow full browsing and cookie access for agents with green proofs
- Throttle or sandbox agents missing attestations
- Deny agents with mismatched UA policy or unsigned extensions
This policy can run:
- In the CDP proxy before sensitive methods
- As a sidecar verifying an agent before handing it credentials
- On the server (site) side, gating enhanced APIs or scraping endpoints
9) Threats and countermeasures
- Dependency confusion/typosquatting: pin PURLs with digests in materials; use private upstream registries; run
npm ci --ignore-scripts - Malicious extensions: ban unpacked extensions in prod; verify CRX signatures; pin publisher keys; SBOM and sign
- CDP MITM: terminate TLS to proxy; restrict proxy egress to browser; enforce method allowlists; hash‑chain audit logs
- UA spoof drift: enforce via launch flags, CDP policy, and JS freezes; provide signed switchers instead of ad‑hoc changes
- Telemetry forgery: nonce‑based signing; Sigstore verification; Rekor inclusion; cross‑checking artifact digests with attestations
- Secret exfiltration: disable sensitive CDP methods; inject CSP; run in network namespaces and credential brokers with short‑lived tokens
10) Reference implementation blueprint
Repository structure:
/agent/— Playwright/Puppeteer controller; reproducible build script/proxy/— CDP policy proxy with SBOM and provenance/extensions/— Curated CRX bundle and manifest with publisher keys/policy/— OPA/Rego rules for UA and CDP methods/telemetry/— Proof server and client library using Sigstore.github/workflows/build.yml— Build, SBOM, Cosign attest, push to registry
GitHub Actions workflow snippet tying it together:
yamlname: build-supply-chain on: push: branches: [main] permissions: id-token: write contents: read packages: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - name: Build agent run: ./scripts/build_agent.sh - name: SBOM (agent) run: | syft packages dir:./dist/agent -o spdx-json > dist/agent.spdx.json cosign attest --keyless --type spdxjson --predicate dist/agent.spdx.json dist/agent - name: Attest SLSA run: cosign attest --keyless --type slsaprovenance --predicate slsa.json dist/agent - name: Verify run: | cosign verify-attestation --type slsaprovenance dist/agent cosign verify-attestation --type spdxjson dist/agent
Operational tips:
- Store digest‑pinned artifacts in your registry and block mutable tags in prod
- Fail builds on SBOM scan findings above a severity threshold (with emergency override)
- Regularly rehearse rebuild+verify from scratch, ideally with a second, independent runner
11) FAQs and trade‑offs
- What if a site blocks a locked UA? Maintain a small, signed set of UA profiles (“switchers”), published as policy updates with attestation. Avoid ad‑hoc randomization.
- Is Sigstore keyless secure enough? Yes, if you scope OIDC issuers and identities. Enforce certificate identity and SAN regex on verification. For air‑gapped environments, use key‑ful Cosign with HSM.
- Can I get Chromium itself reproducible? Some distributions achieve close reproducibility; otherwise, rely on distribution provenance (Debian snapshot + attestation) and content digests.
- How does this help if the runtime host is compromised? These measures mitigate supply‑chain and pipeline tampering. Host compromise is a separate problem; consider TPM/SPIFFE attestation, sandboxing, and ephemeral workers.
- What about privacy? Only include what’s necessary in proofs. Use artifact and policy digests rather than device identifiers.
12) Conclusion: make trust the default, not a promise
Agentic browsers are powerful; power without verifiability is risk. By adopting reproducible builds, SLSA/in‑toto provenance, SBOM‑backed extension and proxy checks, locked UA/Client Hints, and signed “what is my browser agent” telemetry, you create a chain of custody that others can verify — not just take on faith.
Ship agents like you ship production apps: pinned, signed, attested, and measured. The payoff is less fragility, easier incident response, and a credible basis for collaboration with site operators who need to trust your traffic.
References and tools
- Sigstore (Cosign, Fulcio, Rekor): https://sigstore.dev/
- SLSA levels and provenance: https://slsa.dev/
- in‑toto: https://in-toto.io/
- Syft (SBOM) and Grype (scanning): https://github.com/anchore/syft, https://github.com/anchore/grype
- Chromium DevTools Protocol: https://chromedevtools.github.io/devtools-protocol/
- Playwright: https://playwright.dev/ — Puppeteer: https://pptr.dev/
- OPA/Rego: https://www.openpolicyagent.org/
- SPDX: https://spdx.dev/ — CycloneDX: https://cyclonedx.org/
- diffoscope: https://diffoscope.org/
- Nix: https://nixos.org/ — Bazel: https://bazel.build/ — apko/melange: https://github.com/chainguard-dev
