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.
- Framework and NixOS - Day One
- Framework and NixOS - Day Two
- Framework and NixOS - Secure Boot
- Framework and NixOS - Declarative Encrypted Disk Partitions
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:
- My github repository’s default branch is
main
, notmaster
, 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. git@github.com: Permission denied (publickey)
- Because Isudo
when doing anixos-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:
- Does the root user have access to the ssh key necessary to pull the private repository?
- Did you push your secrets file to the remote repository?
- 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.
passwd
and input a password you know is wrong, you’ll immediately get apermission denied
error, so you know your password is not that.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!- 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.