Security policy
Anvil sits on the money path. A defect here does not corrupt a dashboard — it lets an agent double-charge a customer, or blocks a payment that should have gone through. We would much rather hear about it from you than from a customer.
This page serves two readers. If you are evaluating Anvil, start with "What protects your data" below. If you are reporting a vulnerability, skip to reporting — and please do not open a public issue for it.
What protects your data
Stated as implemented, not aspirational. Anvil is operated by one person; where a control is small or absent, this table says so rather than borrowing the vocabulary of a larger company. The contractual version of the same list is section 5 of the DPA.
| Control | Where it stands today |
|---|---|
| Encryption in transit | TLS on every hosted surface, HSTS in production. |
| Encryption at rest | Provided by our host for the Postgres database and Redis, and by GitHub for the daily backup artifact. |
| Tenant isolation | Every tenant-scoped read and write is filtered by workspace at the query layer. An automated check fails the build on a raw un-namespaced key, and a two-tenant end-to-end test proves one workspace cannot reach another's policies, receipts, spend counters, or approvals. This is the class of bug we care about most. |
| Authentication | Google OAuth for the control plane; hashed API keys for the runtime API. Reads and the interface fail closed in production when no control token is configured. |
| Authorisation for high-risk actions | Maker-checker approvals with self-approval blocked. An override requires an armed, single-use token and is recorded against the operator who used it. |
| Credential storage | API keys are stored as hashes with a non-secret prefix and are not recoverable from the database. Connector credentials are held in memory only. Webhook signing secrets are stored in cleartext because the server is the signer and needs the value to compute the HMAC — see the DPA for why hashing is not available there. |
| Audit records | A decision record for every guarded call, linkable into a tamper-evident hash chain that anvil verify checks. HMAC signing of that chain is off unless a signing key is configured, and the product labels the unsigned state honestly rather than implying attestation. |
| Durability | Money-path execution truth is written to Postgres *before* the action, so a crash cannot lose the record that made double-execution possible. The service refuses to boot in production on undeclared ephemeral storage. |
| Backups and recovery | A daily pg_dump that is restored into an empty database and row-count-verified in the same run — a dump that does not restore fails that day's job, loudly. Kept 14 days as a private build artifact. Our host does not offer native Postgres backups on our plan, which is why this exists. Recovery is a manual restore by one operator; there is no documented RTO/RPO, no second region, and no automatic failover. |
| Secure development | Every change goes through pull-request review, a typed build, and a test suite that gates deploys. A security fix ships with a regression test that fails on the vulnerable code. |
| Dependency management | Automated dependency audit in the pipeline. |
| Production access | One person, through individually authenticated accounts at the host, the source host, and the identity provider. No shared logins. Administrative actions in the product are attributed to the operator who took them. |
| Personnel programme | None. No background checks, security training, or offboarding process, because there is nobody to run them on. This row changes when the team does. |
| Independent assessment | None today. No SOC 2, no ISO 27001, no completed third-party penetration test. If one is a requirement for you, tell us and we will tell you honestly where we are. |
| Data location | United States. No EU or UK data residency option. |
Two things worth saying out loud. The backup row is the strongest control here — a restore that is proven every day is rarer than a compliance badge, and it is the one that would actually save your data. The independent-assessment row is the weakest, and we are not going to dress it up: there is no audit report to send you.
Reporting a vulnerability
Email henry@anvilcontrol.ai with SECURITY in the subject line. A human reads that inbox every business day.
Include whatever you have — we will work with a one-line hunch:
- what you were poking at (URL, endpoint, CLI command, or npm package + version),
- what happened vs. what you expected,
- the smallest set of steps that reproduces it,
- whether you believe it is remotely exploitable without credentials.
If you need encryption before you will send details, say so in a first email with no details and we will arrange a channel.
What we commit to
| Stage | Commitment |
|---|---|
| Acknowledgement | 1 business day — a human reply, not an autoresponder |
| Triage + severity | 3 business days — we tell you our severity call and our planned fix window |
| Fix for critical | 7 days target for anything that lets money move incorrectly, crosses a tenant boundary, or exposes credentials |
| Fix for high / medium | 30 / 90 days target |
| Disclosure | Coordinated. We will tell you when the fix ships, and we are happy to credit you by name or stay quiet — your call |
If we are going to miss one of those windows we will tell you before it passes rather than after. During beta, "1 business day" has in practice meant same-day.
Safe harbour
We will not pursue or support legal action against you for good-faith security research that:
- stays within the scope below,
- uses only accounts you created or were given,
- does not access, modify, or retain other people's data — if you stumble into someone else's data, stop, tell us, and delete it,
- does not degrade the service for other users (no load/stress testing, no resource exhaustion),
- gives us a reasonable window to fix the issue before you publish.
We do not run a paid bounty programme today. We are a small team and we will say so honestly rather than imply a reward that does not exist. We will credit you, and we will tell you exactly what we changed.
Scope
In scope
- The hosted planes:
api.anvilcontrol.ai(the guard/money API),app.anvilcontrol.ai(the control plane),go.anvilcontrol.ai(onboarding + the ghost intake), andanvilcontrol.ai. - The ghost intake specifically. It is deliberately anonymous — it accepts pasted logs and, in connector mode, live third-party read credentials, and routes content to an LLM provider (see Data handling in the ghost intake). Anything that lets one submitter read another's analysis, exfiltrate a stored credential, escape the redaction pass, or turn the analyzer into an SSRF/RCE primitive is the highest-severity class of report we can receive. Please look at it.
- The published npm package
@useanvil/sdkand theanvilCLI, including theanvil protectcodemod (it writes to a user's source tree) and the postinstall script. - The control-plane and ghost sources in this repository.
Report classes we care about most, in order: tenant-boundary escapes (one workspace reading or writing another's policies, receipts, spend counters, or approvals); anything that makes an enforce-mode guard fail open silently; idempotency/dedup bypass (the double-charge window); credential exposure (API keys, install tokens, connector credentials, ANVIL_CP_AUTH_SECRET handoff tokens, ANVIL_METRICS_TOKEN); authentication or approval bypass, including self-approval of a maker-checker gate; audit-record forgery or hash-chain tampering.
Out of scope
- Denial of service, volumetric or otherwise, and anything that requires degrading the service to demonstrate.
- Reports generated solely by an automated scanner with no demonstrated impact, and "best practice" findings with no exploit path (missing headers on a static page, cookie flags on a page with no session, TLS configuration reported by an SSL grader).
- Social engineering of our team or our users, physical access, and anything targeting a third party (Railway, Cloudflare, Resend, Postgres/Redis providers, Anthropic) rather than Anvil — report those to them.
- Missing rate limits with no demonstrated abuse, self-XSS, and issues that require a fully-compromised client or a malicious browser extension.
- The demo surfaces (
demo/,demo-merchant/,demo-suzy/, anything behind?demo=1), which serve synthetic data on purpose.
Which versions we fix
| Surface | Supported |
|---|---|
@useanvil/sdk | The current latest on npm, and the previous minor for critical fixes |
| Hosted planes | Always current — there is one deploy, and we patch it |
Every guard call the SDK makes arrives with an x-anvil-sdk-version header, so when a fix ships we can identify which tenants are still running an affected client and reach out directly instead of hoping they read a changelog. If you report something that only affects an old client, we will tell you what fraction of live traffic it still describes.
Data handling in the ghost intake
Because it shapes what a report about it means, here is what the intake actually does. The customer-facing version of this is on the intake screen itself and at anvilcontrol.ai/privacy.
- What it takes. Log/config text you paste or upload, or — in connector mode — read-scoped credentials you supply for a third-party system, which are used to pull a bounded window of recent activity.
- Redaction is best-effort, not DLP.
src/ghost/redaction.tsis pattern-based. It catches the common shapes (keys, bearer tokens, emails, card-like numbers) and it will miss things. We say so in the UI. Do not treat it as a guarantee, and please do report a bypass — but understand that a novel secret format slipping through is a known limitation we disclose, not an undisclosed vulnerability. - External model calls are conditional, and the intake screen says which way this deployment is set. They require a gate flag (
GHOST_ENABLE_AIon the ghost server, orGHOST_AI_ENABLEDfor the CLI/SDK paths) and a provider key; with either missing, no byte of your content leaves our servers. When they are enabled, a redacted excerpt goes to Anthropic and/or Google for inference and the model's answer is cached on our servers to avoid repeat calls. Your content is not used to train models either way. The disclosure rendered on the intake reads the same config (src/ghost/data-disclosure.ts), so the screen and this page cannot disagree. - Retention. The log text you paste is not retained — it is parsed in memory and never written to a file or a database row. The report derived from it is held so it can be rendered, downloaded and emailed: up to 30 minutes in the Redis job store, and up to 12 hours in server memory (report plus rendered PDF), keyed to your session, then dropped. Connector credentials are held in memory for the pull window only. The privacy page states the same numbers, and a test holds the two to the same value.
Fixes you will see from us
When we ship a security fix we will: name it in CHANGELOG.md, add a regression test in tests/ that fails on the vulnerable code, and — if it touched the money path — say in the changelog entry whether any tenant's money could have moved incorrectly. If the answer is yes, affected tenants hear from us before the changelog does.
For customers under a signed agreement
This page is the public channel and applies to everyone. If you have a signed MSA/DPA with us, the notification, breach, and remediation obligations in that agreement control where they differ from the commitments above — they are stricter, not looser.