Currently the entire Nix store is world-readable. This precludes some use cases:
- Storing configuration files with secrets in the Nix store.
- Users building private stuff that shouldn't be visible to other users.
Therefore we want the ability for users to add/substitute paths and build derivations such that they're only readable by them.
- A store path is either public (readable to everybody) or only readable by some users/groups.
- Access is transitive: if a user can access store path P, they can access every path in P's closure. So if a path is public, its entire closure is public. But a private path can have public paths in its closure.
- Access can be widened but not contracted: additional users can be granted access to a private path, and private paths can be made public. But public paths cannot be made private, and access to private paths cannot be revoked. This restriction is primarily to avoid some potential race conditions and could be lifted.
- When a user adds a private path via
addToStore
, the path is created readable only by that user (via an ACL). If the path already exists as a private path, the user is added to the path's ACL. If the path already exists as a public path, nothing changes. A precondition ofaddToStore
is that all references must be readable by the user. - When a user builds a derivation path drv, the user must have access to drv. If drv is private, then the resulting outputs will be private. If the outputs already exist as private paths, then the user will be added to the paths' ACLs.
- If a path is substituted while building a private derivation, the substituted path will be private. But as an optimisation, if a substituter is marked as "public" in some way (e.g. cache.nixos.org), then the substituted path will be public.
-
Build something such that the resulting path is only readable by the calling user:
alice# nix build --private
So the store path in
./result
will have an ACL that includesalice
(and other users that have built the same derivation). -
Copy a store path from a remote machine, making the result readable by the caller only:
alice# nix copy --from ssh://foo --private /nix/store/abcd...
Note that the ACLs of the path on the remote machine don't matter (except that the remote SSH user must have access) and are not propagated.
-
Copy a store path to a remote machine, making the result readable to the remote SSH user only:
alice# nix copy --to ssh://foo --private /nix/store/abcd...
-
Make a closure public:
alice# nix store make-public /nix/store/abcd...
This only works if the caller has access to the store path.
-
Grant access to another user:
alice# nix store grant-access --to bob /nix/store/abcd...
struct Store {
// Build some drvs. If a drv is not public, then `user` must be non-empty and the drv must be accessible to `*user`; the resulting paths will be private and accessible to `*user`.
void buildPaths(const std::vector<DerivedPath> & paths, ..., std::optional<User> user);
// Add a path to the Nix store. If `user` is non-empty, the path will be created as private if it doesn't exist, and `*user* will be added to its ACL.
void addToStore(const ValidPathInfo & info, Source & narSource, ..., std::optional<User> user);
void addTextToStore(..., std::optional<User> user);
...
void makePublic(const std::vector<StorePath> & paths);
void grantAccess(const std::vector<StorePath> & paths, const User & user);
};
typedef std::variant<UserName, GroupName> User;
The Nix daemon will check that any user
passed by the client corresponds to the calling user (or that the calling user is sufficiently privileged, e.g. root or wheel).
There are no changes to the Nix database schema. Who has access to store paths is recorded in ACLs.
- To build a derivation, the
nixbld<N>
user must have access to all paths in the input closures. So we have to temporarily addnixbld<N>
user to the ACLs of the input paths, and remove it afterwards. This cleanup is a problem, e.g. if the Nix daemon or the system crashes, some paths may remain readable tonixbld<N>
. (This is the issue with contracting access rights mentioned above.) So we have durably log when we add anixbld
user to the ACLs of some paths, and use this log to clean up the ACLs during recovery (when the Nix daemon starts).
A private path can refer to a public path, but not the other way around. This means that if we want to build NixOS system closures containing secrets (such as configuration files containing passwords), the referrers-closure of the secret paths must be private. So if we have a foo.conf
containing a password, then config.system.build.toplevel
and all other paths leading to foo.conf
(like systemd units) must be private.
As root:
root# nix shell github:edolstra/nix/acls
root# nix daemon
As user alice
, let's build/substitute a private closure:
alice# nix build nixpkgs#hello --private
Let's check that it's private:
alice# ls -l ./result
lrwxrwxrwx 1 alice users 54 Feb 7 18:05 ./result -> /nix/store/xzhd565l8j29k4mcyjy9f34v1vyflwib-hello-2.10
alice# ls -ldH ./result
dr-xr-x---+ 4 root root 4 Jan 1 1970 ./result
alice# getfacl ./result
user::r-x
user:alice:r-x
group::---
mask::r-x
other::---
alice# nix path-info --json ./result | jq .
[
{
"path": "/nix/store/xzhd565l8j29k4mcyjy9f34v1vyflwib-hello-2.10",
...
"owners": [
"alice"
]
}
]
alice# /nix/store/xzhd565l8j29k4mcyjy9f34v1vyflwib-hello-2.10/bin/hello
Hello, world!
bob# /nix/store/xzhd565l8j29k4mcyjy9f34v1vyflwib-hello-2.10/bin/hello
-bash: /nix/store/xzhd565l8j29k4mcyjy9f34v1vyflwib-hello-2.10/bin/hello: Permission denied
Note that the resulting path will only be non-world-readable if another user hasn't previously built/substituted it as "public".
If another user instantiates the same derivation as "private", the ACL on the .drv will be extended:
bob# nix path-info nixpkgs#hello --private --derivation --json | jq .
[
{
"path": "/nix/store/56gdk1q87ypd0vf8yh1h4f4x2cr6ffdf-hello-2.10.drv",
...
"owners": [
"alice",
"bob"
]
}
]
Design discussion
__private= true;
in /etc/nix/nix.conf, ~/.config/nix.conf, flake.nix, derivation