CI/CD
OpaqueVault works in fully automated environments via machine keys — a 32-byte key file you derive once interactively, then use non-interactively in any pipeline.
The pattern
Section titled “The pattern”- Derive a machine key once on your workstation (one interactive password prompt)
- Place the key file on the target system with
chmod 600 - Use
ov run --machine-key <path>— no password prompt, no terminal required
That’s it. One key file per deployment, protected by filesystem permissions. The key is a master-password-equivalent — keep one per host and lock it down with chmod 600.
-
Create a scoped API key for the deployment
Use a dedicated API key for each deployment rather than your personal key. From a workstation where you are already logged in (
ov auth login), create one:Terminal window ov apikey create --name ci --scope read --expires 90dGive it a name that identifies the deployment (e.g.
ci) and pick a scope:Scope Grants readRead secrets only — the right choice for most deployments readwriteFull secret access (create, update, delete), plus API-key creation adminEverything, including account deletion The plaintext key is printed once, on stdout — metadata and the store-it-now warning go to stderr, so
KEY=$(ov apikey create ...)captures only the key. Copy it then; it cannot be retrieved later (the server keeps only a hash).Then configure
ovon the target system.ov auth loginis interactive only — it prompts for an email and reads the master password from a TTY, so it cannot run in a pipeline. For non-interactive use, write the config file directly:Terminal window # On the CI runner or servermkdir -p ~/.config/ovcat > ~/.config/ov/config.toml <<'EOF'api_url = "https://api.opaquevault.com"api_key = "ov_live_ci_a3f9b2c1..."app_slug = "my-saas"EOFchmod 600 ~/.config/ov/config.tomlOnly
api_keyis required;api_urlandapp_slugfall back tohttps://api.opaquevault.comanddefault. The API key alone grants no access to secret values — it only reaches ciphertext. Decryption still needs the machine key below. -
Derive the machine key (once, on your workstation)
Terminal window ov auth derive-machine-key --out ./machine.kek# prompts for your master password, then writes ./machine.kekThe machine-key file is equivalent in sensitivity to your master password — it can decrypt every secret your account can. Treat it accordingly. Per-app cryptographic isolation for machine keys is tracked in OV-278.
-
Transfer the key to the target system securely
Terminal window # Example: scp to a serverscp -p ./machine.kek deploy@server:/etc/ov/machine.kekssh deploy@server "chmod 600 /etc/ov/machine.kek && ls -la /etc/ov/machine.kek"# -rw------- 1 deploy deploy 32 ...Delete the local copy after transfer.
-
Run without a prompt
Terminal window ov run --machine-key /etc/ov/machine.kek --app my-saas -- ./bin/server
GitHub Actions
Section titled “GitHub Actions”Store the machine key as a base64-encoded GitHub secret (OV_MACHINE_KEY):
# On your workstation — encode for GitHub secretsbase64 -i machine.kek | pbcopy # macOSbase64 -w0 machine.kek | xclip # LinuxAdd to your repo: Settings → Secrets → New repository secret → OV_MACHINE_KEY.
Then in your workflow:
name: Deploy
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Install ov run: curl -fsSL https://get.opaquevault.com | sh
- name: Configure ov env: OV_API_KEY: ${{ secrets.OV_API_KEY }} run: | mkdir -p ~/.config/ov printf 'api_url = "https://api.opaquevault.com"\napi_key = "%s"\napp_slug = "my-saas"\n' \ "$OV_API_KEY" > ~/.config/ov/config.toml chmod 600 ~/.config/ov/config.toml
- name: Write machine key run: | mkdir -p /tmp/ov echo "${{ secrets.OV_MACHINE_KEY }}" | base64 -d > /tmp/ov/machine.kek chmod 600 /tmp/ov/machine.kek
- name: Run tests run: ov run --machine-key /tmp/ov/machine.kek --app my-saas -- go test ./...
- name: Deploy run: ov run --machine-key /tmp/ov/machine.kek --app my-saas -- ./scripts/deploy.shTwo secrets in GitHub (OV_API_KEY + OV_MACHINE_KEY). All your real secrets stay behind zero-knowledge encryption — the machine key decrypts them locally in the runner.
Docker
Section titled “Docker”Bind-mount the key file read-only at container startup:
# DockerfileFROM debian:bookworm-slimRUN curl -fsSL https://get.opaquevault.com | shCOPY ./bin/server /app/serverENTRYPOINT ["ov", "run", "--machine-key", "/run/secrets/machine.kek", "--app", "myapp", "--", "/app/server"]docker run \ -v /etc/ov/machine.kek:/run/secrets/machine.kek:ro \ myapp:latestThe machine key is never baked into the image — it’s injected at runtime from the host.
systemd
Section titled “systemd”[Unit]Description=My AppAfter=network.target
[Service]Type=simpleUser=deployExecStart=/usr/local/bin/ov run \ --machine-key /etc/ov/machine.kek \ --app myapp \ -- /opt/myapp/serverRestart=on-failureRestartSec=5
[Install]WantedBy=multi-user.targetsystemctl daemon-reloadsystemctl enable --now myappThe deploy user must own /etc/ov/machine.kek with chmod 600.
Key rotation
Section titled “Key rotation”If a machine key is compromised:
-
Revoke the deployment API key immediately — this cuts off vault access regardless of the key file.
Terminal window ov apikey list # find the row by NAME / PREFIX, copy its IDov apikey revoke <id> --confirm # alias of `ov apikey delete`Revocation takes effect server-side immediately — the machine key file becomes useless without a live API key, because reads still have to go through the API to fetch ciphertext.
-
Create a replacement key and update
~/.config/ov/config.tomlon the deployment:Terminal window ov apikey create --name ci --scope read --expires 90d -
Rotate the machine key:
Terminal window ov auth derive-machine-key --out machine.kek --force# transfer new key to server, update any secrets that stored it
Further reading
Section titled “Further reading”- Machine Keys reference — full security properties, troubleshooting, and all flags
- ov run reference — all flags
- ov agent — long-running daemon for Linux CI runners