Skip to content

Commit d38dbaa

Browse files
committed
Merge bitcoin#28167: init: Add option for rpccookie permissions (replace 26088)
73f0a6c doc: detail -rpccookieperms option (willcl-ark) d2afa26 test: add rpccookieperms test (willcl-ark) f467aed init: add option for rpccookie permissions (willcl-ark) 7df03f1 util: add perm string helper functions (willcl-ark) Pull request description: This PR picks up bitcoin#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 73f0a6c ryanofsky: Code review ACK 73f0a6c. 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 73f0a6c Tree-SHA512: e800d59a44aca10e1c58ca69bf3fdde9f6ccf5eab4b7b962645af6d6bc0cfa3a357701e409c8c60d8d7744fcd33a91e77ada11790aa88cd7811ef60fab86ab11
2 parents f0745d0 + 73f0a6c commit d38dbaa

File tree

8 files changed

+132
-11
lines changed

8 files changed

+132
-11
lines changed

doc/init.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ it will use a special cookie file for authentication. The cookie is generated wi
3535
content when the daemon starts, and deleted when it exits. Read access to this file
3636
controls who can access it through RPC.
3737

38-
By default the cookie is stored in the data directory, but it's location can be overridden
39-
with the option '-rpccookiefile'.
38+
By default the cookie is stored in the data directory, but its location can be
39+
overridden with the option `-rpccookiefile`. Default file permissions for the
40+
cookie are "owner" (i.e. user read/writeable) via default application-wide file
41+
umask of `0077`, but these can be overridden with the `-rpccookieperms` option.
4042

4143
This allows for running bitcoind without having to do any manual configuration.
4244

src/httprpc.cpp

