Experimentally allow forcing nix-daemon trust; use this to test

We finally test the status quo of remote build trust in a number of
ways. We create a new experimental feature on `nix-daemon` to do so.

PR #3921, which improves the situation with trustless remote building,
will build upon these changes. This code / tests was pull out of there
to make this, so everything is easier to review, and in particular we
test before and after so the new behavior in that PR is readily apparent
from the testsuite diff alone.
This commit is contained in:
John Ericson 2023-04-17 09:41:39 -04:00
parent 3f9589f17e
commit d41e1bed5e
12 changed files with 145 additions and 17 deletions

View file

@ -1067,6 +1067,8 @@ void processConnection(
opCount++; opCount++;
debug("performing daemon worker op: %d", op);
try { try {
performOp(tunnelLogger, store, trusted, recursive, clientVersion, from, to, op); performOp(tunnelLogger, store, trusted, recursive, clientVersion, from, to, op);
} catch (Error & e) { } catch (Error & e) {

View file

@ -12,7 +12,7 @@ struct ExperimentalFeatureDetails
std::string_view description; std::string_view description;
}; };
constexpr std::array<ExperimentalFeatureDetails, 11> xpFeatureDetails = {{ constexpr std::array<ExperimentalFeatureDetails, 12> xpFeatureDetails = {{
{ {
.tag = Xp::CaDerivations, .tag = Xp::CaDerivations,
.name = "ca-derivations", .name = "ca-derivations",
@ -189,6 +189,16 @@ constexpr std::array<ExperimentalFeatureDetails, 11> xpFeatureDetails = {{
runtime dependencies. runtime dependencies.
)", )",
}, },
{
.tag = Xp::DaemonTrustOverride,
.name = "daemon-trust-override",
.description = R"(
Allow forcing trusting or not trusting clients with
`nix-daemon`. This is useful for testing, but possibly also
useful for various experiments with `nix-daemon --stdio`
networking.
)",
},
}}; }};
static_assert( static_assert(

View file

@ -28,6 +28,7 @@ enum struct ExperimentalFeature
AutoAllocateUids, AutoAllocateUids,
Cgroups, Cgroups,
DiscardReferences, DiscardReferences,
DaemonTrustOverride,
}; };
/** /**

View file

@ -273,8 +273,12 @@ static std::pair<TrustedFlag, std::string> authPeer(const PeerInfo & peer)
/** /**
* Run a server. The loop opens a socket and accepts new connections from that * Run a server. The loop opens a socket and accepts new connections from that
* socket. * socket.
*
* @param forceTrustClientOpt If present, force trusting or not trusted
* the client. Otherwise, decide based on the authentication settings
* and user credentials (from the unix domain socket).
*/ */
static void daemonLoop() static void daemonLoop(std::optional<TrustedFlag> forceTrustClientOpt)
{ {
if (chdir("/") == -1) if (chdir("/") == -1)
throw SysError("cannot change current directory"); throw SysError("cannot change current directory");
@ -317,9 +321,18 @@ static void daemonLoop()
closeOnExec(remote.get()); closeOnExec(remote.get());
PeerInfo peer = getPeerInfo(remote.get()); PeerInfo peer { .pidKnown = false };
auto [_trusted, user] = authPeer(peer); TrustedFlag trusted;
auto trusted = _trusted; std::string user;
if (forceTrustClientOpt)
trusted = *forceTrustClientOpt;
else {
peer = getPeerInfo(remote.get());
auto [_trusted, _user] = authPeer(peer);
trusted = _trusted;
user = _user;
};
printInfo((std::string) "accepted connection from pid %1%, user %2%" + (trusted ? " (trusted)" : ""), printInfo((std::string) "accepted connection from pid %1%, user %2%" + (trusted ? " (trusted)" : ""),
peer.pidKnown ? std::to_string(peer.pid) : "<unknown>", peer.pidKnown ? std::to_string(peer.pid) : "<unknown>",
@ -410,38 +423,47 @@ static void forwardStdioConnection(RemoteStore & store) {
* Unlike `forwardStdioConnection()` we do process commands ourselves in * Unlike `forwardStdioConnection()` we do process commands ourselves in
* this case, not delegating to another daemon. * this case, not delegating to another daemon.
* *
* @note `Trusted` is unconditionally passed because in this mode we * @param trustClient Whether to trust the client. Forwarded directly to
* blindly trust the standard streams. Limiting access to those is * `processConnection()`.
* explicitly not `nix-daemon`'s responsibility.
*/ */
static void processStdioConnection(ref<Store> store) static void processStdioConnection(ref<Store> store, TrustedFlag trustClient)
{ {
FdSource from(STDIN_FILENO); FdSource from(STDIN_FILENO);
FdSink to(STDOUT_FILENO); FdSink to(STDOUT_FILENO);
processConnection(store, from, to, Trusted, NotRecursive); processConnection(store, from, to, trustClient, NotRecursive);
} }
/** /**
* Entry point shared between the new CLI `nix daemon` and old CLI * Entry point shared between the new CLI `nix daemon` and old CLI
* `nix-daemon`. * `nix-daemon`.
*
* @param forceTrustClientOpt See `daemonLoop()` and the parameter with
* the same name over there for details.
*/ */
static void runDaemon(bool stdio) static void runDaemon(bool stdio, std::optional<TrustedFlag> forceTrustClientOpt)
{ {
if (stdio) { if (stdio) {
auto store = openUncachedStore(); auto store = openUncachedStore();
if (auto remoteStore = store.dynamic_pointer_cast<RemoteStore>()) // If --force-untrusted is passed, we cannot forward the connection and
// must process it ourselves (before delegating to the next store) to
// force untrusting the client.
if (auto remoteStore = store.dynamic_pointer_cast<RemoteStore>(); remoteStore && (!forceTrustClientOpt || *forceTrustClientOpt != NotTrusted))
forwardStdioConnection(*remoteStore); forwardStdioConnection(*remoteStore);
else else
processStdioConnection(store); // `Trusted` is passed in the auto (no override case) because we
// cannot see who is on the other side of a plain pipe. Limiting
// access to those is explicitly not `nix-daemon`'s responsibility.
processStdioConnection(store, forceTrustClientOpt.value_or(Trusted));
} else } else
daemonLoop(); daemonLoop(forceTrustClientOpt);
} }
static int main_nix_daemon(int argc, char * * argv) static int main_nix_daemon(int argc, char * * argv)
{ {
{ {
auto stdio = false; auto stdio = false;
std::optional<TrustedFlag> isTrustedOpt = std::nullopt;
parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) { parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
if (*arg == "--daemon") if (*arg == "--daemon")
@ -452,11 +474,20 @@ static int main_nix_daemon(int argc, char * * argv)
printVersion("nix-daemon"); printVersion("nix-daemon");
else if (*arg == "--stdio") else if (*arg == "--stdio")
stdio = true; stdio = true;
else return false; else if (*arg == "--force-trusted") {
experimentalFeatureSettings.require(Xp::DaemonTrustOverride);
isTrustedOpt = Trusted;
} else if (*arg == "--force-untrusted") {
experimentalFeatureSettings.require(Xp::DaemonTrustOverride);
isTrustedOpt = NotTrusted;
} else if (*arg == "--default-trust") {
experimentalFeatureSettings.require(Xp::DaemonTrustOverride);
isTrustedOpt = std::nullopt;
} else return false;
return true; return true;
}); });
runDaemon(stdio); runDaemon(stdio, isTrustedOpt);
return 0; return 0;
} }
@ -482,7 +513,7 @@ struct CmdDaemon : StoreCommand
void run(ref<Store> store) override void run(ref<Store> store) override
{ {
runDaemon(false); runDaemon(false, std::nullopt);
} }
}; };

View file

@ -0,0 +1,2 @@
outPath=$(readlink -f $TEST_ROOT/result)
grep 'FOO BAR BAZ' ${remoteDir}/${outPath}

View file

@ -0,0 +1,29 @@
source common.sh
enableFeatures "daemon-trust-override"
restartDaemon
[[ $busybox =~ busybox ]] || skipTest "no busybox"
unset NIX_STORE_DIR
unset NIX_STATE_DIR
# We first build a dependency of the derivation we eventually want to
# build.
nix-build build-hook.nix -A passthru.input2 \
-o "$TEST_ROOT/input2" \
--arg busybox "$busybox" \
--store "$TEST_ROOT/local" \
--option system-features bar
# Now when we go to build that downstream derivation, Nix will fail
# because we cannot trustlessly build input-addressed derivations with
# `inputDrv` dependencies.
file=build-hook.nix
prog=$(readlink -e ./nix-daemon-untrusting.sh)
proto=ssh-ng
expectStderr 1 source build-remote-trustless.sh \
| grepQuiet "you are not privileged to build input-addressed derivations"

View file

@ -0,0 +1,9 @@
source common.sh
# Remote trusts us
file=build-hook.nix
prog=nix-store
proto=ssh
source build-remote-trustless.sh
source build-remote-trustless-after.sh

View file

@ -0,0 +1,9 @@
source common.sh
# Remote trusts us
file=build-hook.nix
prog=nix-daemon
proto=ssh-ng
source build-remote-trustless.sh
source build-remote-trustless-after.sh

View file

@ -0,0 +1,14 @@
source common.sh
enableFeatures "daemon-trust-override"
restartDaemon
# Remote doesn't trusts us, but this is fine because we are only
# building (fixed) CA derivations.
file=build-hook-ca-fixed.nix
prog=$(readlink -e ./nix-daemon-untrusting.sh)
proto=ssh-ng
source build-remote-trustless.sh
source build-remote-trustless-after.sh

View file

@ -0,0 +1,14 @@
requireSandboxSupport
[[ $busybox =~ busybox ]] || skipTest "no busybox"
unset NIX_STORE_DIR
unset NIX_STATE_DIR
remoteDir=$TEST_ROOT/remote
# Note: ssh{-ng}://localhost bypasses ssh. See tests/build-remote.sh for
# more details.
nix-build $file -o $TEST_ROOT/result --max-jobs 0 \
--arg busybox $busybox \
--store $TEST_ROOT/local \
--builders "$proto://localhost?remote-program=$prog&remote-store=${remoteDir}%3Fsystem-features=foo%20bar%20baz - - 1 1 foo,bar,baz"

View file

@ -70,6 +70,10 @@ nix_tests = \
check-reqs.sh \ check-reqs.sh \
build-remote-content-addressed-fixed.sh \ build-remote-content-addressed-fixed.sh \
build-remote-content-addressed-floating.sh \ build-remote-content-addressed-floating.sh \
build-remote-trustless-should-pass-0.sh \
build-remote-trustless-should-pass-1.sh \
build-remote-trustless-should-pass-3.sh \
build-remote-trustless-should-fail-0.sh \
nar-access.sh \ nar-access.sh \
pure-eval.sh \ pure-eval.sh \
eval.sh \ eval.sh \

3
tests/nix-daemon-untrusting.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
exec nix-daemon --force-untrusted "$@"