Added sops, updated readme, added main config

This commit is contained in:
Alexander Derevianko
2025-07-26 14:35:38 +02:00
parent baf2f6438f
commit 274872ec95
11 changed files with 446 additions and 6 deletions
+47 -3
View File
@@ -1,10 +1,10 @@
#+TITLE: Installing NixOS on a Proxmox VM using nixos-anywhere #+TITLE: Installing NixOS on a Proxmox VM using nixos-anywhere
#+AUTHOR: #+AUTHOR: Alexander Derevianko
#+DATE: #+DATE: <2025-07-26 Sat>
#+OPTIONS: toc:t num:nil #+OPTIONS: toc:t num:nil
*Abstract* *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: * Table of Contents :TOC:
- [[#prerequisites-on-the-target-vm][Prerequisites on the Target VM]] - [[#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]] - [[#note-on-hardware-configuration][Note on Hardware Configuration]]
- [[#key-configuration-details][Key Configuration Details]] - [[#key-configuration-details][Key Configuration Details]]
- [[#disko-configuration-for-proxmox-mbr-boot][Disko Configuration for Proxmox (MBR Boot)]] - [[#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]] - [[#todos][TODOs]]
* Prerequisites on the Target VM * 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]]. 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 * 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. - [ ] 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.
Generated
+22 -1
View File
@@ -78,7 +78,28 @@
"disko": "disko", "disko": "disko",
"home-manager": "home-manager", "home-manager": "home-manager",
"nixos-hardware": "nixos-hardware", "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"
} }
} }
}, },
+16
View File
@@ -13,6 +13,10 @@
url = "github:nix-community/home-manager/release-25.05"; url = "github:nix-community/home-manager/release-25.05";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { outputs = {
@@ -21,6 +25,7 @@
nixos-hardware, nixos-hardware,
disko, disko,
home-manager, home-manager,
sops-nix,
... ...
} @ inputs: let } @ inputs: let
inherit (self) outputs; inherit (self) outputs;
@@ -37,6 +42,17 @@
./minimal ./minimal
]; ];
}; };
susano = nixpkgs.lib.nixosSystem {
specialArgs = {inherit inputs outputs extraHomeModules; };
modules = [
disko.nixosModules.disko
home-manager.nixosModules.home-manager
sops-nix.nixosModules.sops
./main
];
};
}; };
}; };
} }
+9
View File
@@ -0,0 +1,9 @@
keys:
- &primary age19wvqtn4ju6k4vs8fxr34unl6xx4cv04jw0lx9ps20xlde927zfssgl4qke
- &susano age1puzhjqxkxxfygm00taqql9vsv26cn2drqr3fk097mnu6t90fn9rqx7vtvs
creation_rules:
- path_regex: secrets/secrets.yaml$
key_groups:
- age:
- *primary
- *susano
+134
View File
@@ -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";
}
+55
View File
@@ -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";
};
};
};
};
};
};
};
};
};
}
+28
View File
@@ -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&#39;s
# still possible to use this option, but it&#39;s recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.&lt;interface&gt;.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.ens18.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
+35
View File
@@ -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
];
}
+35
View File
@@ -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
+22
View File
@@ -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;
};
};
}
+43 -2
View File
@@ -1,7 +1,8 @@
{ config, pkgs, extraHomeModules, inputs, ... }: { config, pkgs, extraHomeModules, inputs, lib, ... }:
let let
username = "susano"; username = "susano";
flakeInputs = lib.filterAttrs (_: lib.isType "flake") inputs;
in { in {
imports = imports =
[ # Include the results of the hardware scan. [ # Include the results of the hardware scan.
@@ -9,6 +10,46 @@ in {
./disko-config.nix ./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. # Bootloader.
boot.loader.grub.enable = true; boot.loader.grub.enable = true;
boot.loader.grub.useOSProber = true; boot.loader.grub.useOSProber = true;
@@ -39,7 +80,7 @@ in {
users.users.${username} = { users.users.${username} = {
isNormalUser = true; isNormalUser = true;
description = "NixOS Proxmox Homelab"; description = "NixOS Proxmox Homelab";
initialPassword = "test"; hashedPassword = "$6$7LSgOtcEozV0gkN9$pCltKL683UqJ3M7C4ZIgZsytAGtQS375g64ckuJQPFtUjxiGCxehJtkP91Pba.rIZNe3eZqnJfIQNwnJWmyVJ0";
extraGroups = [ "networkmanager" "wheel" ]; extraGroups = [ "networkmanager" "wheel" ];
packages = with pkgs; [ packages = with pkgs; [
]; ];