You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Zebra's RPC authentication relies exclusively on cookie-based auth, where a random token is written to disk on every startup and clients must read that file to authenticate. This mechanism creates three categories of operational failure that compound in containerized deployments:
Credential volatility: Every restart generates a new cookie, invalidating all existing client connections. Zaino reads the cookie once at startup and holds it in memory; a Zebra restart silently breaks all RPC calls until Zaino is also restarted.
Filesystem coupling: Clients must share a filesystem path with Zebra to read the cookie file. In Docker, this requires a shared named volume (shared_cookie_volume), cross-UID permission management (Zebra runs as UID 10001, Zaino as 1000, Zallet as 65532), and a dedicated fix-permissions.sh script.
Startup ordering: Zaino calls std::process::exit(1) if the cookie file does not exist at startup. This creates a hard dependency on Zebra having already started and written the file, requiring container restart policies or init-container workarounds.
The Z3 Docker Compose stack (the reference deployment for Zebra + Zaino + Zallet) documents these problems directly in its .env:
For local directories, Zebra (10001) writes and Zaino (1000) reads. Consider using Docker volume (default) to avoid cross-user permission issues.
The regtest stack already abandoned cookie auth entirely (ZEBRA_RPC__ENABLE_COOKIE_AUTH=false) and uses static credentials in Zaino/Zallet. The lightwalletd Docker Compose also disables cookie auth. User/password authentication would eliminate all three failure modes: credentials are static (survive restarts), require no filesystem sharing (work natively in containers), and can be configured before any service starts.
Specifications
Auth Resolution Order
When both mechanisms are configured, the resolution order is:
If rpc_user + (rpc_password or rpc_password_file) are set: HTTP Basic Auth with static credentials
Else if enable_cookie_auth is true: current cookie behavior (backward compatible)
Else: no authentication
This makes user/password the primary mechanism when configured, with cookie auth as a fallback for existing setups that have not migrated.
Config Fields
Three new fields in [rpc]:
[rpc]
# RPC authentication username (can also be set via ZEBRA_RPC__RPC_USER env var)rpc_user = "zaino"# RPC authentication password (cannot be set via env var; blocked by the sensitive-key deny list)rpc_password = "strong-random-password"# Alternative: path to a file containing the password (Docker secrets pattern)# Can be set via ZEBRA_RPC__RPC_PASSWORD_FILE env varrpc_password_file = "/run/secrets/rpc_password"
The rpc_password_file field follows the Docker secrets convention used by PostgreSQL (POSTGRES_PASSWORD_FILE), MySQL (MYSQL_ROOT_PASSWORD_FILE), and others. It naturally sidesteps Zebra's sensitive-key deny list (which blocks env vars ending in password, secret, token, cookie, or private_key) because the leaf key RPC_PASSWORD_FILE does not match any suffix.
When both rpc_password and rpc_password_file are set, rpc_password_file takes precedence. The file contents are read once at startup and held in memory (same lifecycle as the current cookie).
Wire Format
The wire format is unchanged: HTTP Basic Auth (Authorization: Basic base64(user:password)). Clients that already support HTTP Basic Auth (zcash-cli, Zaino, curl, every HTTP library) work without modification.
Credential Comparison
Credentials must be compared using constant-time operations (subtle::ConstantTimeEq or equivalent) to prevent timing side-channel attacks. The current cookie auth uses plain == comparison, which is a known vulnerability (Bitcoin Core fixed CVE-2013-4165 with TimingResistantEqual for the same reason).
Complex Code or Requirements
Sensitive-Key Deny List Interaction
Zebra's config loader (zebrad/src/config.rs) blocks environment variables whose leaf key ends with password. This means ZEBRA_RPC__RPC_PASSWORD is blocked by design, which is correct (passwords should not appear in env vars, which are visible in /proc/*/environ and Docker inspect output). The rpc_password_file field provides the container-native alternative.
Bug Fixes (in scope, since the auth middleware is being modified)
Three existing bugs should be fixed while modifying the auth code:
Bitcoin Core PR #32423 (May 2025): Undeprecated rpcuser/rpcpassword, acknowledging static credentials are appropriate when the config file is the trust boundary
Motivation
Zebra's RPC authentication relies exclusively on cookie-based auth, where a random token is written to disk on every startup and clients must read that file to authenticate. This mechanism creates three categories of operational failure that compound in containerized deployments:
Credential volatility: Every restart generates a new cookie, invalidating all existing client connections. Zaino reads the cookie once at startup and holds it in memory; a Zebra restart silently breaks all RPC calls until Zaino is also restarted.
Filesystem coupling: Clients must share a filesystem path with Zebra to read the cookie file. In Docker, this requires a shared named volume (
shared_cookie_volume), cross-UID permission management (Zebra runs as UID 10001, Zaino as 1000, Zallet as 65532), and a dedicatedfix-permissions.shscript.Startup ordering: Zaino calls
std::process::exit(1)if the cookie file does not exist at startup. This creates a hard dependency on Zebra having already started and written the file, requiring container restart policies or init-container workarounds.The Z3 Docker Compose stack (the reference deployment for Zebra + Zaino + Zallet) documents these problems directly in its
.env:The regtest stack already abandoned cookie auth entirely (
ZEBRA_RPC__ENABLE_COOKIE_AUTH=false) and uses static credentials in Zaino/Zallet. The lightwalletd Docker Compose also disables cookie auth. User/password authentication would eliminate all three failure modes: credentials are static (survive restarts), require no filesystem sharing (work natively in containers), and can be configured before any service starts.Specifications
Auth Resolution Order
When both mechanisms are configured, the resolution order is:
rpc_user+ (rpc_passwordorrpc_password_file) are set: HTTP Basic Auth with static credentialsenable_cookie_authis true: current cookie behavior (backward compatible)This makes user/password the primary mechanism when configured, with cookie auth as a fallback for existing setups that have not migrated.
Config Fields
Three new fields in
[rpc]:The
rpc_password_filefield follows the Docker secrets convention used by PostgreSQL (POSTGRES_PASSWORD_FILE), MySQL (MYSQL_ROOT_PASSWORD_FILE), and others. It naturally sidesteps Zebra's sensitive-key deny list (which blocks env vars ending inpassword,secret,token,cookie, orprivate_key) because the leaf keyRPC_PASSWORD_FILEdoes not match any suffix.When both
rpc_passwordandrpc_password_fileare set,rpc_password_filetakes precedence. The file contents are read once at startup and held in memory (same lifecycle as the current cookie).Wire Format
The wire format is unchanged: HTTP Basic Auth (
Authorization: Basic base64(user:password)). Clients that already support HTTP Basic Auth (zcash-cli, Zaino, curl, every HTTP library) work without modification.Credential Comparison
Credentials must be compared using constant-time operations (
subtle::ConstantTimeEqor equivalent) to prevent timing side-channel attacks. The current cookie auth uses plain==comparison, which is a known vulnerability (Bitcoin Core fixed CVE-2013-4165 withTimingResistantEqualfor the same reason).Complex Code or Requirements
Sensitive-Key Deny List Interaction
Zebra's config loader (
zebrad/src/config.rs) blocks environment variables whose leaf key ends withpassword. This meansZEBRA_RPC__RPC_PASSWORDis blocked by design, which is correct (passwords should not appear in env vars, which are visible in/proc/*/environand Docker inspect output). Therpc_password_filefield provides the container-native alternative.Bug Fixes (in scope, since the auth middleware is being modified)
Three existing bugs should be fixed while modifying the auth code:
Empty 401 response (Send an unauthenticated error to RPC callers when no/unmatched credentials are provided #9190): When authentication fails, the middleware returns
future::err(BoxError), which jsonrpsee converts to an empty HTTP 500 instead of a proper 401 withWWW-Authenticateheader. Fix: return anHttpResponsedirectly with status 401.Timing side-channel:
Cookie::authenticateuses==for comparison. Fix: usesubtle::ConstantTimeEqfor both cookie and password comparison.RFC 7617 parsing:
split(':').nth(1)truncates passwords containing colons. Fix: usesplitn(2, ':').nth(1).Testing
rpc_password_filereads file contents correctlyRelated Work
github.com/ZcashFoundation/z3): Primary downstream consumer; regtest already uses user/password via Zainorpcuser/rpcpassword, acknowledging static credentials are appropriate when the config file is the trust boundary