Credential Storage
Wayland Core stores credentials through a CredentialsStore trait. The backend is selected in config.toml and the default is Auto, an ordered ladder that will not write a secret in cleartext:
1. OS keyring used only when a live WRITE probe succeeded2. Encrypted vault used only when unlock material was supplied3. legacy plaintext READ AND DELETE ONLY, never written4. otherwise the write FAILS with `no_secure_backend_for_write`The legacy plaintext tier exists so a machine that already holds a credentials.toml keeps resolving its keys, and those keys are promoted up the ladder on the next read. It is not a write target, and no put reaches it. Writing cleartext requires an operator to name backend = "plaintext" explicitly (crates/wcore-config/src/credentials.rs, open_secure_ladder_store and build_ladder).
The encrypted-file backend is for headless or server deployments where the OS keychain is unavailable.
All three backends use flat string keys with dotted prefixes for namespacing, for example providers.anthropic.api_key or bedrock.secret_access_key.
Backend selection
Section titled “Backend selection”Set the backend in ~/.wayland-core/config.toml (or your project-local .wayland-core.toml):
[storage.credentials]backend = "keyring" # or "plaintext" or "encrypted_file"service_name = "wayland-core" # optional; scopes keyring entries (default: "wayland-core")For the encrypted-file backend, two file paths must also be specified:
[storage.credentials]backend = { encrypted_file = { cipher_path = "~/.wayland/credentials.enc", key_params_path = "~/.wayland/credentials.kdf.json" } }Set WAYLAND_VAULT=plaintext before startup to skip any auto-migration prompt and keep the plaintext backend regardless of what the config file says.
Plaintext backend (opt-in, not the default)
Section titled “Plaintext backend (opt-in, not the default)”PlaintextCredentialsStore stores a [secrets] table in a TOML file. On every write it enforces 0o600 permissions on Unix and attempts a deny-all ACL on Windows via atomic_write - a sibling tempfile is written and then renamed so a crash or interruption cannot leave a half-written file. Reads warn via tracing if the file is world-readable but still load, to avoid stranding users on a freshly created file that the kernel briefly held at the umask default.
Default path: wherever wayland-core --config-path reports on your OS (usually ~/.config/wayland-core/config.toml on Linux, ~/Library/Application Support/wayland-core/config.toml on macOS).
You have to select this backend deliberately. It is appropriate for development machines where the OS keychain is inconvenient, and it offers no encryption at rest: anyone with read access to the file can read the secrets. Confidential material that must never be written in cleartext, such as an OAuth token set whose refresh token is a long-lived bearer credential, opens the secure ladder directly, so naming this backend cannot downgrade it.
Keyring backend
Section titled “Keyring backend”KeyringCredentialsStore uses the keyring crate to store secrets in the platform’s native credential store:
- macOS: Keychain
- Windows: Credential Manager
- Linux: Secret Service (e.g. GNOME Keyring, KWallet)
Each key is stored as a (service, user) pair where service is the configured service_name (default: "wayland-core") and user is the key string. Lookup is O(1).
To switch to the keyring backend:
[storage.credentials]backend = "keyring"Then set your API key once:
wayland-core setup # interactive onboarding stores keys in the selected backendOr set it directly via the environment - the engine’s credential lookup order is environment variable first, then the credential store, then the config file. Keys read from the environment are never written to the store.
Encrypted-file backend
Section titled “Encrypted-file backend”EncryptedFileCredentialsStore is for server or CI deployments where no OS keychain is available. It uses:
- KDF: Argon2id (v0x13), parameters m=65536 KiB (64 MiB), t=3, p=1, output 32 bytes
- AEAD: XChaCha20-Poly1305, 24-byte nonce, 16-byte Poly1305 MAC tag
- Blob layout:
nonce(24) || ciphertext || tag(16)in the cipher file
The KDF parameters are confirmed in crates/wcore-config/src/credentials.rs:
const DEFAULT_M_COST_KIB: u32 = 64 * 1024; // 64 MiBconst DEFAULT_T_COST: u32 = 3;const DEFAULT_P_COST: u32 = 1;pub const NONCE_LEN: usize = 24;pub const TAG_LEN: usize = 16;pub const KEY_LEN: usize = 32;Two-file layout
Section titled “Two-file layout”The backend writes two files:
| File | Contents | Secret? |
|---|---|---|
cipher_path (e.g. ~/.wayland/credentials.enc) | Raw bytes: nonce(24) || ciphertext || tag(16) | Yes - protect with 0o600 |
key_params_path (e.g. ~/.wayland/credentials.kdf.json) | JSON: { salt_b64, m_cost, t_cost, p_cost, version } | No - contains only public KDF tuning params |
The salt is 16 random bytes generated from rand::thread_rng() (seeded from the OS) on vault creation and stored in key_params_path. Storing the KDF parameters separately means you can re-derive the key from your passphrase at any time without knowing the original tuning values. The AEAD nonce is re-randomized on every put() write.
Both files are written with atomic_write (tempfile + rename) and 0o600 permissions are enforced immediately after.
Passphrase resolution
Section titled “Passphrase resolution”The engine resolves the vault passphrase in this order:
WAYLAND_VAULT_PASSPHRASE_FD: a Unix file descriptor number (e.g.3). The engine validates the fd is open and readable before consuming it. This path does not appear in/proc/<pid>/environ.WAYLAND_VAULT_PASSPHRASE: env var (legacy). Emits atracing::warn!about the/proc/<pid>/environvisibility risk on Linux.- Interactive prompt:
rpassword::prompt_password("vault passphrase: ")on a TTY.
The passphrase is held in a zeroize::Zeroizing<String> in memory. Argon2id derivation runs once per process; subsequent get()/put() calls reuse the cached derived key under a parking_lot::Mutex. Cross-process write concurrency is not modeled - if you run multiple writers against the same vault files, serialize at the application layer.
Unlock behavior
Section titled “Unlock behavior”On first access the engine:
- Reads the passphrase via the resolution order above.
- Loads
key_params_pathif it exists; otherwise usesKdfParams::default()(fresh random salt). - If
cipher_pathalready exists, decrypts it immediately to verify the passphrase is correct. A wrong passphrase fails here rather than silently rotating the vault key on the next write.
Headless deployment example
Section titled “Headless deployment example”Pass the passphrase via a file descriptor to avoid environment variable exposure:
# Open a pipe, write the passphrase to fd 3, run wayland-coreexec 3< <(echo -n "my-vault-passphrase")WAYLAND_VAULT_PASSPHRASE_FD=3 wayland-core "run my task"exec 3<&-For CI/CD, inject the passphrase via a secret manager into the fd at process start rather than an environment variable.
Credential key namespace
Section titled “Credential key namespace”Keys follow a dotted convention. Some examples used by the engine:
| Key | What it holds |
|---|---|
providers.anthropic.api_key | Anthropic API key |
providers.openai.api_key | OpenAI API key |
bedrock.access_key_id | AWS access key ID for Bedrock |
bedrock.secret_access_key | AWS secret access key for Bedrock |
providers.<name>.api_key | Any custom provider’s key |
The wayland-core setup interactive onboarding stores keys under these namespaced paths. Channel adapter keys (Slack, Discord, etc.) use channels.<name>.<field> prefixes managed by their respective crates.
Choosing a backend
Section titled “Choosing a backend”| Scenario | Recommended backend |
|---|---|
| Developer workstation, interactive use | keyring |
| Developer workstation, no keychain daemon | plaintext (ensure 0o600; consider full-disk encryption) |
| Server or CI, no GUI/keychain | encrypted_file with passphrase via fd |
| Testing or short-lived container | plaintext or environment variables only |
Related pages
Section titled “Related pages”- Security Model Overview - sandbox, egress gate, threat model
- Configuration - the
[storage]section and config file location - Providers and Auth - how API keys are passed to provider clients