NixOS for all my system

Posted in linux on November 15, 2025 by Adrian Wyssmann ‐ 4 min read

I introduced you to Nixos in my previous post. This is an amazing way of managing your linux in a declarative way with easy rollback.

For my other nodes in my home lab I use(d) Ansible and Terraform which also allows me to create reproducible environments. However, I don’t want to use different tools, so why not use Nixos everywhere.

Overall approach

As a starting point on my journey, I will install Nixos on a spare notebook manually and then apply further changes from the config I have locally on my developer machine. So what I did:

  1. Download latest iso image
  2. Burn image to an usb stick
  3. Boot notebook from usb stick
  4. Install nixos manually on the notebook –> I allowed ssh-access with password, which is important as the trusted ssh from my developer machine is not on the target host yet.

So far still lot of manual steps - especially the manual installation. There are better ways, which I will show in the next post. My focus here is currently to start managing a remote system from my dev machine.

I extend flake.nix with a new hosts - see gitlab for details

      envy = nixpkgs-master.lib.nixosSystem {
        specialArgs = { inherit inputs; };
        inherit system;
        modules = [
          ./configuration.nix
          ./hosts/envy # Include the results of the hardware scan.
          inputs.sops-nix.nixosModules.sops
        ];
      };

In the subfolder hosts/envy I add all the host specific configuration for example hardware.nix, which I manually grabbed from the remote host and added it

# Do not modify this file!  It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations.  Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:

{
  imports = [
    (modulesPath + "/installer/scan/not-detected.nix")
  ];

  boot = {
    initrd = {
      availableKernelModules = [
        "xhci_pci"
        "thunderbolt"
        "nvme"
        "usb_storage"
        "usbhid"
      ];
      kernelModules = [ ];
    };
    kernelModules = [ "kvm-intel" ];
    kernelPackages = pkgs.linuxPackages_latest;
    extraModulePackages = [ ];
  };

  # Enables DHCP on each ethernet and wireless interface. In case of scripted networking
  # (the default) this is the recommended approach. When using systemd-networkd it's
  # still possible to use this option, but it's recommended to use it in conjunction
  # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
  networking.useDHCP = lib.mkDefault true;
  # networking.interfaces.enp0s20f0u1.useDHCP = lib.mkDefault true
  # networking.interfaces.wlo1.useDHCP = lib.mkDefault true;

  nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
  powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

The import of not-detected.nix comes from the manual installation, where the installer handles the hardware detection behinde the scenes and serves as a “catch-all” module for hardware configurations that nixos-generate-config couldn’t automatically map to a more specific hardware profile. After some iterations, this is my current hardware.nix

{ config, lib, pkgs, modulesPath, ... }:

{
  imports =
    [ (modulesPath + "/installer/scan/not-detected.nix")
    ];

  boot.loader = {
    systemd-boot.enable = true;
    efi.canTouchEfiVariables = true;
  };

  boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ];
  boot.initrd.kernelModules = [ ];
  boot.kernelModules = [ "kvm-intel" ];
  boot.extraModulePackages = [ ];

  fileSystems."/" =
    { device = "/dev/disk/by-uuid/9501a69b-01b2-4c99-9bfe-bea99a58942c";
      fsType = "ext4";
    };

  fileSystems."/boot" =
    { device = "/dev/disk/by-uuid/232F-3A24";
      fsType = "vfat";
      options = [ "fmask=0022" "dmask=0022" ];
    };

  swapDevices = [ ];

  # Enables DHCP on each ethernet and wireless interface. In case of scripted networking
  # (the default) this is the recommended approach. When using systemd-networkd it's
  # still possible to use this option, but it's recommended to use it in conjunction
  # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
  # networking.useDHCP = lib.mkDefault true;
  # networking.interfaces.wlp1s0.useDHCP = lib.mkDefault true;

  nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

Once I had a valid config, could start applying the changes. I ran the following command

sudo -E nixos-rebuild switch --flake '.#envy' \
  --upgrade --target-host \
  [email protected] --sudo

As I allowed ssh access via username/password that worked just fine. After that my config is applied on the target host. As part of this apply, I would also close the ssh config and only allow ssk keyy authentication. So my config configurations

  users.users.nixos = {
    ...
    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOrOn3Kj/+ztMtQAaq4pVvXgTsIs1ZOqQDbsA+nJMuRM nixos@envy from clawfinger"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFTCwPNpVjW6R9vqpKgNSWgGS5hZMZcHwexAMl7E/OI2 nixos@envy from clawfinger"
    ];
  };

and

  services.openssh.settings = {
    PasswordAuthentication = false;
    PermitRootLogin = "no";
  };

Secrets and SOPS

If you are using nix-sops, it’s also important to add the host key to your .sops.yaml - in my case it’s called server_envy

keys:
  - &admin_papanito age12q4dwh0zqgfxfswzydr3mq7ppm5htv73aqkrfpel9ppcmml3eqds5zzzhr
  - &admin_nixos age1pu3n34surq08wa0xa7xrhd4ukcah8au6pqw5mj8mgpvypw8e4d0swhf9v2
  - &server_clawfinger age155ygrv7uzel70wp7tde2fp3xg9kjsht3kcu49rt3l89qw5j0tgsqsvccye
  - &server_envy age1fyvzwcvfv2s3s9jr7hdpkkdc3fup65rksgeu9uahvntnrvg243fs4lm0qz
creation_rules:
  - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
    key_groups:
    - age:
      - *admin_papanito
      - *server_clawfinger
      - *server_envy
  - path_regex: secrets/clawfinger/[^/]+\.(yaml|json|env|ini)$
    key_groups:
    - age:
      - *admin_papanito
      - *server_clawfinger
  - path_regex: secrets/envy/[^/]+\.(yaml|json|env|ini)$
    key_groups:
    - age:
      - *admin_papanito
      - *admin_nixos
      - *server_envy

After that, where necessary - e.g. secrets/secrets.yaml and secrets/envy/secrets.yaml re-encrypt the secrets

  sops updatekeys secrets/envy/secrets.yaml
  sops updatekeys secrets/secrets.yaml
  ...

Next

As a next step, I want to install nixos on a remote system without actually sitting on the remote host and doing a manual installation. Let’s see how that works.