# Flakes as package filters: Home-lab part 1
Table of Contents
This post is the first in a series that catalogs the development of my home lab.
As I mentioned in my last post What is Nix?, I’d been using Nix for about 18 months / 2 years when I started building the home lab. The lab was an accident, and kind of grew (and is still growing really) from the options that opened up as each new feature was added.
The broad question I set out to answer with the home lab was ‘is Nix ready for an enterprise level organisation?’. This is something that will be answered over several posts, as the details matter.
TL;DR: Nix’ strengths are reproducability, robust systems, and audit trails. It offers features that can help with security, but that isn’t its goal. You can’t use it with ignorance either. You have to understand what it is doing if you want to understand how it is helping you.
There’s the summary, vague as it is. To understand it, we need to get into the details. This post is focused on just one aspect, and that’s the nixpkgs repository. Specifically, can we “protect” the company from the network users having access to so many packages? packages
Why would I want to restrict the Nix packages to the network users though?
Yep good point. The answer is simple though. A lot of companies don’t want to simply allow folks (not even developers) to install just about anything they like. There are often commercial reasons for this, such as licensing, but obviously there are security reasons too. It’s not a difficult position to understand.
With the nixpkgs, you are effectively able to install almost anything you could want. Kubernetes, Docker, Blender, GIMP, IntelliJ, VPN software. You name it really.
Commercial packages
For the commercial packages angle, Nix has that built in. By default, you cannot install a Nix package that has a commercial license without explicitly setting a config or environment variable first. You have to opt into that to pull a commercial down at all.
Broken packages
Nix also has built in protection for “broken packages”. Sometimes a developer will create a release of their product that is actually broken for some platforms. Nix only builds the packages, it doesn’t intervene in them. So if a Nix package is available that has been identified as broken on one or more platforms, it gets marked in the repository. When you try to install the broken package, Nix will error and tell you it is broken. It will also tell you that you can install it anyway, but you have to pass in an environment variable to state you are accepting the broken package.
That’s all well and good, but what are Nix packages?
Yep, we need to go into a bit more detail on what the packages are. I touched on this in my What is Nix? post, but we need to go a lot deeper now, as this is fundamental.
A Nix package is basically a recipe. The package contains a nix file that describes how to build the package from source. You read that correctly. Nix builds the package from source. It doesn’t have to, and again we’ll go into this more later on (probably a separate blog), but sometimes you may want it to always do this. And sometimes, it is very convenient if it doesn’t do this.
So does it build the package every time I want to use it?
No. If your system has just built the package, the compiled version will still be in your /nix/store, and so Nix will not rebuild it, and instead just create a new ephemeral shell for the package. This needs a little picking apart though, as this is the foundation of the reproducability.
When Nix pulls down the package files, it sets the creation date of all files to 1980-01-01 12:00:00 . This is to ensure all comparisons are based solely on content. Naturally, this works for directories as well as files. Nix generates a hash of a package, which is generated purely from the contents.
This is actually much more powerful than it first appears. The freshly built package isn’t just good enough for my machine. It’s actually good enough for anyone else’s machine that are also running NixOS and have the same platform, such as x86_64. This means someone else doesn’t have to build the package, they could just use my /nix/store. Nix itself takes advantage of this mechanism by caching larger packages in its publicly accessible cache https://cache.nixos.org.
What if I want a package installed more permanently?
If you have access to pixpkgs because you installed the manager in your non-Nix Linux distro, then your only Nix-based option is to define your development environment in a shell.nix or flake.nix. This will only make the package available to shells created via those files.
If you have NixOS, your system is declaratively configured. You will have one or more files defining your Nix-based configuration. In this example, you can add curl “system-wide” by adding it to your environment.systemPackages collection like so:
environment.systemPackages = [ curl];You’ll then perform an action called a ‘rebuild’, and that will add curl system wide to your machine. I’ll cover NixOS in a separate post later on, so don’t worry too much about this right now.
Ok I think I understand what a Nix package is now. How do I restrict the available packages then?
This is actually very simple, and yet also a little complex. I touched on flakes in the What is Nix? post, but this is where they get interesting.
Flakes in Nix are a very powerful concept, but they’re kind of many things.
They are a form of module, where you can configure very complex components, applications, systems etc.
They are build systems too, in many ways. They can have install phases, checks that can test the flake, and they can package up an application or component ready for use.
They are development environments. Flakes are an advanced form of the initial shell.nix that you will first come across with Nix. Flakes will typically have the development environment declared that supports the development of the application or component(s) that they produce. When flakes produce something, that is called an output.
What makes flakes powerful is they have a lock file, that pins all packages (and potentially other flakes) to a commit hash to ensure the build is reproducable on any other Nix based system. As you can guess, the commit hash is used as tags are mutable and therefore cannot be relied upon for reproducability.
Let’s take a look at a very basic flake:
{ description = "A very basic flake";
inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; };
outputs = { self, nixpkgs }: {
packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
packages.x86_64-linux.default = self.packages.x86_64-linux.hello;
};}This one takes the nixos/nixpkgs repo as an input, pointing to the nixos-unstable branch. Its outputs are self and the nixpkgs. The ‘self’ here actually means the two packages that we see on the following lines: ‘hello’ and ‘default’.
As the name suggests, ‘default’ is what you get if you ran the flake without naming any output. To run this flake, you would simply type:
nix run .
And to run the ‘hello’ package you would type:
nix run .#hello
A quick note on stable/unstable packages
We’ve stumbled across the stable / unstable packages, and so it’s worth just saying something here. ‘Unstable’ does NOT mean broken, or beta, or pre-release. If anything it means ‘latest’.
NixOS has 2 ‘stable’ releases a year. One at the end of May, and the other at the end of November. Those releases are ‘fixed’ versions of every package. Those packages will get maintenance and security patches over time, but they will not get feature updates. To get those versions, you need to pull from the ‘unstable’ channel.
Some Nix users will go full ‘unstable’, some stick to ‘stable’. I think most probably do a bit of both, like I do.
Great. I think I understand flakes at a high level at least. Why did you tell me about this?
So I mentioned flakes are like modules. They can be pulled into other flakes as inputs, and they can therefore pull in other flakes as their own inputs. The nixpkgs repository itself is actually a flake. The outputs it produces are the available packages. And so to constrain the packages you want to make available, all you need to do is create a flake of your own, import the nixpkgs as an input, then expose as outputs only the packages that you want to allow.
Here is the flake.nix for my approved-packages repository:
{ description = "Approved Nix packages (isolated)";
inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; nixpkgs-latest.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; };
outputs = { self, nixpkgs, nixpkgs-latest, flake-utils, }: flake-utils.lib.eachDefaultSystem ( system: let # Configure nixpkgs to allow unfree packages pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; latestPkgs = import nixpkgs-latest { inherit system; config.allowUnfree = true; };
coreTools = import ./modules/core-tools.nix { inherit pkgs latestPkgs; }; developer = import ./modules/developer.nix { inherit pkgs latestPkgs; }; applications = import ./modules/applications.nix { inherit pkgs latestPkgs; }; llmTools = import ./modules/llm-tools.nix { inherit pkgs latestPkgs; }; monitoring = import ./modules/monitoring.nix { inherit pkgs latestPkgs; }; k8s = import ./modules/k8s.nix { inherit pkgs latestPkgs; }; security = import ./modules/security.nix { inherit pkgs latestPkgs; }; cicd = import ./modules/cicd.nix { inherit pkgs latestPkgs; };
allPackages = coreTools // developer // applications // llmTools // monitoring // k8s // security // cicd; in { packages = allPackages // { default = pkgs.symlinkJoin { name = "mach-approved-packages-all"; paths = builtins.attrValues allPackages; }; };
# Development shell with formatting tools devShells.default = pkgs.mkShell { buildInputs = [ pkgs.nixfmt-rfc-style pkgs.just latestPkgs.vulnix ];
shellHook = '' just --list ''; }; } );}If you compare this to the basic flake, you’ll see some similarities, and you may well be able to guess what the new parts do.
I’m pulling in both the stable nixpkgs channel, as well as the unstable channel, and a flake-utils. The flake-utils contains some helper expressions, specificallly one that exposes all platforms supported by Nix. This is useful in my case for supporting Nix on both Linux and Mac systems. I’ll go into Macs in a separate post at some point, but for now it’s worth noting that it needs its own packages and is not necessarily compatible with every package and option in Nix / NixOS.
Notice at the bottom of my flake, where I declare a devShell. This is the development environment that I expose for supporting the maintenance tasks for my approved packages repo. I use just a lot in my code bases, and so the shellHook you see there will execute just —list when the development shell is instantiated. To do that, you would type:
nix develop
Superb. So this completely locks down the available packages and makes everything totally safe, right?
No. I mentioned in the TL;DR that Nix is all about reproducability, robustness, and auditing, and not directly security. While your approved-packages flake is exposing the packages, any system using your flake will still have to pull the allowed flakes down from nixpkgs itself.
You could get around this by forking the nixpkgs internally, but that would be a significant amount of data, and not likely worthwhile. The nixpkgs repository is one of the largest in GitHub, and so you would need a very good reason to want to fork it.
So what’s the point then?
Well from a build runner perspective, this is completely fine. We can create a build step that verifies the flake(s) are using the approved-packages flake as the input and can fail the build if they’re not using it.
I’d describe this as good enough, but not great. It’s good enough for parts of an organisation, and for development teams. Those teams will have to accept responsibility for how they consume the nixpkgs though. Ideally they would opt for using the approved-packages repository at all times.
What next?
I said in the intro that the lab grew from the options that opened up as each new feature was added. Well a side-story to all this is that I had been using Opencode for quite some time, as I much prefer its “can-do” attitude over some of the other LLM tools. However, I also have some old machines, one of which is beautiful 2008 Mac Pro tower. It’s a great machine that I’ve kept going with updates ever since I got it. It’s not my main machine, but it is a great Linux test bed, and it was the first machine I put NixOS on.
Unfortunately, Opencode couldn’t run on it. I’d try and pull it down, but when the machine tried to build the package it failed because bun uses CPU instruction sets that my aging Mac doesn’t have. As I mentioned earlier in the post though, if another machine on my network were to build it, I could take the built package from that machine’s /nix/store and just run it.
Well I could do that, but it felt a bit crude. I wanted to explore something more robust and long term. That’s what’s coming next. My home lab needed a nix cache on the network.