286 lines
9.2 KiB
Nix
286 lines
9.2 KiB
Nix
# NixOS configuration for homeserver-1
|
|
# Headless server environment (Command Line Interface only)
|
|
|
|
{ config
|
|
, pkgs
|
|
, lib
|
|
, inputs
|
|
, ...
|
|
}:
|
|
|
|
{
|
|
imports = [
|
|
../../modules/core/common.nix
|
|
(import ../../modules/storage/disko.nix {
|
|
inherit inputs lib config;
|
|
diskoConfigPath = ./disko-config.nix;
|
|
})
|
|
./hardware-configuration.nix
|
|
../../modules/core/management.nix
|
|
../../modules/core/podman.nix
|
|
../../modules/hardware/nvidia.nix
|
|
../../modules/core/known-hosts.nix
|
|
../../modules/services/immich.nix
|
|
../../modules/services/jellyfin.nix
|
|
../../modules/services/backrest.nix
|
|
../../modules/services/homepage.nix
|
|
../../modules/services/gitea.nix
|
|
./homepage.nix
|
|
];
|
|
|
|
services.backrest = {
|
|
enable = true;
|
|
host = "0.0.0.0";
|
|
port = 9898;
|
|
dataDir = "/data/backrest";
|
|
};
|
|
|
|
systemd.services.backrest = {
|
|
after = [ "restic-ssh-key-format.service" ];
|
|
wants = [ "restic-ssh-key-format.service" ];
|
|
};
|
|
|
|
# Allow backrest to read backup source directories recursively.
|
|
# Uses ACLs with a default mask so newly created files also inherit access.
|
|
# Runs after immich and the postgresql backup so the dirs/files exist.
|
|
systemd.services.backrest-permissions = {
|
|
description = "Grant backrest read access to backup source dirs";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [
|
|
"immich-server.service"
|
|
"postgresqlBackup-immich.service"
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
# Recursive read+execute ACL for backrest on immich media
|
|
${pkgs.acl}/bin/setfacl -R -m u:backrest:rx -m m::r-x /data/immich
|
|
# Default ACL so future immich files are readable by backrest
|
|
${pkgs.acl}/bin/setfacl -R -m d:u:backrest:rx -m d:m::r-x /data/immich
|
|
# Recursive read for backrest on postgresql backups
|
|
${pkgs.acl}/bin/setfacl -R -m u:backrest:rx -m m::r-x /data/backup/postgresql
|
|
${pkgs.acl}/bin/setfacl -R -m d:u:backrest:rx -m d:m::r-x /data/backup/postgresql
|
|
# Recursive read for backrest on gitea (repos + dump)
|
|
${pkgs.acl}/bin/setfacl -R -m u:backrest:rx -m m::r-x /data/gitea
|
|
${pkgs.acl}/bin/setfacl -R -m d:u:backrest:rx -m d:m::r-x /data/gitea
|
|
'';
|
|
};
|
|
|
|
services.immich-server = {
|
|
enable = true;
|
|
port = 2283;
|
|
mediaLocation = "/data/immich";
|
|
};
|
|
|
|
services.jellyfin-server = {
|
|
enable = true;
|
|
port = 8096;
|
|
mediaLocation = "/data/jellyfin";
|
|
};
|
|
|
|
# Gitea git server. Public at https://gitea.edley.me via an external reverse
|
|
# proxy (TLS terminator) that reaches us over Tailscale. Only that proxy's
|
|
# Tailscale IP is allowed through the firewall (see modules/services/gitea.nix).
|
|
services.gitea-server = {
|
|
enable = true;
|
|
port = 3000;
|
|
domain = "gitea.edley.me";
|
|
proxyIp = "100.109.85.40";
|
|
};
|
|
|
|
services.postgresqlBackup = {
|
|
enable = true;
|
|
databases = [
|
|
"immich"
|
|
"gitea"
|
|
];
|
|
location = "/data/backup/postgresql";
|
|
compression = "zstd";
|
|
startAt = "weekly";
|
|
};
|
|
|
|
# The services.postgresqlBackup module creates a tmpfiles `d` rule that sets
|
|
# the backup directory to 0700 on every rebuild/switch. On a directory with
|
|
# POSIX ACLs, `chmod 0700` resets the ACL mask to `---`, which nullifies the
|
|
# `user:backrest` read ACL that backrest uses to back up these dumps. Override
|
|
# the mode so rebuilds keep the directory group-traversable (mask stays rx).
|
|
# Note: systemd-tmpfiles dedupes conflicting rules and keeps the FIRST one for
|
|
# a path, so our override must appear before the module's rule.
|
|
systemd.tmpfiles.rules = lib.mkBefore [
|
|
"d /data/backup/postgresql 0750 postgres - - -"
|
|
];
|
|
|
|
sops.secrets = {
|
|
"users/petere-password" = {
|
|
neededForUsers = true;
|
|
};
|
|
"pocket-id-env" = {
|
|
neededForUsers = false;
|
|
};
|
|
"homeserver-1/restic-passphrase" = { };
|
|
"homeserver-1/restic-ssh-key" = { };
|
|
"homeserver-1/homepage-env" = { };
|
|
"homeserver-1/samba-petere-password" = { };
|
|
};
|
|
|
|
# Set petere's Samba password from SOPS at boot. Samba keeps its own password
|
|
# database (smbpasswd/tdbsam), separate from Linux login, so this must run
|
|
# smbpasswd. Idempotent: re-applied on every boot from the secret.
|
|
systemd.services.samba-set-petere-password = {
|
|
description = "Set petere's Samba password from SOPS";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "sops-nix.service" ];
|
|
before = [ "samba-smbd.service" ];
|
|
serviceConfig.Type = "oneshot";
|
|
script = ''
|
|
PASS="$(cat ${config.sops.secrets."homeserver-1/samba-petere-password".path})"
|
|
${pkgs.samba}/bin/smbpasswd -s -a petere <<EOF
|
|
$PASS
|
|
$PASS
|
|
EOF
|
|
'';
|
|
};
|
|
|
|
# Install the restic SSH key (stored multi-line in sops) and set up SSH access
|
|
# for the backrest user so restic can reach mcf-server.
|
|
# Writes to a persistent location (NOT /run) because sops-nix clears /run/secrets.
|
|
systemd.services.restic-ssh-key-format = {
|
|
description = "Install restic SSH key and configure SSH for backrest";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "sops-nix.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
KEY_FILE="${config.sops.secrets."homeserver-1/restic-ssh-key".path}"
|
|
FORMATTED="/data/backrest/restic-ssh-key"
|
|
${pkgs.coreutils}/bin/install -m 640 -o root -g backrest "$KEY_FILE" "$FORMATTED"
|
|
|
|
# SSH config for backrest so restic (via Backrest) uses the correct key
|
|
mkdir -p /data/backrest/.ssh
|
|
cat > /data/backrest/.ssh/config <<EOF
|
|
Host mcf-server
|
|
HostName mcf-server
|
|
User restic-homeserver1
|
|
IdentityFile /data/backrest/restic-ssh-key
|
|
IdentitiesOnly yes
|
|
Host richmond-server
|
|
HostName richmond-server
|
|
User restic-homeserver1
|
|
IdentityFile /data/backrest/restic-ssh-key
|
|
IdentitiesOnly yes
|
|
EOF
|
|
chown -R backrest:backrest /data/backrest/.ssh
|
|
chmod 700 /data/backrest/.ssh
|
|
chmod 600 /data/backrest/.ssh/config
|
|
'';
|
|
};
|
|
|
|
networking.hostName = "homeserver-1";
|
|
|
|
# SSH configuration
|
|
services.openssh.enable = true;
|
|
|
|
# Trusted host keys for restic backup targets (via Backrest).
|
|
my.knownHosts = {
|
|
mcfServer = true;
|
|
richmondServer = true;
|
|
};
|
|
|
|
# Glances system monitor - exposed to the tailnet so the Homepage
|
|
# dashboard can display real-time stats for this machine (localhost).
|
|
services.glances = {
|
|
enable = true;
|
|
port = 61208;
|
|
extraArgs = [ "--webserver" ];
|
|
};
|
|
|
|
# Expose services on Tailscale only (not the LAN).
|
|
# Immich (2283): accessed via nginx proxy on another machine over Tailscale.
|
|
# Backrest (9898), Pocket ID (8443), Homepage (8082), Glances (61208): admin services.
|
|
networking.firewall.interfaces.tailscale.allowedTCPPorts = lib.mkAfter [
|
|
2283 # Immich
|
|
8443 # Pocket ID
|
|
9898 # Backrest
|
|
8082 # Homepage
|
|
61208 # Glances
|
|
];
|
|
|
|
# Add Pocket ID package for tooling
|
|
environment.systemPackages = with pkgs; [ pocket-id ];
|
|
|
|
# Pocket ID service configuration
|
|
services.pocket-id = {
|
|
enable = true;
|
|
environmentFile = config.sops.secrets."pocket-id-env".path;
|
|
settings = {
|
|
APP_URL = "https://homeserver-1.gerbil-opah.ts.net:8443";
|
|
PORT = 8443;
|
|
TRUST_PROXY = true;
|
|
TLS_CERT_FILE = "/etc/ssl/certs/pocket-id.crt";
|
|
TLS_KEY_FILE = "/etc/ssl/private/pocket-id.key";
|
|
};
|
|
};
|
|
|
|
systemd.services.pocket-id = {
|
|
wants = [ "pocket-id-tailscale-cert.service" ];
|
|
after = [ "pocket-id-tailscale-cert.service" ];
|
|
};
|
|
|
|
# Systemd service to obtain TLS cert via Tailscale
|
|
systemd.services.pocket-id-tailscale-cert = {
|
|
description = "Obtain TLS cert for Pocket-ID via Tailscale";
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [
|
|
"network-online.target"
|
|
"tailscaled.service"
|
|
];
|
|
after = [
|
|
"network-online.target"
|
|
"tailscaled.service"
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = pkgs.writeShellScript "get-tailscale-cert" ''
|
|
set -eu
|
|
mkdir -p /etc/ssl/certs /etc/ssl/private
|
|
if [ ! -f /etc/ssl/certs/pocket-id.crt ] || [ ! -f /etc/ssl/private/pocket-id.key ]; then
|
|
${pkgs.tailscale}/bin/tailscale cert --cert-file /etc/ssl/certs/pocket-id.crt --key-file /etc/ssl/private/pocket-id.key homeserver-1.gerbil-opah.ts.net
|
|
fi
|
|
chown root:pocket-id /etc/ssl/private/pocket-id.key
|
|
chmod 640 /etc/ssl/private/pocket-id.key
|
|
chmod 644 /etc/ssl/certs/pocket-id.crt
|
|
'';
|
|
User = "root";
|
|
Group = "root";
|
|
};
|
|
};
|
|
|
|
# NVIDIA GPU Configuration for GeForce GTX 960 (Maxwell GM206)
|
|
my.hardware.nvidia = {
|
|
enable = true;
|
|
# GTX 960 (Maxwell) needs the 580.xx legacy driver branch; the default
|
|
# driver no longer supports it (NVRM: No NVIDIA GPU found).
|
|
package = config.boot.kernelPackages.nvidiaPackages.legacy_580;
|
|
};
|
|
|
|
boot.kernelModules = [ "sg" ];
|
|
|
|
# Root account is locked (no password login); access is via SSH key + sudo.
|
|
users.users.root.hashedPassword = "!";
|
|
|
|
# Standard user account (shared definition in modules/core/users.nix)
|
|
my.users.petere = {
|
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
|
};
|
|
|
|
# Trusted users for Nix operations
|
|
nix.settings.trusted-users = [
|
|
"root"
|
|
"petere"
|
|
];
|
|
}
|