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 --scope my-saas/production --out ./machine.kek# prompts for your master password, then writes key material to ./machine.kekThe v0.20 format-v3 machine-key file is bearer material for its recorded scope. Repeat
--scope app[/env]for the partitions this job needs. Omitting--scopeintentionally mints a wildcard keyring; it does not use the configured app. Treat the file accordingly. The command writes key material to the file; stdout contains only confirmation text. -
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 ...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):
Derive that key with v0.20.0 or newer, and run v0.20.0 or newer in the workflow: v0.19 and older read only v2 machine keys and reject a v3 keyring. The stable installer installs v0.20.0 once it is published; the install step below pins the version explicitly, so the workflow keeps running the release you derived its key with, and verifies both the checksum manifest’s Minisign signature and the archive checksum as described in Release signing & verification.
# 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 verified ov v0.20.0 run: | sudo apt-get update sudo apt-get install -y minisign version=v0.20.0 version_no_v=${version#v} release_dir=$(mktemp -d) curl -fsSL -o "$release_dir/ov-release.pub" https://opaquevault.com/.well-known/ov-release.pub curl -fsSL -o "$release_dir/ov_${version_no_v}_checksums.txt" \ "https://releases.opaquevault.com/${version}/ov_${version_no_v}_checksums.txt" curl -fsSL -o "$release_dir/ov_${version_no_v}_checksums.txt.minisig" \ "https://releases.opaquevault.com/${version}/ov_${version_no_v}_checksums.txt.minisig" curl -fsSL -o "$release_dir/ov_linux_amd64.tar.gz" \ "https://releases.opaquevault.com/${version}/ov_linux_amd64.tar.gz" minisign -Vm "$release_dir/ov_${version_no_v}_checksums.txt" \ -p "$release_dir/ov-release.pub" (cd "$release_dir" && sha256sum -c --ignore-missing "ov_${version_no_v}_checksums.txt") tar -xzf "$release_dir/ov_linux_amd64.tar.gz" -C "$release_dir" sudo install -m 0755 "$release_dir/ov" /usr/local/bin/ov ov --version | grep -F '0.20.0'
- 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”Download the Linux v0.20.0 archive for the image’s architecture, verify its
signed checksum by following Release signing & verification,
and extract ov to ./bin/ov in the build context. Derive the v3 key with
v0.20.0 or newer. Then bind-mount the key file read-only at container startup:
# DockerfileFROM debian:bookworm-slimCOPY --chmod=0755 ./bin/ov /usr/local/bin/ovRUN ov --version | grep -F '0.20.0'COPY ./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