Private Nix Flake with Public Git Subtree
· 6 min read
Recently I moved my personal NixOS configuration to a private repo, and split out part of the config into a public repo. It’s not rare to see private NixOS config with a public counterpart. Such setup often results in extra friction and requires additional efforts when maintaining. There are some tricks to make it easier to work with.
TL;DR
- What we want:
- A way to create private Nix flake, while exposing general and reusable stuff to the public.
- Good developer experience. We want to work on the flake without extra steps compared to a full private flake. We don’t want to update the lock file whenever the public part is updated.
- The public part can be used on its own, without having access to the private one.
- How to do it:
- Create a public flake for general and reusable config, and a private flake for sensitive config. The public flake exports modules and packages, consumed by the private flake.
- Put both private flake and public flake in a private repo. Let the private flake import and evaluate the public flake manually, without making the public flake as flake input.
- Make public and private flakes use the exact same flake inputs. Symlink the public
flake.lock
to the private flake, leaving only one lock file to maintain. The caveat is that the private flake cannot have extra flake inputs that the public flake doesn’t have. - Use git subtree to sync the public flake with a separate public repo.
- Example https://github.com/yelite/private-flake-example.
Background
I used to put my config in a public repo. When I just started to use NixOS, public NixOS config repos from others are my favorite learning resource. They contain many tricks that cannot be easily found in the official doc. It’s so cool to see my config being available to help others get into the world of Nix. Besides the educational purpose, it’s convenient to have the config repo public so that it’s easy to bring my dev environment to some random ephemeral VMs, where I don’t want to install my ssh keys.
I made my config private when I was setting up my home lab server. I found that the NixOS config of my home lab server contains lots of sensitive information, which would reveal my home network topology, exposed ports and even public IP address. I certainly don’t want to make these sensitive bits accessible to everyone on the Internet.
Noted that sensitive information is different than secrets. I am okay to have sensitive information,
like IP addresses and exposed network ports, in my Nix store, because if the attacker has access to
my Nix store, it’s likely they also have access to these information through other channels
(netstat
, etc..). I don’t want to introduce things like nix-age
or sops-nix
to my config,
which are designed for safe transportation of secrets in nix repo and they add unnecessary
complexity for my use case.
It’s clear that I need a setup that puts all general config in a public flake and all sensitive
config in a private flake, then somehow connect two flakes together. Typically this means the public
flake needs to export packages and NixOS modules, and the private flake needs to add the public
flake as its input. The drawback is, for most changes, we need to first create commit in the public
flake, then run nix flake update public_flake_name
in the private flake to update the lock file,
before the private flake can see the latest change. This results more friction in the dev flow,
slowing down the iteration speed.
Import and evaluate the public flake directly
The slowness of the setup described above is caused by the public flake being pinned by the lock file of private flake. We can circumvent this by manually importing and evaluating the private flake, without adding it as a flake input.
First we need to put both flakes in the same repo, the folder structure should be like this
The public flake.lock
is symlinked to the private flake. This ensures both flakes are based on the
same flake inputs, and things will work identically in both flakes.
The public/flake.nix
should contain all the reusable code that can be made public. It should
export, for example, nixosModules
, homeModules
or packages
, for the private flake to consume.
The private flake imports the public flake, and evaluates it manually, as shown in the highlighted area below
Publish the public repo through git subtree
Now that we have a private repo containing both private flake and public flake, we need a way to make the public flake actually public. Git subtree can be used to map a subdirectory in the working tree to a different git repo. We can use subtree to push the public flake to a public repo.
The public repo should contain files from the public/
directory, and will have complete history
with commits that touch files under public/
.
You can create commit in the public repo directly. New changes in the public repo can be pulled into the private repo by
Compared to git submodule, git subtree makes the dev workflow closer to a single-repo setup. If you want to make change involving both private and public flakes, you need to create two commits if using submodule, but only one if using subtree. Although pushing the public subtree requires manual work, it’s easy to automate this in CI if an up-to-date public repo is needed.
Related links
- https://github.com/yelite/private-flake-example
- An example to demonstrate tricks covered in this post.
- https://gvolpe.com/blog/private-flake/
- A blog post on the same topic, which inspired me to write this post.
- https://www.atlassian.com/git/tutorials/git-subtree
- Introduction to git subtree
- https://github.com/yelite/lite-config
- A flake created by me to make it easier to create NixOS, nix-darwin and Home Manager configurations. It’s used by my own system configurations and works well with the private flake approach covered in this post.