This is part 5 in my series of posts learning how to setup NixOS on my Framework 16" AMD laptop. It is not meant as a guide, necessarily, but more as a captain’s log of my thoughts and processes along the way.

Re-enabling Secure Boot

Picking up right where I left off yesterday, I remembered that I had to disable secure boot enforcement to boot from the live installation USB. So I rebooted back into the UEFI interface and re-enabled secure boot enforcement, hit f10 to save and exit, and the booted back into NixOS. I was very thankful that I had no problems here, and I’m pretty sure that’s because I remembered to save the contents of /etc/secureboot before I reformatted my drive, and then restore it before re-enforcing secure boot. Otherwise I would have had to go through the secure boot key installation process again.

Getting connected to WiFi

At the end of the previous post, I mentioned how my WiFi credentials weren’t declared as part of my Nix config – as a result of this, I can’t really do anything on my laptop until I get my network configured. In the live media, it was easy because the system bar had a button you could click to configure WiFi.

Hyprland doesn’t provide any such bar, and my Waybar configuration does tell me the status of my network, but I can’t click it to get into the configuration. So I will have to find out to do this on the terminal.

I did a little digging in /etc to see if I could find what controlled the network, and wouldn’t you know it, there’s a /etc/NetworkManager. Now I just have to learn how to use NetworkManager, or otherwise configure the network via the config file. I did a little searching and found out NetworkManager provides nmcli, which I can use to interact with my network devices. Looks like I can simply use nmcli device wifi connect – or more completely:

nmcli device wifi connect MY_WIFI_SSID password thepasswordisonthefridge

After a small amount of time, it returned and told me Device 'wlp1s0' successfully activated with '<uuid>'. Waybar automatically updated to show that I’m connected to my network.

It’s just about time to actually start working on adding safeguarded secrets to my Nix configurations.

Restoring my backup home directory

Before I get into the next steps, I need to restore the files I backed up from my home directory – specifically, I need to restore the .ssh/ folder and the nixcfg folder from my flashdrive into my new home directory. Once I did this, all my file permissions were messed up, all files were world readable by default, which I didn’t want (and in the case of ssh, it won’t allow it). It also captured the file mode change in the git repo, from 644 to 755, which I didn’t want. To fix this, I ran find . -type f -exec chmod 644 {} \; – ChatGPT told me to. Sure enough, that cleared the diff on all the files that I didn’t actually change the contents of.

Once I fixed all my file permissions, I wanted to make sure to capture the changes to my configuration that I made during the live USB – specifically, now I can remove the original filesystem declarations from my hardware config, and commit that change to add my disk config to my hosts/serenity/configuration.nix file.

I didn’t bother to restore all the other random stuff that was in my home directory, just the nixcfg and .ssh directories – the other stuff wasn’t really placed there by me, and I don’t care to keep it if I don’t need it. I’d like to be more particular about the way I manage files on here, which means not just letting random junk gunk up the output of my ls commands.

One Rebuild For Good Measure

Now that I’m roughly back to where I started before having to format my disk, I am going to run a switch command (which I have aliased to sudo nixos-rebuild switch --flake ~/nixcfg), just to make sure everything is in a consistent state. While doing this, I noticed that sudo is prompting me for my password again – this means that my fingerprint is no longer enrolled, which makes sense. So I’ll go ahead and do that again real quick with sudo fprintd-enroll dade. I wonder if we can manage the fingerprint enrollment with sops-nix as well… I suppose I’ll have to look into how fprintd works.

Installing Sops-nix

As I’ve previously mentioned, I am planning to use sops-nix to manage secrets in my nix configuration. I want to be able to capture secrets in a relatively safe way, since they are very often a necessary part of any machine. Whether it’s SSH keys, API keys, your WiFi password, or your user password, you probably want them present automatically on a new system – after all, that is kind of the whole appeal to NixOS, isn’t it?

Getting started, I need to add sops-nix to my flake.nix inputs, outputs, and system modules. I’ve cut out other existing config using ... where appropriate.

