Security model
What runs on your server, what Rowsafe can and can't do, how backups are encrypted, and which credentials exist.
Rowsafe never needs your data or your secrets. The part that touches your data, the agent, is open source (Apache-2.0), so you can read exactly what runs on your servers.
This page describes a PostgreSQL server. Other engines follow the same model with their own pieces:
- The agent runs as
mysqlnext to MySQL and MariaDB, and as its ownrowsafeuser next to MongoDB, ClickHouse, Redis and Valkey. Never as root. - It logs in with an account of its own that only Rowsafe uses.
- The poolers are ProxySQL and chproxy. MongoDB's and ClickHouse's settings files are written only by root's tuning helper.
- Root's helpers act only on what root allowed for that server (by database).
What each database's account may do: MySQL and MariaDB, MongoDB, ClickHouse, Redis and Valkey.
What runs on your server
- The agent (
rowsafe-agent), as thepostgressystem user. It refuses to run as root. - pgBackRest, run by the agent and by PostgreSQL's
archive_command. - A hardened systemd unit:
NoNewPrivileges, a read-only system except its own directories (and/etc/postgresql, where Debian and Ubuntu keeppg_hba.conf, for standbys and the security check), a private/dev, no capabilities, restricted address families. The out-of-memory killer picks the agent before production PostgreSQL, and it gets half the default CPU and IO weight. - Only if you allow it at install: a small root helper,
rowsafe-pg-restart, the one part that runs as root. It restarts, stops or starts PostgreSQL when a person asks (Restart, and Rewind the whole database). It reads the agent's request with the agent's own privileges, accepts one strict line, and acts only on the PostgreSQL units root listed in/etc/rowsafe/restart-allowedat install. At most one restart a minute.
The agent:
- connects to PostgreSQL only over the local Unix socket, as the
postgresrole (a superuser is needed forALTER SYSTEMand backups). With connection pooling on, it also logs in over TCP asrowsafe_pgbouncer, to check that PgBouncer can reach PostgreSQL; - makes only outbound HTTPS requests, to
api.rowsafe.shand your bucket (or Rowsafe Storage). Nothing listens on your server, except PgBouncer if you turn on connection pooling; - runs only a fixed set of tasks: inspect, adopt (plan or apply), check, backup, drill (Proof), restore point (Mark), restart and connection pooling (each only where you allowed it), maintenance, the short list of fixes below, the security check and its fixes, PostgreSQL updates and upgrades, security updates and reboots (only where you allowed them), Databases & users (create or remove databases, users and extensions, when a person asks), the standby tasks, and the Rewind tasks (restore a copy, compare, bring back rows, rewind in place, undo, clean up). It never runs commands or SQL sent over the network.
Proof, the weekly restore test, restores your data onto the same server, into a scratch cluster with no network listener, and deletes it afterwards. If you choose another server for it, the copy is restored there instead: another of yours (the backup settings sealed to its agent's key, as for a fork), or a temporary Rowsafe Cloud server, deleted right after. A Rewind copy is restored the same way and kept until it expires (24 hours by default, at most 7 days). The agent deletes it on time even when it can't reach Rowsafe.
Rewind runs only when a person, or their AI agent, asks. Restoring a copy, bringing rows back, rewinding a whole database, undoing it and deleting the data kept aside all start from the dashboard or rowsafe rewind, by an owner or admin. Bringing rows back and rewinding ask you to type the database name, and Rowsafe saves a Mark first. The comparison and the rows stay on your server: only table names, counts and sizes reach Rowsafe. Read-only API keys can't do any of it. An AI agent (MCP) does it only as the person who connected it, when that person is an owner or admin, and only with a backup to go back to; otherwise Rowsafe refuses. See Restore a database.
Fixes run only when a person, or their AI agent, asks. The fixed set of maintenance actions: VACUUM and ANALYZE tables, REINDEX INDEX CONCURRENTLY, DROP INDEX CONCURRENTLY, cancel a query (pg_cancel_backend), end a session (pg_terminate_backend) and drop an inactive replication slot (and each other database's equivalents, such as MySQL's OPTIMIZE TABLE, MongoDB's killOp, ClickHouse's KILL QUERY or Redis's CLIENT KILL).
- Never on its own: only when an owner or admin clicks Apply fix in the dashboard (or runs
rowsafe fix) and confirms. - The service proposes each fix from a health finding. The client can only pick one by its ID, never send SQL or object names.
- The agent checks again right before it runs: the object still exists, the session is the same one and isn't a Rowsafe, replication or autovacuum session, the slot is still inactive. An index that backs a constraint is never dropped.
- Before dropping an index, Rowsafe saves a Mark.
- Read-only API keys can't apply fixes. An AI agent (MCP) applies one the same way, by its ID, as the person who connected it when they're an owner or admin; otherwise Rowsafe refuses.
See Apply fix.
Updates are opt-in, per capability. Rowsafe installs the database's minor releases, upgrades it to a new major, installs the server's security updates or reboots the server only when someone clicks it (or opted a database in to automatic minor updates) and confirms, and only what root allowed on the server: updates is allowed unless root says no; security-updates and reboot only when root says yes (permissions). A root helper does it, from a fixed list of requests checked against root's allow lists. It builds the package names itself (only the database server's own packages for an update or upgrade, only upgrades from the security repository for security updates) and never runs a command it was sent. Rowsafe saves a Mark first, and a major upgrade needs a passed rehearsal on a restored copy. See Update and upgrade PostgreSQL.
Databases and users change only when a person, or their AI agent, asks. Rowsafe creates and removes databases, users and extensions, and sets new passwords, only when an owner or admin clicks and confirms in the dashboard (removing a database asks for its name and saves a Mark first), or uses the CLI with a read-write API key.
- The agent checks every name against the catalogs and builds the statements itself. It never removes PostgreSQL's own databases, superusers, its own user or Rowsafe's.
- New passwords are end-to-end encrypted to your browser: it makes a one-time key pair whose private key never leaves its memory, the agent encrypts the password to it (ECDH P-256, HKDF-SHA256, AES-256-GCM), and Rowsafe relays the ciphertext once, then deletes it.
- PostgreSQL stores only a SCRAM verifier, so the password isn't in any statement or log.
- AI agents (MCP) read the list and can make some changes as the person who connected them. They never make or see a password, except an app database's password made on their own machine.
See Create databases and users.
Standby secrets move only between your servers. A standby needs your bucket settings, your passphrase and a replication password. Each agent has its own X25519 key pair (the private key never leaves the server). The primary's agent encrypts what the standby needs to the standby agent's key, and Rowsafe only relays the encrypted box. You confirm the standby's key fingerprint before pairing, and every handoff is logged on the primary with the fingerprint it was sealed to.
Because Rowsafe relays the keys, a compromised Rowsafe service could present a key of its own. Where that matters, set ROWSAFE_STANDBY=pinned with your own servers' fingerprints in ROWSAFE_STANDBY_PEERS, or ROWSAFE_STANDBY=off: the agent itself then refuses any other key. See agent configuration.
Forks use the same handoff. A fork to another server needs read access to the source's backups. The source's agent seals the bucket settings and passphrase to the fork server agent's key, after you confirm its fingerprint, and the fork server deletes them after the restore. On the same server, nothing moves. ROWSAFE_STANDBY covers forks too. Creating a PostgreSQL cluster for a fork goes with restarts: only where root allowed it (allowed by default where restarts are; --no-allow-create-cluster says no), on ports 5440 to 5499, through a separate sandboxed unit.
Restarts need a person, or their AI agent. Rowsafe never restarts or stops PostgreSQL (or MySQL, MariaDB, MongoDB, ClickHouse, Redis, Valkey) on its own.
- It restarts only when someone clicks Restart (or runs
rowsafe restart) and confirms. It stops and starts the database only to rewind the whole database, or undo that. Only on servers where root allows it. - The installer asks on a terminal and suggests yes (without a terminal, it's allowed).
--no-allow-restartorsudo rowsafe-allow --remove restartsays no and removes the helper; owners and admins can turn it off in the dashboard. The installer itself can also restart PostgreSQL during setup, when you answer yes on the server. - Only admins and owners can restart, never with a read-only API key; through MCP only as an agent acting as its owner or admin. At most once every 2 minutes.
- In Docker, Rowsafe can restart or stop PostgreSQL only if you add the optional container control service, which alone holds the Docker socket and acts on that one container. See its threat model.
Moving a database in needs a person and your password, which Rowsafe never sees. Move in starts only when an owner or admin pastes a connection string. The browser (or the CLI) encrypts it to a key the agent on your server made for that move, and Rowsafe relays only ciphertext. The password stays on your server in a 0600 file until you finish. The new login's connection string is encrypted on your server to a key your browser holds, and shown once. AI agents on MCP can't start or change a move.
Security fixes run only when a person asks. Every 15 minutes, the security check reads which addresses may connect, the pg_hba rules, TLS, password_encryption and the kind of each user's password (none, MD5 or SCRAM, never the hash). Rowsafe's servers also try the database's port from the internet like a stranger, never logging in, and only at the server's own addresses. The fixes are a fixed list, each confirmed by an owner or admin and audited:
- allow only chosen addresses in
pg_hba.conf(backed up first, checked by PostgreSQL before it takes effect, put back if anything is off); - turn on or require TLS, or renew a self-signed certificate;
scram-sha-256hashing;- remove
CREATEon schemapublicfrom everyone; - set a user's password. It's made in the person's browser; only its SCRAM verifier reaches your server, and Rowsafe never sees the password.
The firewall is opt-in. With --allow-firewall at install, a small root helper can let only chosen addresses reach the database's port, in an nftables table of its own. It never touches SSH or other ports, and puts the previous rules back unless the agent confirms within a minute that it still reaches Rowsafe.
Connection pooling needs a person, or their AI agent. Rowsafe installs and manages PgBouncer only on servers where root allows it (allowed unless root says no: --no-allow-pooler), and only when someone turns pooling on and confirms. The agent never gets root: a separate root helper does the work, checks every value, writes PgBouncer's configuration from its own template, and only ever installs or removes the pgbouncer package. PgBouncer looks passwords up through a role that sees the password hashes of login roles, never of superusers. Read-only API keys can't change pooling; through MCP, only an agent acting as an owner or admin can. See Connection pooling.
AI agents act as the person who connected them, never as more. Through the MCP server, agents can read what the dashboard shows, run backups and checks, and work on masked copies.
- A change to production (a fix, a restart, a rewind, an upgrade, a failover, ...) is recorded, checked, then run as the dashboard's own call by the person behind the agent: who allowed the app with Act for you on the consent page, or made the API key.
- Rowsafe asks the dashboard for that person's role just before each change, and fails closed: an owner's or admin's change runs at once; a member's, or one Rowsafe can't confirm, is refused and nothing changes. Nothing waits for an approval.
- The same checks, root's allow lists, Marks and audit log apply as for the person's own click. Only that one change runs, and paying stays an owner's.
See How AI agents act.
Consent and the safety nets.
- An app acts as its person only when the consent page said so. Apps and keys from before that, and keys made by other keys, can't make changes until their person allows them in the dashboard.
- Rowsafe refuses a change when a server's key must be compared (standbys, moves, forks: a person does those), when it would replace or delete data and Rowsafe has no backup yet, when it needs a first payment the person can't make, or when it would go over the agents' budget.
- Rowsafe never deletes backups for an agent: a database an agent removes keeps them until an owner deletes them.
- Only an owner, in the dashboard, chooses what AI agents may do. No agent, app or API key can change that setting.
- Every agent change is audited as the person through their agent, and owners get a digest email.
- Within these limits, a stolen app token or API key can do what its person can, like any command-line token. Disconnect apps and revoke keys you don't use.
What Rowsafe can and can't do
Someone who controlled the Rowsafe service, or held one of your API keys, could:
- queue the fixed tasks for your databases: plans, backups, checks, Proof runs and Marks, and apply an adoption plan (which sets
archive_mode,archive_command,archive_timeoutand, if it isminimal,wal_level); - restart PostgreSQL (a few seconds of downtime), only on a server where you allowed restarts at install;
- turn connection pooling on, change it or turn it off, only on a server where you allowed it at install: install or remove the
pgbouncerpackage, create therowsafe_pgbouncerrole and its password lookup function, change where PgBouncer listens (public addresses only where root also allowed--allow-pooler-public), and disconnect apps that connect through it; - restore copies of your databases next to production (they use disk and some CPU until they expire), compare them with production, and bring rows back from a copy: insert rows missing in production, or set changed rows back to older values. It never deletes rows this way;
- on a server where you allowed restarts, rewind a whole database to an earlier moment (downtime while it restores), and delete the data kept aside for undo. Every change is still in your bucket until retention removes it, so you could still restore it by hand;
- on a server where you allowed it: install PostgreSQL's minor releases (a restart), upgrade PostgreSQL to a newer major after a rehearsal (minutes of downtime; undo for 7 days), install the server's security updates, or reboot the server;
- create databases, users and extensions, give a user access to databases, remove a user (handing what it owns to another user) or a database (after a Mark), or set a new password on an existing user, which locks out apps using the old one until they get the new one. They could not read a password Rowsafe made: it is encrypted to the browser or CLI that asked;
- with standby servers (
ROWSAFE_STANDBY=on, the default): promote a standby, which stops the old primary for good, even where restarts aren't allowed; and pair a server of its own as a standby, which would receive the bucket settings.ROWSAFE_STANDBY=pinnedoroffrules both out; - fork a database onto another of your servers, unmasked; with
ROWSAFE_STANDBY=on, also present a key of its own as a fork target, which would receive read access to that database's backups (pinnedoroffrules it out); and, where you allowed--allow-create-cluster, have an empty PostgreSQL cluster created on a port in 5440-5499; - change who can connect to your database (to addresses they choose, or back to the previous rules), turn on or require TLS, and set a user's password, which would lock out apps using the old one; and, where you allowed the firewall, limit PostgreSQL's port to chosen addresses;
- run the maintenance actions above: vacuum or analyze tables (extra load while they run), rebuild or drop indexes (queries that used a dropped index get slower until you recreate it), cancel queries or end sessions (an open transaction is rolled back, losing its uncommitted changes), and drop an inactive replication slot (whatever read from it has to start over);
- change PostgreSQL settings an owner or admin picks in Tuning (and the logging settings of Turn on useful logging), with
ALTER SYSTEMand a reload, after a Mark. Values PostgreSQL might not start with are refused, and settings that need a restart wait for one; - during a move you started, switch it over (the old database goes read-only until you make it writable again) or cancel it (deleting the partial copy on your server);
- read metadata: server and database names and sizes, table counts, PostgreSQL settings, task logs, metrics and long-running query text (unless you turn it off);
- withhold agent updates, or offer a release that was properly signed but is known to be bad. Agents still self-test it, put it on probation and roll back;
- with Rowsafe Storage only: delete or withhold your backups, because Rowsafe runs that bucket. They still couldn't read them. If that matters to you, use your own bucket;
- with a connected cloud account (Create a server for me): create, resize or delete servers there, and get into the servers it reaches with your cloud's own tools. The list below doesn't hold for those servers; see Servers Rowsafe creates.
They could not:
- run arbitrary commands or SQL on your server;
- read your backups: they are encrypted with a passphrase that never leaves your server, except encrypted to another of your servers' agents for a standby, a fork to another server or Proof there, or to a temporary Rowsafe Cloud server you chose for Proof (see above);
- with your own bucket, reach it at all: its keys never leave your server, except in that same way;
- make an agent run a binary that wasn't signed with Rowsafe's release key, or downgrade an agent;
- change the firewall on a server where you didn't allow it, or touch any port but PostgreSQL's;
- learn a password: a new one is made in the person's browser, and roles are reported without their hashes;
- restart PostgreSQL on a server where you didn't allow it, or stop it, except to fence an old primary after a standby promotion (see above);
- install PgBouncer on a server where you didn't allow it, or point it at another server;
- install or remove packages other than PostgreSQL's own,
pgbouncer(where you allowed pooling) and the security repository's upgrades, downgrade PostgreSQL, or do any of the updates on a server where you didn't allow them; - read your rows: comparisons and restores run on your server and report only counts and sizes;
- drop a table: the maintenance actions never touch table data, and an index that backs a primary key, unique or exclusion constraint is never dropped (removing a whole database is the exception above, always after a Mark);
- change the settings Rowsafe uses for backups, restores, standbys or its own connection through Tuning, or restart PostgreSQL to apply a setting on a server where you didn't allow restarts.
Encryption
- Backups and WAL are compressed, then encrypted on your server with
aes-256-cbc, using your passphrase, before upload. Your bucket, or Rowsafe Storage, holds only ciphertext. - The passphrase and the bucket keys live only on your server, in
/etc/rowsafe/agent.envand the generated pgBackRest configuration, both mode 0600 and owned bypostgres. Anyone who can becomepostgrescan already read the database itself. - Rowsafe can't recover a lost passphrase. Keep it in your secret manager.
- All traffic between the agent, the CLI and Rowsafe uses HTTPS.
Servers Rowsafe creates
With Create a server for me, Rowsafe creates a server in your own cloud account (Hetzner, DigitalOcean, Vultr, AWS, Google Cloud, Azure or OVHcloud). For that, and only that, it keeps a credential for your cloud account:
- Limited to a dedicated place: one Hetzner project, a DigitalOcean team, a Vultr account or sub-user, an IAM user whose policy only changes what is tagged
rowsafe=1, a Google Cloud project's service account, an Azure app limited to one resource group, or one OVHcloud Public Cloud project. Each page shows exactly what access it gives: Hetzner, DigitalOcean, Vultr, AWS, Google Cloud, Azure, OVHcloud. - Stored encrypted. Rowsafe checks it, then seals it with AES-256-GCM under a key kept outside its database. It's never shown again, logged or put in an error message.
- Used only when a person asks. Rowsafe creates, resizes, changes the firewall of or deletes a server only when an owner or admin clicks and confirms (or uses a read-write API key), then follows that change through. Every change is in the audit log. AI agents (MCP) can't create, resize or delete servers in your cloud account, only in Rowsafe Cloud.
- Revocable. Delete the token or key in your cloud and it stops working at once. Remove under Settings → Cloud accounts makes Rowsafe forget it.
On the server itself:
- Rowsafe never adds its own SSH key, and doesn't need one. Only the public keys you give go on the server, and SSH stays closed in the firewall unless you open it to addresses you choose (on some OVHcloud projects the firewall runs on the server itself and starts a few minutes after boot: why).
- The server gets no secret from Rowsafe except a one-time enrollment token that works only for that server and expires soon. Backups go to Rowsafe Storage with the same temporary, per-organization access as any server; no bucket key passes through Rowsafe or the server's setup.
- The backup passphrase is made on the server and never leaves it, except encrypted to your browser when an owner or admin clicks Show passphrase: the browser makes a one-time key pair, the agent encrypts the passphrase to it, and Rowsafe relays the ciphertext once. Each time is audited. New database passwords work the same way.
- What Rowsafe may do on it (restarts, updates, reboots, firewall, new clusters) is what you chose when you created it, enforced on the server by root's allow lists like any server, and still only when someone clicks and confirms.
Know the trade-off. A cloud credential that can create and delete servers is powerful. Someone who controlled the Rowsafe service could use it to create servers your cloud bills you for, resize or delete the servers in that project, team, account or resource group, change who can reach them, and get into them with your cloud's own tools (resetting a password, a rescue system, changing what runs at boot) to read the database. Rowsafe never does any of that on its own. If it matters to you, revoke the credential once the server is ready: the server, its backups, Proof and Pulse keep working; only resizing, firewall changes and deleting from the dashboard stop.
Rowsafe Cloud servers run in Rowsafe's own cloud accounts instead. Everything above about the server holds (no Rowsafe SSH key, a passphrase only you have), except that Rowsafe keeps it patched: security updates install by themselves without restarting the database, and what needs a restart is applied in a weekly maintenance window you can change or turn off (critical security fixes are applied after 7 days' notice even then). Rowsafe holds the cloud account, so it could reach the server's disk. To rule that out, create the server in your own cloud account. AI agents create, change or delete Rowsafe Cloud servers only as the owner or admin who connected them, or within a monthly budget an owner set.
Credentials
| Credential | Prefix | Held by | Allows |
|---|---|---|---|
| API key | rsk_ | You: CLI, scripts, CI, AI agents | The Rowsafe API for one organization. Read-only keys can only read. |
| Enrollment token | rse_ | The installer, once | Enrolling one host. Single use, 1 hour by default. |
| Agent token | rsa_ | /var/lib/rowsafe/agent.json (0600) | The agent API for one host: its own tasks and heartbeats. |
| Bucket keys | agent.env and pgBackRest configuration, on your server | Your bucket. | |
| Rowsafe Storage credentials | /var/lib/rowsafe/storage-credentials.json and pgBackRest configuration (0600), on your server | Your organization's space in Rowsafe Storage only, for 7 days; renewed daily. | |
| Encryption passphrase | The same files, and your secret manager | Decrypting every backup. | |
| Cloud account credential | Rowsafe, sealed with AES-256-GCM, only if you use Create a server for me | Creating, resizing and deleting servers where you allowed it: one Hetzner project, DigitalOcean team, Vultr account, Google Cloud project, Azure resource group or OVHcloud Public Cloud project, or what the AWS policy allows. |
Rowsafe stores only SHA-256 hashes of API keys, enrollment tokens and agent tokens.
Usage counts
The CLI sends nothing about how you use it unless you run rowsafe telemetry on. Then it sends, after each command, the command's name, the CLI's version, OS, exit code, a duration range, whether a terminal or an AI agent ran it, whether you're logged in, and a random ID: never arguments, names, connection strings or errors. rowsafe telemetry status shows exactly what goes out; rowsafe telemetry off, ROWSAFE_TELEMETRY=0 or DO_NOT_TRACK=1 stop it. Details: Usage counts.
Signed releases
The installer and the agent only install releases signed with Rowsafe's Ed25519 release key, which is built into them and never stored on the Rowsafe service. Every release also has build provenance, an SBOM and reproducible builds you can check yourself: see Verifying releases.
Webhooks
Webhooks are signed with a per-channel secret (verify them). Rowsafe sends them only to public https URLs, checks every address after DNS resolution, and never follows redirects.