Merge bitcoin/bitcoin#28167: init: Add option for rpccookie permissions (replace 26088)

73f0a6cbd0 doc: detail -rpccookieperms option (willcl-ark)
d2afa2690c test: add rpccookieperms test (willcl-ark)
f467aede78 init: add option for rpccookie permissions (willcl-ark)
7df03f1a92 util: add perm string helper functions (willcl-ark)

Pull request description:

  This PR picks up #26088 by aureleoules which adds a bitcoind launch option `-rpccookieperms` to set the file permissions of the cookie generated by bitcoin core.

  Example usage to make the generated cookie group-readable: `./src/bitcoind -rpccookieperms=group`.

  Accepted values for `-rpccookieperms` are `[owner|group|all]`. We let `fs::perms` handle platform-specific permissions changes.

ACKs for top commit:
  achow101:
    ACK 73f0a6cbd0
  ryanofsky:
    Code review ACK 73f0a6cbd0. Main change since last review is no longer throwing a skip exception in the rpc test on windows, so other checks can run after it, and overall test result is passing, not skipped. Also were clarifying renames and documentation improvements.
  tdb3:
    cr ACK 73f0a6cbd0

Tree-SHA512: e800d59a44aca10e1c58ca69bf3fdde9f6ccf5eab4b7b962645af6d6bc0cfa3a357701e409c8c60d8d7744fcd33a91e77ada11790aa88cd7811ef60fab86ab11
pull/30363/head
Ryan Ofsky 4 months ago
commit d38dbaad98
No known key found for this signature in database
GPG Key ID: 46800E30FC748A66

@ -35,8 +35,10 @@ it will use a special cookie file for authentication. The cookie is generated wi
content when the daemon starts, and deleted when it exits. Read access to this file content when the daemon starts, and deleted when it exits. Read access to this file
controls who can access it through RPC. controls who can access it through RPC.
By default the cookie is stored in the data directory, but it's location can be overridden By default the cookie is stored in the data directory, but its location can be
with the option '-rpccookiefile'. overridden with the option `-rpccookiefile`. Default file permissions for the
cookie are "owner" (i.e. user read/writeable) via default application-wide file
umask of `0077`, but these can be overridden with the `-rpccookieperms` option.
This allows for running bitcoind without having to do any manual configuration. This allows for running bitcoind without having to do any manual configuration.

