feat(refactor): WIP refactor server modules

This commit is contained in:
2025-10-12 21:07:30 +02:00
parent 57cb48a11c
commit d2bd385367
43 changed files with 1004 additions and 1414 deletions

View File

@@ -3,109 +3,253 @@
enable = true; enable = true;
email = "adam@cnst.dev"; email = "adam@cnst.dev";
domain = "cnix.dev"; domain = "cnix.dev";
ip = "192.168.88.14";
user = "share"; user = "share";
group = "share"; group = "share";
uid = 994; uid = 994;
gid = 993; gid = 993;
traefik = { infra = {
enable = true; authentik = {
};
tailscale = {
enable = true;
};
unbound = {
enable = true;
};
homepage-dashboard = {
enable = true;
};
n8n = {
enable = true;
};
bazarr = {
enable = true;
};
prowlarr = {
enable = true;
};
lidarr = {
enable = true;
};
sonarr = {
enable = true;
};
radarr = {
enable = true;
};
jellyseerr = {
enable = true;
};
jellyfin = {
enable = true;
};
uptime-kuma = {
enable = true;
};
gitea = {
enable = true;
url = "git.cnst.dev";
cloudflared = {
tunnelId = "33e2fb8e-ecef-4d42-b845-6d15e216e448";
credentialsFile = config.age.secrets.giteaCloudflared.path;
};
};
vaultwarden = {
enable = true;
url = "vault.cnst.dev";
cloudflared = {
tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5";
credentialsFile = config.age.secrets.vaultwardenCloudflared.path;
};
};
www = {
enable = true;
url = "cnst.dev";
cloudflared = {
tunnelId = "e5076186-efb7-405a-998c-6155af7fb221";
credentialsFile = config.age.secrets.wwwCloudflared.path;
};
};
authentik = {
enable = true;
url = "auth.cnst.dev";
cloudflared = {
tunnelId = "b66f9368-db9e-4302-8b48-527cda34a635";
credentialsFile = config.age.secrets.authentikCloudflared.path;
};
};
nextcloud = {
enable = true;
adminpassFile = config.age.secrets.nextcloudAdminPass.path;
};
fail2ban = {
enable = true;
apiKeyFile = config.age.secrets.cloudflareFirewallApiKey.path;
zoneId = "0027acdfb8bbe010f55b676ad8698dfb";
};
keepalived = {
enable = true;
interface = "enp6s0";
};
podman = {
enable = true;
gluetun.enable = true;
qbittorrent = {
enable = true; enable = true;
port = 8080; url = "auth.cnst.dev";
port = 9000;
cloudflared = {
tunnelId = "b66f9368-db9e-4302-8b48-527cda34a635";
credentialsFile = config.age.secrets.authentikCloudflared.path;
};
}; };
slskd = { traefik = {
enable = true; enable = true;
}; };
pihole = { tailscale = {
enable = true; enable = true;
port = 8053;
}; };
unbound = {
enable = true;
};
fail2ban = {
enable = true;
apiKeyFile = config.age.secrets.cloudflareFirewallApiKey.path;
zoneId = "0027acdfb8bbe010f55b676ad8698dfb";
};
keepalived = {
enable = true;
interface = "enp6s0";
};
gluetun = {
enable = true;
};
podman = {
enable = true;
};
www = {
enable = true;
url = "cnst.dev";
port = 8283;
cloudflared = {
tunnelId = "e5076186-efb7-405a-998c-6155af7fb221";
credentialsFile = config.age.secrets.wwwCloudflared.path;
};
};
};
services = {
# homepage-dashboard = {
# enable = true;
# subdomain = "";
# port = "8082";
# };
# n8n = {
# enable = true;
# subdomain = "n8n";
# port = "5678";
# homepage = {
# name = "n8n";
# description = "A workflow automation platform";
# icon = "n8n.svg";
# category = "Services";
# };
# };
# bazarr = {
# enable = true;
# subdomain = "bazarr";
# port = 6767;
# homepage = {
# name = "Bazarr";
# description = "Subtitle manager";
# icon = "bazarr.svg";
# category = "Arr";
# };
# };
# prowlarr = {
# enable = true;
# subdomain = "prowlarr";
# port = 9696;
# homepage = {
# name = "prowlarr";
# description = "PVR indexer";
# icon = "prowlarr.svg";
# category = "Arr";
# };
# };
# flaresolverr = {
# enable = true;
# subdomain = "flaresolverr";
# port = 8191;
# homepage = {
# name = "FlareSolverr";
# description = "Proxy to bypass Cloudflare/DDoS-GUARD protection";
# icon = "flaresolverr.svg";
# category = "Arr";
# };
# };
# lidarr = {
# enable = true;
# subdomain = "lidarr";
# port = 8686;
# homepage = {
# name = "Lidarr";
# description = "Music collection manager";
# icon = "lidarr.svg";
# category = "Arr";
# };
# };
# sonarr = {
# enable = true;
# subdomain = "sonarr";
# port = 8989;
# homepage = {
# name = "Sonarr";
# description = "Internet PVR for Usenet and Torrents";
# icon = "sonarr.svg";
# category = "Arr";
# };
# };
# radarr = {
# enable = true;
# subdomain = "radarr";
# port = 7878;
# homepage = {
# name = "Radarr";
# description = "Movie collection manager";
# icon = "radarr.svg";
# category = "Arr";
# };
# };
# jellyseerr = {
# enable = true;
# subdomain = "jellyseerr";
# port = 5055;
# homepage = {
# name = "Jellyseerr";
# description = "Media request and discovery manager";
# icon = "jellyserr.svg";
# category = "Arr";
# };
# };
# jellyfin = {
# enable = true;
# subdomain = "fin";
# exposure = "tailscale";
# port = 8096;
# homepage = {
# name = "Jellyfin";
# description = "The Free Software Media System";
# icon = "jellyfin.svg";
# category = "Media";
# };
# };
# uptime-kuma = {
# enable = true;
# subdomain = "uptime";
# port = 3001;
# homepage = {
# name = "Uptime Kuma";
# description = "Service monitoring tool";
# icon = "uptime-kuma.svg";
# category = "Services";
# };
# };
# gitea = {
# enable = true;
# subdomain = "git";
# exposure = "tunnel";
# port = 5003;
# cloudflared = {
# tunnelId = "33e2fb8e-ecef-4d42-b845-6d15e216e448";
# credentialsFile = config.age.secrets.giteaCloudflared.path;
# };
# homepage = {
# name = "Gitea";
# description = "Git with a cup of tea";
# icon = "gitea.svg";
# category = "Services";
# };
# };
vaultwarden = {
enable = true;
# subdomain = "vault";
# exposure = "tunnel";
# port = 8222;
# cloudflared = {
# tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5";
# credentialsFile = config.age.secrets.vaultwardenCloudflared.path;
# };
# homepage = {
# name = "Vaultwarden";
# description = "Password manager";
# icon = "vaultwarden.svg";
# category = "Services";
# };
};
nextcloud = {
enable = true;
subdomain = "cloud";
exposure = "local";
port = 8182;
adminpassFile = config.age.secrets.nextcloudAdminPass.path;
homepage = {
name = "Nextcloud";
description = "A safe home for all your data";
icon = "nextcloud.svg";
category = "Services";
};
};
# qbittorrent = {
# enable = true;
# subdomain = "qbt";
# port = 8080;
# homepage = {
# name = "qBittorrent";
# description = "Torrent client";
# icon = "qbittorrent.svg";
# category = "Downloads";
# };
# };
# slskd = {
# enable = true;
# subdomain = "slskd";
# port = 5030;
# homepage = {
# name = "Soulseek";
# description = "Web-based Soulseek client";
# icon = "slskd.svg";
# category = "Downloads";
# };
# };
# pihole = {
# enable = true;
# subdomain = "pihole";
# port = 8053;
# homepage = {
# name = "PiHole";
# description = "Adblocking and DNS service";
# icon = "pi-hole.svg";
# category = "Services";
# path = "/admin";
# };
# };
}; };
}; };
} }

