Skip to main content

lsupg Nix Support

I have been reading Ian Henry’s How to Learn Nix series of blog posts and am just about caught up. I have learned a number of things through the series and have really enjoyed it. One of the things that I have learned is directly related to my lsupg software, which lists items in a container that can be upgraded.

One of the challenges of using containers in production is managing software upgrades. When an upgrade is available, a new image needs to be built, and then containers need to be re-run using the updated image. This is particularly important for security upgrades. Note that updating official images is not sufficient, because the official images tend to lag behind OS upgrades. In practice, one should apply upgrades on top of official images or simply avoid third-party images altogether.

lsupg is a utility that helps determine what needs to be updated. Some companies simply upgrade everything on a fixed schedule (every night), and lsupg can help prune the image hierarchy so that only images that have upgrades (or depend on images that have upgrades) are rebuilt. lsupg provides an abstraction layer over different operating systems, and my initial release includes support for Nix. When using Docker, it can be used with the nixos/nix image.

Support for Nix is implemented using the following two commands:

$ nix-channel --update
$ nix-env --upgrade --dry-run

The first command syncs the package channel, and the second command lists available upgrades of installed packages. It seems straightforward, but, thanks to Ian’s blog series, I learned that nix-env --upgrade is fundamentally broken and should not be used!

Docker Demonstration

I will demonstrate the lsupg issue using the same package that Ian used in his How to install Python blog post: python3.

First, run an ephemeral container for the demonstration:

$ docker run --rm -it --hostname nix nixos/nix

The container has the nixpkgs-unstable channel configured by default, so we update it to synchronize the package channel:

nix:/# nix-channel --list
nixpkgs https://nixos.org/channels/nixpkgs-unstable
nix:/# nix-channel --update
unpacking channels...
created 1 symlinks in user environment

The following command installs one of the many Python 3 packages, making it available in the current environment:

nix:/# nix-env -iA nixpkgs.python3
installing 'python3-3.8.9'
...
building '/nix/store/pa4gyj30a360yw845pwh810s9pvs4zp1-user-environment.drv'...
created 76 symlinks in user environment

This command installs the package via attribute path. This is critically important because it installs the current “stable” version of Python 3. If one were to install by name instead (nix-env -i python3), the version with the highest version number (an alpha release) is installed instead. The consensus is that the nix-env -i command is fundamentally broken and should never be used.

When lsupg lists available upgrades, it runs the command without specifying any names or attributes in order to determine all available upgrades. Unfortunately, upgrades are determined by name, even when a package was installed by attribute path. In this case, the command checks for upgrades to the python3 name and registers the upgrade to the alpha release as available.

nix:/# nix-env --upgrade --dry-run
(dry run; not doing anything)
upgrading 'python3-3.8.9' to 'python3-3.10.0a5'
...

This is not desired. The consensus is that the nix-env -u command is fundamentally broken and should never be used. (Unfortunately both broken commands are demonstrated in the Quick Start of the Nix manual!)

Aside: Python Versions

The current “stable” version of Python 3 in Nixpkgs (3.8.9) is actually old… The latest Python 3.8 release is 3.8.11, and the more recent releases include security fixes! Also, Python 3.9 has been out for more than half a year. Conventional operating systems like Debian tend to use old versions of such packages because they have to work with many other packages that might be installed on the system. I do not know why Nixpkgs does not keep up to date even though it does not have such constraints, but I suspect that it has to do with compatibility with Python modules defined in the monolithic repository.

Nix security is a topic that is too large to include in this blog entry…

Declarative Package Management

The “correct” way to manage “installed” Nix packages is to do so declaratively using attribute paths. When using NixOS, packages are listed by attribute path in the /etc/nix/configuration.nix configuration file. Unfortunately, there is no standard for declarative package management outside of NixOS. If there were a standard configuration file, then a general upgrade command would be possible. Without a standard configuration file, the “correct” way to upgrade “installed” Nix packages depends on how the system is configured.

Note that even the NixOS manual includes a broken upgrade command in the Ad-Hoc Package Management section!

lsupg Plans

Nix container images tend to not use NixOS. The nixos/nix image installs Nix on top of Alpine Linux. Due to the issue described in this blog entry, Nix support in lsupg is currently broken.

I am currently considering two options:

  • Remove support for Nix. Frankly, people who use Nix are probably not very security-conscious or use it in environments where security is not a significant concern. If there is not a good way to monitor upgrade availability in Nix, one can always just rebuild everything periodically (daily).

  • Only support environments that install packages declaratively using a fixed configuration path. For example, /etc/nix/packages.nix might be a good name for such a file.

I plan on giving it some more thought and consulting friends before making a decision and moving forward.