Merge pull request 'refactor' (#6) from refactor into main

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2025-10-14 21:56:13 +02:00
57 changed files with 1565 additions and 1894 deletions

View File

@@ -1,9 +1,8 @@
{
description = "cnix nix";
outputs =
inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
outputs = inputs:
inputs.flake-parts.lib.mkFlake {inherit inputs;} {
systems = [
"x86_64-linux"
"aarch64-linux"
@@ -17,25 +16,23 @@
./fmt-hooks.nix
];
perSystem =
{
config,
pkgs,
...
}:
{
devShells.default = pkgs.mkShell {
packages = [
pkgs.git
config.packages.repl
];
name = "dots";
env.DIRENV_LOG_FORMAT = "";
shellHook = ''
${config.pre-commit.installationScript}
'';
};
perSystem = {
config,
pkgs,
...
}: {
devShells.default = pkgs.mkShell {
packages = [
pkgs.git
config.packages.repl
];
name = "dots";
env.DIRENV_LOG_FORMAT = "";
shellHook = ''
${config.pre-commit.installationScript}
'';
};
};
};
inputs = {

View File

@@ -4,114 +4,111 @@
homeImports,
self,
...
}:
{
flake.nixosConfigurations =
let
cLib = import ../lib inputs.nixpkgs.lib;
userConfig = "${self}/home";
systemConfig = "${self}/system";
hostConfig = "${self}/hosts";
}: {
flake.nixosConfigurations = let
# clib = import ../lib inputs.nixpkgs.lib;
userConfig = "${self}/home";
systemConfig = "${self}/system";
hostConfig = "${self}/hosts";
cnstConfig = "${self}/users/cnst";
toothpickConfig = "${self}/users/toothpick";
cnstConfig = "${self}/users/cnst";
toothpickConfig = "${self}/users/toothpick";
umodPath = "${self}/modules/home";
smodPath = "${self}/modules/system";
umodPath = "${self}/modules/home";
smodPath = "${self}/modules/system";
inherit (inputs.nixpkgs.lib) nixosSystem;
inherit (self) outputs;
inherit (inputs.nixpkgs.lib) nixosSystem;
inherit (self) outputs;
specialArgs = {
inherit
cLib
inputs
outputs
self
userConfig
systemConfig
hostConfig
cnstConfig
toothpickConfig
umodPath
smodPath
;
};
in
{
kima = nixosSystem {
inherit specialArgs;
modules = [
./kima
"${self}/nix"
{
home-manager = {
users.cnst.imports = homeImports."cnst@kima";
extraSpecialArgs = specialArgs;
};
}
self.nixosModules.nixos
self.nixosModules.settings
inputs.chaotic.nixosModules.default
inputs.agenix.nixosModules.default
];
};
bunk = nixosSystem {
inherit specialArgs;
modules = [
./bunk
"${self}/nix"
{
home-manager = {
users.cnst.imports = homeImports."cnst@bunk";
extraSpecialArgs = specialArgs;
};
}
self.nixosModules.nixos
self.nixosModules.settings
inputs.chaotic.nixosModules.default
inputs.agenix.nixosModules.default
];
};
sobotka = nixosSystem {
inherit specialArgs;
modules = [
./sobotka
"${self}/nix"
self.nixosModules.nixos
self.nixosModules.settings
self.nixosModules.server
inputs.agenix.nixosModules.default
inputs.authentik.nixosModules.default
];
};
ziggy = nixosSystem {
inherit specialArgs;
modules = [
./ziggy
"${self}/nix"
self.nixosModules.nixos
self.nixosModules.settings
self.nixosModules.server
inputs.agenix.nixosModules.default
];
};
toothpc = nixosSystem {
inherit specialArgs;
modules = [
./toothpc
"${self}/nix"
{
home-manager = {
users.toothpick.imports = homeImports."toothpick@toothpc";
extraSpecialArgs = specialArgs;
};
}
self.nixosModules.nixos
self.nixosModules.settings
inputs.chaotic.nixosModules.default
inputs.agenix.nixosModules.default
];
};
specialArgs = {
inherit
# clib
inputs
outputs
self
userConfig
systemConfig
hostConfig
cnstConfig
toothpickConfig
umodPath
smodPath
;
};
in {
kima = nixosSystem {
inherit specialArgs;
modules = [
./kima
"${self}/nix"
{
home-manager = {
users.cnst.imports = homeImports."cnst@kima";
extraSpecialArgs = specialArgs;
};
}
self.nixosModules.nixos
self.nixosModules.settings
inputs.chaotic.nixosModules.default
inputs.agenix.nixosModules.default
];
};
bunk = nixosSystem {
inherit specialArgs;
modules = [
./bunk
"${self}/nix"
{
home-manager = {
users.cnst.imports = homeImports."cnst@bunk";
extraSpecialArgs = specialArgs;
};
}
self.nixosModules.nixos
self.nixosModules.settings
inputs.chaotic.nixosModules.default
inputs.agenix.nixosModules.default
];
};
sobotka = nixosSystem {
inherit specialArgs;
modules = [
./sobotka
"${self}/nix"
self.nixosModules.nixos
self.nixosModules.settings
self.nixosModules.server
inputs.agenix.nixosModules.default
inputs.authentik.nixosModules.default
];
};
ziggy = nixosSystem {
inherit specialArgs;
modules = [
./ziggy
"${self}/nix"
self.nixosModules.nixos
self.nixosModules.settings
self.nixosModules.server
inputs.agenix.nixosModules.default
];
};
toothpc = nixosSystem {
inherit specialArgs;
modules = [
./toothpc
"${self}/nix"
{
home-manager = {
users.toothpick.imports = homeImports."toothpick@toothpc";
extraSpecialArgs = specialArgs;
};
}
self.nixosModules.nixos
self.nixosModules.settings
inputs.chaotic.nixosModules.default
inputs.agenix.nixosModules.default
];
};
};
}

View File

@@ -3,108 +3,264 @@
enable = true;
email = "adam@cnst.dev";
domain = "cnix.dev";
ip = "192.168.88.14";
user = "share";
group = "share";
uid = 994;
gid = 993;
traefik = {
enable = true;
};
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;
infra = {
authentik = {
enable = true;
url = "auth.cnst.dev";
port = 9000;
cloudflared = {
tunnelId = "b66f9368-db9e-4302-8b48-527cda34a635";
credentialsFile = config.age.secrets.authentikCloudflared.path;
};
};
traefik = {
enable = true;
};
tailscale = {
enable = true;
};
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;
};
};
};
vaultwarden = {
enable = true;
url = "vault.cnst.dev";
cloudflared = {
tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5";
credentialsFile = config.age.secrets.vaultwardenCloudflared.path;
services = {
homepage-dashboard = {
enable = true;
subdomain = "dash";
exposure = "local";
port = 8082;
};
};
www = {
enable = true;
url = "cnst.dev";
cloudflared = {
tunnelId = "e5076186-efb7-405a-998c-6155af7fb221";
credentialsFile = config.age.secrets.wwwCloudflared.path;
n8n = {
enable = true;
subdomain = "n8n";
exposure = "local";
port = 5678;
homepage = {
name = "n8n";
description = "A workflow automation platform";
icon = "n8n.svg";
category = "Services";
};
};
};
authentik = {
enable = true;
url = "auth.cnst.dev";
cloudflared = {
tunnelId = "b66f9368-db9e-4302-8b48-527cda34a635";
credentialsFile = config.age.secrets.authentikCloudflared.path;
bazarr = {
enable = true;
subdomain = "bazarr";
exposure = "local";
port = 6767;
homepage = {
name = "Bazarr";
description = "Subtitle manager";
icon = "bazarr.svg";
category = "Arr";
};
};
prowlarr = {
enable = true;
subdomain = "prowlarr";
exposure = "local";
port = 9696;
homepage = {
name = "prowlarr";
description = "PVR indexer";
icon = "prowlarr.svg";
category = "Arr";
};
};
flaresolverr = {
enable = true;
subdomain = "flaresolverr";
exposure = "local";
port = 8191;
homepage = {
name = "FlareSolverr";
description = "Proxy to bypass Cloudflare/DDoS-GUARD protection";
icon = "flaresolverr.svg";
category = "Arr";
};
};
lidarr = {
enable = true;
subdomain = "lidarr";
exposure = "local";
port = 8686;
homepage = {
name = "Lidarr";
description = "Music collection manager";
icon = "lidarr.svg";
category = "Arr";
};
};
sonarr = {
enable = true;
subdomain = "sonarr";
exposure = "local";
port = 8989;
homepage = {
name = "Sonarr";
description = "Internet PVR for Usenet and Torrents";
icon = "sonarr.svg";
category = "Arr";
};
};
radarr = {
enable = true;
subdomain = "radarr";
exposure = "local";
port = 7878;
homepage = {
name = "Radarr";
description = "Movie collection manager";
icon = "radarr.svg";
category = "Arr";
};
};
jellyseerr = {
enable = true;
subdomain = "jellyseerr";
exposure = "local";
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";
exposure = "local";
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;
homepage = {
name = "Nextcloud";
description = "A safe home for all your data";
icon = "nextcloud.svg";
category = "Services";
};
};
};
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;
subdomain = "qbt";
exposure = "local";
port = 8080;
homepage = {
name = "qBittorrent";
description = "Torrent client";
icon = "qbittorrent.svg";
category = "Downloads";
};
};
slskd = {
enable = true;
subdomain = "slskd";
exposure = "local";
port = 5030;
homepage = {
name = "Soulseek";
description = "Web-based Soulseek client";
icon = "slskd.svg";
category = "Downloads";
};
};
pihole = {
enable = true;
subdomain = "pihole";
exposure = "local";
port = 8053;
homepage = {
name = "PiHole";
description = "Adblocking and DNS service";
icon = "pi-hole.svg";
category = "Services";
path = "/admin";
};
};
};
};

View File

@@ -4,6 +4,10 @@
username = "cnst";
mail = "adam@cnst.dev";
sshUser = "sobotka";
domains = {
local = "cnix.dev";
public = "cnst.dev";
};
};
};
}

