Skip to content

Instantly share code, notes, and snippets.

@edolstra
Last active April 19, 2024 04:47
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save edolstra/afa5a41d4acbc0d6c8cccfede7fd4792 to your computer and use it in GitHub Desktop.
Save edolstra/afa5a41d4acbc0d6c8cccfede7fd4792 to your computer and use it in GitHub Desktop.
Nix store ACLs

Goal

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.

Semantics

  • 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 of addToStore 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.

User interface

  • 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 includes alice (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...

Store API

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).

Database schema

There are no changes to the Nix database schema. Who has access to store paths is recorded in ACLs.

Implementation notes

  • To build a derivation, the nixbld<N> user must have access to all paths in the input closures. So we have to temporarily add nixbld<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 to nixbld<N>. (This is the issue with contracting access rights mentioned above.) So we have durably log when we add a nixbld user to the ACLs of some paths, and use this log to clean up the ACLs during recovery (when the Nix daemon starts).

NixOS

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.

Quick start

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"
    ]
  }
]
@tomberek
Copy link

Design discussion

  • groups
  • check if addToStore contents are identical (consider non-deterministic derivations and/or impure derivations) to existing paths
  • disclose known information leakage (existence detection, collision attack, timing attacks) in documentation
  • default __private= true; in /etc/nix/nix.conf, ~/.config/nix.conf, flake.nix, derivation

@Ericson2314
Copy link

Curious to help plan this! Not changing the DB will be risky for things like CA trust maps: we don't want trust maps about private paths to leak out. Dovetails with per-user trust maps.

@jkarni
Copy link

jkarni commented May 20, 2022

I'm also very interested in this, and could probably offer a small bounty

@zimbatm
Copy link

zimbatm commented May 20, 2022

@infinisil
Copy link

Related new RFC: NixOS/rfcs#143

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment