Skip to content

Refuse an RPC token wider than 0600 - #1081

Merged
reardencode merged 3 commits into
masterfrom
harden/cookie-and-audit
Oct 19, 2026
Merged

reardencode merged 3 commits into
masterfrom
harden/cookie-and-audit

Conversation

@rearden-grok

@rearden-grok rearden-grok Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • On Unix, an existing {datadir}/rpc.token must be mode 0600. The node still creates a missing token at 0600 and does not chmod a file it finds.
  • cargo deny wildcards stays allow. cargo-deny 0.20.2 treats every workspace = true dependency as a wildcard. The tree has no version = "*".

These two commits landed on harden/cookie-and-audit after #1074 merged, so they were not in that PR.

Test plan

  • token_file_refuses_group_or_world_bits (0644 and 0640 refused, 0600 accepted)
  • cargo deny check with wildcards = "deny" reproduced error[wildcard] on workspace = true, then the setting was put back
  • Required checks on this PR (qc, test, windows, macos, coverage)

Made with Cursor

reardencode and others added 2 commits October 18, 2026 18:46
cargo-deny 0.20.2 reports error[wildcard] on every workspace = true line. There is still no version star in the tree.

Co-authored-by: Cursor <cursoragent@cursor.com>
The node creates rpc.token owner-only, then accepted a group- or world-readable file on the next start. The mode is read from the same handle as the secret.

Co-authored-by: Cursor <cursoragent@cursor.com>

@rearden-grok rearden-grok Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposition

One item is still open. The mode check is doing what it should, and the Linux test job is red because one unit test was not updated to match it.

auth::tests::resolve_reuses_existing_token still plants the token with fs::write and then expects resolve_rpc_auth to succeed. Under umask 022 that file is mode 0644, so the new check refuses it. Job https://github.com/reardencode/rbitcoin/actions/runs/39187126120/job/117851794861 failed there:

RPC token …/rpc.token: mode 644 is wider than 0600

That test is the reuse path (an operator-supplied token is kept, not regenerated). Chmod it to 0600 before resolve_rpc_auth, the same way the other planted tokens in this PR were fixed.

Same gap, smaller: empty_token_file_errors writes \n and only asserts is_err. On Unix that error is now the mode check, so the test no longer reaches the empty-token branch.

What holds

The check matches the cookie owner ceiling and it is on the opened handle, not a path stat done earlier:

  • File::open, then metadata() on that handle, then read. A replace of the path between the check and the read does not change which inode was accepted.
  • Unix mask is mode & 0o777 then mode & 0o077 != 0. Group or world read, write, or execute refuses startup. That is the same owner rule as --rpc-cookie-perms owner.
  • write_token_file still uses OpenOptionsExt::mode(0o600) and create_new. Umask can only clear bits, so a file this process creates cannot be wider than 0600. An existing file is not chmodded.
  • Callers that plant a token and then start a node (cross_surface, end_of_ibd, asmap_journey, integration_multinode, the RPC server test, write_rpc_token) chmod 0600 on Unix. The core-functional bitcoind shim already chmods the rpcpassword seed to 0600 before the node starts. rbitcoin-cli tests read the file themselves and do not call read_token_file.
  • Windows has no mode bits. The changelog, CLI help, docs/rpc.md, and the operator table say so.
  • There is no version = "*" in the workspace. CI pins cargo-deny 0.20.2 (.github/workflows/ci.yml). With wildcards = "deny" that release reports error[wildcard] on every workspace = true dependency. Leaving wildcards = "allow", with that reason in deny.toml, matches the tool we actually run.

Not blocking

These are the same class of gap as the cookie check. This PR does not make them worse.

  • Owner execute and owner read-only (0700, 0400) pass. The mask is group and world, not "exactly 0600". The changelog line "must be mode 0600" is tighter than the code. The error string says "wider than 0600", which is the cookie wording for this mask. 0700 does not grant group or world read.
  • Setuid, setgid, and sticky are stripped by & 0o777 before the test, same as the cookie.
  • The check does not compare uid. A root node will read another user's mode-0600 token. A non-root node cannot open that file.
  • open follows symlinks. fstat is the target. A symlink whose target is owner-only is accepted. O_NOFOLLOW is not used.
  • POSIX ACLs are visible only to the extent they show up in the mode bits. On Linux the ACL mask usually does. A filesystem that hides an extra grant from st_mode would not be refused.
  • The parent directory mode is not checked. A world-writable datadir can still unlink the token. Replacing it with a group- or world-readable file now fails startup instead of loading a leaked bearer. Replacing it with another user's 0600 file fails the open unless the node is root.
  • write_token_file does not fstat the handle it just created. A normal Unix umask cannot widen mode(0o600). A filesystem that ignored the create mode would serve that token until the next start, which would then refuse it.
  • read_token_file still trims. A trailing newline is accepted. The cookie refuses a line ending because mempool and bitcoin-cli disagree. The token has one reader. That split is unchanged.
  • There is no --rpc-token-perms. Group read of the bearer is refused on purpose. Another local user already has the mode-0660 unix socket path (--rpc-socket).
  • rbitcoin-cli does not apply this check. A wide token file is already readable. This node will not start and accept it.

wildcards = "deny" stays blocked on the cargo-deny diagnostic above, not on a version = "*" somewhere in the tree. Collapsing the multiple-versions skip list is a noise_sv2 pin change, separate from this lint.

.permissions()
.mode()
& 0o777;
if mode & 0o077 != 0 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refusal is right, and it is already failing CI. resolve_reuses_existing_token (about forty lines above, not in this diff) still does fs::write of fixed-token\n and then resolve_rpc_auth(...).unwrap(). Umask 022 makes that file 0644, so the unwrap panics with mode 644 is wider than 0600. Linux test job 117851794861. Chmod 0600 in that test before resolve. empty_token_file_errors has the same plant and only asserts is_err, so on Unix it now stops at this branch and no longer reaches the empty-token check.

fs: :write follows umask, so those tests hit the mode refusal before the behavior they name.
Co-authored-by: Cursor <cursoragent@cursor.com>
@rearden-grok

rearden-grok Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

The two tests that create a token with a normal write now set the file to mode 0600 before they read it. A normal write was leaving the file group- and world-readable, so the new check stopped them before they did what they were written for.

The reuse test keeps an existing token instead of minting a new one. The empty-file test now fails because the file is blank, and it checks that the error says so.

@reardencode
reardencode merged commit 494aad2 into master Oct 19, 2026
18 checks passed
@rearden-grok
rearden-grok Bot deleted the harden/cookie-and-audit branch October 19, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant