with one click
secrets
// MUST be invoked before any work involving: ov secrets commands, KeePass .kdbx credential management, credential import/export, or secret database administration.
// MUST be invoked before any work involving: ov secrets commands, KeePass .kdbx credential management, credential import/export, or secret database administration.
| name | secrets |
| description | MUST be invoked before any work involving: ov secrets commands, KeePass .kdbx credential management, credential import/export, or secret database administration. |
Manage credentials in a KeePass .kdbx database. Part of ov's credential store hierarchy — the kdbx backend is used when the system keyring is unavailable (headless servers, SSH sessions) or when explicitly selected via ov settings set secret_backend kdbx.
Resolution order (first match wins):
| Priority | Backend | When used |
|---|---|---|
| 1 | Environment variable | OV_VNC_PASSWORD, etc. |
| 2 | System keyring | GNOME Keyring, KDE Wallet, KeePassXC |
| 3 | KeePass .kdbx | Headless/SSH or secret_backend: kdbx |
| 4 | Config file | Plaintext fallback in config.yml |
Iteration-capable keyring read path (since 2026-04): when the system
keyring backend is active, ov's read path (KeyringStore.Get →
keyringGetViaSSClient → ssClient.findItemAnyCollection) does NOT rely on
the Secret Service default alias alone. It iterates all healthy collections,
skipping any that return DBus errors on property reads. This makes ov
resilient to broken default alias stubs (commonly seen in KeePassXC
FdoSecrets setups) — credentials stored in a non-default collection are still
findable. Set keyring_collection_label via /ov-build:settings to pin ov to a
specific collection by label. When every candidate collection is locked
(requires interactive unlock), source=locked is returned and
ov config mount waits indefinitely via event-driven DBus signal
subscription (zero CPU) until the user unlocks the keyring. See /ov-advanced:enc
for the full iteration order, source classification, event-driven waiting
behavior, and troubleshooting.
| Action | Command | Description |
|---|---|---|
| Create database | ov secrets init [path] | Create new .kdbx, auto-set config |
| List entries | ov secrets list [service] | List entries, optionally filter by service |
| Get value | ov secrets get <service> <key> | Print credential value |
| Set value | ov secrets set <service> <key> [value] | Set credential (prompts if no value) |
| Generate value | ov secrets set <service> <key> --generate | Generate 32-char hex random value |
| Delete entry | ov secrets delete <service> <key> | Remove an entry |
| Import | ov secrets import [--dry-run] | Import from config.yml + keyring |
| Export | ov secrets export [--format yaml|json] | Export all entries (plaintext!) |
| Show path | ov secrets path | Print resolved .kdbx file path |
ov secrets init # Default: ~/.config/ov/secrets.kdbx
ov secrets init /path/to/secrets.kdbx # Custom path
ov secrets init --force # Overwrite existing database
Creates a new .kdbx database with password confirmation. Automatically sets secrets.kdbx_path in config. After init, activate with:
ov settings set secret_backend kdbx
Or it auto-activates when the system keyring is unavailable.
ov secrets list # All ov entries
ov secrets list ov/vnc # Filter by service prefix
Prints service/key pairs, one per line.
ov secrets get ov/vnc my-image # Print value
ov secrets set ov/vnc my-image # Prompt for value securely
ov secrets set ov/vnc my-image mypassword # Set value inline
ov secrets set ov/vnc my-image --generate # Generate and print random value
ov secrets delete ov/vnc my-image # Remove entry
The --generate flag produces 16 random bytes as 32-character hex string, printed to stdout.
ov secrets import --dry-run # Preview what would be imported
ov secrets import # Import from config.yml + keyring
Collects credentials from:
config.ymlkeyring_keys config listShows source and success/failure for each entry.
ov secrets export # YAML format (default)
ov secrets export --format json # JSON format
WARNING: Exports plaintext credentials. Outputs nested map: service -> key -> value.
ov secrets path # Prints resolved path or default location
ov secrets init # Create database
ov settings set secret_backend kdbx # Activate kdbx backend
ov secrets import --dry-run # Preview migration
ov secrets import # Migrate existing credentials
secret_accepts / secret_requires)A layer can declare credential-backed env vars in layer.yml via the
secret_accepts: / secret_requires: sections. At ov config time, the
declared values are resolved from the credential store, provisioned as
per-image podman secrets, and injected into the container at runtime via
Secret=<name>,type=env,target=<var> directives — never landing in
deploy.yml or the generated quadlet as plaintext. See /ov-build:layer
(secret_accepts / secret_requires) for the authoring side.
The credential store namespace for these entries defaults to ov/secret
with the env var name as the key. Layer authors can override with an
explicit key: ov/api-key/openrouter in layer.yml, which is useful when
multiple consumers should resolve the same upstream credential (e.g.,
openwebui and hermes both pointing at ov/api-key/openrouter so one
ov secrets set populates both).
Storage commands:
# Default path (matches layer.yml `secret_accepts: [{name: WEBUI_ADMIN_PASSWORD}]`)
ov secrets set ov/secret WEBUI_ADMIN_PASSWORD <password>
# Explicit key path (matches layer.yml `key: ov/api-key/openrouter`)
ov secrets set ov/api-key openrouter sk-or-xxxxxxxx
ov secrets set ov/api-key ollama gsk-yyyyyyyy
ov secrets set ov/api-key immich <immich-key-from-web-ui>
Auto-generated secret_requires: tokens. Since 2026-05-06,
secret_requires: entries that miss everywhere (env + credential
store) auto-generate a 32-byte hex token via generateRandomHex(32) +
DefaultCredentialStore.Set, persisted at ov/secret/<NAME> (or the
declared key: override). The first deploy that resolves the secret
writes; every subsequent deploy reads the persisted value. Race-free
across multi-layer declarations because DefaultCredentialStore caches
via sync.Once — when k3s-server and k3s-agent both declare
K3S_CLUSTER_TOKEN, whichever resolves first writes, and the other
reads. No operator setup required for ov update k3s-vm to succeed
on a fresh host.
secret_accepts: entries do NOT auto-generate (they're optional by
contract; the caller falls back to dep.Default when missing). Only
secret_requires: triggers the auto-gen path.
Retrieve an auto-generated value (e.g., to log into a service for the first time):
ov secrets get ov/secret K3S_CLUSTER_TOKEN
ov secrets get ov/secret WEBUI_ADMIN_PASSWORD
Override a secret_requires: value with a specific value before
the first deploy:
ov secrets set ov/secret K3S_CLUSTER_TOKEN <value>
The auto-gen path is skipped whenever any non-empty value is already resolvable (env var, keyring, kdbx, or config-file fallback).
Persistence venue. When no kdbx and no keyring are available
(headless first-boot, fresh CI runner), the auto-generated token
lands in ~/.config/ov/config.yml via ConfigFileStore (mode 0600).
Operators on multi-user hosts should run ov secrets init BEFORE
the first deploy to land subsequent secrets in an encrypted kdbx
instead.
Rotation: update the store and re-run ov config. The
RotateOnConfig flag on credential-backed secrets bypasses the
podmanSecretExists short-circuit, so the podman secret is re-created
with the new value on every ov config:
ov secrets set ov/api-key/openrouter <new-value>
ov config openwebui --update-all
systemctl --user restart ov-openwebui.service
One-shot -e import: the -e NAME=VAL CLI flag on ov config
auto-imports the value into the credential store when NAME matches a
secret_accepts / secret_requires declaration on the target image. The
plaintext is stripped from c.Env before it can reach saveDeployState
or the quadlet writer. First-time setup:
ov config openwebui -e WEBUI_ADMIN_PASSWORD=<password> -e OPENROUTER_API_KEY=sk-or-xxx
Subsequent ov config openwebui resolves from the store without needing
-e again.
Migration from legacy plaintext: on the first ov config after
upgrading to a version with this feature, any pre-existing NAME=VAL
entry in deploy.yml whose NAME is now a secret_accepts /
secret_requires declaration on the image is automatically moved to the
credential store. The plaintext is stripped, deploy.yml.bak.<ts> is
written as a rollback point, and the migration logs each entry on stderr.
Idempotent — safe to run on a clean host.
Distinction from layer-owned secrets:: the layer.yml secrets:
field (e.g., immich's db-password) creates per-image secrets that are
auto-generated once at ov config time and never rotated. Credential-
backed secret_accepts / secret_requires are user-owned, shareable
across consumers, and refreshed on every ov config. Both flow through
the same Secret=<name>,type=env,target=<var> quadlet emission; only the
rotation semantics differ. See /ov-build:layer (secret_accepts / secret_requires).
The .kdbx file is a standard KeePass database. You can open it with KeePassXC or any KeePass-compatible tool for backup or manual editing.
The kdbx master password is cached in the Linux kernel keyring (user keyring, key ov-kdbx-password) after the first interactive prompt. All subsequent ov commands within the timeout window reuse the cached password automatically -- no re-prompting.
Password resolution chain (first attempt):
OV_KDBX_PASSWORD environment variable (CI/automation)Configuration:
ov settings set secrets.kdbx_cache false # Disable caching entirely
ov settings set secrets.kdbx_cache_timeout 7200 # Cache for 2 hours instead of 1
Default: enabled, 3600 seconds (1 hour) TTL. Uses keyctl syscalls via golang.org/x/sys/unix. Source: ov/keyctl.go.
The master-password prompt has two safeguards that match standard CLI conventions:
When openKdbx rejects the supplied password (Wrong password? Database integrity check failed), ov re-prompts up to 3 times total (matching sudo's passwd_tries=3 default). Between attempts you see:
Sorry, try again.
Retries bypass both the OV_KDBX_PASSWORD env var AND the kernel-keyring cache, so a stale cached value or a wrong-env-var setup doesn't loop forever — every retry forces a fresh interactive prompt. After the third failed attempt, the underlying decoding kdbx: Wrong password? error is returned and the command exits non-zero. Errors other than wrong-password (file not found, malformed blob, I/O error) return immediately without retry.
When stdin is not a terminal AND OV_KDBX_PASSWORD is unset, ov aborts immediately with:
ov: error: kdbx password required but stdin is not a terminal; set OV_KDBX_PASSWORD=<password> or run interactively
This catches CI runs, SSH-non-interactive contexts, systemd service units, and tooling that loses TTY (like agent shells). Previously such contexts hung forever on term.ReadPassword. The guard fires unconditionally on no-TTY — systemd-ask-password is NOT a graceful fallback because askPassword invokes it with --timeout=0 AND passes our stdin through, so it inherits the same no-TTY hang.
The acceptance contract:
| Context | Behaviour |
|---|---|
| Interactive terminal | Prompt; on wrong, "Sorry, try again." up to 3× |
OV_KDBX_PASSWORD set, correct | Use env value, succeed silently |
OV_KDBX_PASSWORD set, wrong | Try env value (1st), then re-prompt — but if no TTY, fail-fast on the retry |
| Non-TTY, no env var | Fail-fast within ~50 ms, actionable error |
systemd unit (INVOCATION_ID set), no env var | Fail-fast (same code path as non-TTY) |
Source: ov/credential_kdbx.go (defaultKdbxAskPassword, openKdbxWithRetry, isWrongKdbxPassword, KdbxStore.openValidated). Tests: ov/credential_kdbx_test.go (TestOpenKdbxWithRetry_*, TestIsWrongKdbxPassword, TestKdbxStore_OpenValidated_ReusesCachedPassword).
| Key | Description |
|---|---|
secret_backend | Force backend: keyring, kdbx, or config |
keyring_collection_label | Preferred Secret Service collection label (empty = iterate naturally). Pin ov to a specific collection in multi-database setups. |
secrets.kdbx_path | Path to .kdbx file |
secrets.kdbx_key_file | Optional key file for .kdbx |
secrets.kdbx_cache | Enable/disable kernel keyring caching (default: true) |
secrets.kdbx_cache_timeout | TTL in seconds for cached password (default: 3600) |
Separate from ov's credential store, project-level environment variables (e.g., GMAIL_USER, GMAIL_PASSWORD) are stored in .secrets — a GPG-encrypted file at the project root. direnv decrypts it in memory via ov secrets gpg env when entering the directory (eval "$(ov secrets gpg env)" in .envrc).
This is NOT managed by ov secrets (kdbx). The two systems serve different purposes:
| System | What it manages | How it works |
|---|---|---|
ov secrets (kdbx/keyring) | Container-level credentials (VNC passwords, service secrets) | Provisioned at ov config time into Podman secrets |
ov secrets gpg + direnv | Project-level shell env vars (API keys, credentials) | GPG-encrypted .secrets file, decrypted by direnv on cd |
ov secrets gpg — GPG-Encrypted .secrets ManagementManage GPG-encrypted .secrets environment files directly from the CLI. All commands shell out to gpg (must be in PATH).
| Action | Command | Description |
|---|---|---|
| Show contents | ov secrets gpg show [-f FILE] | Decrypt and print to stdout |
| Export for eval | ov secrets gpg env [-f FILE] | Decrypt .secrets as export KEY='value' for eval/direnv |
| Edit in editor | ov secrets gpg edit [-f FILE] | Decrypt, open $EDITOR, re-encrypt |
| Encrypt file | ov secrets gpg encrypt -r KEY_ID [-i .env] [-o .secrets] | Encrypt plaintext env file |
| Decrypt file | ov secrets gpg decrypt [-i .secrets] [-o FILE] | Decrypt to file or stdout |
| Set a key | ov secrets gpg set KEY VALUE [-f FILE] [-r KEY_ID] | Add or update KEY=VALUE |
| Remove a key | ov secrets gpg unset KEY [-f FILE] | Remove a key from .secrets |
| Add recipient | ov secrets gpg add-recipient KEY_ID [-f FILE] | Re-encrypt with additional recipient |
| List recipients | ov secrets gpg recipients [-f FILE] | List GPG key IDs that can decrypt |
| Import key | ov secrets gpg import-key <path> | Import GPG key from file, directory, or Secret Service |
| Export key | ov secrets gpg export-key [path] [--to-keystore] | Export GPG key to directory and/or KeePassXC |
| Setup | ov secrets gpg setup | Configure gpg-agent, import/generate key, store passphrase |
| Doctor | ov secrets gpg doctor [-f FILE] | Health check: GPG agent, keys, Secret Service, .secrets |
ov secrets gpg envDecrypt .secrets and output export KEY='value' lines to stdout. Designed for eval or direnv:
eval "$(ov secrets gpg env)" # Load secrets into current shell
eval "$(ov secrets gpg env -f .secrets.prod)" # Load from specific file
Behavior:
.envrc — no error when .secrets is absent)shellQuote)dotenv_gpg_if_exists direnvrc function — no external dependency neededUsage in .envrc:
eval "$(ov secrets gpg env)"
# Create .secrets from a plaintext .env file
ov secrets gpg encrypt -r 420DE2B3 -i .env -o .secrets
rm .env
# View contents
ov secrets gpg show
# Add a new secret
ov secrets gpg set API_KEY sk-test-abc123
# Edit in your editor
ov secrets gpg edit
# Remove a secret
ov secrets gpg unset OLD_KEY
# Add another person who can decrypt
ov secrets gpg add-recipient THEIR_KEY_ID
# See who can decrypt
ov secrets gpg recipients
-f, --file — Path to encrypted file (default: .secrets in current directory)-r, --recipient — GPG key ID (repeatable, required for encrypt, optional for set on new files)-i, --input — Input file for encrypt/decrypt (default: .env / .secrets)-o, --output — Output file for encrypt/decrypt (default: .secrets / stdout)ov secrets gpg import-key — Import GPG KeysImport from file, directory, or KeePassXC Secret Service:
ov secrets gpg import-key ~/Sync/Conf/gpg/ # From directory (all .asc/.gpg + ownertrust.txt)
ov secrets gpg import-key ~/key.asc # From single file
ov secrets gpg import-key --from-keystore # From KeePassXC Secret Service
ov secrets gpg import-key --from-keystore --key-id XX # Specific key from KeePassXC
ov secrets gpg import-key ~/keys/ --passphrase "xxx" # With loopback pinentry (no GUI prompt)
Directory import auto-detects .asc, .gpg, and ownertrust.txt files. Keystore import retrieves armored keys stored with schema org.gnupg.Key — these are created by ov secrets gpg export-key --to-keystore.
The --passphrase flag uses GPG's loopback pinentry mode, avoiding the GUI prompt during import.
ov secrets gpg export-key — Export GPG KeysExport to filesystem and/or KeePassXC:
ov secrets gpg export-key ~/Sync/Conf/gpg/ # To directory (public.asc, secret.asc, ownertrust.txt)
ov secrets gpg export-key --to-keystore # To KeePassXC Secret Service
ov secrets gpg export-key ~/backup/ --to-keystore # Both filesystem and KeePassXC
ov secrets gpg export-key --key-id XXXX --to-keystore # Specific key
ov secrets gpg export-key --to-keystore --passphrase X # Also store passphrase in Secret Service
Keystore export stores the armored private key as a Secret Service entry (schema org.gnupg.Key, attributes: keyid, uid). This enables key recovery via import-key --from-keystore without filesystem backups.
ov secrets gpg setup — One-Stop GPG ConfigurationConfigures the full GPG/KeePassXC chain in one command:
ov secrets gpg setup # Interactive
ov secrets gpg setup --import ~/Sync/Conf/gpg/ # Import key, then configure
ov secrets gpg setup --from-keystore # Restore key from KeePassXC
ov secrets gpg setup --passphrase "xxx" # Batch mode (no prompts)
Steps performed:
gpg, gpg-connect-agent, pinentry-qt (libsecret), secret-tool~/.gnupg/gpg-agent.conf (pinentry-qt, 8h cache, allow-preset-passphrase)gpg-agent.socket, gpg-agent-extra.socket)Flags:
--import <path> — Import key from file/directory before setup--from-keystore — Import key from KeePassXC before setup--passphrase <value> — Batch mode: generate key + store passphrase without prompts--key-id <id> — Use specific existing key--skip-secret-service — Skip passphrase storage in Secret Serviceov secrets gpg doctor — Health CheckRead-only diagnostic of the full GPG/direnv chain:
ov secrets gpg doctor # Check everything
ov secrets gpg doctor -f .secrets.prod # Check specific file
Checks: gpg binary, gpg-agent status, gpg-agent.conf (pinentry, cache TTL), systemd sockets, secret keys, Secret Service availability, passphrase storage for each keygrip, key backups in Secret Service, .secrets file recipients and decrypt test.
Returns non-zero exit code if any critical issue found.
gpg --decrypt .secrets
→ gpg-agent (needs passphrase for subkey keygrip)
→ spawns pinentry-qt
→ pinentry-qt: secret_password_lookup_nonpageable_sync()
schema: "org.gnupg.Passphrase", attr: keygrip="<40-char-hex>"
→ KeePassXC returns stored passphrase (if found) → no GUI prompt
→ OR: pinentry-qt shows GUI dialog → user enters passphrase
→ gpg-agent caches passphrase (8h default, 12h max)
→ decryption succeeds
Key facts:
org.freedesktop.secrets D-Bus APIorg.gnupg.Passphrase with attribute keygripov secrets gpg setup stores passphrases for all keygrips automaticallyTwo Secret Service schemas:
org.gnupg.Passphrase — passphrase per keygrip (compatible with pinentry-qt auto-lookup)org.gnupg.Key — armored private key backup (for import-key --from-keystore recovery)Both are standard Secret Service entries visible in KeePassXC's FdoSecrets-exposed group.
When decryption fails, ov secrets gpg now prints actionable diagnostics instead of raw GPG errors:
ov secrets gpg commands to fix each issue# Option A: Import existing key from backup
ov secrets gpg setup --import ~/Sync/Conf/gpg/
# Option B: Restore from KeePassXC (if key was previously exported)
ov secrets gpg setup --from-keystore
# Option C: Generate fresh key
ov secrets gpg setup
# Then re-encrypt .secrets: ov secrets gpg encrypt -r <NEW_KEY_ID> -i .env -o .secrets
ov secrets gpg export-key --to-keystore # Key backup
ov secrets gpg export-key ~/Sync/Conf/gpg/ # Filesystem backup too
ov secrets gpg import-key --from-keystore # From KeePassXC
ov secrets gpg setup # Configure agent + store passphrase
| Symptom | Cause | Fix |
|---|---|---|
gpg: No secret key | Key not in keyring | ov secrets gpg import-key <path> |
| Constant passphrase prompts | Passphrase not in Secret Service | ov secrets gpg setup (stores for all keygrips) |
decrypting .secrets failed | Diagnostics printed automatically | Follow suggested ov secrets gpg commands |
Secret Service not available | KeePassXC not running/unlocked | Start KeePassXC, unlock database |
secret-tool store fails | No FdoSecrets exposed group | KeePassXC → right-click group → "Mark as Secret Service exposed" |
| Passphrase stored but still prompted | Stored for wrong keygrip | ov secrets gpg doctor shows which keygrips need passphrases |
For GPG agent forwarding into containers (so gpg --decrypt works inside), use the agent-forwarding layer. See /ov-foundation:agent-forwarding for details. The container's GPG uses the host's agent via a forwarded socket — no GPG agent runs inside the container.
/ov-core:config — secret_backend, secrets.kdbx_path settings keys/ov-advanced:enc — encrypted volume credential lookup, iteration-capable ssClient, broken-collection troubleshooting, source classification (env/keyring/config/locked/unavailable/default)/ov-build:settings — keyring_collection_label, secret_backend, and other runtime config keys/ov-core:doctor — Secret Service collection health + shadow index consistency checks/ov-core:service — container secrets (secrets field in layer.yml, provisioned at ov config)/ov-foundation:agent-forwarding — SSH/GPG agent forwarding into containers/ov-foundation:gnupg — GnuPG package layer/ov-openwebui:openwebui — two-tier secrets pattern: podman secrets (WEBUI_SECRET_KEY auto-generated) + GPG .secrets (API keys)/ov-coder:direnv — direnv environment loaderov/secrets_cmd.go (CLI commands), ov/secrets_gpg.go (GPG .secrets commands, key management, diagnostics), ov/credential_kdbx.go (KdbxStore backend).
MUST be invoked when the task involves ov secrets commands, KeePass .kdbx credential management, GPG-encrypted .secrets file management, GPG key management, Secret Service integration, credential import/export, or secret database administration. Invoke this skill BEFORE reading source code or launching Explore agents.
[HINT] Download the complete skill directory including SKILL.md and all related files