Repository navigation
Local hardening: bounded Tor cookie read, Core's bitcoin.conf comments, no request dumps in trace logs - #940
Merged
Merged
Conversation
… only The control port names the cookie file in its PROTOCOLINFO reply, and satd read it with fs::read, to end of file and without checking what it was. Bitcoin Core reads it with ReadBinaryFile(cookiefile, TOR_COOKIE_SIZE) (src/torcontrol.cpp:616), which never reads more than 32 bytes. satd now opens the file non-blocking, refuses anything that is not a regular file, and reads at most 33 bytes, so /dev/zero or a FIFO named as the cookie fails authentication at once instead of filling memory or blocking startup in open(2). A file longer than 32 bytes is still refused, as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts out of logs
bitcoin.conf: satd skipped only lines that start with `#`, so a comment
after a value became part of it. `txindex=1 # c` left txindex off,
`rpcport=8332 # c` fell back to the default port, and
`rpcpassword=pw # c` set the password to the whole string, all without
a warning. Bitcoin Core's GetConfigOptions (src/common/config.cpp:41-62)
drops everything from the first `#`, trims " \t\r\n", and refuses an
rpcpassword line holding a `#` ("using # in rpcpassword can be ambiguous
and should be avoided"). satd's parser and sat-cli's now do the same,
with the same message.
Trace logging: under -loglevel=trace (or RUST_LOG=trace) jsonrpsee-server
logged every HTTP request with its headers, the Authorization header
included, before satd's auth layer ran (jsonrpsee-server 0.26
src/server.rs:1026), and jsonrpsee-core logs call parameters; tungstenite
logs WebSocket payloads. Core logs no credentials or parameters at any
level. A global filter layer now drops TRACE output from the jsonrpsee
and tungstenite targets whatever the reloadable filter allows; their
DEBUG output is unchanged.
Debug: Config, RpcAuthEntry, the reorg webhook target and the satd-auth
operator credentials (behind RpcAuth) derived Debug over their secrets.
Their Debug output now names them without the password, cookie token,
rpcauth salt and tag, or webhook secret. Nothing printed them today.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Tor cookie read. With
-listenonion/-torcontrol, satd reads the SAFECOOKIE cookie from the path the control port names in itsPROTOCOLINFOreply, withstd::fs::read: to end of file, whatever the file is (node/src/net/tor.rs:152). A path such as/dev/zerois read until memory runs out, and a FIFO blocks startup inopen(2)(CONTROL_TIMEOUTcovers only the socket). Bitcoin Core reads the cookie withReadBinaryFile(cookiefile, TOR_COOKIE_SIZE)(src/torcontrol.cpp:616,src/util/readwritefile.cpp:16-35), which never reads more than 32 bytes.Credentials in trace logs.
-loglevel=trace(orRUST_LOG=trace) enables jsonrpsee-server'strace!("{:?}", request)(jsonrpsee-server 0.26src/server.rs:1026), which runs before satd's auth layer and prints every HTTP request with its headers,authorization: Basic <base64 user:password>included. jsonrpsee-core also logs call parameters and raw request bodies at TRACE (src/server/rpc_module.rs:310, 356, 409), and tungstenite logs every WebSocket frame's payload (src/protocol/frame/mod.rs:228, 257). Bitcoin Core logs neither credentials nor RPC parameters at any level.Inline
#comments in bitcoin.conf. satd skipped only lines starting with#(satd/src/config.rs:8110), so a comment after a value became part of the value, with no warning:txindex=1 # c→"1 # c", which is not a boolean, so txindex stayed off;rpcport=8332 # cfailed to parse and the default port was used;rpcpassword=pw # cset the password topw # c;[test] # cwas refused as an unknown key.Core's
GetConfigOptions(src/common/config.cpp:41-62) drops everything from the first#, trims" \t\r\n"from the line, key and value, and refuses anrpcpasswordline that holds a#at all:parse error on line N, using # in rpcpassword can be ambiguous and should be avoided. sat-cli's own reader (sat-cli/src/conf.rs) copied satd's rule, so it had the same bug. bitcoin-cli reads the file with Core's parser (src/bitcoin-cli.cpp:182).Debug output of secret-bearing types.
Config(rpcpassword,rpcauth,torpassword,esplorauserpass,reorgwebhooksecret),RpcAuthEntry, the reorgWebhookTarget, and satd-auth'sCookieCredential/UserPassCredential/RpcAuthCredential(insideOperatorCreds, insideRpcAuth) all derivedDebug. Nothing prints them today, but a future{:?}or a panic message would write the secrets to the log.Fix
read_cookie_fileopens the cookie withO_NONBLOCK, refuses anything that is not a regular file, and reads at most 33 bytes. A file that is not exactly 32 bytes is still refused, as before (Core would accept a longer file and use its first 32 bytes; Tor always writes 32, and satd keeps its existing stricter check).config::request_dump_guard()is a global filter layer installed beside the reloadableEnvFilterinmain. It drops TRACE spans and events whose target starts withjsonrpseeortungstenite, whatever-loglevel,-debug, theloggingRPC orRUST_LOGsay. It is a separate layer becauseEnvFilterlets the most specific directive win: aRUST_LOGnaming a longer target (jsonrpsee_core::server=trace) would override ajsonrpsee_core=debugdirective. Their DEBUG output is unchanged. hyper is built without tracing, h2 leaves header fields out of its frameDebugon purpose, and tower-http'sTraceLayer(Esplora) records no headers by default, so they need nothing.ConfigFile::parseand sat-cli'sConfFile::parsefollowGetConfigOptions: strip from the first#, trim only" \t\r\n", and refuse a#on any line whose key containsrpcpassword, with Core's message. A section header with a trailing comment now opens the section.includeconffiles go through the same parser. sat-cli prints the parse error and exits, as bitcoin-cli does.Debugimpls.Configshows the network and datadir only (finish_non_exhaustive);RpcAuthEntryand the satd-auth credentials show the user (or the cookie path) only;WebhookTargetshows the URL and whether a secret is set.Tests
net::tor::tests::safecookie_refuses_a_fifo_cookie_pathfs::readcall site the client blocked inopen(2)and the test failed with "authenticate blocked on a FIFO named as the Tor cookie file". With the regular-file check removed: "cookie file … is 0 bytes" instead of "not a regular file".net::tor::tests::safecookie_refuses_a_device_cookie_pathnet::tor::tests::cookie_read_stops_one_byte_past_the_cookie_lengthassertion left == right failed, 1 MiB read).net::tor::tests::safecookie_refuses_a_cookie_file_longer_than_32_bytesconfig::localharden_tests::an_inline_comment_is_not_part_of_the_valueunrecognized key '[test] # the testnet3 section'.config::localharden_tests::inline_comments_reach_the_running_config_as_core_reads_themunrecognized key '[regtest] # this chain'.config::localharden_tests::a_hash_on_an_rpcpassword_line_is_refused_with_cores_messagerpcpassword=pw # old passwordparsed as["pw # old password"]. Also fails with only the rpcpassword check disabled.config::localharden_tests::whitespace_is_trimmed_as_core_trims_itleft: ["x"]).config::localharden_tests::config_debug_output_carries_no_secretsconfig::localharden_tests::trace_logging_leaves_out_the_request_dumpsis_request_dumpdisabled: "jsonrpsee-server TRACE is logged under -loglevel=trace" (new API, so not run on the old code; the end-to-end test below fails on the old binary). Also checks that their DEBUG output and satd's own TRACE output still get through, and thatRUST_LOG-style directives naming the targets cannot re-enable them.local_hardening::trace_logging_never_writes_rpc_credentials_or_parameters(new test target)TRACE jsonrpsee-server: Request { … headers: {"authorization": "Basic dHJhY2Vsb2d1c2Vy…". Runs a regtest node with-loglevel=traceand user/password auth, callsgetblockcountandecho <marker>, then checks the log for the Basic credential, the password and the parameter, and that jsonrpsee's DEBUG lines are still there.operator::tests::debug_output_names_credentials_without_their_secrets(satd-auth)conf::tests::an_inline_comment_is_not_part_of_the_value,a_hash_on_an_rpcpassword_line_is_refused19000\t# portdid not parse;rpcpassword=a#bwas accepted).comments_and_bare_keyswas edited in place: it asserted the olda#bpassword.Local gate:
cargo clippy --all-targets --all-features -D warnings,cargo clippy -p node --no-default-features --all-targets -D warnings,nodetor tests,satdunit tests,satd-auth,sat-cli, the newlocal_hardeningtarget, and the regtestsat_cli/logging/sighup/reload/getconfig/log_format/conftests.Notes
umask(077)at startup (src/common/system.cpp:92-93; current Core has no-syspermsopt-out); satd sets none, so a new datadir is 0755 andmempool.dat,peers.datand the ban list are 0644 (the cookie and the SV2 key are already 0600). This is left out because shipped packaging relies on group access into the datadir:contrib/systemd/satd.service:11-15, 71-88(andsatd@.service:35-37, 76-80) chmod the cookie to 0640 sosatdgroup members can run sat-cli, and on every chain but mainnet that cookie sits in<datadir>/<network>/, which satd creates. Under umask 077 that directory would be 0700 and group members could no longer reach the cookie. The appliance depends on the same path (contrib/appliance/provision/10-satd.sh:89-92). It needs a decision on how the units grant that access first.[ main ]with spaces inside the brackets is still read as[main]; Core would name the sectionmainand ignore it. A bare key without=is still read askey=1, where Core refuses the line. Both are existing satd leniencies and unchanged here, except that a barerpcpasswordline holding a#is refused.CliArgsstill derivesDebugover--rpcpasswordand friends; nothing prints it, and a hand-writtenDebugfor the clap struct is not small.Release notes
satd now reads
bitcoin.confcomments as Bitcoin Core does: a#anywhere on a line starts a comment, so a line such astxindex=1 # keep itis no longer silently misread, and anrpcpasswordline containing#is refused at startup with Core's error. sat-cli reads the file the same way.-loglevel=traceno longer writes RPC request headers (including theAuthorizationcredentials) or RPC parameters to the log. With Tor control enabled, satd reads at most 33 bytes from the cookie file the control port names and refuses anything that is not a regular file, so a misbehaving control port cannot make startup hang or exhaust memory.🤖 Generated with Claude Code