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 --out ./machine.kek
    # prompts for your master password, then writes ./machine.kek

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

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

    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):

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


Bind-mount the key file read-only at container startup:

# Dockerfile
FROM debian:bookworm-slim
RUN curl -fsSL https://get.opaquevault.com | sh
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