View File

@@ -123,28 +123,31 @@
server = { server = {
imports = [ imports = [
./server ./server
./server/fail2ban
./server/homepage-dashboard ./server/infra/authentik
./server/nextcloud ./server/infra/fail2ban
./server/vaultwarden ./server/infra/keepalived
./server/bazarr ./server/infra/podman
./server/prowlarr ./server/infra/postgres
./server/lidarr ./server/infra/tailscale
./server/radarr ./server/infra/traefik
./server/sonarr ./server/infra/unbound
./server/jellyseerr ./server/infra/www
./server/jellyfin
./server/n8n ./server/services/bazarr
./server/podman ./server/services/flaresolverr
./server/unbound ./server/services/gitea
./server/uptime-kuma ./server/services/homepage-dashboard
./server/keepalived ./server/services/jellyfin
./server/gitea ./server/services/jellyseerr
./server/postgres ./server/services/lidarr
./server/traefik ./server/services/n8n
./server/www ./server/services/nextcloud
./server/authentik ./server/services/prowlarr
./server/tailscale ./server/services/radarr
./server/services/sonarr
./server/services/uptime-kuma
./server/services/vaultwarden
]; ];
}; };
settings = { settings = {

View File

@@ -1,62 +0,0 @@
{
config,
lib,
...
}: let
unit = "bazarr";
srv = config.server;
cfg = config.server.${unit};
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "${unit}.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Bazarr";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Subtitle manager";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "bazarr.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Arr";
};
};
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
services.traefik = {
dynamicConfigOptions = {
http = {
services.bazarr.loadBalancer.servers = [{url = "http://127.0.0.1:${toString config.services.${unit}.listenPort}";}];
routers = {
bazarr = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "bazarr";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
};
}

View File

@@ -27,6 +27,11 @@ in {
Domain name to be used to access the server services via Caddy reverse proxy Domain name to be used to access the server services via Caddy reverse proxy
''; '';
}; };
ip = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "The local IP of the service.";
};
user = lib.mkOption { user = lib.mkOption {
default = "share"; default = "share";
type = lib.types.str; type = lib.types.str;
@@ -62,6 +67,86 @@ in {
Time zone to be used for the server services Time zone to be used for the server services
''; '';
}; };
services = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
options = {
enable = lib.mkEnableOption "the service";
subdomain = lib.mkOption {
type = lib.types.str;
default = "";
description = "The subdomain for the service (e.g., 'jellyfin')";
};
exposure = lib.mkOption {
type = lib.types.enum ["local" "tunnel" "tailscale"];
default = "local";
description = "Controls where the service is exposed";
};
port = lib.mkOption {
type = lib.types.int;
default = 80;
description = "The port to host service on.";
};
configDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/${name}";
description = "Configuration directory for ${name}.";
};
cloudflared = lib.mkOption {
type = lib.types.submodule {
options = {
credentialsFile = lib.mkOption {
type = lib.types.str;
example = "/path/to/cloudflare-credentials.json";
# example = lib.literalExpression ''
# pkgs.writeText "cloudflare-credentials.json" '''
# {"AccountTag":"secret","TunnelSecret":"secret","TunnelID":"secret"}
# '''
# '';
};
tunnelId = lib.mkOption {
type = lib.types.str;
example = "00000000-0000-0000-0000-000000000000";
};
};
};
description = "Cloudflare tunnel configuration for this service.";
};
homepage = lib.mkOption {
type = lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
default = "";
description = "Display name on the homepage.";
};
description = lib.mkOption {
type = lib.types.str;
default = "";
description = "A short description for the homepage tile.";
};
icon = lib.mkOption {
type = lib.types.str;
default = "Zervices c00l stuff";
description = "Icon file name for the homepage tile.";
};
category = lib.mkOption {
type = lib.types.str;
default = "";
description = "Homepage category grouping.";
};
path = lib.mkOption {
type = lib.types.str;
default = "";
example = "/admin";
description = "Optional path suffix for homepage links (e.g. /admin).";
};
};
};
description = "Homepage metadata for this service.";
};
};
}));
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {

View File

@@ -1,15 +1,14 @@
{ {
config, config,
lib, lib,
pkgs,
self, self,
... ...
}: let }: let
unit = "authentik"; unit = "authentik";
cfg = config.server.${unit}; cfg = config.server.infra.${unit};
srv = config.server; srv = config.server.infra.www.domain;
in { in {
options.server.${unit} = { options.server.infra.${unit} = {
enable = lib.mkEnableOption { enable = lib.mkEnableOption {
description = "Enable ${unit}"; description = "Enable ${unit}";
}; };
@@ -17,6 +16,10 @@ in {
type = lib.types.str; type = lib.types.str;
default = "auth.${srv.www.domain}"; default = "auth.${srv.www.domain}";
}; };
port = lib.mkOption {
type = lib.types.port;
description = "The local port the service runs on";
};
cloudflared = { cloudflared = {
credentialsFile = lib.mkOption { credentialsFile = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@@ -31,21 +34,11 @@ in {
example = "00000000-0000-0000-0000-000000000000"; example = "00000000-0000-0000-0000-000000000000";
}; };
}; };
homepage.name = lib.mkOption { homepage = {
type = lib.types.str; name = "Authentik";
default = "Authentik"; description = "An open-source IdP for modern SSO";
}; icon = "authentik.svg";
homepage.description = lib.mkOption { category = "Services";
type = lib.types.str;
default = "An open-source IdP for modern SSO";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "authentik.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Services";
}; };
}; };
@@ -59,8 +52,8 @@ in {
}; };
}; };
server = { server.infra = {
fail2ban = lib.mkIf cfg.enable { fail2ban = {
jails = { jails = {
authentik = { authentik = {
serviceName = "authentik"; serviceName = "authentik";

View File

@@ -5,9 +5,9 @@
pkgs, pkgs,
... ...
}: let }: let
cfg = config.server.fail2ban; cfg = config.server.infra.fail2ban;
in { in {
options.server.fail2ban = { options.server.infra.fail2ban = {
enable = lib.mkEnableOption { enable = lib.mkEnableOption {
description = "Enable cloudflare fail2ban"; description = "Enable cloudflare fail2ban";
}; };
@@ -61,6 +61,7 @@ in {
); );
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.fail2ban = { services.fail2ban = {
enable = true; enable = true;

View File

@@ -3,27 +3,24 @@
config, config,
self, self,
... ...
}: }: let
let
unit = "keepalived"; unit = "keepalived";
cfg = config.server.${unit}; cfg = config.server.infra.${unit};
hostCfg = hostCfg = hostname:
hostname: if hostname == "sobotka"
if hostname == "sobotka" then then {
{ ip = "192.168.88.14";
ip = "192.168.88.14"; priority = 20;
priority = 20; state = "MASTER";
state = "MASTER"; }
} else if hostname == "ziggy"
else if hostname == "ziggy" then then {
{ ip = "192.168.88.12";
ip = "192.168.88.12"; priority = 10;
priority = 10; state = "BACKUP";
state = "BACKUP"; }
} else throw "No keepalived config defined for host ${hostname}";
else
throw "No keepalived config defined for host ${hostname}";
_self = hostCfg config.networking.hostName; _self = hostCfg config.networking.hostName;
@@ -34,9 +31,8 @@ let
# Remove self from peers # Remove self from peers
peers = builtins.filter (ip: ip != _self.ip) allPeers; peers = builtins.filter (ip: ip != _self.ip) allPeers;
in in {
{ options.server.infra.${unit} = {
options.server.${unit} = {
enable = lib.mkEnableOption { enable = lib.mkEnableOption {
description = "Enable ${unit}"; description = "Enable ${unit}";
}; };

View File

@@ -0,0 +1,6 @@
---
filenames:
- /var/log/traefik/access.log
poll_without_inotify: true
labels:
type: traefik

View File

@@ -0,0 +1,162 @@
{
config,
lib,
self,
...
}: let
infra = config.server.infra;
cfg = config.server.services;
getPiholeSecret = hostname:
if hostname == "ziggy"
then [config.age.secrets.piholeZiggy.path]
else if hostname == "sobotka"
then [config.age.secrets.pihole.path]
else throw "Unknown hostname: ${hostname}";
in {
options.server.infra = {
podman.enable = lib.mkEnableOption "Enables Podman";
gluetun.enable = lib.mkEnableOption "Enables gluetun";
};
config = lib.mkIf infra.podman.enable {
age.secrets = {
pihole.file = "${self}/secrets/${config.networking.hostName}Pihole.age";
slskd.file = "${self}/secrets/slskd.age";
};
virtualisation = {
containers.enable = true;
podman.enable = true;
};
networking.firewall = lib.mkIf cfg.pihole.enable {
allowedTCPPorts = [
53
5335
];
allowedUDPPorts = [
53
5335
];
};
virtualisation.oci-containers.containers = lib.mkMerge [
(lib.mkIf infra.gluetun.enable {
gluetun = {
image = "qmcgaw/gluetun";
ports = [
"8388:8388"
"58846:58846"
"8080:8080"
"5030:5030"
"5031:5031"
"50300:50300"
];
devices = ["/dev/net/tun:/dev/net/tun"];
autoStart = true;
extraOptions = [
"--cap-add=NET_ADMIN"
];
volumes = ["/var:/gluetun"];
environmentFiles = [
config.age.secrets.gluetunEnvironment.path
];
environment = {
DEV_MODE = "false";
VPN_SERVICE_PROVIDER = "mullvad";
VPN_TYPE = "wireguard";
SERVER_CITIES = "Stockholm";
};
};
})
(lib.mkIf cfg.qbittorrent.enable {
qbittorrent = {
image = "ghcr.io/hotio/qbittorrent:latest";
autoStart = true;
dependsOn = ["gluetun"];
ports = [
"8080:8080"
"58846:58846"
];
extraOptions = [
"--network=container:gluetun"
];
volumes = [
"/var/lib/qbittorrent:/config:rw"
"/mnt/data/media/downloads:/downloads:rw"
];
environmentFiles = [
config.age.secrets.gluetunEnvironment.path
];
environment = {
PUID = "994";
PGID = "993";
TZ = "Europe/Stockholm";
WEBUI_PORT = "${builtins.toString cfg.qbittorrent.port}";
};
};
})
(lib.mkIf cfg.slskd.enable {
slskd = {
image = "slskd/slskd:latest";
autoStart = true;
dependsOn = ["gluetun"];
ports = [
"5030:5030"
"5031:5031"
"50300:50300"
];
extraOptions = [
"--network=container:gluetun"
];
volumes = [
"/var/lib/slskd:/app:rw"
"/mnt/data/media/downloads:/downloads:rw"
];
environmentFiles = [
config.age.secrets.gluetunEnvironment.path
config.age.secrets.slskd.path
];
environment = {
TZ = "Europe/Stockholm";
PUID = "981";
PGID = "982";
SLSKD_REMOTE_CONFIGURATION = "true";
SLSKD_REMOTE_FILE_MANAGEMENT = "true";
SLSKD_DOWNLOADS_DIR = "/downloads";
SLSKD_UMASK = "022";
};
};
})
(lib.mkIf cfg.pihole.enable {
pihole = {
autoStart = true;
image = "pihole/pihole:2025.08.0";
volumes = [
"/var/lib/pihole:/etc/pihole/"
"/var/lib/dnsmasq.d:/etc/dnsmasq.d/"
];
environment = {
TZ = "Europe/Stockholm";
CUSTOM_CACHE_SIZE = "0";
WEBTHEME = "default-darker";
};
environmentFiles = getPiholeSecret config.networking.hostName;
ports = [
"53:53/tcp"
"53:53/udp"
"8053:80/tcp"
];
extraOptions = [
"--cap-add=NET_ADMIN"
"--cap-add=SYS_NICE"
"--cap-add=SYS_TIME"
];
};
})
];
};
}

View File

@@ -7,10 +7,10 @@
}: let }: let
inherit (lib) types mkOption; inherit (lib) types mkOption;
cfg = config.server.postgresql; cfg = config.server.infra.postgresql;
in { in {
options = { options = {
server.postgresql = { server.infra.postgresql = {
upgradeTargetPackage = mkOption { upgradeTargetPackage = mkOption {
type = types.nullOr types.package; type = types.nullOr types.package;
default = null; default = null;

View File

@@ -7,7 +7,7 @@
}: let }: let
inherit (lib) types mkOption; inherit (lib) types mkOption;
cfg = config.server.postgresql; cfg = config.server.infra.postgresql;
database = {name, ...}: { database = {name, ...}: {
options = { options = {
@@ -31,7 +31,7 @@
}; };
in { in {
options = { options = {
server.postgresql = { server.infra.postgresql = {
databases = mkOption { databases = mkOption {
type = types.listOf (types.submodule database); type = types.listOf (types.submodule database);
default = []; default = [];

View File

@@ -5,16 +5,11 @@
... ...
}: }:
with lib; let with lib; let
cfg = config.server.tailscale; cfg = config.server.infra.tailscale;
in { in {
options.server.tailscale = { options.server.infra.tailscale = {
enable = mkEnableOption "Enable tailscale server configuration"; enable = mkEnableOption "Enable tailscale server configuration";
url = lib.mkOption {
type = lib.types.str;
default = "ts.cnst.dev";
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
age.secrets.sobotkaTsAuth.file = "${self}/secrets/sobotkaTsAuth.age"; age.secrets.sobotkaTsAuth.file = "${self}/secrets/sobotkaTsAuth.age";

View File

@@ -0,0 +1,192 @@
{
lib,
config,
pkgs,
self,
...
}: let
inherit (lib) mkEnableOption mkIf types;
cfg = config.server.infra.traefik;
srv = config.server;
# Generates all Traefik routers from the central service list
generateRouters = services:
lib.mapAttrs' (
name: service: let
domain =
if service.exposure == "tunnel"
then "cnst.dev"
else if service.exposure == "tailscale"
then "ts.cnst.dev"
else srv.domain;
in
lib.nameValuePair "${service.subdomain}" {
entryPoints = ["websecure"];
rule = "Host(`${service.subdomain}.${domain}`)";
service = service.subdomain;
tls.certResolver = "letsencrypt";
}
) (lib.filterAttrs (name: service: service.enable) services);
# Generates all Traefik backend services
generateServices = services:
lib.mapAttrs' (name: service:
lib.nameValuePair "${service.subdomain}" {
loadBalancer.servers = [{url = "http://localhost:${toString service.port}";}];
}) (lib.filterAttrs (name: service: service.enable) services);
getCloudflareCredentials = hostname:
if hostname == "ziggy"
then config.age.secrets.cloudflareDnsCredentialsZiggy.path
else if hostname == "sobotka"
then config.age.secrets.cloudflareDnsCredentials.path
else throw "Unknown hostname: ${hostname}";
in {
options.server.infra.traefik = {
enable = mkEnableOption "Enable global Traefik reverse proxy with ACME";
};
config = mkIf cfg.enable {
age.secrets = {
traefikEnv = {
file = "${self}/secrets/traefikEnv.age";
mode = "640";
owner = "traefik";
group = "traefik";
};
crowdsecApi.file = "${self}/secrets/crowdsecApi.age";
};
systemd.services.traefik = {
serviceConfig = {
EnvironmentFile = [config.age.secrets.traefikEnv.path];
};
};
networking.firewall.allowedTCPPorts = [80 443];
services = {
tailscale.permitCertUid = "traefik";
traefik = {
enable = true;
staticConfigOptions = {
log = {
level = "DEBUG";
};
accesslog = {filepath = "/var/lib/traefik/logs/access.log";};
tracing = {};
api = {
dashboard = true;
insecure = false;
};
certificatesResolvers = {
vpn.tailscale = {};
letsencrypt = {
acme = {
email = "adam@cnst.dev";
storage = "/var/lib/traefik/cert.json";
dnsChallenge = {
provider = "cloudflare";
resolvers = [
"1.1.1.1:53"
"1.0.0.1:53"
];
};
};
};
};
entryPoints = {
# redis = {
# address = "0.0.0.0:6381";
# };
# postgres = {
# address = "0.0.0.0:5433";
# };
web = {
address = ":80";
forwardedHeaders.insecure = true;
http.redirections.entryPoint = {
to = "websecure";
scheme = "https";
permanent = true;
};
# http.middlewares = "crowdsec@file";
};
websecure = {
address = ":443";
forwardedHeaders.insecure = true;
http.tls = {
certResolver = "letsencrypt";
domains = [
{
main = "cnix.dev";
sans = ["*.cnix.dev"];
}
{
main = "ts.cnst.dev";
sans = ["*ts.cnst.dev"];
}
];
};
# http.middlewares = "crowdsec@file";
};
experimental = {
address = ":1111";
forwardedHeaders.insecure = true;
};
};
experimental = {
# Install the Crowdsec Bouncer plugin
plugins = {
#enabled = "true";
bouncer = {
moduleName = "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin";
version = "v1.4.5";
};
};
};
};
dynamicConfigOptions = {
http = {
# Generate the services from your central list
services = generateServices srv.services;
# Generate the routers and manually add the special 'api' router
routers =
(generateRouters srv.services)
// {
api = {
entryPoints = ["websecure"];
rule = "Host(`traefik.${srv.domain}`)";
service = "api@internal";
tls.certResolver = "letsencrypt";
};
};
# middlewares = {
# crowdsec = {
# plugin = {
# bouncer = {
# enabled = "true";
# logLevel = "DEBUG";
# crowdsecLapiKeyFile = config.age.secrets.crowdsecApi.path;
# crowdsecMode = "live";
# crowdsecLapiHost = ":4223";
# };
# };
# };
# };
};
};
};
};
};
}

View File

@@ -5,7 +5,13 @@
... ...
}: let }: let
unit = "unbound"; unit = "unbound";
cfg = config.server.${unit}; cfg = config.server.infra.${unit};
srv = config.server;
generateLocalRecords = services:
lib.mapAttrsToList (
name: service: "local-data: \"${service.subdomain}.${srv.domain}. A ${srv.ip}\""
) (lib.filterAttrs (name: service: service.enable) services);
hostIp = hostname: hostIp = hostname:
if hostname == "ziggy" if hostname == "ziggy"
@@ -14,11 +20,12 @@
then "192.168.88.14" then "192.168.88.14"
else throw "No IP defined for host ${hostname}"; else throw "No IP defined for host ${hostname}";
in { in {
options.server.${unit} = { options.server.infra.${unit} = {
enable = lib.mkEnableOption { enable = lib.mkEnableOption {
description = "Enable ${unit}"; description = "Enable ${unit}";
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services = { services = {
# resolved.enable = lib.mkForce false; # resolved.enable = lib.mkForce false;
@@ -97,6 +104,10 @@ in {
"255.255.255.255/32" "255.255.255.255/32"
"2001:db8::/32" "2001:db8::/32"
]; ];
local-data = generateLocalRecords srv.services;
local-data-ptr = [
"local-data: \"traefik.${srv.domain}. A ${srv.ip}\""
];
}; };
}; };
}; };

View File

@@ -1,15 +1,13 @@
{ {
lib, lib,
config, config,
pkgs,
self, self,
... ...
}: let }: let
inherit (lib) mkOption mkEnableOption mkIf types; inherit (lib) mkIf mkEnableOption mkOption types;
cfg = config.server.www; cfg = config.server.infra.www;
srv = config.server;
in { in {
options.server.www = { options.server.infra.www = {
enable = mkEnableOption { enable = mkEnableOption {
description = "Enable personal website"; description = "Enable personal website";
}; };
@@ -20,6 +18,12 @@ in {
Public domain name to be used to access the server services via Traefik reverse proxy Public domain name to be used to access the server services via Traefik reverse proxy
''; '';
}; };
port = lib.mkOption {
type = lib.types.int;
default = 8283;
description = "The port to host webservice on.";
};
cloudflared = { cloudflared = {
credentialsFile = lib.mkOption { credentialsFile = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@@ -41,8 +45,8 @@ in {
wwwCloudflared.file = "${self}/secrets/wwwCloudflared.age"; wwwCloudflared.file = "${self}/secrets/wwwCloudflared.age";
}; };
server = { server.infra = {
fail2ban = lib.mkIf config.server.www.enable { fail2ban = {
jails = { jails = {
nginx-404 = { nginx-404 = {
serviceName = "nginx"; serviceName = "nginx";

View File

@@ -1,65 +0,0 @@
{
config,
lib,
pkgs,
...
}: let
unit = "jellyfin";
cfg = config.server.${unit};
srv = config.server;
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "fin.${srv.tailscale.url}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Jellyfin";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "The Free Software Media System";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "jellyfin.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Media";
};
};
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
environment.systemPackages = with pkgs; [
jellyfin-ffmpeg
];
services.traefik = {
dynamicConfigOptions = {
http = {
services.${unit}.loadBalancer.servers = [{url = "http://localhost:8096";}];
routers = {
jellyfinRouter = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "${unit}";
tls.certResolver = "letsencrypt";
};
};
};
};
};
};
}

View File

@@ -1,61 +0,0 @@
{
config,
lib,
...
}: let
unit = "jellyseerr";
srv = config.server;
cfg = config.server.${unit};
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
url = lib.mkOption {
type = lib.types.str;
# default = "seer.${srv.tailscale.url}";
default = "jellyseerr.${srv.domain}";
};
port = lib.mkOption {
type = lib.types.port;
default = 5055;
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Jellyseerr";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Media request and discovery manager";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "jellyseerr.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Arr";
};
};
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
port = cfg.port;
};
services.traefik = {
dynamicConfigOptions = {
http = {
services.jellyseerr.loadBalancer.servers = [{url = "http://localhost:${toString cfg.port}";}];
routers = {
jellyseerr = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "${unit}";
tls.certResolver = "letsencrypt";
};
};
};
};
};
};
}

View File

@@ -1,62 +0,0 @@
{
config,
lib,
...
}: let
unit = "lidarr";
srv = config.server;
cfg = config.server.${unit};
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "${unit}.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Lidarr";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Music collection manager";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "lidarr.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Arr";
};
};
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
services.traefik = {
dynamicConfigOptions = {
http = {
services.lidarr.loadBalancer.servers = [{url = "http://127.0.0.1:8686";}];
routers = {
lidarr = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "lidarr";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
};
}

View File

@@ -1,64 +0,0 @@
{
config,
lib,
...
}: let
unit = "n8n";
srv = config.server;
cfg = config.server.${unit};
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "${unit}.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "n8n";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "A workflow automation platform";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "n8n.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Services";
};
};
config = lib.mkIf cfg.enable {
services = {
n8n = {
enable = true;
openFirewall = true;
};
traefik = {
dynamicConfigOptions = {
http = {
services.n8n.loadBalancer.servers = [{url = "http://127.0.0.1:5678";}];
routers = {
n8n = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "n8n";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
};
};
}

View File

@@ -1,323 +0,0 @@
{
config,
lib,
pkgs,
self,
...
}: let
srv = config.server;
cfg = config.server.podman;
piholeUrl =
if config.networking.hostName == "sobotka"
then "pihole0"
else if config.networking.hostName == "ziggy"
then "pihole1"
else throw "Unknown hostname";
getPiholeSecret = hostname:
if hostname == "ziggy"
then [config.age.secrets.piholeZiggy.path]
else if hostname == "sobotka"
then [config.age.secrets.pihole.path]
else throw "Unknown hostname: ${hostname}";
in {
options.server.podman = {
enable = lib.mkEnableOption "Enables Podman";
gluetun.enable = lib.mkEnableOption "Enables gluetun";
qbittorrent = {
enable = lib.mkEnableOption "Enable qBittorrent";
url = lib.mkOption {
type = lib.types.str;
default = "qbt.${srv.domain}";
};
port = lib.mkOption {
type = lib.types.int;
default = 8080;
description = "The port to host qBittorrent on.";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "qBittorrent";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Torrent client";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "qbittorrent.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Downloads";
};
};
slskd = {
enable = lib.mkEnableOption "Enable Soulseek";
url = lib.mkOption {
type = lib.types.str;
default = "slskd.${srv.domain}";
};
port = lib.mkOption {
type = lib.types.int;
default = 5030;
description = "The port to host Soulseek webui on.";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "slskd";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Web-based Soulseek client";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "slskd.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Downloads";
};
};
pihole = {
enable = lib.mkEnableOption {
description = "Enable";
};
port = lib.mkOption {
type = lib.types.int;
default = 8053;
description = "The port to host PiHole on.";
};
url = lib.mkOption {
type = lib.types.str;
default = "${piholeUrl}.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "PiHole";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Adblocking and DNS service";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "pi-hole.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Services";
};
homepage.path = lib.mkOption {
type = lib.types.str;
default = "/admin";
description = "Optional path suffix for homepage links (e.g. /admin).";
};
};
};
config = lib.mkIf cfg.enable {
age.secrets = {
pihole.file = "${self}/secrets/${config.networking.hostName}Pihole.age";
slskd.file = "${self}/secrets/slskd.age";
};
virtualisation = {
containers.enable = true;
podman.enable = true;
};
networking.firewall = lib.mkIf cfg.pihole.enable {
allowedTCPPorts = [
53
5335
];
allowedUDPPorts = [
53
5335
];
};
services = {
traefik = lib.mkMerge [
(lib.mkIf cfg.pihole.enable {
dynamicConfigOptions = {
http = {
services = {
pihole.loadBalancer.servers = [{url = "http://localhost:${toString cfg.pihole.port}";}];
};
routers = {
pihole = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.pihole.url}`)";
service = "pihole";
tls.certResolver = "letsencrypt";
};
};
};
};
})
(lib.mkIf cfg.qbittorrent.enable {
dynamicConfigOptions = {
http = {
services = {
qbittorrent.loadBalancer.servers = [{url = "http://localhost:${toString cfg.qbittorrent.port}";}];
};
routers = {
qbittorrent = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.qbittorrent.url}`)";
service = "qbittorrent";
tls.certResolver = "letsencrypt";
};
};
};
};
})
(lib.mkIf cfg.slskd.enable {
dynamicConfigOptions = {
http = {
services = {
slskd.loadBalancer.servers = [{url = "http://localhost:${toString cfg.slskd.port}";}];
};
routers = {
slskd = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.slskd.url}`)";
service = "slskd";
tls.certResolver = "letsencrypt";
};
};
};
};
})
];
};
virtualisation.oci-containers.containers = lib.mkMerge [
(lib.mkIf cfg.gluetun.enable {
gluetun = {
image = "qmcgaw/gluetun";
ports = [
"8388:8388"
"58846:58846"
"8080:8080"
"5030:5030"
"5031:5031"
"50300:50300"
];
devices = ["/dev/net/tun:/dev/net/tun"];
autoStart = true;
extraOptions = [
"--cap-add=NET_ADMIN"
];
volumes = ["/var:/gluetun"];
environmentFiles = [
config.age.secrets.gluetunEnvironment.path
];
environment = {
DEV_MODE = "false";
VPN_SERVICE_PROVIDER = "mullvad";
VPN_TYPE = "wireguard";
SERVER_CITIES = "Stockholm";
};
};
})
(lib.mkIf cfg.qbittorrent.enable {
qbittorrent = {
image = "ghcr.io/hotio/qbittorrent:latest";
autoStart = true;
dependsOn = ["gluetun"];
ports = [
"8080:8080"
"58846:58846"
];
extraOptions = [
"--network=container:gluetun"
];
volumes = [
"/var/lib/qbittorrent:/config:rw"
"/mnt/data/media/downloads:/downloads:rw"
];
environmentFiles = [
config.age.secrets.gluetunEnvironment.path
];
environment = {
PUID = "994";
PGID = "993";
TZ = "Europe/Stockholm";
WEBUI_PORT = "${builtins.toString cfg.qbittorrent.port}";
};
};
})
(lib.mkIf cfg.slskd.enable {
slskd = {
image = "slskd/slskd:latest";
autoStart = true;
dependsOn = ["gluetun"];
ports = [
"5030:5030"
"5031:5031"
"50300:50300"
];
extraOptions = [
"--network=container:gluetun"
];
volumes = [
"/var/lib/slskd:/app:rw"
"/mnt/data/media/downloads:/downloads:rw"
];
environmentFiles = [
config.age.secrets.gluetunEnvironment.path
config.age.secrets.slskd.path
];
environment = {
TZ = "Europe/Stockholm";
PUID = "981";
PGID = "982";
SLSKD_REMOTE_CONFIGURATION = "true";
SLSKD_REMOTE_FILE_MANAGEMENT = "true";
SLSKD_DOWNLOADS_DIR = "/downloads";
SLSKD_UMASK = "022";
};
};
})
(lib.mkIf cfg.pihole.enable {
pihole = {
autoStart = true;
image = "pihole/pihole:2025.08.0";
volumes = [
"/var/lib/pihole:/etc/pihole/"
"/var/lib/dnsmasq.d:/etc/dnsmasq.d/"
];
environment = {
TZ = "Europe/Stockholm";
CUSTOM_CACHE_SIZE = "0";
WEBTHEME = "default-darker";
};
environmentFiles = getPiholeSecret config.networking.hostName;
ports = [
"53:53/tcp"
"53:53/udp"
"8053:80/tcp"
];
extraOptions = [
"--cap-add=NET_ADMIN"
"--cap-add=SYS_NICE"
"--cap-add=SYS_TIME"
];
};
})
];
};
}

View File

@@ -1,80 +0,0 @@
{
config,
lib,
...
}: let
unit = "prowlarr";
srv = config.server;
cfg = config.server.${unit};
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "${unit}.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Prowlarr";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "PVR indexer";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "prowlarr.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Arr";
};
};
config = lib.mkIf cfg.enable {
services = {
${unit} = {
enable = true;
};
flaresolverr = {
enable = true;
};
traefik = {
dynamicConfigOptions = {
http = {
services = {
prowlarr = {
loadBalancer.servers = [{url = "http://127.0.0.1:9696";}];
};
flaresolverr = {
loadBalancer.servers = [{url = "http://127.0.0.1:8191";}];
};
};
routers = {
prowlarr = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "prowlarr";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
flaresolverr = {
entryPoints = ["websecure"];
rule = "Host(`flaresolverr.${srv.domain}`)";
service = "flaresolverr";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
};
};
}

View File

@@ -1,62 +0,0 @@
{
config,
lib,
...
}: let
unit = "radarr";
srv = config.server;
cfg = config.server.${unit};
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "${unit}.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Radarr";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Film collection manager";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "radarr.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Arr";
};
};
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
services.traefik = {
dynamicConfigOptions = {
http = {
services.radarr.loadBalancer.servers = [{url = "http://127.0.0.1:7878";}];
routers = {
radarr = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "radarr";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
};
}

View File

@@ -0,0 +1,17 @@
{
config,
lib,
...
}: let
unit = "bazarr";
srv = config.server;
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
};
}

View File

@@ -0,0 +1,16 @@
{
config,
lib,
...
}: let
unit = "flaresolverr";
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services = {
${unit} = {
enable = true;
};
};
};
}

View File

@@ -6,60 +6,15 @@
... ...
}: let }: let
unit = "gitea"; unit = "gitea";
srv = config.server; cfg = config.server.services.${unit};
cfg = config.server.${unit};
in { in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "git.${srv.domain}";
};
port = lib.mkOption {
type = lib.types.int;
default = 5003;
description = "The port to host Gitea on.";
};
cloudflared = {
credentialsFile = lib.mkOption {
type = lib.types.str;
example = lib.literalExpression ''
pkgs.writeText "cloudflare-credentials.json" '''
{"AccountTag":"secret"."TunnelSecret":"secret","TunnelID":"secret"}
'''
'';
};
tunnelId = lib.mkOption {
type = lib.types.str;
example = "00000000-0000-0000-0000-000000000000";
};
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Gitea";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Git with a cup of tea";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "gitea.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Services";
};
};
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
age.secrets = { age.secrets = {
giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age"; giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age";
}; };
server = { server.infra = {
fail2ban = lib.mkIf config.server.fail2ban.enable { fail2ban = {
jails = { jails = {
gitea = { gitea = {
serviceName = "gitea"; serviceName = "gitea";
@@ -144,24 +99,7 @@ in {
}; };
}; };
services.traefik = { server.infra.postgresql.databases = [
dynamicConfigOptions = {
http = {
services.gitea.loadBalancer.servers = [{url = "http://127.0.0.1:5003";}];
routers = {
gitea = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "gitea";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
server.postgresql.databases = [
{ {
database = "gitea"; database = "gitea";
} }

View File

@@ -5,37 +5,9 @@
... ...
}: let }: let
unit = "homepage-dashboard"; unit = "homepage-dashboard";
cfg = config.server.homepage-dashboard; cfg = config.server.services.homepage-dashboard;
srv = config.server; srv = config.server;
in { in {
options.server.homepage-dashboard = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
misc = lib.mkOption {
default = [];
type = lib.types.listOf (
lib.types.attrsOf (
lib.types.submodule {
options = {
description = lib.mkOption {
type = lib.types.str;
};
href = lib.mkOption {
type = lib.types.str;
};
siteMonitor = lib.mkOption {
type = lib.types.str;
};
icon = lib.mkOption {
type = lib.types.str;
};
};
}
)
);
};
};
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
age.secrets = { age.secrets = {
homepageEnvironment = { homepageEnvironment = {
@@ -112,31 +84,24 @@ in {
"Downloads" "Downloads"
"Services" "Services"
]; ];
hl = config.server; hl = config.server.services;
mergedServices = hl // hl.podman;
homepageServices = x: (lib.attrsets.filterAttrs ( homepageServices = x: (lib.attrsets.filterAttrs (
name: value: value ? homepage && value.homepage.category == x _name: value: value ? homepage && value.homepage.category == x
) )
mergedServices); srv.services);
in in
lib.lists.forEach homepageCategories (cat: { lib.lists.forEach homepageCategories (cat: {
"${cat}" = "${cat}" =
lib.lists.forEach lib.lists.forEach (lib.attrsets.mapAttrsToList (name: _value: name) (homepageServices "${cat}"))
(lib.attrsets.mapAttrsToList (name: value: {
inherit name;
url = value.url;
homepage = value.homepage;
}) (homepageServices "${cat}"))
(x: { (x: {
"${x.homepage.name}" = { "${hl.${x}.homepage.name}" = {
icon = x.homepage.icon; icon = hl.${x}.homepage.icon;
description = x.homepage.description; description = hl.${x}.homepage.description;
href = "https://${x.url}${x.homepage.path or ""}"; href = "https://${hl.${x}.url}";
siteMonitor = "https://${x.url}${x.homepage.path or ""}"; siteMonitor = "https://${hl.${x}.url}";
}; };
}); });
}) })
++ [{Misc = cfg.misc;}]
++ [ ++ [
{ {
Glances = let Glances = let
@@ -212,25 +177,6 @@ in {
} }
]; ];
}; };
traefik = {
dynamicConfigOptions = {
http = {
services.homepage.loadBalancer.servers = [
{url = "http://127.0.0.1:${toString config.services.${unit}.listenPort}";}
];
routers = {
homepage = {
entryPoints = ["websecure"];
rule = "Host(`cnix.dev`)";
service = "homepage";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
}; };
}; };
} }

View File

@@ -0,0 +1,21 @@
{
config,
lib,
pkgs,
...
}: let
unit = "jellyfin";
cfg = config.server.services.${unit};
srv = config.server;
in {
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
environment.systemPackages = with pkgs; [
jellyfin-ffmpeg
];
};
}

View File

@@ -0,0 +1,15 @@
{
config,
lib,
...
}: let
unit = "jellyseerr";
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
port = cfg.port;
};
};
}

View File

@@ -0,0 +1,17 @@
{
config,
lib,
...
}: let
unit = "lidarr";
srv = config.server;
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
};
}

View File

@@ -0,0 +1,17 @@
{
config,
lib,
...
}: let
unit = "n8n";
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services = {
n8n = {
enable = true;
openFirewall = true;
};
};
};
}

View File

@@ -6,52 +6,16 @@
... ...
}: let }: let
unit = "nextcloud"; unit = "nextcloud";
cfg = config.server.${unit}; cfg = config.server.services.${unit};
srv = config.server; srv = config.server;
in { in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
adminpassFile = lib.mkOption {
type = lib.types.path;
};
adminuser = lib.mkOption {
type = lib.types.str;
default = "cnst";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "cloud.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Nextcloud";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "A safe home for all your data";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "nextcloud.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Services";
};
};
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
age.secrets = { age.secrets = {
nextcloudAdminPass.file = "${self}/secrets/nextcloudAdminPass.age"; nextcloudAdminPass.file = "${self}/secrets/nextcloudAdminPass.age";
nextcloudCloudflared.file = "${self}/secrets/nextcloudCloudflared.age"; nextcloudCloudflared.file = "${self}/secrets/nextcloudCloudflared.age";
}; };
server.fail2ban = lib.mkIf config.server.fail2ban.enable { server.infra.fail2ban = lib.mkIf srv.infra.fail2ban.enable {
jails = { jails = {
nextcloud = { nextcloud = {
serviceName = "${unit}"; serviceName = "${unit}";
@@ -107,7 +71,7 @@ in {
dbhost = "/run/postgresql"; dbhost = "/run/postgresql";
dbname = "nextcloud"; dbname = "nextcloud";
adminuser = "cnst"; adminuser = "cnst";
adminpassFile = cfg.adminpassFile; adminpassFile = config.age.secrets.nextcloudAdminPass.path;
}; };
}; };
@@ -126,21 +90,9 @@ in {
forceSSL = false; forceSSL = false;
}; };
}; };
traefik.dynamicConfigOptions.http = {
routers.nextcloud = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "nextcloud";
tls.certResolver = "letsencrypt";
};
services.nextcloud.loadBalancer.servers = [
{url = "http://127.0.0.1:8182";}
];
};
}; };
server.postgresql.databases = [ server.infra.postgresql.databases = [
{ {
database = "nextcloud"; database = "nextcloud";
} }

View File

@@ -0,0 +1,16 @@
{
config,
lib,
...
}: let
unit = "prowlarr";
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services = {
${unit} = {
enable = true;
};
};
};
}

View File

@@ -0,0 +1,17 @@
{
config,
lib,
...
}: let
unit = "radarr";
srv = config.server;
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
};
}

View File

@@ -0,0 +1,17 @@
{
config,
lib,
...
}: let
unit = "sonarr";
srv = config.server;
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
};
}

View File

@@ -0,0 +1,16 @@
{
config,
lib,
...
}: let
unit = "uptime-kuma";
cfg = config.server.services.${unit};
in {
config = lib.mkIf cfg.enable {
services = {
${unit} = {
enable = true;
};
};
};
}

View File

@@ -5,57 +5,37 @@
self, self,
... ...
}: let }: let
inherit (lib) mkIf mkEnableOption; unit = "vaultwarden";
vcfg = config.services.vaultwarden.config; cfg = config.server.services.${unit};
cfg = config.server.vaultwarden; www = config.server.infra.www;
in { in {
options = { config = lib.mkIf cfg.enable {
server.vaultwarden = {
enable = mkEnableOption "Enables vaultwarden";
url = lib.mkOption {
type = lib.types.str;
default = "${cfg.domain}";
};
cloudflared = {
credentialsFile = lib.mkOption {
type = lib.types.str;
example = lib.literalExpression ''
pkgs.writeText "cloudflare-credentials.json" '''
{"AccountTag":"secret"."TunnelSecret":"secret","TunnelID":"secret"}
'''
'';
};
tunnelId = lib.mkOption {
type = lib.types.str;
example = "00000000-0000-0000-0000-000000000000";
};
};
};
};
config = mkIf cfg.enable {
age.secrets = { age.secrets = {
vaultwardenCloudflared.file = "${self}/secrets/vaultwardenCloudflared.age"; vaultwardenCloudflared.file = "${self}/secrets/vaultwardenCloudflared.age";
vaultwardenEnvironment.file = "${self}/secrets/vaultwardenEnvironment.age"; vaultwardenEnvironment.file = "${self}/secrets/vaultwardenEnvironment.age";
}; };
server = { server.infra = {
fail2ban = lib.mkIf config.server.fail2ban.enable { fail2ban = {
jails = { jails = {
vaultwarden = { vaultwarden = {
serviceName = "vaultwarden"; serviceName = "${unit}";
failRegex = ''^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$''; failRegex = ''^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$'';
}; };
}; };
}; };
}; };
systemd.services.backup-vaultwarden.serviceConfig = {
User = "root";
Group = "root";
};
services = { services = {
cloudflared = {
enable = true;
tunnels.${cfg.cloudflared.tunnelId} = {
credentialsFile = cfg.cloudflared.credentialsFile;
default = "http_status:404";
ingress."${cfg.url}".service = "http://localhost:${toString cfg.port}";
};
};
vaultwarden = { vaultwarden = {
enable = true; enable = true;
environmentFile = config.age.secrets.vaultwardenEnvironment.path; environmentFile = config.age.secrets.vaultwardenEnvironment.path;
@@ -63,10 +43,10 @@ in {
backupDir = "/var/backup/vaultwarden"; backupDir = "/var/backup/vaultwarden";
config = { config = {
DOMAIN = "https://${cfg.url}"; DOMAIN = "https://vault.${www.url}";
SIGNUPS_ALLOWED = false; SIGNUPS_ALLOWED = false;
ROCKET_ADDRESS = "127.0.0.1"; ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = 8222; ROCKET_PORT = cfg.port;
IP_HEADER = "CF-Connecting-IP"; IP_HEADER = "CF-Connecting-IP";
logLevel = "warn"; logLevel = "warn";
@@ -76,14 +56,10 @@ in {
showPasswordHint = false; showPasswordHint = false;
}; };
}; };
cloudflared = { };
enable = true; systemd.services.backup-vaultwarden.serviceConfig = {
tunnels.${cfg.cloudflared.tunnelId} = { User = "root";
credentialsFile = cfg.cloudflared.credentialsFile; Group = "root";
default = "http_status:404";
ingress."${cfg.url}".service = "http://${vcfg.ROCKET_ADDRESS}:${toString vcfg.ROCKET_PORT}";
};
};
}; };
}; };
} }

View File

@@ -1,62 +0,0 @@
{
config,
lib,
...
}: let
unit = "sonarr";
srv = config.server;
cfg = config.server.${unit};
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/${unit}";
};
url = lib.mkOption {
type = lib.types.str;
default = "${unit}.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Sonarr";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Series collection manager";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "sonarr.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Arr";
};
};
config = lib.mkIf cfg.enable {
services.${unit} = {
enable = true;
user = srv.user;
group = srv.group;
};
services.traefik = {
dynamicConfigOptions = {
http = {
services.sonarr.loadBalancer.servers = [{url = "http://127.0.0.1:8989";}];
routers = {
sonarr = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "sonarr";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
};
}

View File

@@ -1,104 +0,0 @@
{
lib,
config,
pkgs,
self,
...
}: let
inherit (lib) mkEnableOption mkIf types;
cfg = config.server.traefik;
getCloudflareCredentials = hostname:
if hostname == "ziggy"
then config.age.secrets.cloudflareDnsCredentialsZiggy.path
else if hostname == "sobotka"
then config.age.secrets.cloudflareDnsCredentials.path
else throw "Unknown hostname: ${hostname}";
in {
options.server.traefik = {
enable = mkEnableOption "Enable global Traefik reverse proxy with ACME";
};
config = mkIf cfg.enable {
age.secrets.traefikEnv = {
file = "${self}/secrets/traefikEnv.age";
mode = "640";
owner = "traefik";
group = "traefik";
};
systemd.services.traefik = {
serviceConfig = {
EnvironmentFile = [config.age.secrets.traefikEnv.path];
};
};
networking.firewall.allowedTCPPorts = [80 443];
services = {
tailscale.permitCertUid = "traefik";
traefik = {
enable = true;
staticConfigOptions = {
log = {
level = "DEBUG";
};
tracing = {};
api = {
dashboard = true;
};
certificatesResolvers = {
vpn.tailscale = {};
letsencrypt = {
acme = {
email = "adam@cnst.dev";
storage = "/var/lib/traefik/cert.json";
dnsChallenge = {
provider = "cloudflare";
resolvers = [
"1.1.1.1:53"
"1.0.0.1:53"
];
};
};
};
};
entryPoints = {
redis = {
address = "0.0.0.0:6381";
};
postgres = {
address = "0.0.0.0:5433";
};
web = {
address = "0.0.0.0:80";
http.redirections.entryPoint = {
to = "websecure";
scheme = "https";
permanent = true;
};
};
websecure = {
address = "0.0.0.0:443";
http.tls = {
certResolver = "letsencrypt";
domains = [
{
main = "cnix.dev";
sans = ["*.cnix.dev"];
}
{
main = "ts.cnst.dev";
sans = ["*ts.cnst.dev"];
}
];
};
};
};
};
};
};
};
}

View File

@@ -1,62 +0,0 @@
{
config,
lib,
...
}: let
unit = "uptime-kuma";
cfg = config.server.${unit};
srv = config.server;
in {
options.server.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
configDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/uptime-kuma";
};
url = lib.mkOption {
type = lib.types.str;
default = "uptime.${srv.domain}";
};
homepage.name = lib.mkOption {
type = lib.types.str;
default = "Uptime Kuma";
};
homepage.description = lib.mkOption {
type = lib.types.str;
default = "Service monitoring tool";
};
homepage.icon = lib.mkOption {
type = lib.types.str;
default = "uptime-kuma.svg";
};
homepage.category = lib.mkOption {
type = lib.types.str;
default = "Services";
};
};
config = lib.mkIf cfg.enable {
services = {
${unit} = {
enable = true;
};
traefik = {
dynamicConfigOptions = {
http = {
services.uptime-kuma.loadBalancer.servers = [{url = "http://127.0.0.1:3001";}];
routers = {
uptime-kuma = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`)";
service = "uptime-kuma";
tls.certResolver = "letsencrypt";
# middlewares = ["authentik"];
};
};
};
};
};
};
};
}

11
secrets/crowdsecApi.age Normal file
View File

@@ -0,0 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 t9iOEg lf7aPbZX2v3WGzE/KI/069DBObphqrDtjq7rhNriGl8
Vv+Pqk6DbcE5R1A9135gVKroCex1xKsCLPETZdT3yTg
-> ssh-ed25519 KUYMFA XxtBSmCwrQCZ9G3VcCrbzTdMshTK1pjlHPYj7fke818
9tO2EcnHPD6v3TNeuZdL+zP39SM5R5q7om5sCFDB8lg
-> ssh-ed25519 76RhUQ I6O/fYFRqYxExC9uLijZr6/kFze7uze0cIudCsl2jTo
WAwb822vVj5UtUAdE1oVJ0/q6nQbWqdx0OHuGEogO7M
-> ssh-ed25519 Jf8sqw gWBoe4HhXNw7Ih58lQ/L2vBoQfbU1ht8+ZSLUx/4TWk
xor0ieJ2UI5bK4rSlCM0dX61PVbxYE37FNry0YSmHG4
--- Cp8b3eTb3NfjPFvBE12a2c+Yni2jW6DZUK10IaXmmvo
w<EFBFBD>xq<EFBFBD><EFBFBD>z:<3A>.{<7B>?<3F><><EFBFBD>f<EFBFBD><66><1D><><16><>A<EFBFBD>jT<6A><54>{<7B>J <20><>

View File

@@ -0,0 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 t9iOEg MLi7IOM8QlpvlCMSmo4SwZbTwZ9pyysSbiMMuWD/dyU
cotV5TJf7oyyXIaAmu8n9Ie1rl27i8w7hsduwtQFnis
-> ssh-ed25519 KUYMFA BhFQ/RXOH8L7gl/FSabAUv28fbaod+muvTGSV3rYQSs
fWqwAkhSAmg6YB+yEtj0e83Q4XO/r+TBnMTN7vXBNqU
-> ssh-ed25519 76RhUQ b1fDfGPNdJ9c3wtr8ww0mW5K4fKJxpxxTZy/ZCECWzs
qhbvucUrv7dzOPKUmUaRs/AtXtwQfy/qp5HnaYzZ5eQ
-> ssh-ed25519 Jf8sqw 19D2ztjyxJfGQAiUOTdgWyC0ZFso/wrC9VPEkmI34U8
PavT5O8M6Zc2Num9Hb2sY+F3UmMPqRgjUZxuvP6AhyM
--- uYOcbsL7JWoDF2mRUDLhXrbp6ssLFbQ9+a6RhAXNNPA
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> X<><58>PC<50><43>h<EFBFBD>;

View File

@@ -63,6 +63,8 @@ in {
"wwwCloudflared.age".publicKeys = kima ++ sobotka; "wwwCloudflared.age".publicKeys = kima ++ sobotka;
"authentikCloudflared.age".publicKeys = kima ++ sobotka; "authentikCloudflared.age".publicKeys = kima ++ sobotka;
"sobotkaTsAuth.age".publicKeys = kima ++ sobotka; "sobotkaTsAuth.age".publicKeys = kima ++ sobotka;
"mikrotikSecret.age".publicKeys = kima ++ sobotka;
"crowdsecApi.age".publicKeys = kima ++ sobotka;
# Ziggy-specific # Ziggy-specific
"cloudflareDnsCredentialsZiggy.age".publicKeys = kima ++ ziggy; "cloudflareDnsCredentialsZiggy.age".publicKeys = kima ++ ziggy;