{
  inputs = {
    ...
    sops-nix = {
      url = "github:Mic92/sops-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, disko, lanzaboote, sops-nix, ...}@inputs: {
    nixosConfigurations.serenity = nixpkgs.lib.nixosSystem {
      ...
      modules = [
        ...
        sops-nix.nixosModules.sops
      ];
    };
  };

}

Just to make sure sops-nix gets picked up, I ran a switch here and confirmed that I saw the new inputs getting added. I also noticed that sops-nix brings a sops-nix/nixpkgs-stable input with it. My system input for nixpkgs is unstable, so I suppose this is probably for the best.

Generating encryption keys

I’m going to be using age for the encryption method, for two reasons: GPG sucks, and I use ed25519 ssh keys pretty much exclusively, which I can use conveniently with ssh-to-age. I anticipate I’m going to need age somewhat regularly, so I’m going to go ahead and add it to my home.nix and do another switch.

{
  ...
  home.packages = with pkgs; [
    ...
    age
  ];
  ...
}

Next, according to the sops-nix README, I can use nix-shell -p ssh-to-age --run "ssh-to-age -private-key -i ~/.ssh/id_ed25519 > ~/.config/sops/age/keys.txt" to convert my ssh key to an age key. Unfortunately, my only ssh key for this host is ~/.ssh/id_ed25519_sk – a security key backed ed25519 key. ssh-to-age doesn’t support this ssh key format, so I guess I’ll be using an individually generated age key using age-keygen -o ~/.config/sops/age/keys.txt.

Next, I need to get a public key for my laptop, since it’ll be the target machine of these initial secrets I setup. But currently I don’t have ssh host keys on my laptop. To do this, I’m going to use ssh-keygen to generate an ed25519 host keypair. I don’t have sshd running, because this is my laptop and I don’t need an ssh server on my laptop (yet, anyways), so I’m just going to generate the key in /etc/ssh/ and hope it doesn’t get overwritten in some mysterious future accident. sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key and away we go. This should have created /etc/ssh/ssh_host_ed25519_key and /etc/ssh/ssh_host_ed25519_key.pub, which matches the naming scheme used by sshd on some of my ubuntu servers.

Creating our sops configuration

Now I can create a ~/nixcfg/.sops.yaml file and add my public key to it. This follows pretty much exactly what the readme suggests, but I’ve pruned out the pgp related things and named my key appropriately.

keys:
  - &serenity_dade <my user age public key here>
creation_rules:
  - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
    key_groups:
    - age:
      - *serenity_dade

Now I need to update my .sops.yaml with an age key based on the new host key. nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' will produce the age public key I need to copy into .sops.yaml, and now it will look like this:

keys:
  - &serenity_dade <my user age public key here>
  - &host_serenity <my ssh-to-age host public key here>
creation_rules:
  - path_regex: secrets.yaml$
    key_groups:
    - age:
      - *serenity_dade
      - *host_serenity

Note: I understand that my public keys are just that, public. The only reason I’m using placeholders is because I’m writing my notes on a separate machine from my Nix laptop (for now) and didn’t feel like transcribing the public keys manually.

Separating our secrets from our config

Before I get too far into this setup, I realized that I don’t really want to publish my secrets, even in their encrypted form, to my public github repo. I’d prefer if the secrets are in a separate repository that isn’t public. We can argue about how encrypted secrets, if properly encrypted, are entirely useless in the hands of an attacker. But it just makes me feel better.

Thankfully, Emergent Mind has a guide on how to separate your nix-secrets from your config, which should help me get moving on this quickly.

So I’m going to make a new directory in my home, mkdir ~/nix-secrets && cd ~/nix-secrets && git init and then move my .sops.yaml file into the nix-secrets directory instead of the nixcfg directory. mv ~/nixcfg/.sops.yaml ~/nix-secrets/.sops.yaml.

Now, from within my nix-secrets repository, I’m going to make a new secrets.yaml file with nix-shell -p sops --run "sops secrets.yaml". This will automatically open my editor with an example sops yaml file, showing how to add secrets. I deleted all of these lines to start with a blank file.

Next, in a second terminal, I ran mkpasswd and input my preferred user password when prompted. It gave me a hash beginning with $y$, which I believe is the hash identifier for yescrypt. I then copied this hash into my sops terminal, using a key of dade_passwd. My file looks like this:

dade_passwd: $y$<param>$<salt>$<hash>

For the sake of keeping it simple, I’m going to consider that the end of my secrets.yaml file for now, so I save and exit the editor. Just to confirm the encryption worked correctly, I can cat secrets.yaml and see that the secrets are encrypted, along with some metadata. I can also make sure that I’m able to decrypt the encrypted values by re-running nix-shell -p sops --run "sops secrets.yaml, and I can see my normal hash and none of the metadata.

Now I need to create the corresponding repository on Github and then push my changes so that I can reference them in my nixcfg. After creating the repository in Github, I followed the instructions to push my local repository to it:

git add .
git commit -m "Initial config and secret"
git remote add origin git@github.com:0xdade/nix-secrets.git
git push -u origin main

This pushed my changes up to Github, I refreshed, and I can see the encrypted file contents in Github.

Adding our secrets to nixcfg

Now that we have a remote github repository with our secrets in it, we can add it to our nixcfg flake as an input.

{
  description = "nixos config flake"
  inputs = {
    ...
    nix-secrets = {
      url = "git+ssh://git@github.com/0xdade/nix-secrets?shallow=1&ref=main"
      flake = false;
    };
  };
}

Unfortunately, Emergent Mind’s guide for this doesn’t seem to work well for me for two reasons:

  1. My github repository’s default branch is main, not master, and I was getting an error about the master ref. In hindsight this might have also been a secondary error due to the next one.
  2. git@github.com: Permission denied (publickey) - Because I sudo when doing a nixos-rebuild, my ssh private keys aren’t being used by default.

We can solve this first bit by explicitly passing ref=main in the url, e.g. git+ssh://git@github.com/0xdade/nix-secrets?ref=main&shallow=1.

Solving the second part, however, is proving to be somewhat difficult. Now, one easy way to do this would be to copy my user’s private ssh key to /root/.ssh/id_ed25519_sk. This would probably work, but it feels less than ideal to have to duplicate the private key. After struggling with it for a while, this is ultimately what I ended up doing, though. I’d like to figure out a way to not need to do this, but I’m not sure that’s viable right now.

Now that I have the nix-secrets repository setup as an input to my nix flake, I can update ~/nixcfg/hosts/serenity/configuration.nix to get the path to the secrets module and hook it up to automatically set my password.

We’re going to add a let/in block to the top of our file, such that it looks like this:

{ config, pkgs, lib, inputs, ...};
let
  secretspath = builtins.toString inputs.nix-secrets;
in
{
  imports = [
  ...
  ];
  ...
}

Now we can reference secretspath later in our configuration. First, I’m going to add users.mutableUsers = false; to my config, which will force my password to always be what it is set to in my secrets file. This helps to ensure the deterministic behavior that we want.

Next, I’m going to modify my user attribute set such that it looks like this:

  users.users.dade = {
    isNormalUser = true;
    description = "dade";
    hashedPasswordFile = config.sops.secrets.dade_passwd.path;
    extraGroups = [ "networkmanager" "wheel" "video" ];
    shell = pkgs.zsh;
  };

Finally, I need to add a new attribute set called sops to my configuration file. This should be at the same indentation level as home-manager or users.users.dade, which is to say, at the top level of the config. This is where we’re going to use the secretspath variable we defined, as well as set the dade_passwd secret from our user block.

  sops = {
    defaultSopsFile = "${secretspath}/secrets.yaml"
    age = {
      sshKeyPaths = ["/etc/ssh/ssh_host_ed25519_key"];
      keyFile = "/var/lib/sops-nix/key.txt";
      generateKey = true;
    };
    secrets = {
      dade_passwd = {
        neededForUsers = true;
      };
    };
  };

The neededForUsers block is important, as it tells sops-nix to make this secret available before users are created, so that the hashed password can be retrieved from config.sops.secrets.dade_passwd.path. By default, secrets go into /run/secrets, but when neededForUsers is true, those secrets will go into /run/secrets-for-users.

The moment of truth

After all of this, we should be able to simply switch and our password will now be deterministically set by our nix configuration. Just for the sake of safety, I’d recommend opening a second terminal and going into root access. This should just help to prevent a reboot if you need to go back a generation to fix something.

If we run into any issues, here are some culprits I might look for:

  1. Does the root user have access to the ssh key necessary to pull the private repository?
  2. Did you push your secrets file to the remote repository?
  3. Did you update your flake.nix to make sure it is referencing the latest version of your secrets repository?

Testing our changes

Since I have my fingerprint enrolled in fprintd, sudo defaults to asking for my fingerprint instead of my password, which means checking that my password is what I expect is kind of annoying. What I ultimately ended up doing to test this out is using the passwd command to try to change my password.

  1. passwd and input a password you know is wrong, you’ll immediately get a permission denied error, so you know your password is not that.
  2. passwd and input your correct password, you’ll then get prompted for a new password – this means your sops-nix user password is correctly set!
  3. Following on from the previous step, you can input a new password and see that you’re still met with a permission denied error, which means that the user’s password is not mutable. Perfect!

Next Time on Dragon Ball Z

Now that I have the basics of my secrets management in place, I’m going to try to add my WiFi credentials to my secrets, as well as look into adding my fprintd fingerprint, if possible.

Additionally, I’d like to make a backup age key to store in my password manager and include as a target in my key groups in sops. While I basically know the values of all of these secrets anyways, I want to make sure I can recover them in case of hardware failure where I might lose the host ssh key and my user’s ssh key at the same time. It would also be cool to figure out if I can use an age key that is backed by my yubikey instead of just one sitting in my home directory.

I also want to look into updating my flake inputs, since that’s how I will update packages on my machine, and it’s important to know how to do that. I’ll probably create a couple useful shell aliases for this, as well.

Finally, I’ll probably spend a bit of time setting up some pre-commit hooks for my nixcfg repo, that way I can make sure everything is consistently formatted. Even though I’m the only one who will ever really use this repo, I want it to be consistent for future me.