How symflip handles your SSH keys and secrets
A deploy tool asks for the keys to your production box. Here's exactly what symflip does with your SSH keys and secrets, where they live, and the honest limits of the design.
To deploy for you, symflip needs two things most tools are cagey about: a way into your server, and your app's secrets to write onto it. That's a lot of trust to hand a piece of software, and "trust us, it's encrypted" is not an answer.
So here is the whole story. What symflip stores, how it's protected, what touches your server, and where the honest limits of the design are. No hand-waving.
What symflip actually holds
Two kinds of sensitive data, and nothing else you'd lose sleep over:
- A deploy key, the SSH private key symflip uses to connect to your server.
- Your project secrets, the environment variables (
DATABASE_URL, API keys, and the like) that symflip writes into your app's environment on each release.
Both are sealed before they ever hit the database, and neither is ever written to a log or handed back out through the API.
Everything is sealed at rest
Every secret value and every deploy key is encrypted with
NaCl secretbox, which pairs XSalsa20 for
confidentiality with Poly1305 for authentication, before it's stored. Two
properties matter here:
- It's authenticated. If a stored value is tampered with, whether that's a flipped byte in the database or a truncated blob, decryption fails rather than handing back garbage. Corruption is caught, not silently used.
- A fresh random nonce per value. Sealing the same secret twice produces two different ciphertexts. Someone who gets a look at your database can't even tell which projects share a secret value, let alone read one.
value_enc = nonce || secretbox(secret, nonce, key)
(24 random bytes, different every time)
The one key that opens it never lives in the database
All of that sealing happens under a single 32-byte key that symflip reads from
one place: the ENCRYPTION_KEY environment variable. It is never stored in the
database it protects.
That separation is the point. A leaked database backup, which is the single most common way secrets escape, is inert on its own. Without the key, held only in the running process's environment, the sealed columns are noise.
symflip is deliberately strict about that key. It refuses to start without one rather than quietly inventing a throwaway that would make your data unrecoverable later. And if it detects the public development key from our own example configs while running in a production posture, it refuses to boot at all. That exact copy-paste accident is how a fleet ends up sealed under a key the whole internet knows.
Your deploy key: authorized by you, revocable by you
When you add a server, symflip generates a fresh Ed25519 keypair scoped to
that one project. The private half is sealed and stored the moment it's created,
no endpoint ever returns it, and it's never logged. The only thing you ever
see, and the only thing that leaves symflip, is the public line, which you add to
your server's authorized_keys.
It's worth being precise about what "a key you control" means here, because it's a phrase deploy tools throw around loosely. symflip generates the key, so you don't hold its private half. What you do hold is the authorization: nothing can use that key until you paste its public line onto your box, and you can revoke it instantly by deleting that one line. No dashboard, no waiting on us. The key is scoped to a single project, so revoking one never touches another.
Getting onto your server, safely
symflip reaches your server over an ordinary, encrypted SSH connection, using public-key auth with the deploy key above. The part worth calling out is how it handles your server's identity.
On the first connection, symflip records the server's host-key fingerprint and pins it. Every connection after that verifies the server presents the same key. If it ever doesn't, the deploy stops with a hard error. symflip never guesses, and it never falls back to connecting anyway.
first connect -> pin SHA256:... as this server's identity
every connect -> fingerprint matches? yes -> proceed
no -> stop. do not deploy.
That mismatch means one of two things: your server was legitimately rebuilt, or something is sitting in the middle of the connection. Only you can tell those apart, so re-trusting a changed key is an explicit action you take, never something symflip does silently on your behalf.
Secrets are decrypted late, and nothing lingers
Your secrets stay sealed right up until the moment they're needed. Only during a deploy, on the way to writing your app's environment file onto the release, does symflip decrypt them, and the deploy key is likewise unsealed only at the instant a connection is opened.
And because symflip is agentless, nothing it runs stays behind on your server holding any of this. There's no daemon, no sidecar, no secrets cache sitting on your box. What lands on the server is your app's own environment file, the one you defined, plus the public key you chose to authorize. That's it.
Rotating the key
Keys should be rotatable, so they are. deployer rotate-key re-seals every stored
secret and deploy key under a new ENCRYPTION_KEY in a single all-or-nothing
transaction: it either finishes with everything under the new key, or changes
nothing. There's no in-between state where half your data is sealed under a key
you're about to retire.
The honest limits
Good security writing says what a design doesn't do, so here goes.
It's one symmetric key in an environment variable. Not a hardware security
module, not a cloud KMS, not envelope encryption with a separate data key per
record. That's a deliberate trade. symflip is one Go binary plus Postgres that you
can run yourself, and bolting on a mandatory KMS would trade that simplicity for a
dependency most of our users don't want. Protect ENCRYPTION_KEY the way you'd
protect any root credential, and keep it out of your database backups.
The running process necessarily handles plaintext. To use a key or write a secret, symflip has to decrypt it in memory at deploy time. Encryption at rest protects your database and your backups. It can't protect against someone who already has code execution inside the running app, and no at-rest scheme can.
Neither of these is a footnote we're hiding. They're the shape of the trade-off, and now you can weigh it with eyes open.
The short version
Your secrets and deploy keys are sealed with authenticated encryption before they touch the database. The key that opens them lives only in the app's environment, never beside the data. Your deploy key is scoped to one project, never exposed, and revocable by you in one line. Connections verify your server's identity every time. And nothing symflip runs stays behind on your box.
That's the standard a tool touching your production servers should meet, and it's the one symflip is built to.
Try it free. Connect your first server in minutes.