Authentication
walletrs gates every RPC except Ping behind a bearer token. The same token covers both the gRPC and HTTP surfaces — there is no separate token store. Tokens are compared in constant time (subtle::ConstantTimeEq) so length-prefix matches don’t leak.
Three modes
Section titled “Three modes”The bearer token has three configuration modes, resolved at startup.
Operator-supplied (recommended for production)
Section titled “Operator-supplied (recommended for production)”Set WALLETRS_AUTH_TOKEN to any non-empty value. Recommended for production so you can rotate the token independently from the binary lifecycle.
export WALLETRS_AUTH_TOKEN=$(openssl rand -hex 32)./walletrsAuto-generated
Section titled “Auto-generated”Leave WALLETRS_AUTH_TOKEN unset. walletrs generates a hex-encoded 32-byte token at startup, prints it once with the prefix STORE THIS — generated auth token: <token>, and enforces it from there.
[2026-04-27T12:34:56Z INFO walletrs::wallet::service::auth] STORE THIS — generated auth token: 7e3f...c8a1This is convenient for first-time runs — copy the token from the log line, then transition to operator-supplied for production.
Disabled
Section titled “Disabled”Set WALLETRS_AUTH_DISABLED=1 (also accepts true). Bypasses the auth check entirely on both surfaces. Acceptable for trusted local environments (loopback only) but never for anything internet-reachable.
export WALLETRS_AUTH_DISABLED=1./walletrsThe startup log emits a clear warning: WALLETRS_AUTH_DISABLED is set — gRPC requests are not authenticated.
Sending the token
Section titled “Sending the token”Standard Authorization: Bearer <token> header on every request:
curl -sS -X POST http://127.0.0.1:8080/wallet/list_managed_keys \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"user_id":"alice","key_type":""}'A missing or wrong token returns 401 Unauthorized with body:
{ "code": 16, "message": "missing or invalid bearer token" }Send the same value as the authorization metadata key:
let token: MetadataValue<_> = "Bearer <YOUR_TOKEN>".parse()?;let mut client = WalletServiceClient::with_interceptor(channel, move |mut req: Request<()>| { req.metadata_mut().insert("authorization", token.clone()); Ok(req)});A missing or wrong token returns Status::unauthenticated.
Ping bypasses on both surfaces
Section titled “Ping bypasses on both surfaces”The Ping RPC is exempt so liveness probes don’t need the token. It is exempt by path:
- gRPC:
/walletrpc.WalletService/Ping - HTTP:
POST /wallet/ping
Anything else requires a valid token (when auth is required).
Token rotation
Section titled “Token rotation”There is no in-place rotation API. The procedure is:
- Generate a new token. Stash it in your secrets manager.
- Update clients to send the new value.
- Restart walletrs with the new
WALLETRS_AUTH_TOKEN.
Because the old and new walletrs instances are mutually exclusive (they bind the same port), there is a brief window during the restart where neither token is accepted. For zero-downtime rotation, run two walletrs instances behind a reverse proxy and rotate one at a time.
Constant-time comparison
Section titled “Constant-time comparison”Bearer-token comparison goes through subtle::ConstantTimeEq (the ct_eq method on byte slices). Two consequences:
- Equal-length token bytes take the same time regardless of how many leading bytes match.
- Unequal-length inputs still reject without short-circuiting on a prefix match — short tokens that happen to be prefixes of the real one don’t accelerate the comparison.
This is not a substitute for a proper secret store, but it removes the easiest timing oracle.