Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication & Authorization

satd has one authentication model shared by every API surface: JSON-RPC, Esplora, the streaming APIs (events gRPC and streamws), and the MCP server. It also keeps full backward compatibility with Bitcoin Core's cookie, rpcuser, and rpcauth credentials.

There are two layers:

  1. Core-compatible operator auth: the cookie file, -rpcuser/-rpcpassword, and -rpcauth. This is the default and behaves exactly like Bitcoin Core. A valid operator credential has full access to everything, unless Core's -rpcwhitelist limits that user to a list of JSON-RPC methods (see Per-user method allowlists).
  2. The unified bearer-token layer (satd-auth): opt-in, capability-scoped bearer tokens loaded from an -authfile, each rate-limited and quota-bounded. Scoped tokens let you expose the node to partially trusted consumers, such as a BTCPay instance or a watchtower, without giving any of them operator credentials.

Note. The default is pure Bitcoin Core behavior. With no -authfile configured, the bearer layer is inert: the only credentials that work are the Core-compatible ones, and every authenticated request acts as the full-capability operator. Scoped tokens are opt-in, per surface. A surface with no bearer tokens enabled does not install the capability gate at all.

How the bearer layer differs from Core-style auth

Core-style operator authUnified bearer tokens
Credentials.cookie file, -rpcuser/-rpcpassword, -rpcauth (HMAC)Opaque high-entropy tokens, sent as Authorization: Bearer <token>
GranularityFull operator access, or a per-user list of JSON-RPC methods with -rpcwhitelistPer-token capabilities (for example read-only, Esplora-only, stream-only)
Multi-tenantNo; one shared identityYes; each token has its own id, scope, quota, rate limit, and expiry
Rate / quota limitsNone; the operator is unlimitedPer-token request rate (429/RESOURCE_EXHAUSTED) and watch-set quota
Where definedFlags, bitcoin.conf, or the generated cookieA TOML -authfile, reloadable on SIGHUP
DefaultOn (cookie auto-generated)Off until -authfile is set and the surface opts in
CompatibilityBitcoin Core wire-identicalsatd extension

Both layers coexist. On a bearer-enabled surface the operator (Basic) credential is tried first, so existing Core tooling is not affected. A Bearer token is consulted only when the request does not carry a valid operator Basic credential. A matching cookie, userpass, or rpcauth credential always resolves to the full-capability operator.

A request whose credential does not authenticate is answered 401 after a 250 ms pause and logged at warn level as ThreadRPCServer incorrect password attempt from <ip>:<port>, as in Core. A request with no Authorization header gets its 401 at once and is not logged. The warning is limited to 60 lines a minute per listener; the first line of the next minute carries a suppressed count of what it left out.

Per-user method allowlists (rpcwhitelist)

satd implements Bitcoin Core's -rpcwhitelist and -rpcwhitelistdefault with Core's semantics, so a bitcoin.conf that restricts RPC users works unchanged.

rpcauth=monitor:<salt>$<hash>
rpcwhitelist=monitor:getblockcount,getbestblockhash,getblockchaininfo
  • rpcwhitelist=<user>:<method>,<method>,... limits <user> to the listed JSON-RPC methods. Methods are separated by commas or spaces, and names are case-sensitive. The option is repeatable. A user listed more than once keeps only the methods common to every list, and that includes a user listed on both the command line and in bitcoin.conf.
  • rpcwhitelist=<user> or rpcwhitelist=<user>: gives that user an empty list: every call is refused.
  • rpcwhitelistdefault decides what users with no list may call. It defaults to 1 as soon as any rpcwhitelist is set, which means an unlisted user may call nothing. Set rpcwhitelistdefault=0 to leave unlisted users unrestricted. rpcwhitelistdefault=1 with no list at all refuses every user.
  • The user is the name the request authenticated as. That is __cookie__ for the cookie file, the -rpcuser name, or an -rpcauth name. Under a deny default, list __cookie__ if sat-cli or bitcoin-cli should keep working with the cookie.

A refused request gets HTTP 403 with an empty body, as in Core, and the method does not run. A batch is refused whole if any call in it is not allowed. A body that is not JSON still gets the -32700 parse error first. The refusal is logged as RPC User <name> not allowed to call method <m>.