+17-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include <netaddress.h>
1212
#include <rpc/protocol.h>
1313
#include <rpc/server.h>
14+
#include <util/fs.h>
15+
#include <util/fs_helpers.h>
1416
#include <util/strencodings.h>
1517
#include <util/string.h>
1618
#include <walletinitinterface.h>
@@ -19,6 +21,7 @@
1921
#include <iterator>
2022
#include <map>
2123
#include <memory>
24+
#include <optional>
2225
#include <set>
2326
#include <string>
2427
#include <vector>
@@ -291,8 +294,20 @@ static bool InitRPCAuthentication()
291294
{
292295
if (gArgs.GetArg("-rpcpassword", "") == "")
293296
{
294-
LogPrintf("Using random cookie authentication.\n");
295-
if (!GenerateAuthCookie(&strRPCUserColonPass)) {
297+
LogInfo("Using random cookie authentication.\n");
298+
299+
std::optional<fs::perms> cookie_perms{std::nullopt};
300+
auto cookie_perms_arg{gArgs.GetArg("-rpccookieperms")};
301+
if (cookie_perms_arg) {
302+
auto perm_opt = InterpretPermString(*cookie_perms_arg);
303+
if (!perm_opt) {
304+
LogInfo("Invalid -rpccookieperms=%s; must be one of 'owner', 'group', or 'all'.\n", *cookie_perms_arg);
305+
return false;
306+
}
307+
cookie_perms = *perm_opt;
308+
}
309+
310+
if (!GenerateAuthCookie(&strRPCUserColonPass, cookie_perms)) {
296311
return false;
297312
}
298313
} else {

src/init.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ void SetupServerArgs(ArgsManager& argsman)
659659
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);
660660
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);
661661
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);
662+
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);
662663
argsman.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
663664
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);
664665
argsman.AddArg("-rpcservertimeout=<n>", strprintf("Timeout during HTTP requests (default: %d)", DEFAULT_HTTP_SERVER_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);

src/rpc/request.cpp

+15-6
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55

66
#include <rpc/request.h>
77

8-
#include <util/fs.h>
9-
108
#include <common/args.h>
119
#include <logging.h>
1210
#include <random.h>
1311
#include <rpc/protocol.h>
12+
#include <util/fs.h>
1413
#include <util/fs_helpers.h>
1514
#include <util/strencodings.h>
1615

@@ -95,7 +94,7 @@ static fs::path GetAuthCookieFile(bool temp=false)
9594

9695
static bool g_generated_cookie = false;
9796

98-
bool GenerateAuthCookie(std::string *cookie_out)
97+
bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms)
9998
{
10099
const size_t COOKIE_SIZE = 32;
101100
unsigned char rand_pwd[COOKIE_SIZE];
@@ -109,19 +108,29 @@ bool GenerateAuthCookie(std::string *cookie_out)
109108
fs::path filepath_tmp = GetAuthCookieFile(true);
110109
file.open(filepath_tmp);
111110
if (!file.is_open()) {
112-
LogPrintf("Unable to open cookie authentication file %s for writing\n", fs::PathToString(filepath_tmp));
111+
LogInfo("Unable to open cookie authentication file %s for writing\n", fs::PathToString(filepath_tmp));
113112
return false;
114113
}
115114
file << cookie;
116115
file.close();
117116

118117
fs::path filepath = GetAuthCookieFile(false);
119118
if (!RenameOver(filepath_tmp, filepath)) {
120-
LogPrintf("Unable to rename cookie authentication file %s to %s\n", fs::PathToString(filepath_tmp), fs::PathToString(filepath));
119+
LogInfo("Unable to rename cookie authentication file %s to %s\n", fs::PathToString(filepath_tmp), fs::PathToString(filepath));
121120
return false;
122121
}
122+
if (cookie_perms) {
123+
std::error_code code;
124+
fs::permissions(filepath, cookie_perms.value(), fs::perm_options::replace, code);
125+
if (code) {
126+
LogInfo("Unable to set permissions on cookie authentication file %s\n", fs::PathToString(filepath_tmp));
127+
return false;
128+
}
129+
}
130+
123131
g_generated_cookie = true;
124-
LogPrintf("Generated RPC authentication cookie %s\n", fs::PathToString(filepath));
132+
LogInfo("Generated RPC authentication cookie %s\n", fs::PathToString(filepath));
133+
LogInfo("Permissions used for cookie: %s\n", PermsToSymbolicString(fs::status(filepath).permissions()));
125134

126135
if (cookie_out)
127136
*cookie_out = cookie;

src/rpc/request.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <string>
1212

1313
#include <univalue.h>
14+
#include <util/fs.h>
1415

1516
enum class JSONRPCVersion {
1617
V1_LEGACY,
@@ -23,7 +24,7 @@ UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue
2324
UniValue JSONRPCError(int code, const std::string& message);
2425

2526
/** Generate a new RPC authentication cookie and write it to disk */
26-
bool GenerateAuthCookie(std::string *cookie_out);
27+
bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms=std::nullopt);
2728
/** Read the RPC authentication cookie from disk */
2829
bool GetAuthCookie(std::string *cookie_out);
2930
/** Delete RPC authentication cookie from disk */

src/util/fs_helpers.cpp

+40
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <fstream>
1717
#include <map>
1818
#include <memory>
19+
#include <optional>
1920
#include <string>
2021
#include <system_error>
2122
#include <utility>
@@ -269,3 +270,42 @@ bool TryCreateDirectories(const fs::path& p)
269270
// create_directories didn't create the directory, it had to have existed already
270271
return false;
271272
}
273+
274+
std::string PermsToSymbolicString(fs::perms p)
275+
{
276+
std::string perm_str(9, '-');
277+
278+
auto set_perm = [&](size_t pos, fs::perms required_perm, char letter) {
279+
if ((p & required_perm) != fs::perms::none) {
280+
perm_str[pos] = letter;
281+
}
282+
};
283+
284+
set_perm(0, fs::perms::owner_read, 'r');
285+
set_perm(1, fs::perms::owner_write, 'w');
286+
set_perm(2, fs::perms::owner_exec, 'x');
287+
set_perm(3, fs::perms::group_read, 'r');
288+
set_perm(4, fs::perms::group_write, 'w');
289+
set_perm(5, fs::perms::group_exec, 'x');
290+
set_perm(6, fs::perms::others_read, 'r');
291+
set_perm(7, fs::perms::others_write, 'w');
292+
set_perm(8, fs::perms::others_exec, 'x');
293+
294+
return perm_str;
295+
}
296+
297+
std::optional<fs::perms> InterpretPermString(const std::string& s)
298+
{
299+
if (s == "owner") {
300+
return fs::perms::owner_read | fs::perms::owner_write;
301+
} else if (s == "group") {
302+
return fs::perms::owner_read | fs::perms::owner_write |
303+
fs::perms::group_read;
304+
} else if (s == "all") {
305+
return fs::perms::owner_read | fs::perms::owner_write |
306+
fs::perms::group_read |
307+
fs::perms::others_read;
308+
} else {
309+
return std::nullopt;
310+
}
311+
}