@ -11,6 +11,8 @@
#include <netaddress.h> #include <netaddress.h>
#include <rpc/protocol.h> #include <rpc/protocol.h>
#include <rpc/server.h> #include <rpc/server.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/strencodings.h> #include <util/strencodings.h>
#include <util/string.h> #include <util/string.h>
#include <walletinitinterface.h> #include <walletinitinterface.h>
@ -19,6 +21,7 @@
#include <iterator> #include <iterator>
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional>
#include <set> #include <set>
#include <string> #include <string>
#include <vector> #include <vector>
@ -291,8 +294,20 @@ static bool InitRPCAuthentication()
{ {
if (gArgs.GetArg("-rpcpassword", "") == "") if (gArgs.GetArg("-rpcpassword", "") == "")
{ {
LogPrintf("Using random cookie authentication.\n"); LogInfo("Using random cookie authentication.\n");
if (!GenerateAuthCookie(&strRPCUserColonPass)) {
std::optional<fs::perms> cookie_perms{std::nullopt};
auto cookie_perms_arg{gArgs.GetArg("-rpccookieperms")};
if (cookie_perms_arg) {
auto perm_opt = InterpretPermString(*cookie_perms_arg);
if (!perm_opt) {
LogInfo("Invalid -rpccookieperms=%s; must be one of 'owner', 'group', or 'all'.\n", *cookie_perms_arg);
return false;
}
cookie_perms = *perm_opt;
}
if (!GenerateAuthCookie(&strRPCUserColonPass, cookie_perms)) {
return false; return false;
} }
} else { } else {

@ -659,6 +659,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-rpcbind=<addr>[:port]", "Bind to given address to listen for JSON-RPC connections. Do not expose the RPC server to untrusted networks such as the public internet! This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC); argsman.AddArg("-rpcbind=<addr>[:port]", "Bind to given address to listen for JSON-RPC connections. Do not expose the RPC server to untrusted networks such as the public internet! This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC);
argsman.AddArg("-rpcdoccheck", strprintf("Throw a non-fatal error at runtime if the documentation for an RPC is incorrect (default: %u)", DEFAULT_RPC_DOC_CHECK), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC); argsman.AddArg("-rpcdoccheck", strprintf("Throw a non-fatal error at runtime if the documentation for an RPC is incorrect (default: %u)", DEFAULT_RPC_DOC_CHECK), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);
argsman.AddArg("-rpccookiefile=<loc>", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::RPC); argsman.AddArg("-rpccookiefile=<loc>", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg("-rpccookieperms=<readable-by>", strprintf("Set permissions on the RPC auth cookie file so that it is readable by [owner|group|all] (default: owner [via umask 0077])"), ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC); argsman.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
argsman.AddArg("-rpcport=<port>", strprintf("Listen for JSON-RPC connections on <port> (default: %u, testnet: %u, signet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), signetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC); argsman.AddArg("-rpcport=<port>", strprintf("Listen for JSON-RPC connections on <port> (default: %u, testnet: %u, signet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), signetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC);
argsman.AddArg("-rpcservertimeout=<n>", strprintf("Timeout during HTTP requests (default: %d)", DEFAULT_HTTP_SERVER_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC); argsman.AddArg("-rpcservertimeout=<n>", strprintf("Timeout during HTTP requests (default: %d)", DEFAULT_HTTP_SERVER_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);

@ -5,12 +5,11 @@
#include <rpc/request.h> #include <rpc/request.h>
#include <util/fs.h>
#include <common/args.h> #include <common/args.h>
#include <logging.h> #include <logging.h>
#include <random.h> #include <random.h>
#include <rpc/protocol.h> #include <rpc/protocol.h>
#include <util/fs.h>
#include <util/fs_helpers.h> #include <util/fs_helpers.h>
#include <util/strencodings.h> #include <util/strencodings.h>
@ -95,7 +94,7 @@ static fs::path GetAuthCookieFile(bool temp=false)
static bool g_generated_cookie = false; static bool g_generated_cookie = false;
bool GenerateAuthCookie(std::string *cookie_out) bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms)
{ {
const size_t COOKIE_SIZE = 32; const size_t COOKIE_SIZE = 32;
unsigned char rand_pwd[COOKIE_SIZE]; unsigned char rand_pwd[COOKIE_SIZE];
@ -109,7 +108,7 @@ bool GenerateAuthCookie(std::string *cookie_out)
fs::path filepath_tmp = GetAuthCookieFile(true); fs::path filepath_tmp = GetAuthCookieFile(true);
file.open(filepath_tmp); file.open(filepath_tmp);
if (!file.is_open()) { if (!file.is_open()) {
LogPrintf("Unable to open cookie authentication file %s for writing\n", fs::PathToString(filepath_tmp)); LogInfo("Unable to open cookie authentication file %s for writing\n", fs::PathToString(filepath_tmp));
return false; return false;
} }
file << cookie; file << cookie;
@ -117,11 +116,21 @@ bool GenerateAuthCookie(std::string *cookie_out)
fs::path filepath = GetAuthCookieFile(false); fs::path filepath = GetAuthCookieFile(false);
if (!RenameOver(filepath_tmp, filepath)) { if (!RenameOver(filepath_tmp, filepath)) {
LogPrintf("Unable to rename cookie authentication file %s to %s\n", fs::PathToString(filepath_tmp), fs::PathToString(filepath)); LogInfo("Unable to rename cookie authentication file %s to %s\n", fs::PathToString(filepath_tmp), fs::PathToString(filepath));
return false; return false;
} }
if (cookie_perms) {
std::error_code code;
fs::permissions(filepath, cookie_perms.value(), fs::perm_options::replace, code);
if (code) {
LogInfo("Unable to set permissions on cookie authentication file %s\n", fs::PathToString(filepath_tmp));
return false;
}
}
g_generated_cookie = true; g_generated_cookie = true;
LogPrintf("Generated RPC authentication cookie %s\n", fs::PathToString(filepath)); LogInfo("Generated RPC authentication cookie %s\n", fs::PathToString(filepath));
LogInfo("Permissions used for cookie: %s\n", PermsToSymbolicString(fs::status(filepath).permissions()));
if (cookie_out) if (cookie_out)
*cookie_out = cookie; *cookie_out = cookie;

@ -11,6 +11,7 @@
#include <string> #include <string>
#include <univalue.h> #include <univalue.h>
#include <util/fs.h>
enum class JSONRPCVersion { enum class JSONRPCVersion {
V1_LEGACY, V1_LEGACY,
@ -23,7 +24,7 @@ UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue
UniValue JSONRPCError(int code, const std::string& message); UniValue JSONRPCError(int code, const std::string& message);
/** Generate a new RPC authentication cookie and write it to disk */ /** Generate a new RPC authentication cookie and write it to disk */
bool GenerateAuthCookie(std::string *cookie_out); bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms=std::nullopt);
/** Read the RPC authentication cookie from disk */ /** Read the RPC authentication cookie from disk */
bool GetAuthCookie(std::string *cookie_out); bool GetAuthCookie(std::string *cookie_out);
/** Delete RPC authentication cookie from disk */ /** Delete RPC authentication cookie from disk */

@ -16,6 +16,7 @@
#include <fstream> #include <fstream>
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include <system_error> #include <system_error>
#include <utility> #include <utility>
@ -269,3 +270,42 @@ bool TryCreateDirectories(const fs::path& p)
// create_directories didn't create the directory, it had to have existed already // create_directories didn't create the directory, it had to have existed already
return false; return false;
} }
std::string PermsToSymbolicString(fs::perms p)
{
std::string perm_str(9, '-');
auto set_perm = [&](size_t pos, fs::perms required_perm, char letter) {
if ((p & required_perm) != fs::perms::none) {
perm_str[pos] = letter;
}
};
set_perm(0, fs::perms::owner_read, 'r');
set_perm(1, fs::perms::owner_write, 'w');
set_perm(2, fs::perms::owner_exec, 'x');
set_perm(3, fs::perms::group_read, 'r');
set_perm(4, fs::perms::group_write, 'w');
set_perm(5, fs::perms::group_exec, 'x');
set_perm(6, fs::perms::others_read, 'r');
set_perm(7, fs::perms::others_write, 'w');
set_perm(8, fs::perms::others_exec, 'x');
return perm_str;
}
std::optional<fs::perms> InterpretPermString(const std::string& s)
{
if (s == "owner") {
return fs::perms::owner_read | fs::perms::owner_write;
} else if (s == "group") {
return fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read;
} else if (s == "all") {
return fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read |
fs::perms::others_read;
} else {
return std::nullopt;
}
}

@ -12,6 +12,7 @@
#include <cstdio> #include <cstdio>
#include <iosfwd> #include <iosfwd>
#include <limits> #include <limits>
#include <optional>
/** /**
* Ensure file contents are fully committed to disk, using a platform-specific * Ensure file contents are fully committed to disk, using a platform-specific
@ -62,6 +63,19 @@ void ReleaseDirectoryLocks();
bool TryCreateDirectories(const fs::path& p); bool TryCreateDirectories(const fs::path& p);
fs::path GetDefaultDataDir(); fs::path GetDefaultDataDir();
/** Convert fs::perms to symbolic string of the form 'rwxrwxrwx'
*
* @param[in] p the perms to be converted
* @return Symbolic permissions string
*/
std::string PermsToSymbolicString(fs::perms p);
/** Interpret a custom permissions level string as fs::perms
*
* @param[in] s Permission level string
* @return Permissions as fs::perms
*/
std::optional<fs::perms> InterpretPermString(const std::string& s);
#ifdef WIN32 #ifdef WIN32
fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true); fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true);
#endif #endif

@ -11,12 +11,15 @@ from test_framework.util import (
) )
import http.client import http.client
import os
import platform
import urllib.parse import urllib.parse
import subprocess import subprocess
from random import SystemRandom from random import SystemRandom
import string import string
import configparser import configparser
import sys import sys
from typing import Optional
def call_with_auth(node, user, password): def call_with_auth(node, user, password):
@ -84,6 +87,40 @@ class HTTPBasicsTest(BitcoinTestFramework):
self.log.info('Wrong...') self.log.info('Wrong...')
assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status) assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status)
def test_rpccookieperms(self):
p = {"owner": 0o600, "group": 0o640, "all": 0o644}
if platform.system() == 'Windows':
self.log.info(f"Skip cookie file permissions checks as OS detected as: {platform.system()=}")
return
self.log.info('Check cookie file permissions can be set using -rpccookieperms')
cookie_file_path = self.nodes[1].chain_path / '.cookie'
PERM_BITS_UMASK = 0o777
def test_perm(perm: Optional[str]):
if not perm:
perm = 'owner'
self.restart_node(1)
else:
self.restart_node(1, extra_args=[f"-rpccookieperms={perm}"])
file_stat = os.stat(cookie_file_path)
actual_perms = file_stat.st_mode & PERM_BITS_UMASK
expected_perms = p[perm]
assert_equal(expected_perms, actual_perms)
# Remove any leftover rpc{user|password} config options from previous tests
self.nodes[1].replace_in_config([("rpcuser", "#rpcuser"), ("rpcpassword", "#rpcpassword")])
self.log.info('Check default cookie permission')
test_perm(None)
self.log.info('Check custom cookie permissions')
for perm in ["owner", "group", "all"]:
test_perm(perm)
def run_test(self): def run_test(self):
self.conf_setup() self.conf_setup()
self.log.info('Check correctness of the rpcauth config option') self.log.info('Check correctness of the rpcauth config option')
@ -115,6 +152,8 @@ class HTTPBasicsTest(BitcoinTestFramework):
(self.nodes[0].chain_path / ".cookie.tmp").mkdir() (self.nodes[0].chain_path / ".cookie.tmp").mkdir()
self.nodes[0].assert_start_raises_init_error(expected_msg=init_error) self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
self.test_rpccookieperms()
if __name__ == '__main__': if __name__ == '__main__':
HTTPBasicsTest().main() HTTPBasicsTest().main()

Loading…
Cancel
Save