26
lib/server/default.nix Normal file
View File

@@ -0,0 +1,26 @@
{lib}: let
server = {
mkDomain = config: service: let
localDomain = config.settings.accounts.domains.local;
publicDomain = config.settings.accounts.domains.public;
tailscaleDomain = "ts.${publicDomain}";
in
if service.exposure == "tunnel"
then publicDomain
else if service.exposure == "tailscale"
then tailscaleDomain
else localDomain;
mkFullDomain = config: service: let
domain = server.mkDomain config service;
in "${service.subdomain}.${domain}";
mkHostDomain = config: service: let
domain = server.mkDomain config service;
in "${domain}";
mkSubDomain = config: service: "${service.subdomain}";
};
in {
server = server;
}

View File

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

View File

@@ -3,10 +3,9 @@
pkgs,
lib,
osConfig,
cLib,
clib,
...
}:
let
}: let
inherit (lib) mkIf mkEnableOption;
cfg = osConfig.nixos.programs.hyprland;
@@ -14,9 +13,8 @@ let
# hyprlockPkg = pkgs.hyprlock;
#
bg = osConfig.settings.theme.background;
inherit (cLib.theme.bgs) resolve;
in
{
inherit (clib.theme.bgs) resolve;
in {
config = mkIf cfg.enable {
programs.hyprlock = {
enable = true;

View File

@@ -3,16 +3,15 @@
pkgs,
inputs,
osConfig,
cLib,
clib,
...
}:
let
}: let
inherit (lib) mkIf;
cfg = osConfig.nixos.programs.hyprland;
hyprpaperFlake = inputs.hyprpaper.packages.${pkgs.system}.default;
bg = osConfig.settings.theme.background;
bgs = cLib.theme.bgs;
bgs = clib.theme.bgs;
monitorMappings = [
{
@@ -32,8 +31,7 @@ let
bg = bg.primary;
}
];
in
{
in {
config = mkIf cfg.enable {
services.hyprpaper = {
enable = true;

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

@@ -1,101 +1,16 @@
{
self,
lib,
config,
pkgs,
...
}: let
hardDrives = [
"/dev/disk/by-label/data"
];
inherit (lib) mkOption types;
cfg = config.server;
ifTheyExist = groups: builtins.filter (group: builtins.hasAttr group config.users.groups) groups;
clib = import "${self}/lib/server" {inherit lib;};
in {
options.server = {
enable = lib.mkEnableOption "The server services and configuration variables";
email = mkOption {
default = "";
type = types.str;
description = ''
Email name to be used to access the server services via Caddy reverse proxy
'';
};
domain = mkOption {
default = "";
type = types.str;
description = ''
Domain name to be used to access the server services via Caddy reverse proxy
'';
};
user = lib.mkOption {
default = "share";
type = lib.types.str;
description = ''
User to run the server services as
'';
};
group = lib.mkOption {
default = "share";
type = lib.types.str;
description = ''
Group to run the server services as
'';
};
uid = lib.mkOption {
default = 1000;
type = lib.types.int;
description = ''
UID to run the server services as
'';
};
gid = lib.mkOption {
default = 1000;
type = lib.types.int;
description = ''
GID to run the server services as
'';
};
timeZone = lib.mkOption {
default = "Europe/Stockholm";
type = lib.types.str;
description = ''
Time zone to be used for the server services
'';
};
};
config = lib.mkIf cfg.enable {
users = {
groups.${cfg.group} = {
gid = cfg.gid;
};
users.${cfg.user} = {
uid = cfg.uid;
isSystemUser = true;
group = cfg.group;
extraGroups = ifTheyExist [
"audio"
"video"
"docker"
"libvirtd"
"qemu-libvirtd"
"rtkit"
"fail2ban"
"vaultwarden"
"qbittorrent"
"lidarr"
"prowlarr"
"bazarr"
"sonarr"
"radarr"
"media"
"share"
"render"
"input"
"authentik"
"traefik"
];
};
};
};
imports = [
{
_module.args.clib = clib;
}
./options.nix
./infra
./services
];
}

View File

@@ -1,170 +0,0 @@
# "inspired" by @jtojnar <3
{
config,
lib,
self,
...
}: let
unit = "gitea";
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 = "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 {
age.secrets = {
giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age";
};
server = {
fail2ban = lib.mkIf config.server.fail2ban.enable {
jails = {
gitea = {
serviceName = "gitea";
failRegex = ''.*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>'';
};
};
};
};
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}";
};
};
${unit} = {
enable = true;
appName = "cnix code forge";
database = {
type = "postgres";
socket = "/run/postgresql";
name = "gitea";
user = "gitea";
createDatabase = false;
};
lfs = {
enable = true;
};
settings = {
cors = {
ENABLED = true;
SCHEME = "https";
ALLOW_DOMAIN = cfg.url;
};
log = {
MODE = "console";
};
mailer = {
ENABLED = false;
MAILER_TYPE = "sendmail";
FROM = "noreply+adam@cnst.dev";
SENDMAIL_PATH = "/run/wrappers/bin/sendmail";
};
picture = {
DISABLE_GRAVATAR = true;
};
repository = {
DEFAULT_BRANCH = "main";
DEFAULT_REPO_UNITS = "repo.code,repo.issues,repo.pulls";
DISABLE_DOWNLOAD_SOURCE_ARCHIVES = true;
};
indexer = {
REPO_INDEXER_ENABLED = true;
};
oauth2_client = {
ENABLE_AUTO_REGISTRATION = true;
ACCOUNT_LINKING = "auto";
};
server = {
DOMAIN = cfg.url;
LANDING_PAGE = "explore";
HTTP_PORT = cfg.port;
ROOT_URL = "https://${cfg.url}/";
};
security = {
DISABLE_GIT_HOOKS = false;
};
service = {
DISABLE_REGISTRATION = true;
};
session = {
COOKIE_SECURE = true;
};
};
};
};
services.traefik = {
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";
}
];
};
}