src/util/fs_helpers.h

+14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <cstdio>
1313
#include <iosfwd>
1414
#include <limits>
15+
#include <optional>
1516

1617
/**
1718
* Ensure file contents are fully committed to disk, using a platform-specific
@@ -62,6 +63,19 @@ void ReleaseDirectoryLocks();
6263
bool TryCreateDirectories(const fs::path& p);
6364
fs::path GetDefaultDataDir();
6465

66+
/** Convert fs::perms to symbolic string of the form 'rwxrwxrwx'
67+
*
68+
* @param[in] p the perms to be converted
69+
* @return Symbolic permissions string
70+
*/
71+
std::string PermsToSymbolicString(fs::perms p);
72+
/** Interpret a custom permissions level string as fs::perms
73+
*
74+
* @param[in] s Permission level string
75+
* @return Permissions as fs::perms
76+
*/
77+
std::optional<fs::perms> InterpretPermString(const std::string& s);
78+
6579
#ifdef WIN32
6680
fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true);
6781
#endif

test/functional/rpc_users.py

+39
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
)
1212

1313
import http.client
14+
import os
15+
import platform
1416
import urllib.parse
1517
import subprocess
1618
from random import SystemRandom
1719
import string
1820
import configparser
1921
import sys
22+
from typing import Optional
2023

2124

2225
def call_with_auth(node, user, password):
@@ -84,6 +87,40 @@ def test_auth(self, node, user, password):
8487
self.log.info('Wrong...')
8588
assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status)
8689

90+
def test_rpccookieperms(self):
91+
p = {"owner": 0o600, "group": 0o640, "all": 0o644}
92+
93+
if platform.system() == 'Windows':
94+
self.log.info(f"Skip cookie file permissions checks as OS detected as: {platform.system()=}")
95+
return
96+
97+
self.log.info('Check cookie file permissions can be set using -rpccookieperms')
98+
99+
cookie_file_path = self.nodes[1].chain_path / '.cookie'
100+
PERM_BITS_UMASK = 0o777
101+
102+
def test_perm(perm: Optional[str]):
103+
if not perm:
104+
perm = 'owner'
105+
self.restart_node(1)
106+
else:
107+
self.restart_node(1, extra_args=[f"-rpccookieperms={perm}"])
108+
109+
file_stat = os.stat(cookie_file_path)
110+
actual_perms = file_stat.st_mode & PERM_BITS_UMASK
111+
expected_perms = p[perm]
112+
assert_equal(expected_perms, actual_perms)
113+
114+
# Remove any leftover rpc{user|password} config options from previous tests
115+
self.nodes[1].replace_in_config([("rpcuser", "#rpcuser"), ("rpcpassword", "#rpcpassword")])
116+
117+
self.log.info('Check default cookie permission')
118+
test_perm(None)
119+
120+
self.log.info('Check custom cookie permissions')
121+
for perm in ["owner", "group", "all"]:
122+
test_perm(perm)
123+
87124
def run_test(self):
88125
self.conf_setup()
89126
self.log.info('Check correctness of the rpcauth config option')
@@ -115,6 +152,8 @@ def run_test(self):
115152
(self.nodes[0].chain_path / ".cookie.tmp").mkdir()
116153
self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
117154

155+
self.test_rpccookieperms()
156+
118157

119158
if __name__ == '__main__':
120159
HTTPBasicsTest().main()

0 commit comments

Comments
 (0)