Skip to content

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.

The bearer token has three configuration modes, resolved at startup.

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.

Terminal window
export WALLETRS_AUTH_TOKEN=$(openssl rand -hex 32)
./walletrs

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...c8a1

This is convenient for first-time runs — copy the token from the log line, then transition to operator-supplied for production.

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.

Terminal window
export WALLETRS_AUTH_DISABLED=1
./walletrs

The startup log emits a clear warning: WALLETRS_AUTH_DISABLED is set — gRPC requests are not authenticated.

Standard Authorization: Bearer <token> header on every request:

Terminal window
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.

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).

There is no in-place rotation API. The procedure is:

  1. Generate a new token. Stash it in your secrets manager.
  2. Update clients to send the new value.
  3. 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.

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.