refactor: split services into reusable services/ modules

- services/{samba,avahi,audiobookshelf,containers,caddy,tailscale}.nix
- common.nix grows firewall base + timezone; hosts import what they need
- jupiter/vm/vps import service modules; drop the jupiter/services.nix monolith
- each module opens its own firewall ports; caddy/tailscale shared by hosts
- verified: jupiter/vps/vbox eval + jupiter builds, config equivalent
This commit is contained in:
erik
2026-07-13 19:13:11 +02:00
parent 2b170af346
commit fb782cb9fe
11 changed files with 187 additions and 222 deletions
+21 -20
View File
@@ -1,14 +1,25 @@
{ config, pkgs, lib, ... }:
# Real-host config: hardware + disk layout + bootloader + shared services.
# ZimaBlade NAS host: hardware + disk + the services it runs.
{
imports = [
./hardware-configuration.nix
./disk-config.nix # disko: OS-disk partitions + filesystems
./secrets.nix # sops-nix: samba password etc.
./services.nix
./secrets.nix # sops-nix: samba password, tailscale key, ...
../common.nix # shared base: user / ssh / nix / firewall
../services/samba.nix
../services/avahi.nix
../services/audiobookshelf.nix
../services/containers.nix
../services/caddy.nix
../services/tailscale.nix
];
# ---- Host identity ----
networking.hostName = "jupiter";
networking.networkmanager.enable = true;
users.users.darman.extraGroups = [ "docker" ]; # merges with common.nix
# ---- Boot ----
# systemd-boot for UEFI. If ZimaBlade boots legacy/BIOS, switch to grub.
boot.loader.systemd-boot.enable = true;
@@ -19,10 +30,6 @@
# hardware-configuration.nix) or stage-1 can't mount root and the box panics.
boot.initrd.availableKernelModules = [ "mmc_block" "sdhci_pci" "sdhci_acpi" ];
# Trust wheel users so `nixos-rebuild --target-host darman@…` can push a
# laptop-built (unsigned) closure without a signature error.
nix.settings.trusted-users = [ "root" "@wheel" ];
# Warm reboot hangs at firmware reset on this board (cold power-cycle works).
# Force the PCI-chipset reset method. If a warm `reboot` still hangs, try the
# next value: acpi -> bios -> cold -> efi.
@@ -40,18 +47,12 @@
options = [ "nofail" ]; # don't block boot if the array is degraded/absent
};
# ---- Tailscale (via self-hosted headscale) ----
# Auto-registers on boot using a headscale pre-auth key from sops.
# Generate the key on the headscale server, e.g.:
# headscale preauthkeys create --user <user> --expiration 1h
# then put it in secrets/jupiter.yaml (./edit_secrets, key: tailscale_authkey).
services.tailscale = {
enable = true;
openFirewall = true; # UDP 41641 for direct connections
authKeyFile = config.sops.secrets.tailscale_authkey.path;
extraUpFlags = [ "--login-server=https://vpn.mgaction.town" ];
# ---- Caddy vhosts (LAN) ----
# whoami on :80, audiobookshelf via a pihole local-DNS name -> jupiter IP.
services.caddy.virtualHosts = {
"http://localhost".extraConfig = "reverse_proxy localhost:8080";
"http://audiobookshelf.jupiter.sol".extraConfig = "reverse_proxy localhost:8000";
};
# Reach jupiter's services (ssh, samba, audiobookshelf) over the tailnet
# without opening those ports on the LAN.
networking.firewall.trustedInterfaces = [ "tailscale0" ];
system.stateVersion = "26.05";
}
-174
View File
@@ -1,174 +0,0 @@
{ config, pkgs, lib, ... }:
# Portable system + service config. Contains NO bootloader or filesystem
# settings, so it can be reused by both the real host (configuration.nix)
# and the VirtualBox test image (see flake.nix).
{
# ---- Networking ----
networking.hostName = "jupiter";
networking.networkmanager.enable = true;
networking.firewall = {
enable = true;
allowedTCPPorts = [
22 # ssh
445 139 # samba
80 443 # reverse proxy (caddy)
];
};
# ---- Locale / time ----
time.timeZone = "Europe/Berlin";
i18n.defaultLocale = "en_US.UTF-8";
# ---- Users ----
users.users.darman = {
isNormalUser = true;
description = "darman";
extraGroups = [ "wheel" "networkmanager" "docker" ];
# Password login for ssh is disabled below — these keys are the only way in.
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZpkPVhzi1zG5JI9hWyUgdyvNIQbp4ts4jw3idpMhhN erik@laptop"
];
};
security.sudo.wheelNeedsPassword = false;
# ---- SSH ----
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
# ---- Storage / NAS ----
services.samba = {
enable = true;
openFirewall = true;
settings = {
global = {
"workgroup" = "WORKGROUP";
"server string" = "jupiter";
"security" = "user";
};
data = {
"path" = "/mnt/data";
"browseable" = "yes";
"read only" = "no";
"guest ok" = "no";
"valid users" = "darman";
};
};
};
# Samba keeps its own NTLM password DB, separate from the system password.
# `services.samba` never sets it, so logins fail until provisioned.
# This runs AFTER samba-smbd so its state dir (/var/lib/samba/private) exists
# — an activation script runs too early and smbpasswd fails to init the passdb.
# Reads a single-line password from the first file that exists:
# Real host: /run/secrets/samba_password (sops-nix, see secrets.nix)
# VM test: /etc/samba/smb-password (plaintext, see vm.nix)
# smbpasswd prompts new + confirm, so the value is fed twice.
systemd.services.samba-smbpasswd = {
description = "Provision Samba password for darman";
after = [ "samba-smbd.service" ];
requires = [ "samba-smbd.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
for f in /run/secrets/samba_password /etc/samba/smb-password; do
if [ -f "$f" ]; then
pw=$(head -n1 "$f")
printf '%s\n%s\n' "$pw" "$pw" | ${pkgs.samba}/bin/smbpasswd -a -s darman
exit 0
fi
done
echo "no samba password source found" >&2
'';
};
services.avahi = {
enable = true;
nssmdns4 = true;
publish = {
enable = true;
userServices = true;
};
};
# ---- Audiobookshelf ----
# Native module. Listens on localhost only; reached through caddy.
# Library/media paths are set in the web UI — point them at /mnt/data/...
# The service runs as user `audiobookshelf`, which must be able to READ those
# dirs on the RAID (chown/chmod or add it to the owning group as needed).
services.audiobookshelf = {
enable = true;
host = "0.0.0.0"; # reachable over tailscale0 (trusted) + localhost
port = 8000;
# openFirewall stays OFF: :8000 is blocked on the LAN, but tailscale0 is a
# trusted interface, so the VPS reaches it via jupiter.hosts.mgaction.town:8000.
# Local caddy (below) still serves LAN clients via localhost:8000.
};
users.users.audiobookshelf.extraGroups = [ "users" ];
# ---- Containers ----
virtualisation.podman = {
enable = true;
dockerCompat = true;
defaultNetwork.settings.dns_enabled = true;
};
virtualisation.oci-containers = {
backend = "podman";
containers = {
whoami = {
image = "traefik/whoami:latest";
ports = [ "8080:80" ];
autoStart = true;
};
};
};
# ---- Reverse proxy ----
# Caddy binds nothing unless it has a vhost. This proxies the whoami
# container so :80 actually serves. Add one block per service.
# Real host: swap `http://localhost` for your domain to get automatic HTTPS,
# e.g. `services.caddy.virtualHosts."jelly.example.com".extraConfig`.
services.caddy = {
enable = true;
virtualHosts."http://localhost".extraConfig = ''
reverse_proxy localhost:8080
'';
# Reach at http://audiobookshelf.sol — add a pihole local-DNS record
# audiobookshelf.sol -> jupiter's IP. (Or browse jupiter.sol directly if you
# set services.audiobookshelf.host = "0.0.0.0" + openFirewall instead.)
virtualHosts."http://audiobookshelf.jupiter.sol".extraConfig = ''
reverse_proxy localhost:8000
'';
};
# ---- System packages ----
environment.systemPackages = with pkgs; [
vim
git
htop
tmux
curl
];
# ---- Nix settings ----
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
system.stateVersion = "26.05";
}
+19 -5
View File
@@ -1,15 +1,23 @@
{ config, pkgs, lib, modulesPath, ... }:
# VirtualBox test image. Reuses services.nix but adds console/SSH login
# credentials so you can actually get into the VM. Disk + bootloader are
# provided by the virtualbox-image module, so hardware-configuration.nix
# is intentionally NOT imported here.
# VirtualBox test image — mirrors jupiter's services (minus tailscale/sops/disk)
# and adds console/SSH login credentials. Disk + bootloader come from the
# virtualbox-image module, so hardware-configuration.nix is NOT imported.
{
imports = [
(modulesPath + "/virtualisation/virtualbox-image.nix")
./services.nix
../common.nix # shared base: user / ssh / nix / firewall
../services/samba.nix
../services/avahi.nix
../services/audiobookshelf.nix
../services/containers.nix
../services/caddy.nix
];
networking.hostName = "jupiter";
networking.networkmanager.enable = true;
users.users.darman.extraGroups = [ "docker" ];
# Allow password login for testing (real host is key-only).
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
@@ -26,6 +34,12 @@
mode = "0600";
};
# Caddy vhosts (same as the LAN host, so the proxy path is testable).
services.caddy.virtualHosts = {
"http://localhost".extraConfig = "reverse_proxy localhost:8080";
"http://audiobookshelf.jupiter.sol".extraConfig = "reverse_proxy localhost:8000";
};
# Guest additions for clipboard/resize (optional).
virtualisation.virtualbox.guest.enable = true;