<!-- Generated raw Markdown for /trust/security; product pin 638fa1e9ca7face19a442cef43754f0d627c7705. -->

This page collects the security-relevant behavior of the pinned
implementation: who holds keys and how, the profile system that keeps
non-local operation switched off, the behaviors that fail closed, and the
replay/timing caveats an integrator must design around. The trust model these
mechanisms live inside is [Trust & Limits](/trust/).

## Key handling

The complete secret inventory is three environment variables, all held by the
facilitator operator:

| secret | used for |
|---|---|
| `SUBETHA_TREASURY_PK` | burn-address seed derivation; custodial fee-sweep signing (entropy only in split mode) |
| `SUBETHA_RELAYER_PK` | submitting the proof-gated mints; relaying gasless permit burns |
| `SUBETHA_ADMIN_TOKEN` | `Authorization: Bearer` auth for the `/admin/*` endpoints |

The rules around them, as implemented:

- **Environment only, no defaults.** Keys are never read from the config
  file, and the daemon has no default network, contracts, or keys — it cannot
  silently run against the wrong chain.
- **Dev keys are fenced.** The well-known anvil dev keys are accepted (with a
  warning) only when *all four* local predicates hold: `profile = "local"`, a
  loopback listen host, a loopback RPC host, and chain id 31337. Any missing
  predicate aborts startup with an error naming the failed predicate.
- **Secrets stay out of output.** Error messages name the variable, never the
  value; logs redact known secret values and signature-grade hex; every URL
  the daemon prints is credential-redacted, and a credential-embedding RPC
  URL draws a warning from the config checker.
- **This site shows no key material.** Every key or token value in these docs
  is a placeholder (`0x…`) — supply your own via the environment. The payer
  and provider hold, at most, their own EOA keys; in the shipped
  configuration the provider process holds no key at all (see
  [architecture](/concepts/architecture/)).

## Profiles: non-local operation is gated off

The facilitator's top-level `profile` key (`"local"` / `"testnet"` /
`"production"`) classifies a deployment **explicitly** — a loopback bind or
chain 31337 never implies `local`, and an unset profile is treated as
non-local, fail-safe. In this release, payments are local-experiment only.
Under any non-local profile:

- `POST /challenge`, `/verify`, and `/settle` refuse with terminal,
  profile-specific errors;
- the finalize loop still runs for liveness but never changes chain state,
  and journal entries stay frozen until the same `local` configuration they
  were accepted under returns;
- `GET /supported` advertises no kinds, so the official x402 middleware
  **fails its initialization** — a provider pointed at a gated facilitator
  refuses to boot rather than come up half-working;
- unset profile with no admin token refuses to start outright.

The unlock condition is code, not configuration: non-local acceptance turns
on only when the per-profile confirmation/finality policy is implemented and
tested.

## Fail-closed behaviors

- **No confirmed burn, no resource.** `accepted` requires the burn
  transaction's successful receipt, its `IndexedTransfer` covering the quoted
  amount, and the burn-address balance. If settlement fails, the buffered
  response is discarded and the client receives a 402 — nothing is served on
  a failed settle. See
  [Accepted vs finalized](/concepts/accepted-vs-finalized/).
- **Signatures do not lock funds — so no optimistic serve.** Serving on a
  verified signature alone would let a payer drain its balance before the
  burn executes; the implementation therefore always waits for the burn to be
  on-chain. The residual payer misbehavior is griefing (a reverted burn
  wastes facilitator gas), never a served-but-unpaid resource.
- **Permit mode fails fast.** A payer configured for the gasless path errors
  on offers lacking the helper contract — it never silently degrades to a
  gas-paying transfer.
- **Admin surface locked by default.** `/admin/*` is always
  Bearer-authenticated; with no token set (allowed only on local loopback) it
  answers 401 to everything, and locked versus wrong-token responses share
  one body so the response does not reveal whether a token is configured.

## Replay, idempotency, and double-spend

- **Same payload, safe replay.** An accepted payment answers any identical
  re-send with the same accepted result — no second burn, no double charge. A
  broadcast gasless permit is reconciled from its on-chain replay ledger even
  across a daemon restart; the burn is never double-executed.
- **Unknown outcome ⇒ re-send, never re-sign.** After a timeout or lost
  response, every party re-sends the *same* payload. Neither payer client
  ships automatic retry, and building a fresh 402/payload for an
  already-signed payment risks paying twice.
- **Offers bind to one payment.** Each 402 carries a fresh challenge nonce
  and a fresh one-time `payTo`; the facilitator compares echoed requirements
  against its own challenge record and rejects any mismatch, so a tampered or
  replayed offer fails. Chain-level double-spend of a burn is blocked by the
  official verifier's cumulative accounting.

## Timing caveats (TOCTOU and expiry)

- **Verify is point-in-time.** The middleware runs verify → handler → settle;
  a payer can move its balance or consume its permit nonce between verify and
  settle, making settle fail *after* the handler ran. Funds are never at
  risk — nothing is served — but providers of computationally expensive
  resources should apply ordinary rate limiting for the wasted compute.
- **Expiry is judged by chain inclusion, not broadcast.** A burn included
  after the offer's expiry is refused on every path, with a manual,
  operator-judged recovery procedure and no automation. Payer clients refuse
  to *start* a self-transfer broadcast inside the last 30 seconds of an
  offer's life so they never initiate a burn likely to land late.
- **A receipt is not finality.** On the local stack reorgs do not occur in
  practice; the finalize pass still re-verifies chain evidence before minting
  and fails the entry rather than minting on vanished evidence.

## Reporting a vulnerability

Report vulnerabilities **privately** via
[GitHub Security Advisories](https://github.com/peaceandwhisky/SubEtha/security/advisories/new)
— never in a public issue. In scope is everything in the product repository:
the TypeScript packages, the Python payer client, and the contracts
(`PermitBurner.sol`, `FeeSplit.sol`); of particular interest is anything that
bypasses a spending policy, leaks a key into logs or errors, breaks the
documented hiding of the
[payer–provider correspondence](/concepts/privacy/), or redirects a settlement.
Out of scope: the zERC20 protocol, SDK, and contracts themselves (report
upstream at [kbizikav/zERC20](https://github.com/kbizikav/zERC20)), the
demo's well-known anvil dev keys, and denial-of-service against your own
local stack. The authoritative policy is
[`SECURITY.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/SECURITY.md)
at the pin.
