[{"data":1,"prerenderedAt":10},["ShallowReactive",2],{"public:public\u002Fcontent\u002Fsite\u002Fsecurity":3},{"slug":4,"title":5,"description":6,"draft":7,"updated":8,"html":9},"security","Security","How Verestore isolates every restore, keeps customer data inside the run, protects your read-only keys and signs the evidence.",false,"2026-11-04","\u003Ch1>Security\u003C\u002Fh1>\n\u003Cp>Verestore needs read access to your production backups: a read-only key, nothing installed on your side. This page explains what we do with that access, what we can never see, how our independent countersignature makes the evidence impossible for anyone (you included) to forge, and the tests that prove it. Everything runs in the EU (Scaleway, Paris). \u003C\u002Fp>\n\u003Ch2>Architecture in one paragraph\u003C\u002Fh2>\n\u003Cp>A \u003Cstrong>control plane\u003C\u002Fstrong> (the web app and API, in Scaleway&#39;s Paris region) schedules restore tests and keeps the evidence. Each test runs on a \u003Cstrong>separate runner cluster\u003C\u002Fstrong> that hosts nothing else:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>a fresh Kubernetes namespace per run, under the \u003Cstrong>gVisor\u003C\u002Fstrong> sandbox runtime;\u003C\u002Fli>\n\u003Cli>a fresh PostgreSQL of your backup&#39;s major version as a sidecar;\u003C\u002Fli>\n\u003Cli>the runner, which downloads your newest backup, restores it, runs aggregate-only checks and uploads a signed result.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The namespace, the restored database and the downloaded dump are deleted when the run ends, whether it succeeds, fails or times out. A sweeper removes anything older than its deadline plus 15 minutes.\u003C\u002Fp>\n\u003Ch2>What leaves a run, and what never does\u003C\u002Fh2>\n\u003Cp>A run&#39;s only output is a \u003Cstrong>result document\u003C\u002Fstrong>: table names, row counts, checksums, durations, versions and a scrubbed log excerpt. \u003Cstrong>Row data never leaves the run.\u003C\u002Fstrong> This is enforced, not promised:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>The result format forbids data.\u003C\u002Fstrong> The result&#39;s JSON Schema closes every object, bounds every string and array, and rejects keys that could carry records (\u003Ccode>data\u003C\u002Fcode>, \u003Ccode>values\u003C\u002Fcode>, \u003Ccode>records\u003C\u002Fcode>, \u003Ccode>rows_sample\u003C\u002Fcode>…). It is checked by the runner before upload and by the control plane on upload. Tests inject such keys into every object and substitute every string field; every variant is rejected. Loosening the schema on purpose made those tests fail.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Canary values prove nothing leaks.\u003C\u002Fstrong> The test database contains sentinel values (an e-mail address and a code). Tests select them, hash them, and make PostgreSQL fail on them, including a restore error whose own message contains the sentinel. They then search the result, the report, the logs and the error messages. Two deliberate leaks introduced to test the tests (unscrubbed logs; a printed row) were both caught.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Your own SQL checks return aggregates only:\u003C\u002Fstrong> a count, a hash or one number. Failed queries report a 5-character SQLSTATE, never the message. The checks run as a read-only role that can&#39;t read files, copy data out, import large objects or create tables.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Secrets are scrubbed\u003C\u002Fstrong> from anything that leaves a run: connection strings, tokens, IP and e-mail addresses.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Your read-only key\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Write-only.\u003C\u002Fstrong> You give us a key that can only \u003Cstrong>list and read your backups\u003C\u002Fstrong>: one bucket prefix, or one bucket or Azure Blob container where the store cannot scope a prefix (see the \u003Ca href=\"\u002Fdocs\">docs\u003C\u002Fa> for copy-paste policies). For Azure Blob that is a read + list SAS token or a service principal with Storage Blob Data Reader. We check it can do exactly that before the first run.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Encrypted so only the orchestrator can open it.\u003C\u002Fstrong> The key is sealed (X25519 + AES-256-GCM) to a per-organisation key, which is itself sealed to a master key held in Scaleway Secret Manager. Only the orchestrator, the service that starts runs, can read the master key; the web application can only encrypt. Every sealed value is bound to its row, so it can&#39;t be moved.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Never shown again.\u003C\u002Fstrong> No page or API call returns it, to you or to anyone at Verestore. Only the orchestrator, which has no user-facing interface, can open it, when a run starts; someone with that service&#39;s access to Secret Manager could too, so that access is restricted to the service. Tests check every read endpoint of the API, the server logs and every database column for the key, raw, hex- or base64-encoded.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>When a run starts,\u003C\u002Fstrong> the orchestrator decrypts the key. For S3-compatible stores the key stays there: the orchestrator finds your newest backup and gives the run a link valid for that one object and 15 minutes. An Azure Blob credential goes into the run&#39;s Kubernetes Secret, which is encrypted at rest and deleted with the namespace.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Isolation of each run\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>One Kubernetes \u003Cstrong>namespace per run\u003C\u002Fstrong>, enforcing the \u003Ccode>restricted\u003C\u002Fcode> Pod Security Standard. Every container runs as a non-root user with a read-only filesystem and no Linux capabilities. Pods get no service-account token.\u003C\u002Fli>\n\u003Cli>The \u003Cstrong>gVisor\u003C\u002Fstrong> sandbox runtime is required: an admission policy refuses any run pod without it.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Network:\u003C\u002Fstrong> no inbound traffic at all. Outbound traffic is allowed only to the cluster DNS, your storage hostname (and, for an Azure service principal, Microsoft Entra ID&#39;s sign-in host) and our API, by name (Cilium FQDN policy). DNS lookups of any other name are refused. A test pod inside a run namespace proves it can&#39;t reach other ports, other hosts or the internet.\u003C\u002Fli>\n\u003Cli>The scratch PostgreSQL listens on the pod&#39;s loopback interface only.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Plain SQL dumps can&#39;t run commands:\u003C\u002Fstrong> they restore in a psql session that refuses every psql meta-command (\u003Ccode>\\!\u003C\u002Fcode>, \u003Ccode>\\o\u003C\u002Fcode>, \u003Ccode>\\copy\u003C\u002Fcode>…), tested with hostile dumps.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Your dump restores without superuser rights:\u003C\u002Fstrong> it runs as the scratch database&#39;s owner, so it can create its own tables and common extensions but can&#39;t change PostgreSQL&#39;s catalog, run programs or touch the server&#39;s files.\u003C\u002Fli>\n\u003Cli>Every run&#39;s network policy also denies private, link-local and cloud-metadata addresses, even if your storage hostname were later pointed at one.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Evidence you can verify\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Signed results:\u003C\u002Fstrong> the runner signs each result with a key minted for that single run (Ed25519 over the RFC 8785 canonical JSON).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Countersigned only when traceable:\u003C\u002Fstrong> our signer countersigns a result only if it can trace it to a dispatch our orchestrator signed, the run&#39;s own key signed it, and it names the job it ran. A row written straight into our database gets no countersignature.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Countersigned verdicts:\u003C\u002Fstrong> the control plane verifies the signature on upload, computes the verdict, and a separate signing service countersigns it. A changed verdict no longer verifies.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Reports\u003C\u002Fstrong> print the result hash and both signatures. Our public keys are published at \u003Ccode>\u002F.well-known\u002Fverestore-jwks.json\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Data we keep\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>Account data (e-mail, organisation, members), target configuration (bucket endpoint and prefix, never the key in readable form), schedules and check sets.\u003C\u002Fli>\n\u003Cli>Run results (names, counts, hashes, timings, scrubbed excerpts), reports and evidence packs, for the life of your account.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Free restore check:\u003C\u002Fstrong> your uploaded dump is stored in a quarantine bucket, deleted when the run finishes and in any case within 24 hours.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Sub-processors\u003C\u002Fh2>\n\u003Cp>See the \u003Ca href=\"\u002Flegal\u002Fsub-processors\">sub-processor list\u003C\u002Fa>. A \u003Ca href=\"\u002Flegal\u002Fdpa\">Data Processing Agreement\u003C\u002Fa> is part of our terms and can be \u003Ca href=\"\u002Flegal\u002Fdpa\">downloaded\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2>Reporting a vulnerability\u003C\u002Fh2>\n\u003Cp>E-mail \u003Cstrong>\u003Ca href=\"mailto:security@verestore.dev\">security@verestore.dev\u003C\u002Fa>\u003C\u002Fstrong>. We acknowledge within two business days and don&#39;t take legal action against good-faith research.\u003C\u002Fp>\n",1794290783585]