Skip to content

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.


  1. Derive a machine key once on your workstation (one interactive password prompt)
  2. Place the key file on the target system with chmod 600
  3. 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.


  1. 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 90d

    Give it a name that identifies the deployment (e.g. ci) and pick a scope:

    Scope Grants
    read Read secrets only — the right choice for most deployments
    readwrite Full secret access (create, update, delete), plus API-key creation
    admin Everything, 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 ov on the target system. ov auth login is 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 server
    mkdir -p ~/.config/ov
    cat > ~/.config/ov/config.toml <<'EOF'
    api_url = "https://api.opaquevault.com"
    api_key = "ov_live_ci_a3f9b2c1..."
    app_slug = "my-saas"
    EOF
    chmod 600 ~/.config/ov/config.toml

    Only api_key is required; api_url and app_slug fall back to https://api.opaquevault.com and default. The API key alone grants no access to secret values — it only reaches ciphertext. Decryption still needs the machine key below.

  2. 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.kek

    The 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 --scope intentionally 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.

  3. Transfer the key to the target system securely

    Terminal window
    # Example: scp to a server
    scp -p ./machine.kek deploy@server:/etc/ov/machine.kek
    ssh deploy@server "chmod 600 /etc/ov/machine.kek && ls -la /etc/ov/machine.kek"
    # -rw------- 1 deploy deploy ...

    Delete the local copy after transfer.

  4. Run without a prompt

    Terminal window
    ov run --machine-key /etc/ov/machine.kek --app my-saas -- ./bin/server

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.

Terminal window
# On your workstation — encode for GitHub secrets
base64 -i machine.kek | pbcopy # macOS
base64 -w0 machine.kek | xclip # Linux

Add 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.sh

Two 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.


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:

# Dockerfile
FROM debian:bookworm-slim
COPY --chmod=0755 ./bin/ov /usr/local/bin/ov
RUN ov --version | grep -F '0.20.0'
COPY ./bin/server /app/server
ENTRYPOINT ["ov", "run", "--machine-key", "/run/secrets/machine.kek", "--app", "myapp", "--", "/app/server"]
Terminal window
docker run \
-v /etc/ov/machine.kek:/run/secrets/machine.kek:ro \
myapp:latest

The machine key is never baked into the image — it’s injected at runtime from the host.


/etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
User=deploy
ExecStart=/usr/local/bin/ov run \
--machine-key /etc/ov/machine.kek \
--app myapp \
-- /opt/myapp/server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Terminal window
systemctl daemon-reload
systemctl enable --now myapp

The deploy user must own /etc/ov/machine.kek with chmod 600.


If a machine key is compromised:

  1. 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 ID
    ov 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.

  2. Create a replacement key and update ~/.config/ov/config.toml on the deployment:

    Terminal window
    ov apikey create --name ci --scope read --expires 90d
  3. 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