Here’s a patch which adds a new option to disable store enumeration in the daemon:
Patch
From 4c26abd5762bbeb0c952d8ffdb1ad3f1dd1c02f9 Mon Sep 17 00:00:00 2001
From: Claude Code <noreply@anthropic.com>
Date: Sun, 15 Feb 2026 16:06:56 +0000
Subject: [PATCH 1/2] daemon: restrict CollectGarbage to trusted users
Garbage collection was previously allowed for all connected users.
The results include the set of deleted paths, which leaks information
about store contents. Restrict this operation to trusted users only,
consistent with other privileged operations like AddPermRoot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
src/libstore/daemon.cc | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc
index 155fe2432..4f32e80f6 100644
--- a/src/libstore/daemon.cc
+++ b/src/libstore/daemon.cc
@@ -743,6 +743,8 @@ static void performOp(
GCResults results;
logger->startWork();
+ if (!trusted)
+ throw Error("you are not privileged to collect garbage");
if (options.ignoreLiveness)
throw Error("you are not allowed to ignore liveness");
auto & gcStore = require<GcStore>(*store);
--
2.49.0
From f5e8e67d8b36f62e59dd535b0472700736809c21 Mon Sep 17 00:00:00 2001
From: Claude Code <noreply@anthropic.com>
Date: Sun, 15 Feb 2026 16:11:51 +0000
Subject: [PATCH 2/2] daemon: add unlistable-store setting to prevent store
enumeration
Add a new `unlistable-store` boolean setting (default: false). When
enabled, untrusted users are denied operations that reveal store paths
they do not already know:
- QueryAllValidPaths: dumps the entire store inventory
- QueryReferrers: reveals reverse dependencies (e.g. querying
referrers of glibc enumerates most of the store)
- QueryValidDerivers: reveals derivation paths from output paths
- FindRoots: enumerates store paths referenced by all processes
via /proc
"Oracle" operations that require the caller to already know the full
store path (IsValidPath, QueryValidPaths, QueryPathInfo, etc.) remain
permitted, since knowledge of a path implies the caller can compute it
from the derivation inputs.
Intended for multi-tenant systems with a shared Nix store. Combine
with `chmod o-r /nix/store` to also block filesystem-level listing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
src/libstore/daemon.cc | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc
index 4f32e80f6..07483da59 100644
--- a/src/libstore/daemon.cc
+++ b/src/libstore/daemon.cc
@@ -18,6 +18,7 @@
#include "nix/util/git.hh"
#include "nix/util/logging.hh"
#include "nix/store/globals.hh"
+#include "nix/util/config-global.hh"
#ifndef _WIN32 // TODO need graceful async exit support on Windows?
# include "nix/util/monitor-fd.hh"
@@ -27,6 +28,35 @@
namespace nix::daemon {
+struct DaemonSettings : Config
+{
+ Setting<bool> unlistableStore{
+ this,
+ false,
+ "unlistable-store",
+ R"(
+ If set to `true`, users that are not in
+ [`trusted-users`](#conf-trusted-users) cannot enumerate store
+ contents. Operations that list or discover store paths the
+ caller does not already know — such as querying all valid
+ paths, querying referrers, or finding GC roots — will be
+ denied.
+
+ This is intended for multi-tenant systems where a shared Nix
+ store is used and tenants should not be able to discover each
+ other's build outputs. Combine with removing read permission
+ on the store directory (e.g. `chmod o-r /nix/store`) to also
+ prevent filesystem-level enumeration.
+
+ Note that "oracle" queries — checking whether a specific,
+ already-known store path is valid — are still permitted.
+ )"};
+};
+
+static DaemonSettings daemonSettings;
+
+static GlobalConfig::Register rDaemonSettings(&daemonSettings);
+
Sink & operator<<(Sink & sink, const Logger::Fields & fields)
{
sink << fields.size();
@@ -365,6 +395,9 @@ static void performOp(
case WorkerProto::Op::QueryDerivationOutputs: {
auto path = WorkerProto::Serialise<StorePath>::read(*store, rconn);
logger->startWork();
+ if (!trusted && daemonSettings.unlistableStore
+ && (op == WorkerProto::Op::QueryReferrers || op == WorkerProto::Op::QueryValidDerivers))
+ throw Error("you are not privileged to enumerate store paths");
StorePathSet paths;
if (op == WorkerProto::Op::QueryReferrers)
store->queryReferrers(path, paths);
@@ -711,6 +744,8 @@ static void performOp(
case WorkerProto::Op::FindRoots: {
logger->startWork();
+ if (!trusted && daemonSettings.unlistableStore)
+ throw Error("you are not privileged to enumerate store paths");
auto & gcStore = require<GcStore>(*store);
Roots roots = gcStore.findRoots(!trusted);
logger->stopWork();
@@ -833,6 +868,8 @@ static void performOp(
case WorkerProto::Op::QueryAllValidPaths: {
logger->startWork();
+ if (!trusted && daemonSettings.unlistableStore)
+ throw Error("you are not privileged to enumerate store paths");
auto paths = store->queryAllValidPaths();
logger->stopWork();
WorkerProto::write(*store, wconn, paths);
--
2.49.0
All previously discussed caveats apply.
It also makes garbage collection a trusted operation (because the logging leaks store paths, but also because you probably don’t want semi-trusted tenants from triggering GC of the entire store anyway).
The patch is machine-generated; one take-away from it is that the straight-forward implementation is small and unintrusive, so implementation complexity is a non-issue.