aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/init.md6
-rw-r--r--src/httprpc.cpp19
-rw-r--r--src/init.cpp1
-rw-r--r--src/rpc/request.cpp21
-rw-r--r--src/rpc/request.h3
-rw-r--r--src/util/fs_helpers.cpp40
-rw-r--r--src/util/fs_helpers.h14
-rwxr-xr-xtest/functional/rpc_users.py39
8 files changed, 132 insertions, 11 deletions
diff --git a/doc/init.md b/doc/init.md
index 64ab971557..b570fed22c 100644
--- a/doc/init.md
+++ b/doc/init.md
@@ -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
controls who can access it through RPC.
-By default the cookie is stored in the data directory, but it's location can be overridden
-with the option '-rpccookiefile'.
+By default the cookie is stored in the data directory, but its location can be
+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.
diff --git a/src/httprpc.cpp b/src/httprpc.cpp
index 128597157d..af809eaf38 100644
--- a/src/httprpc.cpp
+++ b/src/httprpc.cpp
@@ -11,6 +11,8 @@
#include <netaddress.h>
#include <rpc/protocol.h>
#include <rpc/server.h>
+#include <util/fs.h>
+#include <util/fs_helpers.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <walletinitinterface.h>
@@ -19,6 +21,7 @@
#include <iterator>
#include <map>
#include <memory>
+#include <optional>
#include <set>
#include <string>
#include <vector>
@@ -291,8 +294,20 @@ static bool InitRPCAuthentication()
{
if (gArgs.GetArg("-rpcpassword", "") == "")
{
- LogPrintf("Using random cookie authentication.\n");
- if (!GenerateAuthCookie(&strRPCUserColonPass)) {
+ LogInfo("Using random cookie authentication.\n");
+
+ 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;
}
} else {
diff --git a/src/init.cpp b/src/init.cpp
index c6ef62372e..c9405153b4 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -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("-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("-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("-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);
diff --git a/src/rpc/request.cpp b/src/rpc/request.cpp
index 87b9f18b33..083d1be44f 100644
--- a/src/rpc/request.cpp
+++ b/src/rpc/request.cpp
@@ -5,12 +5,11 @@
#include <rpc/request.h>
-#include <util/fs.h>
-
#include <common/args.h>
#include <logging.h>
#include <random.h>
#include <rpc/protocol.h>
+#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/strencodings.h>
@@ -95,7 +94,7 @@ static fs::path GetAuthCookieFile(bool temp=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;
unsigned char rand_pwd[COOKIE_SIZE];
@@ -109,7 +108,7 @@ bool GenerateAuthCookie(std::string *cookie_out)
fs::path filepath_tmp = GetAuthCookieFile(true);
file.open(filepath_tmp);
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;
}
file << cookie;
@@ -117,11 +116,21 @@ bool GenerateAuthCookie(std::string *cookie_out)
fs::path filepath = GetAuthCookieFile(false);
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;
}
+ 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;
- 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)
*cookie_out = cookie;
diff --git a/src/rpc/request.h b/src/rpc/request.h
index 9968426636..24887e8691 100644
--- a/src/rpc/request.h
+++ b/src/rpc/request.h
@@ -11,6 +11,7 @@
#include <string>
#include <univalue.h>
+#include <util/fs.h>
enum class JSONRPCVersion {
V1_LEGACY,
@@ -23,7 +24,7 @@ UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue
UniValue JSONRPCError(int code, const std::string& message);
/** 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 */
bool GetAuthCookie(std::string *cookie_out);
/** Delete RPC authentication cookie from disk */
diff --git a/src/util/fs_helpers.cpp b/src/util/fs_helpers.cpp
index 8952f20f79..41c8fe3b8f 100644
--- a/src/util/fs_helpers.cpp
+++ b/src/util/fs_helpers.cpp
@@ -16,6 +16,7 @@
#include <fstream>
#include <map>
#include <memory>
+#include <optional>
#include <string>
#include <system_error>
#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
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;
+ }
+}
diff --git a/src/util/fs_helpers.h b/src/util/fs_helpers.h
index ea3778eac3..28dd6d979d 100644
--- a/src/util/fs_helpers.h
+++ b/src/util/fs_helpers.h
@@ -12,6 +12,7 @@
#include <cstdio>
#include <iosfwd>
#include <limits>
+#include <optional>
/**
* Ensure file contents are fully committed to disk, using a platform-specific
@@ -62,6 +63,19 @@ void ReleaseDirectoryLocks();
bool TryCreateDirectories(const fs::path& p);
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
fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true);
#endif
diff --git a/test/functional/rpc_users.py b/test/functional/rpc_users.py
index 66cdd7cf9a..153493fbab 100755
--- a/test/functional/rpc_users.py
+++ b/test/functional/rpc_users.py
@@ -11,12 +11,15 @@ from test_framework.util import (
)
import http.client
+import os
+import platform
import urllib.parse
import subprocess
from random import SystemRandom
import string
import configparser
import sys
+from typing import Optional
def call_with_auth(node, user, password):
@@ -84,6 +87,40 @@ class HTTPBasicsTest(BitcoinTestFramework):
self.log.info('Wrong...')
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):
self.conf_setup()
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].assert_start_raises_init_error(expected_msg=init_error)
+ self.test_rpccookieperms()
+
if __name__ == '__main__':
HTTPBasicsTest().main()