diff --git a/.sops.yaml b/.sops.yaml index 686d572..f7862ad 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -8,12 +8,16 @@ keys: # admin key — for editing secrets on the laptop (~/.config/sops/age/keys.txt) - &admin age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2 - # jupiter host key — derived from its pre-generated SSH host key with - # ssh-to-age. The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key. + # per-host keys — derived from each box's pre-generated SSH host key + # (ssh-to-age). The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key. - &jupiter age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt + - &vps age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp +# Each host's secrets go to admin (edit) + that host only. Order matters: +# the first matching rule wins, so keep these specific paths. creation_rules: - - path_regex: secrets/.*\.yaml$ + - path_regex: secrets/jupiter\.yaml$ key_groups: - - age: - - *admin - - *jupiter + - age: [ *admin, *jupiter ] + - path_regex: secrets/vps\.yaml$ + key_groups: + - age: [ *admin, *vps ] diff --git a/common.nix b/common.nix new file mode 100644 index 0000000..b4674b6 --- /dev/null +++ b/common.nix @@ -0,0 +1,41 @@ +{ pkgs, ... }: + +# Shared base for all hosts: user, SSH hardening, nix settings, packages. +# (jupiter still carries its own copy in services.nix; vps uses this.) +{ + # ---- User ---- + users.users.darman = { + isNormalUser = true; + description = "darman"; + extraGroups = [ "wheel" "networkmanager" ]; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZpkPVhzi1zG5JI9hWyUgdyvNIQbp4ts4jw3idpMhhN erik@laptop" + ]; + }; + security.sudo.wheelNeedsPassword = false; + + # ---- SSH (key-only) ---- + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "no"; + }; + }; + + # ---- Nix ---- + nix.settings = { + experimental-features = [ "nix-command" "flakes" ]; + # trust wheel so `nixos-rebuild --target-host darman@…` can push closures. + trusted-users = [ "root" "@wheel" ]; + }; + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + + environment.systemPackages = with pkgs; [ vim git htop tmux curl ]; + + i18n.defaultLocale = "en_US.UTF-8"; +} diff --git a/flake.nix b/flake.nix index c8876f0..20811a0 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,17 @@ ]; }; + # netcup VPS — public reverse proxy + tailnet node. + vps = nixpkgs.lib.nixosSystem { + inherit system; + specialArgs = { inherit inputs; }; + modules = [ + disko.nixosModules.disko + sops-nix.nixosModules.sops + ./vps/configuration.nix + ]; + }; + # VirtualBox test image. Build the OVA with: # nix build .#nixosConfigurations.jupiter-vbox.config.system.build.virtualBoxOVA # NOTE: no disko here — the virtualbox-image module supplies the disk. diff --git a/secrets/vps.yaml b/secrets/vps.yaml new file mode 100644 index 0000000..42d6745 --- /dev/null +++ b/secrets/vps.yaml @@ -0,0 +1,25 @@ +tailscale_authkey: ENC[AES256_GCM,data:K9An79w/7ZHl3Pb+TYgH1fzFqEsuBjgMqt4O9lS9csY=,iv:I3SWgDQDiWV/ItUs3biU+EG6nzy446Rd2Cf74fJVpVM=,tag:kn3PXvGkcheKF+26jLK3MQ==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZNVRnY1djOFFpQjEzYzdi + ZHhjZWliL2xlK2t6MncxSnk3ZW8ybHFNZ0hNCk0wWHdBajlMQWtjU21QQkxSV1M0 + bzJhbFBSSmdPU2VTV1NyQmhmUnlJUUEKLS0tIHFTTGlIam9MdllGamc2ZVZIaEFL + ZkE4cDk0aTdEMGtxVGM3U2JCMFppalEKnyyQCErY7e5pIFoYS6yYq5puqXAUgClJ + U9VkM7G1MZV+Z91XTKs63fd2QyV8ITTK82BMpwH39UzdbbU+mqFvqw== + -----END AGE ENCRYPTED FILE----- + recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXZER4Q1NFSVJNUFpMS2JQ + dVZseHFzc2lWOXVJQmdIZ2xHeWVDMEYyTFJjCkdKckNQNTZ4Zm9hRCtMc2lMZmFF + TTh3cVBMVWJGRGY3Z2ErWlBlQUZhL0EKLS0tIGxGZDIzZEhsSFQ5SmNZblk5eHpy + ZWpPU1RNZHNIRUhJMXE0dFFtcTRzUmsKnKzMUXY8MKG2Ilgzg1IIr7hvXuvLvzCH + EaWB6C+J7/jG3dXfVTfKlubE7x62XDkWMa8OYMFELke/7GU6XXmHWg== + -----END AGE ENCRYPTED FILE----- + recipient: age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp + lastmodified: "2026-07-12T22:59:51Z" + mac: ENC[AES256_GCM,data:yS13N11tXN2orKAP2ufRNdJbEGkY+bfZiA/7PwSWtgipIlGvnCKbr6yVqMElfNA39Znu+i1sWtk3FxWjRJQ3rM+uRpEdzoseMnYiHkvK0t5hC1ElBxwUdv8v/1RV5/halTV+5G8/lAd48ejgtnLLCpSbw6nPKrDoxb8/Ma5oaj0=,iv:MejfOs5F7eAmwZPsrLUF+6KPxWgACKKbnn5skCisuBo=,tag:X4BI6zDQgqXnnmCVOl25/A==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.1 diff --git a/vps/configuration.nix b/vps/configuration.nix new file mode 100644 index 0000000..9a9c401 --- /dev/null +++ b/vps/configuration.nix @@ -0,0 +1,64 @@ +{ config, pkgs, lib, ... }: + +# netcup VPS (UEFI, /dev/vda). Public reverse proxy + tailnet node. +{ + imports = [ + ./hardware-configuration.nix + ./disk-config.nix # disko: vda partitions + filesystems + ./secrets.nix # sops-nix: tailscale authkey + ../common.nix # shared user / ssh / nix settings + ]; + + # ---- Boot (UEFI) ---- + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + # Root is on the virtio disk — pin these so stage-1 mounts it regardless of + # what nixos-generate-config detects in the installer. + boot.initrd.availableKernelModules = [ "virtio_pci" "virtio_blk" "virtio_scsi" ]; + + networking.hostName = "vps"; + + # ---- Static networking (netcup) ---- + # No LAN fallback: get this right or the box is unreachable (use netcup's + # VNC console / rescue system to fix). Values captured from the running VPS. + networking.useDHCP = false; + networking.usePredictableInterfaceNames = false; # keep the NIC named eth0 + networking.interfaces.eth0 = { + ipv4.addresses = [ { address = "159.195.64.117"; prefixLength = 22; } ]; + ipv6.addresses = [ { address = "2a0a:4cc0:c2:19e1:44b4:8dff:fe4d:c7d7"; prefixLength = 64; } ]; + }; + networking.defaultGateway = { address = "159.195.64.1"; interface = "eth0"; }; + # netcup IPv6 gateway is conventionally fe80::1 — VERIFY with `ip -6 route` + # on the running VPS; wrong v6 gw won't break v4 reachability. + networking.defaultGateway6 = { address = "fe80::1"; interface = "eth0"; }; + # Public resolvers for early boot; tailscale MagicDNS overrides once up. + networking.nameservers = [ "9.9.9.9" "1.1.1.1" "2620:fe::fe" ]; + + networking.firewall = { + enable = true; + allowedTCPPorts = [ 22 80 443 ]; + trustedInterfaces = [ "tailscale0" ]; # reach admin services over the tailnet + }; + + # ---- Tailscale (via headscale) ---- + services.tailscale = { + enable = true; + authKeyFile = config.sops.secrets.tailscale_authkey.path; + extraUpFlags = [ "--login-server=https://vpn.mgaction.town" ]; + }; + + # ---- Public reverse proxy ---- + # Caddy gets automatic public HTTPS (Let's Encrypt) for real domains. + # Proxies to jupiter's audiobookshelf over the tailnet (MagicDNS name). + # Add one block per public service. Needs a public A record -> this VPS IP + # and ports 80/443 open (they are, above). + services.caddy = { + enable = true; + virtualHosts."audiobookshelf.mgaction.town".extraConfig = '' + reverse_proxy http://jupiter.hosts.mgaction.town:8000 + ''; + # TODO: port your other VPS services' vhosts here before deploying. + }; + + system.stateVersion = "26.05"; # set at install time; do NOT bump on upgrades +} diff --git a/vps/disk-config.nix b/vps/disk-config.nix new file mode 100644 index 0000000..c38aeef --- /dev/null +++ b/vps/disk-config.nix @@ -0,0 +1,37 @@ +{ ... }: + +# Declarative disk layout (disko) for the netcup VPS. +# UEFI: GPT with an ESP + ext4 root on the single virtio disk. +# +# ⚠️ /dev/vda is WIPED on install — this destroys everything currently on the +# VPS (docker containers, data, existing caddy config). Back up / port your +# services into NixOS BEFORE running nixos-anywhere. +{ + disko.devices.disk.vps = { + type = "disk"; + device = "/dev/vda"; # netcup virtio disk (256G) + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; +} diff --git a/vps/hardware-configuration.nix b/vps/hardware-configuration.nix new file mode 100644 index 0000000..8010f99 --- /dev/null +++ b/vps/hardware-configuration.nix @@ -0,0 +1,23 @@ +# PLACEHOLDER — replace on install. +# +# disko (disk-config.nix) owns the filesystems, so this file only carries +# kernel modules + platform. nixos-anywhere regenerates it via: +# nixos-generate-config --no-filesystems +# Keep the imports/kernel-module lines; the virtio modules are pinned in +# configuration.nix so root mounts even if the generator misses them. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = [ (modulesPath + "/profiles/qemu-guest.nix") ]; + + boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "virtio_pci" "virtio_blk" "sr_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + # NO fileSystems here — disko defines "/" and "/boot". + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/vps/secrets.nix b/vps/secrets.nix new file mode 100644 index 0000000..b643046 --- /dev/null +++ b/vps/secrets.nix @@ -0,0 +1,12 @@ +{ config, ... }: + +# sops-nix wiring for the VPS. Encrypted values live in ../secrets/vps.yaml, +# decrypted with the VPS's own SSH host key (recipient in ../.sops.yaml). +# The host key is pre-generated on the laptop and shipped at install +# (nixos-anywhere --extra-files -> /etc/ssh/ssh_host_ed25519_key). +{ + sops.defaultSopsFile = ../secrets/vps.yaml; + sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + + sops.secrets.tailscale_authkey = { }; +}