The allowlists apply to every JSON-RPC listener, including the read-only listener, where both filters must allow a call. Four details go beyond Core:

  • WebSocket. satd's JSON-RPC listeners also accept WebSocket upgrades. A user with a list, or any user under a deny default, is refused the upgrade with 403, because calls made over an open WebSocket are not checked against the list.
  • Bearer tokens are not RPC users. A token is scoped by the capabilities in its -authfile entry, and -rpcwhitelist does not apply to it, even under a deny default.
  • -rpcdisableauth is refused together with any rpcwhitelist or with rpcwhitelistdefault=1. The auth-disabled mTLS surface authenticates no user, so it has no name to restrict.
  • Reload. The lists are fixed at startup. A SIGHUP reports a changed rpcwhitelist as needing a restart, while credentials still rotate live.

For scoping that covers Esplora, the streaming APIs and MCP, and that carries rate limits and expiry, use bearer tokens instead.

Capabilities

A bearer token carries a set of capabilities, and each surface enforces the capability it requires. Enforcement fails closed: an unknown method, or a request with no principal, requires the write capability, which neither a read-only nor a submit-only token holds.

CapabilityStringGrants
RPC readrpc:readRead-only JSON-RPC methods (classified by the same table the read-only listener uses).
RPC writerpc:writeMutating, control, and mining JSON-RPC methods, plus any unclassified method (fail-closed). Implies rpc:submit.
RPC submitrpc:submitMempool submission: sendrawtransaction, submitpackage, and the other methods the read-only listener classes as mempool-submit; also Esplora POST /tx (with esplora:read). Lets a broadcaster (a payment processor, a wallet backend) hand transactions to the mempool without holding node control. Implied by rpc:write.
Esplora readesplora:readThe Esplora REST + SSE surface. Reads only: POST /tx additionally needs rpc:submit.
Stream subscribestream:subscribeOpen a streaming subscription (events gRPC, streamws).
Stream watchstream:watchRegister outpoint/script/descriptor/txid watches, bounded by the token's watch quota.
MCPmcp:*The MCP server. One capability; there is no per-tool split.
Test clocktest:clocksetmocktime, which moves the node clock (regtest only). Not implied by rpc:write: it reaches the future-block check, mempool expiry and block-template timestamps, so it must be granted deliberately.
Test nettest:netaddconnection, which opens an outbound connection of a chosen type (regtest only). Not implied by rpc:write: it dials an address the caller chooses and picks the connection's type, which decides whether that peer is asked for transactions and whether it takes part in address relay — reshaping the peer set, not writing to the node. Also sendmsgtopeer, which writes arbitrary bytes onto a peer connection, and addpeeraddress, which writes an address of the caller's choosing into the address book the node dials from. Grant it deliberately.

The operator and loopback-trust principals hold all capabilities.

The authfile

-authfile=<path> points at a TOML file of bearer tokens. The file stores only the SHA-256 digest of each token, never the plaintext.

version = 1

# Payment processor: reads plus broadcast, rate-capped. No node control.
[[token]]
id = "btcpay"                              # logging/accounting id, not the secret
hash = "sha256:<64-hex SHA-256 of the token>"
capabilities = ["rpc:read", "rpc:submit", "esplora:read"]
watch_quota = 50000                        # optional; omit for unlimited
rate_limit = "200/s"                       # optional; omit for unlimited

# Watchtower: streaming subscribe + watch registration, expires end-2026.
[[token]]
id = "watchtower"
hash = "sha256:<64-hex>"
capabilities = ["stream:subscribe", "stream:watch"]
watch_quota = 10000
expires = 2026-12-31T00:00:00Z             # unquoted RFC 3339 datetime, or unix seconds

# AI agent: full MCP tool access.
[[token]]
id = "agent"
hash = "sha256:<64-hex>"
capabilities = ["mcp:*"]

Rules:

  • version = 1 is required. Each [[token]] needs a unique id and a hash of the form sha256:<64 hex>. capabilities defaults to empty; such a token can authenticate but is denied everything. watch_quota, rate_limit ("<n>/s"), and expires are optional. An omitted limit means unlimited.
  • An unknown capability string, a duplicate id or hash, or a wrong version aborts the load with an error. Nothing is ignored silently.
  • On Unix the file must have no group, world, or execute permission bits: 0600 or 0400, like a cookie file or an SSH private key. A 0644 or 0640 file is rejected.
  • Generate a token and its hash with, for example:
    TOKEN=$(openssl rand -hex 32)        # the secret you give the client
    printf 'sha256:%s\n' "$(printf %s "$TOKEN" | sha256sum | cut -d' ' -f1)"
    
  • Edit the file and send SIGHUP to reload it. The reload swaps the token table atomically, and removing a [[token]] revokes it immediately. A parse or permission error keeps the last-good table, so a bad reload never drops auth.
  • Revocation and expiry reach WebSockets that are already open. A WebSocket on the JSON-RPC listener resolves its token again for every frame, so after a reload removes the token, or once its expires has passed, each call on the socket is answered in-band with error -32006 (bearer token revoked or expired), and a subscribemempool subscription opened with it ends at its next event. A reload that changes the token's capabilities or rate limit applies to open sockets the same way.

