Migrating from .env to OpaqueVault
This guide walks you from “I have a .env file and a running service” to “my service boots from OpaqueVault with no plaintext secrets on disk” without breaking production along the way.
If you just ran ov secret import and want to know what to do next, start at Import.
The migration in one picture
Section titled “The migration in one picture”.env file ──(1) import──▶ OpaqueVault (ciphertext on server) │ ├─(2) verify count │ ├─(3) setup Linux: machine key + ov agent install │ macOS: ov agent start (interactive) │ ├─(4) cut over edit systemd unit to `ov run ...` │ ├─(5) reboot-test prove the service comes back up with no .env │ └─(6) delete .env mv .env .env.bakDo not skip steps. Each one is there because skipping it has broken somebody’s service.
1. Import
Section titled “1. Import”ov secret import /opt/myapp/.env.production --app myapp --env productionYou should see per-secret ✓ lines and a final Imported N/M secret(s) into myapp/production summary. The CLI also prints platform-specific “Next steps”; this doc is the long-form version of that.
2. Verify
Section titled “2. Verify”ov secret list --app myapp --env productionThe list length must match the import count. If it doesn’t — stop. Re-run import with --dry-run to see what the parser picked up, fix the source file, and re-import. Don’t continue on a partial import.
3. Setup: unlock the KEK on this machine
Section titled “3. Setup: unlock the KEK on this machine”This is where Linux servers and developer Macs diverge.
Linux server / VM (systemd)
Section titled “Linux server / VM (systemd)”On a server, any reboot kills the in-memory KEK. If your service starts up before you’ve unlocked the vault, ov run will fail and the service will not come up. The fix is a machine key + systemd-managed agent:
# 1. Pre-create /etc/ov writable for your user so derive-machine-key can# populate it WITHOUT sudo — it needs your own ov auth config which lives# in your home directory, not root's. This is a TEMPORARY permission set;# step 3 hardens /etc/ov back to root-owned.sudo install -d -o $USER -g $USER -m 700 /etc/ov
# 2. Derive the machine key as YOURSELF (not sudo):# The key is written 0600 owned by $USER — exactly the permissions the# systemd unit (User=$USER) needs. No chown/chmod dance required on the# key file itself.ov auth derive-machine-key --out /etc/ov/myapp.key
# 3. Install the agent. `ov agent install` writes the systemd unit AND# tightens /etc/ov/ to root-owned 0755 + writes the rendezvous file# at /etc/ov/agent.socket as root-owned 0644 (OV-126 trust model).# --app and --env are REQUIRED — they bake into ExecStart so the agent# is not coupled to any config-file state of the service user. --user# pins the systemd User= field to your service account (cannot be root).sudo ov agent install \ --machine-key /etc/ov/myapp.key \ --app myapp --env production \ --user $USER --enableFinal state after step 3:
| Path | Owner | Mode | Why |
|---|---|---|---|
/etc/ov/ | root:root | 0755 | World-traversable so CLI clients can read the rendezvous file; only root can write |
/etc/ov/agent.socket | root:root | 0644 | Single trust root for socket discovery; tamper-proof for same-UID attackers |
/etc/ov/myapp.key | $USER:$USER | 0600 | Matches the systemd User= that needs to read it |
Why not just sudo everything? ov auth derive-machine-key reads your ov auth config (API key, KEK salt) from your own ~/.config/ov/config. Root has no such config and the command fails with “config not found.” That’s why step 2 runs as your regular user.
What ov agent install does:
- Writes
/etc/systemd/system/ov-agent.servicewithUser=$USER,RuntimeDirectory=ov-agent, and a fully self-contained ExecStart. - Writes
/etc/ov/agent.socket— one-line rendezvous file so MCP clients (ov mcp servefor Claude Code) find the agent without requiring$XDG_RUNTIME_DIR. - With
--enable, runssystemctl daemon-reload && systemctl enable --now ov-agent.
What ov agent install does NOT do: it does not make ov run work non-interactively on its own. The agent is for MCP tool calls (Claude via ov mcp serve). For headless service startup, your service’s systemd unit needs ov run --machine-key /etc/ov/<app>.key — see §4 below. The agent and the machine key are two separate non-interactive paths serving different callers.
macOS dev laptop
Section titled “macOS dev laptop”No systemd, no /etc/ov, no persistence requirement — just unlock the agent for this session:
ov agent start # prompts for master passwordThe agent holds the KEK in memory and serves ov run requests until you stop it or log out. On the next login, run it again.
4. Cut the service over
Section titled “4. Cut the service over”Confirm the service can read secrets from the vault before you change any unit files. On a server, pass --machine-key (see §3) so you don’t prompt for a password:
sudo systemctl stop myappov run --app myapp --env production \ --machine-key /etc/ov/myapp.key \ --secrets DATABASE_URL,STRIPE_SECRET_KEY \ -- /opt/myapp/bin/server# hit an endpoint, check logs, Ctrl-CIf that works, update the systemd unit:
[Service]ExecStart=/usr/local/bin/ov run \ --app myapp --env production \ --machine-key /etc/ov/myapp.key \ --secrets DATABASE_URL,STRIPE_SECRET_KEY \ -- /opt/myapp/bin/server# EnvironmentFile=/opt/myapp/.env.production # ← delete this linesudo systemctl daemon-reloadsudo systemctl start myappKeep the .env file on disk. You’re not done yet.
5. Reboot-test
Section titled “5. Reboot-test”sudo rebootAfter the box comes back up:
systemctl status myapp # expect active (running)journalctl -u myapp -n 50 # expect normal startup logsIf the service didn’t come back up, see Troubleshooting. Do not proceed to step 6 until at least one reboot has succeeded with the service loading secrets from the vault.
6. Retire the .env file
Section titled “6. Retire the .env file”Now you can delete it — and use mv, not cp + rm, so there’s no window where two copies exist:
sudo mv /opt/myapp/.env.production /opt/myapp/.env.production.bakKeep the .bak for one deploy cycle so you have an obvious rollback. After that, delete it and rotate any API keys that were in it, because the plaintext has been on disk.
Machine-key security model
Section titled “Machine-key security model”A machine key on disk is a security downgrade compared to interactive unlock. The KEK-equivalent exists at rest on the filesystem, so the blast radius is bounded by filesystem ACLs rather than by memory-only residence. This is the necessary trade for unattended reboot survival. If your threat model cannot accept a KEK-at-rest, use interactive ov agent start instead and accept that reboots require a human.
A machine key is a 32-byte KEK-equivalent stored as a file. It is equivalent in sensitivity to your master password — it can decrypt every secret your account can, across every app. Treat it accordingly:
| Property | Rule |
|---|---|
| Permissions | chmod 600, owned by the service account. ov run hard-errors on wider permissions. |
| Scope | The key decrypts all of your account’s secrets — there is no per-app cryptographic scoping. Per-app isolation for machine keys is tracked in OV-278. Bound the blast radius with one key per host and tight filesystem ACLs. |
| Storage | /etc/ov/<app>.key on Linux. Never in a repo. Never in a container image. Bind-mount at runtime. |
| Symlinks | Refused at the OS level (O_NOFOLLOW). Always point at the real file. |
| Memory | mlocked and MADV_DONTDUMP — won’t page to swap, won’t appear in core dumps. |
| Rotation | ov auth derive-machine-key --force overwrites. Revoke the associated API key first if you suspect compromise. |
Why not TPM?
Section titled “Why not TPM?”A TPM-sealed machine key (where the KEK never lives unencrypted on disk, only inside the TPM’s secure element) is a strictly better story and is on the roadmap. Until then, the file + chmod 600 model is the best non-interactive option and is how we run our own production services.
The bootstrap paradox: OV’s own servers
Section titled “The bootstrap paradox: OV’s own servers”OpaqueVault’s API server (api.opaquevault.com) cannot use OpaqueVault to manage its own secrets. The server’s TLS key, database password, and HMAC signing key must exist in plaintext somewhere readable at boot, because OV needs those to come up before any vault operation is possible.
If you run your own OV deployment, those bootstrap secrets go in systemd LoadCredential= or a cloud KMS — not in OV. Any other app on that same host can and should use OV normally.
This is the “turtles all the way down” problem every secret manager has. We don’t pretend to solve it; we just tell you about it so you don’t try to migrate OV’s own .env into OV.
Troubleshooting
Section titled “Troubleshooting”ov run fails after a reboot
Section titled “ov run fails after a reboot”Check in this order:
-
Is the agent running?
Terminal window systemctl status ov-agentIf it’s
inactiveorfailed, the machine key file is probably missing, wrong permissions, or owned by the wrong user. -
Is the machine key readable by the service account?
Terminal window ls -la /etc/ov/myapp.key# -rw------- 1 deploy deploy 32 ...Must be
chmod 600and owned by whichever userExecStartruns as. A common mistake is deriving the key as root withsudobut running the service asdeploy. -
Is the routing context right?
Terminal window ov run --app myapp --env production -- env | grep -c .--app/--envonov runare routing flags — they select which(app, env)context the request binds to. A machine key itself is not app-scoped (it decrypts all of your account’s secrets), so a wrong-app failure is a routing mismatch: the secret you expected lives under a different(app, env)than the one you passed. Re-check the flags againstov secret list --app <slug> --env <env>. -
Did the systemd unit get
daemon-reloaded? Editing/etc/systemd/system/myapp.servicewithoutsystemctl daemon-reloadmeans the oldEnvironmentFile=line is still authoritative in the running config. The reboot fixes this implicitly; a misseddaemon-reloadbetween edits does not. -
Did you delete the
.envtoo early? If steps 1-4 look fine but the service still won’t start, your.envmay have had values that weren’t in the import (env-specific vars the import skipped, etc.). Restore.env.bak, start the service, diffenvoutput before/after, and import the missing keys.
ov run works but Claude can’t read secrets via MCP
Section titled “ov run works but Claude can’t read secrets via MCP”That’s a separate path — Claude talks to ov mcp serve, not the systemd agent. See ov mcp serve.
I need to change the master password
Section titled “I need to change the master password”The machine key is derived from your KEK, which is derived from your master password. Rotating the password invalidates every machine key. Plan a redeploy:
- Rotate the password:
ov auth change-password - Re-derive every machine key:
ov auth derive-machine-key --force --out /etc/ov/<app>.key - Restart services.
See also
Section titled “See also”ov agent— daemon lifecycle, socket auth- Machine Keys — security properties, Docker + systemd recipes
- Quickstart — the 5-minute intro if you haven’t done one yet