feat: scaffold netcup vps host (disko/vda, static net, tailscale, caddy)
- new nixosConfigurations.vps: UEFI systemd-boot, disko on /dev/vda - static IPv4 159.195.64.117/22 gw .1 (+ IPv6), eth0 pinned, public DNS - tailscale via headscale + sops authkey (secrets/vps.yaml, own host key) - caddy public reverse proxy: audiobookshelf.mgaction.town -> jupiter tailnet:8000 - shared common.nix (user/ssh/nix) ; .sops.yaml per-host rules
This commit is contained in:
+10
-6
@@ -8,12 +8,16 @@
|
|||||||
keys:
|
keys:
|
||||||
# admin key — for editing secrets on the laptop (~/.config/sops/age/keys.txt)
|
# admin key — for editing secrets on the laptop (~/.config/sops/age/keys.txt)
|
||||||
- &admin age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2
|
- &admin age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2
|
||||||
# jupiter host key — derived from its pre-generated SSH host key with
|
# 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.
|
# (ssh-to-age). The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key.
|
||||||
- &jupiter age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
- &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:
|
creation_rules:
|
||||||
- path_regex: secrets/.*\.yaml$
|
- path_regex: secrets/jupiter\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age: [ *admin, *jupiter ]
|
||||||
- *admin
|
- path_regex: secrets/vps\.yaml$
|
||||||
- *jupiter
|
key_groups:
|
||||||
|
- age: [ *admin, *vps ]
|
||||||
|
|||||||
+41
@@ -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";
|
||||||
|
}
|
||||||
@@ -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:
|
# VirtualBox test image. Build the OVA with:
|
||||||
# nix build .#nixosConfigurations.jupiter-vbox.config.system.build.virtualBoxOVA
|
# nix build .#nixosConfigurations.jupiter-vbox.config.system.build.virtualBoxOVA
|
||||||
# NOTE: no disko here — the virtualbox-image module supplies the disk.
|
# NOTE: no disko here — the virtualbox-image module supplies the disk.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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 = { };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user