View File

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

View File

@@ -0,0 +1,13 @@
{
imports = [
./authentik
./fail2ban
./keepalived
./podman
./postgres
./tailscale
./traefik
./unbound
./www
];
}

View File

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

View File

@@ -3,27 +3,24 @@
config,
self,
...
}:
let
}: let
unit = "keepalived";
cfg = config.server.${unit};
cfg = config.server.infra.${unit};
hostCfg =
hostname:
if hostname == "sobotka" then
{
ip = "192.168.88.14";
priority = 20;
state = "MASTER";
}
else if hostname == "ziggy" then
{
ip = "192.168.88.12";
priority = 10;
state = "BACKUP";
}
else
throw "No keepalived config defined for host ${hostname}";
hostCfg = hostname:
if hostname == "sobotka"
then {
ip = "192.168.88.14";
priority = 20;
state = "MASTER";
}
else if hostname == "ziggy"
then {
ip = "192.168.88.12";
priority = 10;
state = "BACKUP";
}
else throw "No keepalived config defined for host ${hostname}";
_self = hostCfg config.networking.hostName;
@@ -34,9 +31,8 @@ let
# Remove self from peers
peers = builtins.filter (ip: ip != _self.ip) allPeers;
in
{
options.server.${unit} = {
in {
options.server.infra.${unit} = {
enable = lib.mkEnableOption {
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
inherit (lib) types mkOption;
cfg = config.server.postgresql;
cfg = config.server.infra.postgresql;
in {
options = {
server.postgresql = {
server.infra.postgresql = {
upgradeTargetPackage = mkOption {
type = types.nullOr types.package;
default = null;

View File

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

View File

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

View File

@@ -0,0 +1,202 @@
{
lib,
clib,
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);
generateRouters = services: config:
lib.mapAttrs' (
name: service:
lib.nameValuePair name {
entryPoints = ["websecure"];
# FIX 3: Use backticks for the Host rule and interpolation
rule = "Host(`${clib.server.mkFullDomain config service}`)";
service = name;
tls.certResolver = "letsencrypt";
}
) (lib.filterAttrs (_: s: s.enable) services);
generateServices = services:
lib.mapAttrs' (name: service:
lib.nameValuePair name {
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 = {
services = generateServices srv.services;
routers =
(generateRouters srv.services config)
// {
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,25 @@
...
}: let
unit = "unbound";
cfg = config.server.${unit};
cfg = config.server.infra.${unit};
srv = config.server;
svcNames = lib.attrNames srv.services;
localARecords = builtins.concatLists (map (
name: let
s = srv.services.${name};
in
if s != null && s.enable && s.subdomain != null
then [''"${s.subdomain}.${srv.domain}. A ${srv.ip}"'']
else []
)
svcNames);
revParts = lib.lists.reverseList (lib.splitString "." srv.ip);
revName = lib.concatStringsSep "." revParts;
localPTRs = ["${revName}.in-addr.arpa. PTR traefik.${srv.domain}"];
hostIp = hostname:
if hostname == "ziggy"
@@ -14,11 +32,12 @@
then "192.168.88.14"
else throw "No IP defined for host ${hostname}";
in {
options.server.${unit} = {
options.server.infra.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
};
config = lib.mkIf cfg.enable {
services = {
# resolved.enable = lib.mkForce false;
@@ -97,6 +116,10 @@ in {
"255.255.255.255/32"
"2001:db8::/32"
];
local-data = localARecords;
# Example PTR entry: "14.88.168.192.in-addr.arpa. PTR traefik.cnix.dev."
# local-data-ptr = localPTRs;
};
};
};

View File

@@ -1,15 +1,13 @@
{
lib,
config,
pkgs,
self,
...
}: let
inherit (lib) mkOption mkEnableOption mkIf types;
cfg = config.server.www;
srv = config.server;
inherit (lib) mkIf mkEnableOption mkOption types;
cfg = config.server.infra.www;
in {
options.server.www = {
options.server.infra.www = {
enable = mkEnableOption {
description = "Enable personal website";
};
@@ -20,6 +18,12 @@ in {
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 = {
credentialsFile = lib.mkOption {
type = lib.types.str;
@@ -41,8 +45,8 @@ in {
wwwCloudflared.file = "${self}/secrets/wwwCloudflared.age";
};
server = {
fail2ban = lib.mkIf config.server.www.enable {
server.infra = {
fail2ban = {
jails = {
nginx-404 = {
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,153 +0,0 @@
{
config,
pkgs,
lib,
self,
...
}: let
unit = "nextcloud";
cfg = config.server.${unit};
srv = config.server;
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 {
age.secrets = {
nextcloudAdminPass.file = "${self}/secrets/nextcloudAdminPass.age";
nextcloudCloudflared.file = "${self}/secrets/nextcloudCloudflared.age";
};
server.fail2ban = lib.mkIf config.server.fail2ban.enable {
jails = {
nextcloud = {
serviceName = "${unit}";
_groupsre = ''(?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)'';
failRegex = ''
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Two-factor challenge failed:
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Trusted domain error.
'';
datePattern = '',?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"'';
};
};
};
services = {
${unit} = {
enable = true;
package = pkgs.nextcloud32;
hostName = "nextcloud";
configureRedis = true;
caching = {
redis = true;
};
phpOptions = {
"opcache.interned_strings_buffer" = "32";
};
maxUploadSize = "50G";
settings = {
maintenance_window_start = "1";
trusted_proxies = [
"127.0.0.1"
"::1"
];
trusted_domains = ["cloud.${srv.domain}"];
overwriteprotocol = "https";
enabledPreviewProviders = [
"OC\\Preview\\BMP"
"OC\\Preview\\GIF"
"OC\\Preview\\JPEG"
"OC\\Preview\\Krita"
"OC\\Preview\\MarkDown"
"OC\\Preview\\MP3"
"OC\\Preview\\OpenDocument"
"OC\\Preview\\PNG"
"OC\\Preview\\TXT"
"OC\\Preview\\XBitmap"
"OC\\Preview\\HEIC"
];
};
config = {
dbtype = "pgsql";
dbuser = "nextcloud";
dbhost = "/run/postgresql";
dbname = "nextcloud";
adminuser = "cnst";
adminpassFile = cfg.adminpassFile;
};
};
nginx = {
defaultListen = [
{
addr = "127.0.0.1";
port = 8182;
}
{
addr = "127.0.0.1";
port = 8482;
}
];
virtualHosts.nextcloud = {
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 = [
{
database = "nextcloud";
}
];
systemd.services."nextcloud-setup" = {
requires = ["postgresql.service"];
after = ["postgresql.service"];
};
};
}

180
modules/server/options.nix Normal file
View File

@@ -0,0 +1,180 @@
{
lib,
config,
...
}: let
inherit (lib) mkOption types;
ifTheyExist = groups: builtins.filter (group: builtins.hasAttr group config.users.groups) groups;
cfg = config.server;
in {
options.server = {
enable = lib.mkEnableOption "The server services and configuration variables";
email = mkOption {
default = "";
type = types.str;
description = ''
Email name to be used to access the server services via Caddy reverse proxy
'';
};
domain = mkOption {
default = "";
type = types.str;
description = ''
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 {
default = "share";
type = lib.types.str;
description = ''
User to run the server services as
'';
};
group = lib.mkOption {
default = "share";
type = lib.types.str;
description = ''
Group to run the server services as
'';
};
uid = lib.mkOption {
default = 1000;
type = lib.types.int;
description = ''
UID to run the server services as
'';
};
gid = lib.mkOption {
default = 1000;
type = lib.types.int;
description = ''
GID to run the server services as
'';
};
timeZone = lib.mkOption {
default = "Europe/Stockholm";
type = lib.types.str;
description = ''
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 = 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 {
users = {
groups.${cfg.group} = {
gid = cfg.gid;
};
users.${cfg.user} = {
uid = cfg.uid;
isSystemUser = true;
group = cfg.group;
extraGroups = ifTheyExist [
"audio"
"video"
"docker"
"libvirtd"
"qemu-libvirtd"
"rtkit"
"fail2ban"
"vaultwarden"
"qbittorrent"
"lidarr"
"prowlarr"
"bazarr"
"sonarr"
"radarr"
"media"
"share"
"render"
"input"
"authentik"
"traefik"
];
};
};
};
}

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,18 @@
{
imports = [
./bazarr
./flaresolverr
./gitea
./homepage-dashboard
./jellyfin
./jellyseerr
./lidarr
./n8n
./nextcloud
./prowlarr
./radarr
./sonarr
./uptime-kuma
./vaultwarden
];
}

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

@@ -0,0 +1,94 @@
{
config,
lib,
self,
...
}: let
unit = "gitea";
cfg = config.server.services.${unit};
domain = "${cfg.subdomain}.${config.server.infra.www.url}";
in {
config = lib.mkIf cfg.enable {
age.secrets.giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age";
server.infra = {
fail2ban.jails.${unit} = {
serviceName = "${unit}";
failRegex = ''.*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>'';
};
postgresql.databases = [
{database = "gitea";}
];
};
services = {
cloudflared = {
enable = true;
tunnels.${cfg.cloudflared.tunnelId} = {
credentialsFile = cfg.cloudflared.credentialsFile;
default = "http_status:404";
ingress."${domain}".service = "http://localhost:${toString cfg.port}";
};
};
gitea = {
enable = true;
appName = "cnix code forge";
database = {
type = "postgres";
socket = "/run/postgresql";
name = "gitea";
user = "gitea";
createDatabase = false;
};
lfs.enable = true;
settings = {
cors = {
ENABLED = true;
SCHEME = "https";
ALLOW_DOMAIN = domain;
};
log.MODE = "console";
mailer = {
ENABLED = false;
MAILER_TYPE = "sendmail";
FROM = "noreply+adam@cnst.dev";
SENDMAIL_PATH = "/run/wrappers/bin/sendmail";
};
picture.DISABLE_GRAVATAR = true;
repository = {
DEFAULT_BRANCH = "main";
DEFAULT_REPO_UNITS = "repo.code,repo.issues,repo.pulls";
DISABLE_DOWNLOAD_SOURCE_ARCHIVES = true;
};
indexer.REPO_INDEXER_ENABLED = true;
oauth2_client = {
ENABLE_AUTO_REGISTRATION = true;
ACCOUNT_LINKING = "auto";
};
server = {
DOMAIN = domain;
LANDING_PAGE = "explore";
HTTP_PORT = cfg.port;
ROOT_URL = "https://${domain}/";
};
security.DISABLE_GIT_HOOKS = false;
service.DISABLE_REGISTRATION = true;
session.COOKIE_SECURE = true;
};
};
};
};
}

View File

@@ -2,51 +2,27 @@
config,
lib,
self,
clib,
...
}: let
unit = "homepage-dashboard";
cfg = config.server.homepage-dashboard;
cfg = config.server.services.${unit};
srv = config.server;
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 {
age.secrets = {
homepageEnvironment = {
file = "${self}/secrets/homepageEnvironment.age";
};
};
services = {
glances.enable = true;
${unit} = {
enable = true;
environmentFile = config.age.secrets.homepageEnvironment.path;
settings = {
layout = [
{
@@ -81,10 +57,12 @@ in {
};
}
];
headerStyle = "clean";
statusStyle = "dot";
hideVersion = "true";
};
widgets = [
{
openmeteo = {
@@ -105,6 +83,7 @@ in {
};
}
];
services = let
homepageCategories = [
"Arr"
@@ -112,31 +91,37 @@ in {
"Downloads"
"Services"
];
hl = config.server;
mergedServices = hl // hl.podman;
homepageServices = x: (lib.attrsets.filterAttrs (
name: value: value ? homepage && value.homepage.category == x
allServices = srv.services;
getDomain = s: clib.server.mkHostDomain config s;
homepageServicesFor = category:
lib.filterAttrs
(
name: value:
name
!= unit
&& value ? homepage
&& value.homepage.category == category
)
mergedServices);
allServices;
in
lib.lists.forEach homepageCategories (cat: {
"${cat}" =
lib.lists.forEach
(lib.attrsets.mapAttrsToList (name: value: {
inherit name;
url = value.url;
homepage = value.homepage;
}) (homepageServices "${cat}"))
(x: {
"${x.homepage.name}" = {
icon = x.homepage.icon;
description = x.homepage.description;
href = "https://${x.url}${x.homepage.path or ""}";
siteMonitor = "https://${x.url}${x.homepage.path or ""}";
(lib.attrsets.mapAttrsToList (name: _value: name) (homepageServicesFor cat))
(x: let
service = allServices.${x};
domain = getDomain service;
in {
"${service.homepage.name}" = {
icon = service.homepage.icon;
description = service.homepage.description;
href = "https://${domain}";
siteMonitor = "https://${domain}";
};
});
})
++ [{Misc = cfg.misc;}]
++ [
{
Glances = let
@@ -212,25 +197,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

@@ -0,0 +1,101 @@
{
config,
pkgs,
lib,
self,
...
}: let
unit = "nextcloud";
cfg = config.server.services.${unit};
srv = config.server;
in {
config = lib.mkIf cfg.enable {
age.secrets = {
nextcloudAdminPass.file = "${self}/secrets/nextcloudAdminPass.age";
nextcloudCloudflared.file = "${self}/secrets/nextcloudCloudflared.age";
};
server.infra.fail2ban.jails.nextcloud = {
serviceName = "${unit}";
_groupsre = ''(?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)'';
failRegex = ''
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Two-factor challenge failed:
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Trusted domain error.
'';
datePattern = '',?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"'';
};
services = {
${unit} = {
enable = true;
package = pkgs.nextcloud32;
hostName = "nextcloud";
configureRedis = true;
caching = {
redis = true;
};
phpOptions = {
"opcache.interned_strings_buffer" = "32";
};
maxUploadSize = "50G";
settings = {
maintenance_window_start = "1";
trusted_proxies = [
"127.0.0.1"
"::1"
];
trusted_domains = ["cloud.${srv.domain}"];
overwriteprotocol = "https";
enabledPreviewProviders = [
"OC\\Preview\\BMP"
"OC\\Preview\\GIF"
"OC\\Preview\\JPEG"
"OC\\Preview\\Krita"
"OC\\Preview\\MarkDown"
"OC\\Preview\\MP3"
"OC\\Preview\\OpenDocument"
"OC\\Preview\\PNG"
"OC\\Preview\\TXT"
"OC\\Preview\\XBitmap"
"OC\\Preview\\HEIC"
];
};
config = {
dbtype = "pgsql";
dbuser = "nextcloud";
dbhost = "/run/postgresql";
dbname = "nextcloud";
adminuser = "cnst";
adminpassFile = config.age.secrets.nextcloudAdminPass.path;
};
};
nginx = {
defaultListen = [
{
addr = "127.0.0.1";
port = 8182;
}
{
addr = "127.0.0.1";
port = 8482;
}
];
virtualHosts.nextcloud = {
forceSSL = false;
};
};
};
server.infra.postgresql.databases = [
{
database = "nextcloud";
}
];
systemd.services."nextcloud-setup" = {
requires = ["postgresql.service"];
after = ["postgresql.service"];
};
};
}

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

@@ -0,0 +1,59 @@
# from @fufexan & @notthebee
{
config,
lib,
self,
...
}: let
unit = "vaultwarden";
cfg = config.server.services.${unit};
domain = "${cfg.subdomain}.${config.server.infra.www.url}";
in {
config = lib.mkIf cfg.enable {
age.secrets = {
vaultwardenCloudflared.file = "${self}/secrets/vaultwardenCloudflared.age";
vaultwardenEnvironment.file = "${self}/secrets/vaultwardenEnvironment.age";
};
server.infra.fail2ban.jails.${unit} = {
serviceName = "${unit}";
failRegex = ''^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$'';
};
services = {
cloudflared = {
enable = true;
tunnels.${cfg.cloudflared.tunnelId} = {
credentialsFile = cfg.cloudflared.credentialsFile;
default = "http_status:404";
ingress."${domain}".service = "http://localhost:${toString cfg.port}";
};
};
vaultwarden = {
enable = true;
environmentFile = config.age.secrets.vaultwardenEnvironment.path;
backupDir = "/var/backup/vaultwarden";
config = {
DOMAIN = "https://${domain}";
SIGNUPS_ALLOWED = false;
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = cfg.port;
IP_HEADER = "CF-Connecting-IP";
logLevel = "warn";
extendedLogging = true;
useSyslog = true;
invitationsAllowed = true;
showPasswordHint = false;
};
};
};
systemd.services.backup-vaultwarden.serviceConfig = {
User = "root";
Group = "root";
};
};
}

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"];
};
};
};
};
};
};
};
}

View File

@@ -1,89 +0,0 @@
# from @fufexan & @notthebee
{
config,
lib,
self,
...
}: let
inherit (lib) mkIf mkEnableOption;
vcfg = config.services.vaultwarden.config;
cfg = config.server.vaultwarden;
in {
options = {
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 = {
vaultwardenCloudflared.file = "${self}/secrets/vaultwardenCloudflared.age";
vaultwardenEnvironment.file = "${self}/secrets/vaultwardenEnvironment.age";
};
server = {
fail2ban = lib.mkIf config.server.fail2ban.enable {
jails = {
vaultwarden = {
serviceName = "vaultwarden";
failRegex = ''^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$'';
};
};
};
};
systemd.services.backup-vaultwarden.serviceConfig = {
User = "root";
Group = "root";
};
services = {
vaultwarden = {
enable = true;
environmentFile = config.age.secrets.vaultwardenEnvironment.path;
backupDir = "/var/backup/vaultwarden";
config = {
DOMAIN = "https://${cfg.url}";
SIGNUPS_ALLOWED = false;
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = 8222;
IP_HEADER = "CF-Connecting-IP";
logLevel = "warn";
extendedLogging = true;
useSyslog = true;
invitationsAllowed = true;
showPasswordHint = false;
};
};
cloudflared = {
enable = true;
tunnels.${cfg.cloudflared.tunnelId} = {
credentialsFile = cfg.cloudflared.credentialsFile;
default = "http_status:404";
ingress."${cfg.url}".service = "http://${vcfg.ROCKET_ADDRESS}:${toString vcfg.ROCKET_PORT}";
};
};
};
};
}

View File

@@ -2,8 +2,7 @@
lib,
config,
...
}:
let
}: let
inherit (lib) mkOption types;
sshKeys = {
@@ -16,14 +15,14 @@ let
keyName = config.settings.accounts.sshUser or null;
selectedKey =
if keyName != null then
if keyName != null
then
lib.attrByPath [
keyName
] (builtins.abort "No SSH key defined for hostname/key '${toString keyName}'") sshKeys
else
builtins.abort "No accounts.sshUser provided, cannot select SSH key.";
in
{
] (builtins.abort "No SSH key defined for hostname/key '${toString keyName}'")
sshKeys
else builtins.abort "No accounts.sshUser provided, cannot select SSH key.";
in {
options.settings.accounts = {
username = mkOption {
type = types.str;
@@ -46,5 +45,21 @@ in
default = null;
description = "Optional override for selecting an SSH key by name";
};
domains = lib.mkOption {
type = lib.types.submodule {
options = {
local = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "The local domain of the host";
};
public = lib.mkOption {
type = lib.types.str;
default = "example.com";
description = "The public domain of the host";
};
};
};
};
};
}

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><>

Binary file not shown.

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;
"authentikCloudflared.age".publicKeys = kima ++ sobotka;
"sobotkaTsAuth.age".publicKeys = kima ++ sobotka;
"mikrotikSecret.age".publicKeys = kima ++ sobotka;
"crowdsecApi.age".publicKeys = kima ++ sobotka;
# Ziggy-specific
"cloudflareDnsCredentialsZiggy.age".publicKeys = kima ++ ziggy;