Presenting a token

Clients send the raw token in a standard header. The scheme is case-insensitive.

Authorization: Bearer <token>

The server computes SHA-256(token), looks the digest up in the loaded table with a constant-time guard, then checks expiry. A blank token can never authenticate.

Quotas & rate limits

  • Rate limit. A per-token token bucket ("<n>/s", burst equal to the rate). Requests over budget are shed, never queued, so a slow or abusive consumer cannot backpressure the node. JSON-RPC, Esplora, and MCP return HTTP 429 with Retry-After; events gRPC returns RESOURCE_EXHAUSTED; streamws throttles at connection time. A JSON-RPC batch is charged per call, not once per request: a batch of n calls costs n, and a call that crosses the budget is answered in-band with error -32005 whose data.retry_after_secs matches Retry-After. The earlier calls in that batch are still served. A WebSocket on the JSON-RPC listener is charged the same way: the upgrade costs one, and so does every call sent over the socket after it.
  • Watch quota. The streaming watch-set is metered in units. One scripthash costs one unit, and prefix watches are priced by coarseness. A token holds units through an RAII lease, so a disconnect releases its quota automatically. A watch add over quota is refused with an in-band WatchAddRejected event naming the refused items, without tearing down the subscription.

Operator and loopback principals are unlimited.

Per-surface enablement

Bearer support is opt-in per surface: the surface flag turns it on, and it requires -authfile. satd refuses to start if a surface flag is set without an authfile.

SurfaceEnable flagCapability gateDefault without the flag
JSON-RPC (read/write listeners)-rpcauthbearerrpc:read / rpc:submit / rpc:writeCore Basic auth (cookie/userpass/rpcauth)
Esplora REST / SSE-esploraauthbeareresplora:read (+ rpc:submit for POST /tx)-esploraauth Basic, loopback-unauth default
events gRPC-eventsgrpcauthstream:subscribe / stream:watchloopback-trust
streaming WS/SSE (streamws)-streamwsauthstream:subscribe / stream:watchloopback-trust
MCP (HTTP)-mcpauthmcp:*loopback-trust

The read-only JSON-RPC listener (-rpcreadonlybind) does not honor bearer tokens. Client-certificate (mTLS) principals for the Electrum surface are planned but not yet implemented.

Exposing a surface remotely

Binding the streaming or MCP surfaces to a routable address requires auth; the node refuses an unauthenticated remote bind. The chain is:

-eventsgrpcallowremote  →  requires -eventsgrpcauth  →  requires -authfile
-streamwsallowremote    →  requires -streamwsauth    →  requires -authfile
-mcpallowremote         →  requires -mcpauth         →  requires -authfile

Where a bearer token is what authenticates a remote bind, the transport must also be encrypted — the token crosses the wire on every request, and so does everything the connection carries:

-eventsgrpcallowremote + -eventsgrpcauth  →  requires -eventsgrpctlscert / -eventsgrpctlskey
-mcpallowremote                           →  requires -mcpcert / -mcpkey

-eventsgrpcmtls satisfies the events-gRPC requirement on its own: it already requires the certificate and key, and it authenticates without a token.

MCP has one further requirement that is not an authentication gate: its transport validates the Host header against an allowlist that defaults to loopback, so a remote MCP bind also needs -mcpallowedhost naming every hostname clients use, or it answers 403 before auth runs. See MCP.

-streamwsallowremote is the exception, because that transport has no TLS of its own. A remote streamws bind sends its bearer token in cleartext unless a TLS-terminating proxy fronts it — prefer the loopback bind plus a proxy.

For a proxy-terminated or mTLS-terminated deployment, bind to loopback and omit the *-allow-remote flag. JSON-RPC remote exposure is governed by Core's existing -rpcbind/-rpcallowip; there is no separate allow-remote flag for it. -rpcallowip also gates -rpctlsbind, and -rpcreadonlyallowip gates -rpcreadonlytlsbind, but only once the list has at least one entry: an empty allowlist means loopback-only on a plain bind and "no source-address rule" on a TLS bind, which is gated by TLS/mTLS and RPC auth instead.

Transport TLS / mTLS

Native TLS and mutual TLS compose underneath this layer: mTLS gates the connection, and a bearer token presented over it further refines the principal's capabilities. satd terminates TLS natively on the RPC, Esplora, and Electrum surfaces, so no sidecar is required. The *tls*/*mtls* config keys are listed in the Configuration chapter.