Skip to content

Machine Keys (Non-interactive)

By default, ov run prompts for your master password every time it runs. That’s fine for local development but blocks automated deployments — CI pipelines, Docker containers, and systemd services have no terminal to type into.

Machine keys solve this. You derive a scoped keyring file once (interactively, as a human), then automated processes read its derived key material without prompting. The command writes key material directly to the destination file; stdout contains only confirmation text.


ov auth derive-machine-key → writes a scoped v3 keyring to a chmod 600 file
ov run --machine-key <path> → loads the keyring and runs without prompting

The v3 file is an OVKR container containing derived key material, its scope descriptor, and the public key used to verify accepted egress policies. It contains neither your master password nor a raw KEK. ov run validates permissions before reading: if the file isn’t exactly chmod 600 and owned by the current user, it hard-errors and refuses to load the key.

v0.20 refuses v2 machine-key files, even before egress policy adoption. There is no password-free conversion. Record the original explicit scopes without inspecting or pasting the old file, then re-derive with the v0.20 binary:

Terminal window
ov auth derive-machine-key --scope myapp/production --out /etc/ov/machine-v3.key

Repeat --scope app/env for the deployment’s original authorized selectors. Omit --scope only when you intentionally want wildcard access; omission means wildcard, not the configured app or the old file’s scope. Replace the configured path, restart the daemon or service, and restart its ov mcp serve processes so those processes use the new binary and keyring.


  1. Log in and create your app

    If you haven’t already:

    Terminal window
    ov auth login
    ov app create myapp
    ov secret set DATABASE_URL --app myapp
  2. Derive the machine key

    Run this once as a human — it prompts for your master password and writes the derived key to disk:

    Terminal window
    ov auth derive-machine-key --scope myapp/production --out /etc/ov/machine.kek

    The keyring contains only the recorded scope’s derived material. Add repeated --scope selectors when the deployment needs more than one partition. Bind requests at call time via --app <slug> and --env <slug> on ov run and ov agent start; those routing flags do not widen the keyring.

    You’ll see:

    Master password: ▌
    Machine keyring written to /etc/ov/machine.kek
    This file is BEARER MATERIAL for its scope.
    - Keep it chmod 600, owned by the service account
    - Never commit to version control
    - Revoke access by rotating the OV API key for this deployment
    - Rotate with: ov auth derive-machine-key --scope myapp/production --out /etc/ov/machine.kek --force

    Verify the permissions:

    Terminal window
    ls -la /etc/ov/machine.kek
    # -rw------- 1 deploy deploy ... /etc/ov/machine.kek
  3. Test non-interactive injection

    Terminal window
    ov run --machine-key /etc/ov/machine.kek --app myapp -- env | grep DATABASE_URL
    # DATABASE_URL=postgres://...

    No password prompt. If it doesn’t work, see Troubleshooting.


#!/usr/bin/env bash
set -euo pipefail
ov run \
--machine-key /etc/ov/machine.kek \
--app myapp \
--secrets DATABASE_URL,REDIS_URL \
-- ./bin/server

The process starts with DATABASE_URL and REDIS_URL in its environment. OpaqueVault injects them without writing the environment to disk or placing values in command arguments. Child-process output remains application-controlled and can log injected values.


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 machine key with v0.20.0 or newer; v0.19 and older read only v2 keys and reject a v3 keyring, so pin the image to v0.20.0 or newer.

Store the machine key on the host and bind-mount it read-only:

# Dockerfile — no secrets baked in
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 container never has network access to your vault credentials — just the machine key for decrypting the specific app’s secrets.


/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 service account (deploy) must own /etc/ov/machine.kek and the file must be chmod 600.


If you suspect the key file is compromised:

  1. Revoke the OV API key for this deployment immediately — this cuts off access regardless of whether the attacker has the machine key file. A machine key decrypts ciphertext, but the ciphertext still has to be fetched from the API, so a revoked key makes the stolen file inert.

    Terminal window
    ov apikey list # locate this deployment's key by NAME / PREFIX, copy its ID
    ov apikey revoke <id> --confirm # alias of `ov apikey delete`; immediate and irreversible

    It takes effect server-side immediately. Any still-valid key of yours can revoke; no browser is needed during incident response. (The dashboard at app.opaquevault.com → API Keys → Revoke calls the same DELETE /v1/apikeys/{id} route.)

  2. Create a replacement key with ov apikey create --name <name> --scope read [--expires 90d] (the plaintext is printed once on stdout; the CLI does not store it) and update the deployment’s ~/.config/ov/config.toml (ov auth login is interactive-only, so it is not an option on an unattended host — see CI/CD).

  3. Rotate the machine key:

    Terminal window
    ov auth derive-machine-key --scope myapp/production --out /etc/ov/machine.kek --force

    Preserve the deployment’s original explicit selectors, repeating --scope app[/env] when it previously authorized more than one partition.


Property Detail
Permission enforcement Hard error if not exactly chmod 600 and owned by current user — no silent warnings
Symlink protection Opened with O_NOFOLLOW — symlinks to other files are rejected at the OS level
TOCTOU safety Permissions checked on the open file descriptor via fstat, not a separate stat call
Memory protection Scoped keyring buffers are passed to mlock on Linux and macOS. Failure is non-fatal, so swapping remains possible.
Core dump exclusion On Linux, scoped keyring buffers are passed to MADV_DONTDUMP; failure is non-fatal. Darwin has no per-mapping equivalent.
Buffer cleanup Closing the loaded keyring or session is the cleanup boundary for its owned derived-key buffers.

machine key has permissions 0644 — must be 0600

Terminal window
chmod 600 /etc/ov/machine.kek

machine key is owned by uid 0 but current uid is 1001

The file is owned by root but the service runs as a different user. Fix ownership:

Terminal window
chown deploy:deploy /etc/ov/machine.kek

machine-key file predates egress policy (format v2)

Re-derive the file from the master password with the original explicit scopes, then restart the daemon and MCP bridges that use it:

Terminal window
ov auth derive-machine-key --scope myapp/production --out /etc/ov/machine-v3.key

opening machine key: too many levels of symbolic links

The path is a symlink. ov run refuses symlinks by design. Use the real file path.