Security · By Construction
Security
How dbcrate is built so that the worst day at our office still does not give an attacker your backups, and what we ask of you in return.
Effective. 12 May 2026. Status. dbcrate is in beta. The threat model and the cryptography below describe the architecture as it ships today; the operational maturity is still being built out. Where the two diverge, this page says so.
A managed-backup company is only as trustworthy as the worst thing one of its engineers can do at 3 a.m. after a bad week. We took that observation seriously when we drew the architecture, and the result is a system in which the dbcrate company — including its own database, its own engineers, and its own infrastructure — cannot read a customer’s backup contents using its own resources.
Two architectural commitments make that possible:
- Backup bytes never enter our infrastructure. They flow from your agent to the storage destination you nominate, encrypted on the host before they leave it.
- The key that decrypts your backups is yours. Your organisation has one X25519 keypair, generated when the organisation is created. The public key is distributed to your agents over mTLS. The private key exists in two forms: an operational copy held by the control plane envelope-encrypted under a master key kept in a SOPS-encrypted env file (and decrypted only at deploy time), and an optional recovery copy that you can download in standard
ageformat and keep offline.
Together they mean: an attacker who has only our object storage gets ciphertext. An attacker who has only our database snapshot gets ciphertext (the wrapped private key is useless without the master key, and the master key is not in the database). An attacker who has both the database and the master key has, by definition, root in our production deployment — the one threat we do not claim to defeat. Even then, historical backups produced by an agent that has not held a recovery key remain decryptable only by stealing them from your bucket separately.
We take a position on every plausible attacker. Listed in roughly increasing severity.
| Threat | What we do about it |
|---|---|
| Network observer between agent, control plane, and storage | TLS on every connection; mutual TLS between agent and control plane; SFTP destinations enforce known-hosts pinning. |
| Object-storage compromise (attacker reads your bucket) | Backup files are age v1 ciphertext, encrypted to your organisation’s X25519 recipient. Files alone, without your private key, yield no plaintext. |
| Stolen agent host | The agent holds no backup-decrypting key. It encrypts forward to the org public key only. A compromised agent can affect future backups it produces; it cannot read the historical archive. |
| Insider read of the control-plane database | The database holds encrypted credentials, an envelope-encrypted org private key, and metadata. Without the master key from the deployment environment, neither credentials nor backups decrypt. |
| Full control-plane compromise (master key + database) | Acknowledged. We do not pretend to defeat this. The master key lives in a SOPS+age-encrypted env file, decrypted only at deploy time. It is the single thing whose compromise unlocks credentials and backups together. We minimise the surface where it is reachable, log every decryption, and design for fast credential rotation. |
| dbcrate is unreachable (outage, bankruptcy, hostile takeover) | You can fetch a recovery copy of your organisation’s private key from the dashboard at any time, in standard age format, and decrypt your backups using off-the-shelf tools. With your bucket and your recovery key, you have everything you need. |
Backups are encrypted using age v1. We chose age instead of inventing a dbcrate-specific format on purpose: it is small, audited, well-specified, and supported by tools we did not write.
The chain, in one figure:
master key (lives in SOPS-encrypted env file, decrypted at deploy time)
│
▼ wraps
org private key (DB, encrypted) org public key (DB, plaintext)
│ │
│ unwraps │ distributed via
│ (only on customer-initiated │ GET /agent/v1/config
│ operations, audit-logged) ▼
│ per-backup DEK (lives in
│ the age file header in S3)
│ │
│ │ encrypts
▼ ▼
backup ciphertext (lives in your bucket)
Some properties worth stating in writing:
- One DEK per backup. A random 32-byte file key, generated on the agent host, used to encrypt the dump under ChaCha20-Poly1305, and wrapped to the org public key inside the file’s
ageheader. - Agents do not hold the org private key. A backup-only agent that gets compromised cannot decrypt any backup, including the ones it has produced. The matching private key is never delivered to the agent.
- DEKs are not cached in our database. When a managed restore needs the DEK, the control plane issues a small ranged GET against the backup file’s
ageheader, unwraps the DEK in memory, sends it to the agent over mTLS along with the rest of the per-job credentials, and the agent uses it immediately and discards it. The DEK never lands in a row. - Streaming, not staging. The agent pipes
pg_dump –format=customthroughzstdthroughageand straight to upload. The full backup is never written to the agent’s disk. - You can verify independence. Every release that ships an encrypted backup must pass an independence test in CI: a freshly-produced backup, decrypted by stock
age(no dbcrate binary on the path) and then bypg_restore, restored into a fresh Postgres of the matching major version. If that test fails, the release does not ship.
The agent authenticates to the control plane with mutual TLS. The enrolment flow is:
- An operator with dashboard access creates an enrolment token. The token is shown once and is single-use.
- The agent, on first start, generates a private key, builds a certificate signing request, and presents it together with the token to
POST /agent/v1/enroll. - The control plane returns a short-lived leaf certificate signed by an org-scoped intermediate. The agent persists the cert and the private key on its host, in
/var/lib/dbcrate/identity/, mode0700; the key file is mode0600. - From then on every agent request to the control plane is over mTLS with that certificate. The certificate is renewed automatically at ~80% of its lifetime via
POST /agent/v1/certificate/renew.
There are no shared API keys. There is no inbound network surface on the agent — it listens on no ports, opens no listening sockets, runs no debug servers over TCP. All communication is outbound.
Compromised or retired agents are revoked through a signed certificate revocation list (CRL). Agents pull the CRL on a loop and halt if their own serial appears in it.
Credentials — database passwords and storage keys — live in three places, none of them comfortably.
- At rest in the control-plane database, envelope-encrypted: each credential under its own random DEK, with the DEK wrapped under the master key. The master key never touches a database row. Rotating the master key re-wraps the DEKs; it does not require re-encrypting every ciphertext.
- In transit to the agent, only for the duration of one job. The agent fetches credentials with
GET /agent/v1/jobs/<job_id>/credentialsimmediately before running the job. The control plane authorises the fetch (does the agent have a current cert, is this job for this agent, is it in a state that needs credentials), decrypts the credentials in memory, returns them once, and writes an audit row. - On the agent host, only in process memory. Credentials never touch the agent’s disk and never appear in a log line. They are held as raw bytes and zeroed when the job ends.
The control plane’s logging library has a credential redactor; the agent’s slog calls take structured fields, never interpolated strings; both repos run a lint check that fails the build when a known credential field name appears in a log call.
Backups go to a destination you nominate — an S3-compatible bucket on AWS, Cloudflare R2, Backblaze B2, Hetzner Object Storage, Wasabi, Tigris, MinIO, or your own; or an SFTP host you operate. We do not own the bucket. We do not have a copy of the bucket. We have no path that writes backup bytes anywhere else.
A storage destination is validated end-to-end before it is saved: the control plane runs a list / write / read / delete probe to confirm the credentials work and the policies allow what the agent will need, and surfaces the result in the dashboard. If a probe fails, the destination is not saved.
Retention deletion is performed by the control plane against your storage destination directly, on the schedule and rules you set. Agents have no delete code path — the agent binary cannot, even if a compromised control plane told it to, delete a backup file.
Every consequential action in the control plane is recorded in an append-only audit table:
- Sign-ins and sign-out events; failed authentication attempts; password resets and second-factor changes.
- Configuration changes (databases added, schedules edited, retention rules changed, agents enrolled or revoked, storage destinations added or removed).
- Credential decryption events — every time the master key unwraps a DEK, with the actor (a job, a restore, a user-initiated download), the credential identifier, and the reason.
- Restore initiations, including the target.
- Retention deletions, with the storage outcome.
Audit entries are written by a single helper (dbcrate.core.audit.record) and the schema rejects updates and deletes at the application layer. The table is read by the dashboard’s audit view and is part of every relevant export.
The architecture is one half of the story; the practice is the other. The practices we hold to, in the form they exist today:
- TDD on every behaviour change. A failing test is the first thing a behaviour change adds. The agent and the control plane both have pre-commit hooks that run the test suite locally before a commit lands.
- An end-to-end release gate. No agent release is published until the candidate binary has run a full backup-and-restore against each supported Postgres major version (13 through 18 today), against real object storage, on both linux/amd64 and linux/arm64. The decryption step in that gate uses stock
age, not the dbcrate binary, so the format never silently drifts away from the published one. - Boot checks fail loud. The control plane verifies its required configuration, secrets, and migrations at startup, and exits non-zero rather than starting degraded.
- Least privilege on the host. The agent runs as an unprivileged system user (
_dbcrate), with no requirement for root, and no path that escalates. - No host modification. The agent does not install packages, change system services, or write outside its data directory. It fetches the matching
pg_dumpbinary on demand from a signed binary registry; the registry’s manifests are signed under a root key that is itself rotated through a published key-rotation procedure. - Dependencies kept small, and reviewed. Both the agent (Go) and the control plane (Python) maintain short dependency lists. Adding a new dependency is a deliberate, documented choice.
We do not yet hold any third-party security certifications (SOC 2, ISO 27001, similar). Pursuing one is on the company roadmap. The dashboard audit log and the design choices above are the substantive part of what such an audit would attest to; the formal attestation will follow when it is honest to seek one.
A short list of failure modes the architecture leaves to the customer to manage. These are not weasel words; they are the points at which the customer has to do something the system cannot do for them.
- Custody of your recovery key. If you download the recovery copy of your organisation’s private key, you are the custodian of it. Lose it, and you lose the ability to decrypt your backups outside dbcrate. We can never produce another copy of the same key — the recovery export is the operational key; we do not retain a second one.
- Storage destination policy. If the bucket you nominated is configured to be world-readable, the ciphertext in it is world-readable. Defence in depth helps you here.
- Compromise of your database host. If an attacker has root on the host where the agent runs, they can read the database the agent is configured to back up. dbcrate does not change the security model of your database host; it inherits it.
- Insider compromise on our side, with the master key. As discussed in section 2, this is the single threat we do not claim to defeat. We design to make the master key hard to reach and every use of it loud.
If any of these are intolerable for your use case — for example, if you cannot reasonably hold a recovery key offline — tell us, and let us help you find an architecture you can live with.
If you believe you have found a security issue in dbcrate, send a report to security@dbcrate.com. The address has a published mailbox; we read it every working day.
What we ask of you:
- Tell us what you found, where, and how to reproduce it. A working proof-of-concept against a test account is much faster to triage than a description.
- Give us a reasonable window to investigate and fix before going public — 90 days is the convention we work to, sooner where the severity is high and we have agreed a faster timeline with you, longer where the fix is structurally complicated and we have agreed an extension with you.
- Do not test against accounts that are not yours. If you need a test account for your work, write to us and we will create one.
- Do not exfiltrate other customers’ data, perform denial-of-service testing against production, or use social engineering against dbcrate staff.
What you can expect from us:
- We will acknowledge your report within two working days.
- We will tell you our triage in writing within ten working days — severity, expected timeline, whether we have a workaround.
- We will keep you informed as the fix progresses, and we will credit you in the public disclosure unless you ask us not to.
- We do not run a paid bug bounty at this stage of the company. We do not believe small companies should pretend to. If your report leads to a material fix, we will say so publicly and find a substantive way to thank you.
A PGP key for the security mailbox will be published at /.well-known/security.txt alongside the disclosure policy.
Security disclosures: security@dbcrate.com. Privacy questions: privacy@dbcrate.com. Anything else: hello@dbcrate.com.