diff --git a/README.org b/README.org index b835a82..f4e3c6b 100644 --- a/README.org +++ b/README.org @@ -1,10 +1,10 @@ #+TITLE: Installing NixOS on a Proxmox VM using nixos-anywhere -#+AUTHOR: -#+DATE: +#+AUTHOR: Alexander Derevianko +#+DATE: <2025-07-26 Sat> #+OPTIONS: toc:t num:nil *Abstract* -This guide documents the process for a minimal installation of NixOS on a Proxmox virtual machine. It leverages the =nixos-anywhere= tool for remote deployment and =disko= for declarative disk partitioning. +This guide documents the process for a minimal installation of NixOS on a Proxmox virtual machine. It leverages the =nixos-anywhere= tool for remote deployment and =disko= for declarative disk partitioning. It also covers the essential post-installation steps for integrating the new host with =sops-nix= for secrets management. * Table of Contents :TOC: - [[#prerequisites-on-the-target-vm][Prerequisites on the Target VM]] @@ -13,6 +13,9 @@ This guide documents the process for a minimal installation of NixOS on a Proxmo - [[#note-on-hardware-configuration][Note on Hardware Configuration]] - [[#key-configuration-details][Key Configuration Details]] - [[#disko-configuration-for-proxmox-mbr-boot][Disko Configuration for Proxmox (MBR Boot)]] +- [[#post-installation-secrets-management][Post-Installation: Secrets Management]] + - [[#step-1-generating-the-host-age-key][Step 1: Generating the Host AGE Key]] + - [[#step-2-updating-sops-and-re-encrypting-secrets][Step 2: Updating SOPS and Re-encrypting Secrets]] - [[#todos][TODOs]] * Prerequisites on the Target VM @@ -85,5 +88,46 @@ Here is an example snippet for the =disko= configuration: For a complete example, you can refer to the official =disko= repository: [[https://github.com/nix-community/disko/blob/master/example/gpt-bios-compat.nix][gpt-bios-compat.nix]]. +* Post-Installation: Secrets Management +** Step 1: Generating the Host AGE Key +After the initial installation is complete, you will need its host AGE key to manage secrets with tools like =sops-nix=. This key is derived from the host's SSH key. + +1. SSH into the newly installed NixOS machine. + #+begin_src sh + ssh root@192.168.1.85 + #+end_src + +2. Run the following command. It temporarily installs the =ssh-to-age= utility and pipes the public SSH host key to it, converting it to an AGE public key. + #+begin_src sh + nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' + #+end_src + +3. The command will output the new AGE public key. Copy this key for the next step. + +** Step 2: Updating SOPS and Re-encrypting Secrets +The new AGE key must be added to your =.sops.yaml= configuration file. This allows =sops= to encrypt secrets in a way that the new host (=susano=) can decrypt them. + +1. Open the =.sops.yaml= file in the root of your Nix flake. +2. Replace the old key for the =susano= host with the new key you generated. + + #+begin_src yaml + keys: + - &primary age19wvqtn4ju6k4vs8fxr34unl6xx4cv04jw0lx9ps20xlde927zfssgl4qke + - &susano age1vkfq9gpqfpyq3s9e79e6vw8kv9485tzna4fm3dy6p0u9uz9feu8qr9sgcf # <--- REPLACE THIS WITH THE NEW KEY + creation_rules: + - path_regex: secrets/secrets.yaml$ + key_groups: + - age: + - *primary + - *susano + #+end_src + +3. After saving the updated =.sops.yaml= file, run the =updatekeys= command. This re-encrypts the specified secrets file with the new set of keys defined in =.sops.yaml=. For more information, see the [[https://github.com/getsops/sops?tab=readme-ov-file#281updatekeys-command][official documentation]]. + #+begin_src sh + sops updatekeys secrets/secrets.yaml + #+end_src + Your secrets are now encrypted for both the primary key and the new host's key. + * TODOs - [ ] Refactor the =disko= configuration to make the disk device name (e.g., =/dev/sda=) a variable. This will avoid hardcoding the value and make the configuration more portable across different hardware setups. +- [ ] Investigate and resolve the issue where updating a user's password declaratively using a secret managed by =sops= failed after the initial installation. diff --git a/flake.lock b/flake.lock index 1deb0ea..8d6523b 100644 --- a/flake.lock +++ b/flake.lock @@ -78,7 +78,28 @@ "disko": "disko", "home-manager": "home-manager", "nixos-hardware": "nixos-hardware", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1752544651, + "narHash": "sha256-GllP7cmQu7zLZTs9z0J2gIL42IZHa9CBEXwBY9szT0U=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "2c8def626f54708a9c38a5861866660395bb3461", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index b003570..46f42dd 100644 --- a/flake.nix +++ b/flake.nix @@ -13,6 +13,10 @@ url = "github:nix-community/home-manager/release-25.05"; inputs.nixpkgs.follows = "nixpkgs"; }; + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = { @@ -21,6 +25,7 @@ nixos-hardware, disko, home-manager, + sops-nix, ... } @ inputs: let inherit (self) outputs; @@ -37,6 +42,17 @@ ./minimal ]; }; + + susano = nixpkgs.lib.nixosSystem { + specialArgs = {inherit inputs outputs extraHomeModules; }; + modules = [ + disko.nixosModules.disko + home-manager.nixosModules.home-manager + sops-nix.nixosModules.sops + + ./main + ]; + }; }; }; } diff --git a/main/.sops.yaml b/main/.sops.yaml new file mode 100644 index 0000000..fb5a43b --- /dev/null +++ b/main/.sops.yaml @@ -0,0 +1,9 @@ +keys: + - &primary age19wvqtn4ju6k4vs8fxr34unl6xx4cv04jw0lx9ps20xlde927zfssgl4qke + - &susano age1puzhjqxkxxfygm00taqql9vsv26cn2drqr3fk097mnu6t90fn9rqx7vtvs +creation_rules: + - path_regex: secrets/secrets.yaml$ + key_groups: + - age: + - *primary + - *susano diff --git a/main/default.nix b/main/default.nix new file mode 100644 index 0000000..c685fc4 --- /dev/null +++ b/main/default.nix @@ -0,0 +1,134 @@ +{ config, pkgs, extraHomeModules, inputs, lib, ... }: + +let + username = "susano"; + flakeInputs = lib.filterAttrs (_: lib.isType "flake") inputs; +in { + imports = + [ # Include the results of the hardware scan. + ./hardware-configuration.nix + ./disko-config.nix + ./sops.nix + ]; + + nixpkgs = { + # You can add overlays here + overlays = [ + # If you want to use overlays exported from other flakes: + # neovim-nightly-overlay.overlays.default + + # Or define it inline, for example: + # (final: prev: { + # hi = final.hello.overrideAttrs (oldAttrs: { + # patches = [ ./change-hello-to-hi.patch ]; + # }); + # }) + ]; + # Configure your nixpkgs instance + config = { + # Disable if you don't want unfree packages + allowUnfree = true; + }; + }; + + nix = { + settings = { + # Enable flakes and new 'nix' command + experimental-features = "nix-command flakes"; + # Opinionated: disable global registry + flake-registry = ""; + # Workaround for https://github.com/NixOS/nix/issues/9574 + nix-path = config.nix.nixPath; + + # Allow user to reubild nixos without sudo + trusted-users = [ "root" username ]; + }; + # Opinionated: disable channels + channel.enable = false; + + # Opinionated: make flake registry and nix path match flake inputs + registry = lib.mapAttrs (_: flake: {inherit flake;}) flakeInputs; + nixPath = lib.mapAttrsToList (n: _: "${n}=flake:${n}") flakeInputs; + }; + + # Bootloader. + boot.loader.grub.enable = true; + boot.loader.grub.useOSProber = true; + + networking.hostName = username; + networking.networkmanager.enable = true; + + # Set your time zone. + time.timeZone = "Europe/Warsaw"; + + # Select internationalisation properties. + i18n.defaultLocale = "en_US.UTF-8"; + + i18n.extraLocaleSettings = { + LC_ADDRESS = "en_GB.UTF-8"; + LC_IDENTIFICATION = "en_GB.UTF-8"; + LC_MEASUREMENT = "en_GB.UTF-8"; + LC_MONETARY = "en_GB.UTF-8"; + LC_NAME = "en_GB.UTF-8"; + LC_NUMERIC = "en_GB.UTF-8"; + LC_PAPER = "en_GB.UTF-8"; + LC_TELEPHONE = "en_GB.UTF-8"; + LC_TIME = "en_GB.UTF-8"; + }; + + security.rtkit.enable = true; + + users.users.${username} = { + isNormalUser = true; + description = "NixOS Proxmox Homelab"; + hashedPassword = "$6$7LSgOtcEozV0gkN9$pCltKL683UqJ3M7C4ZIgZsytAGtQS375g64ckuJQPFtUjxiGCxehJtkP91Pba.rIZNe3eZqnJfIQNwnJWmyVJ0"; + extraGroups = [ "networkmanager" "wheel" ]; + packages = with pkgs; [ + ]; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBcGhVpjmWEw1GEw0y/ysJPa2v3+u/Rt/iES/Se2huH2 alexander0derevianko@gmail.com" + ]; + + shell = pkgs.zsh; + }; + + environment.systemPackages = with pkgs; [ + vim + wget + ripgrep + ]; + + services.openssh = { + enable = true; + settings = { + # Opinionated: forbid root login through SSH. + PermitRootLogin = "no"; + # Opinionated: use keys only. + # Remove if you want to SSH using passwords + PasswordAuthentication = false; + }; + }; + + programs = { + zsh.enable = true; + }; + + ### + # Home Manger configuration + ### + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + backupFileExtension = "backup"; + extraSpecialArgs = { inherit inputs; }; + + users."${username}" = { + imports = [ + ./home.nix + ] ++ extraHomeModules; + }; + }; + + # DO NOT CHANGE AT ANY POINT! + system.stateVersion = "25.05"; +} diff --git a/main/disko-config.nix b/main/disko-config.nix new file mode 100644 index 0000000..35e76fc --- /dev/null +++ b/main/disko-config.nix @@ -0,0 +1,55 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/sda"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + root = { + size = "100%"; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; # Override existing partition + # Subvolumes must set a mountpoint in order to be mounted, + # unless their parent is mounted + subvolumes = { + # Subvolume name is different from mountpoint + "/rootfs" = { + mountpoint = "/"; + }; + # Subvolume name is the same as the mountpoint + "/home" = { + mountOptions = [ "compress=zstd" ]; + mountpoint = "/home"; + }; + # Sub(sub)volume doesn't need a mountpoint as its parent is mounted + "/home/susano" = { }; + "/nix" = { + mountOptions = [ + "compress=zstd" + "noatime" + ]; + mountpoint = "/nix"; + }; + }; + + mountpoint = "/partition-root"; + swap = { + swapfile = { + size = "8G"; + }; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/main/hardware-configuration.nix b/main/hardware-configuration.nix new file mode 100644 index 0000000..c760612 --- /dev/null +++ b/main/hardware-configuration.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = [ "uhci_hcd" "ehci_pci" "ahci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + # fileSystems."/" = + # { device = "/dev/disk/by-uuid/301d5990-7186-4a90-94aa-997044007358"; + # fsType = "ext4"; + # }; + + # 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.ens18.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/main/home.nix b/main/home.nix new file mode 100644 index 0000000..9a2c708 --- /dev/null +++ b/main/home.nix @@ -0,0 +1,35 @@ +{ config, lib, pkgs, inputs, extraHomeModules, ... }: + +let + username = "susano"; +in { + imports = [ + ]; + + + home = { + stateVersion = "25.05"; + username = username; + homeDirectory = "/home/${username}"; + }; + + dov = { + shell = { + zsh = { + enable = true; + shellAliases = { + ll = "eza -al"; + sc = "source $HOME/.zshrc"; + psax = "ps ax | grep"; + cp = "rsync -ah --progress"; + }; + }; + }; + }; + + programs.home-manager.enable = true; + + home.packages = with pkgs; [ + eza + ]; +} diff --git a/main/secrets/secrets.yaml b/main/secrets/secrets.yaml new file mode 100644 index 0000000..363cddb --- /dev/null +++ b/main/secrets/secrets.yaml @@ -0,0 +1,35 @@ +hello: ENC[AES256_GCM,data:kQFj0v5K91h8DOvtm64tHx6qeJlfTyfxMNJelCtOvKNSf+UhiaCPPjWqDKOc+A==,iv:x/nWPKqwCI8kCo1/Md60DGK7zpOj4Wo1z9zUz6iN7VA=,tag:nR7PRubjoT8Y24lhsaR0Lg==,type:str] +example_key: ENC[AES256_GCM,data:fsWUwTTDcqKyRECWbg==,iv:B7CiWA03R/VQMt0EuymXHMz2+lAOQ9JMBP/fgGRlXuU=,tag:sH0nk4Oa6nKCFQmHmhfEqg==,type:str] +#ENC[AES256_GCM,data:WWchy1xoaqb+YoprbxR9cQ==,iv:lN7qwwOO1KezH7ab7Y1agKwuI3lLNO/bAiiJBWbGXn8=,tag:MC/3jd245bRAD5N9PNGPFg==,type:comment] +example_array: + - ENC[AES256_GCM,data:yoH7Z3R5/JZSNF6HSuI=,iv:MFcdr/hUnQlRq/Uv8j0wVV1mcPXTxv/ie2BU2N+/gyc=,tag:dQnYQI86rUSTHn+GFoi/Eg==,type:str] + - ENC[AES256_GCM,data:XhW7btDAl5pOkTj4QsE=,iv:qk9GlUObq1omvo02L07Y7g6qftZGnSrCyZnrxNRxJow=,tag:bqKzb0EeyztCiu8yh0NuAA==,type:str] +example_number: ENC[AES256_GCM,data:5mNliaa4HqK4Bw==,iv:t6hpGNyi59mEwwvglKT3JwO5RRON5z0mvqt0jdTV+L8=,tag:0KVsAEe24M/ZcBf8TjPcLQ==,type:float] +example_booleans: + - ENC[AES256_GCM,data:4rh2xA==,iv:2wQtaVPzLjQzPezrxd1w4/IZu4bT0rvU8G/edcsQ7VQ=,tag:re5rdTqPNSTZ+CuZjvs86A==,type:bool] + - ENC[AES256_GCM,data:5VhbnIk=,iv:sRnE8roVMQVs1Dk9tOtALWiDtfM4aJiSX5gb/MDHak8=,tag:egUULcUP5vCsy5uUM+j6dA==,type:bool] +user_password: ENC[AES256_GCM,data:Q7rk67ylyjr5Sa+AYCxnQAPLbBP5Fy85wTGLZuqxBG3iJ+MmhEgfeatVA2tcsY7GSaU/vghny+TJtrvhDYYMqa10h/F0wPxUjId78qkhKbnRQs4mqAxA9heSi4ojp1kh/pXN7tj64wNyJA==,iv:FTUojVNz78tn/Uj1N8Oj5Iov9eEMRo5vz+mqHdewxjg=,tag:YF74hLXXUby0IjHrqdkBUQ==,type:str] +sops: + age: + - recipient: age19wvqtn4ju6k4vs8fxr34unl6xx4cv04jw0lx9ps20xlde927zfssgl4qke + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHbE12b2ZsU2VNSjVwR29M + WU1ZT2gwUHo3eXE3a1EzRDVrb2g1V2sybTFvCmp6dmlKZjdxM1ZiZUdrZ2ZZaXNz + ZWRHNmVwVUhUcWJoYVluOXN2aWpSVEUKLS0tIDVHaVhob0J6RlFhb1pvOG5OZy9W + UjRFMDhvOElxc3U2OGZjOFp2aFdodWsKIJFb3ZUuLDAgCel09B8fdpowa+A8R/HT + vq4aS7TFAo4GsTfm6oF7AejnRj8teqqBTD99coQZeRJc8C6J+hp9FQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1puzhjqxkxxfygm00taqql9vsv26cn2drqr3fk097mnu6t90fn9rqx7vtvs + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXaDJoMEphNjBjcFBVdVJ1 + VGJjU1VtRmhtL00zUGJaRDMxRHMyS2x1ekJBCjA1Z3V6YzhHaTNZd2FxZmoybjgr + S3dWUDM0Ty9ZZkV3RFhjRWRnVURJeHMKLS0tIFM5VTVuSFdOQnFBby9OQWJWZ0pt + N0Q0a2J2WURqZlRadUdacGdHZUUrQWMK2Q1nwOWsGSzlcuZfcnq/P/v4i3nriUGY + l9izT0xS6M8cHoh10YK3Qe1LcxfT/v0pXD8ppARdEDbEcJahb5ZHiA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2025-07-26T12:25:08Z" + mac: ENC[AES256_GCM,data:eE/qjURtZPxwGpzvb0C64nZHaLSWL26kTU+fhx7dv+T84pCLtDwSiLrUfR33aSPtu9xTFJIiiFPQCiew99UXMsGKt+CVVGtR7frS5DktvABIfHuPznP3q6ykYMrNplK+xNQx0n6cZ/BNRVgc/kMPB9J3QudglAuxP3rMHrcjubA=,iv:okgPMpyGw0bUCEc/XrKonK5EUYrJjNRkAF/0t7TyoZw=,tag:fBQhqIASwsrl6//mXYcBzQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.10.2 diff --git a/main/sops.nix b/main/sops.nix new file mode 100644 index 0000000..754ea6e --- /dev/null +++ b/main/sops.nix @@ -0,0 +1,22 @@ +{ config, lib, pkgs, ... }: + +{ + sops = { + defaultSopsFile = ./secrets/secrets.yaml; + age = { + # This will automatically import SSH keys as age keys + sshKeyPaths = [ + "/etc/ssh/ssh_host_ed25519_key" + ]; + # This is using an age key that is expected to already be in the filesystem + keyFile = "/var/lib/sops-nix/key.txt"; + # This will generate a new key if the key specified above does not exist + generateKey = true; + # This is the actual specification of the secrets. + }; + + secrets."user_password" = { + neededForUsers = true; + }; + }; +} diff --git a/minimal/default.nix b/minimal/default.nix index 2ff7cf7..9cbbc72 100644 --- a/minimal/default.nix +++ b/minimal/default.nix @@ -1,7 +1,8 @@ -{ config, pkgs, extraHomeModules, inputs, ... }: +{ config, pkgs, extraHomeModules, inputs, lib, ... }: let username = "susano"; + flakeInputs = lib.filterAttrs (_: lib.isType "flake") inputs; in { imports = [ # Include the results of the hardware scan. @@ -9,6 +10,46 @@ in { ./disko-config.nix ]; + nixpkgs = { + # You can add overlays here + overlays = [ + # If you want to use overlays exported from other flakes: + # neovim-nightly-overlay.overlays.default + + # Or define it inline, for example: + # (final: prev: { + # hi = final.hello.overrideAttrs (oldAttrs: { + # patches = [ ./change-hello-to-hi.patch ]; + # }); + # }) + ]; + # Configure your nixpkgs instance + config = { + # Disable if you don't want unfree packages + allowUnfree = true; + }; + }; + + nix = { + settings = { + # Enable flakes and new 'nix' command + experimental-features = "nix-command flakes"; + # Opinionated: disable global registry + flake-registry = ""; + # Workaround for https://github.com/NixOS/nix/issues/9574 + nix-path = config.nix.nixPath; + + # Allow user to reubild nixos without sudo + trusted-users = [ "root" username ]; + }; + # Opinionated: disable channels + channel.enable = false; + + # Opinionated: make flake registry and nix path match flake inputs + registry = lib.mapAttrs (_: flake: {inherit flake;}) flakeInputs; + nixPath = lib.mapAttrsToList (n: _: "${n}=flake:${n}") flakeInputs; + }; + # Bootloader. boot.loader.grub.enable = true; boot.loader.grub.useOSProber = true; @@ -39,7 +80,7 @@ in { users.users.${username} = { isNormalUser = true; description = "NixOS Proxmox Homelab"; - initialPassword = "test"; + hashedPassword = "$6$7LSgOtcEozV0gkN9$pCltKL683UqJ3M7C4ZIgZsytAGtQS375g64ckuJQPFtUjxiGCxehJtkP91Pba.rIZNe3eZqnJfIQNwnJWmyVJ0"; extraGroups = [ "networkmanager" "wheel" ]; packages = with pkgs; [ ];