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

@@ -0,0 +1,133 @@
{
config,
lib,
self,
...
}: let
unit = "authentik";
cfg = config.server.infra.${unit};
srv = config.server.infra.www.domain;
in {
options.server.infra.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
url = lib.mkOption {
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;
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 = "Authentik";
description = "An open-source IdP for modern SSO";
icon = "authentik.svg";
category = "Services";
};
};
config = lib.mkIf cfg.enable {
age.secrets = {
authentikEnv = {
file = "${self}/secrets/authentikEnv.age";
};
authentikCloudflared = {
file = "${self}/secrets/authentikCloudflared.age";
};
};
server.infra = {
fail2ban = {
jails = {
authentik = {
serviceName = "authentik";
failRegex = ''^.*Username or password is incorrect.*IP:\s*<HOST>'';
};
};
};
};
services = {
authentik = {
enable = true;
environmentFile = config.age.secrets.authentikEnv.path;
settings = {
email = {
};
disable_startup_analytics = true;
avatars = "initials";
};
};
cloudflared = {
enable = true;
tunnels.${cfg.cloudflared.tunnelId} = {
credentialsFile = cfg.cloudflared.credentialsFile;
default = "http_status:404";
ingress."${cfg.url}".service = "http://127.0.0.1:9000";
};
};
traefik = {
dynamicConfigOptions = {
http = {
middlewares = {
authentik = {
forwardAuth = {
address = "https://localhost:9443/outpost.goauthentik.io/auth/traefik";
trustForwardHeader = true;
authResponseHeaders = [
"X-authentik-username"
"X-authentik-groups"
"X-authentik-email"
"X-authentik-name"
"X-authentik-uid"
"X-authentik-jwt"
"X-authentik-meta-jwks"
"X-authentik-meta-outpost"
"X-authentik-meta-provider"
"X-authentik-meta-app"
"X-authentik-meta-version"
];
};
};
};
services = {
auth.loadBalancer.servers = [
{
url = "http://localhost:9000";
}
];
};
routers = {
auth = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`) && PathPrefix(`/outpost.goauthentik.io/`)";
service = "auth";
tls.certResolver = "letsencrypt";
};
};
};
};
};
};
};
}

View File

@@ -0,0 +1,131 @@
# from @notthebee
{
lib,
config,
pkgs,
...
}: let
cfg = config.server.infra.fail2ban;
in {
options.server.infra.fail2ban = {
enable = lib.mkEnableOption {
description = "Enable cloudflare fail2ban";
};
apiKeyFile = lib.mkOption {
description = "File containing your API key, scoped to Firewall Rules: Edit";
type = lib.types.str;
example = lib.literalExpression ''
Authorization: Bearer vH6-p0y=i4w3n7TjKqZ@x8D_lR!A9b2cOezXgUuJdE5F
'''
'';
};
zoneId = lib.mkOption {
type = lib.types.str;
};
jails = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
serviceName = lib.mkOption {
example = "vaultwarden";
type = lib.types.str;
};
_groupsre = lib.mkOption {
type = lib.types.lines;
example = ''(?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)'';
default = "";
};
failRegex = lib.mkOption {
type = lib.types.lines;
example = ''
^Login failed from IP: <HOST>$
^Two-factor challenge failed from <HOST>$
'';
};
ignoreRegex = lib.mkOption {
type = lib.types.str;
default = "";
};
datePattern = lib.mkOption {
type = lib.types.str;
default = "";
example = '',?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"'';
description = "Optional datepattern line for the fail2ban filter.";
};
maxRetry = lib.mkOption {
type = lib.types.int;
default = 3;
};
};
}
);
};
};
config = lib.mkIf cfg.enable {
services.fail2ban = {
enable = true;
extraPackages = [
pkgs.curl
pkgs.jq
];
jails =
lib.attrsets.mapAttrs (name: value: {
settings = {
bantime = "24h";
findtime = "10m";
enabled = true;
backend = "systemd";
journalmatch = "_SYSTEMD_UNIT=${value.serviceName}.service";
port = "http,https";
filter = "${name}";
maxretry = 3;
action = "cloudflare-token-agenix";
};
})
cfg.jails;
};
environment.etc = lib.attrsets.mergeAttrsList [
(lib.attrsets.mapAttrs' (
name: value: (lib.nameValuePair "fail2ban/filter.d/${name}.conf" {
text =
''
[Definition]
failregex = ${value.failRegex}
ignoreregex = ${value.ignoreRegex}
''
+ lib.optionalString (value.datePattern != "") ''
datepattern = ${value.datePattern}
''
+ lib.optionalString (value._groupsre != "") ''
_groupsre = ${value._groupsre}
'';
})
)
cfg.jails)
{
"fail2ban/action.d/cloudflare-token-agenix.conf".text = let
notes = "Fail2Ban on ${config.networking.hostName}";
cfapi = "https://api.cloudflare.com/client/v4/zones/${cfg.zoneId}/firewall/access_rules/rules";
in ''
[Definition]
actionstart =
actionstop =
actioncheck =
actionunban = id=$(curl -s -X GET "${cfapi}" \
-H @${cfg.apiKeyFile} -H "Content-Type: application/json" \
| jq -r '.result[] | select(.notes == "${notes}" and .configuration.target == "ip" and .configuration.value == "<ip>") | .id')
if [ -z "$id" ]; then echo "id for <ip> cannot be found"; exit 0; fi; \
curl -s -X DELETE "${cfapi}/$id" \
-H @${cfg.apiKeyFile} -H "Content-Type: application/json" \
--data '{"cascade": "none"}'
actionban = curl -X POST "${cfapi}" -H @${cfg.apiKeyFile} -H "Content-Type: application/json" --data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"${notes}"}'
[Init]
name = cloudflare-token-agenix
'';
}
];
};
}

View File

@@ -0,0 +1,71 @@
{
lib,
config,
self,
...
}: let
unit = "keepalived";
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}";
_self = hostCfg config.networking.hostName;
allPeers = [
"192.168.88.12"
"192.168.88.14"
];
# Remove self from peers
peers = builtins.filter (ip: ip != _self.ip) allPeers;
in {
options.server.infra.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
interface = lib.mkOption {
type = lib.types.str;
example = "eth0";
description = "The network interface keepalived should bind to.";
};
};
config = lib.mkIf cfg.enable {
age.secrets.keepalived.file = "${self}/secrets/keepalived.age";
services.keepalived = {
enable = true;
vrrpInstances.VI = {
state = _self.state;
interface = cfg.interface;
virtualRouterId = 69;
priority = _self.priority;
unicastSrcIp = _self.ip;
unicastPeers = peers;
virtualIps = [
{
addr = "192.168.88.69/24";
}
];
extraConfig = ''
authentication {
auth_type PASS
auth_pass ${config.age.secrets.keepalived.path}
}
'';
};
};
};
}

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

@@ -0,0 +1,6 @@
{
imports = [
./postgres.nix
./postgres-upgrade.nix
];
}

View File

@@ -0,0 +1,48 @@
# taken from @jtojnar
{
config,
lib,
pkgs,
...
}: let
inherit (lib) types mkOption;
cfg = config.server.infra.postgresql;
in {
options = {
server.infra.postgresql = {
upgradeTargetPackage = mkOption {
type = types.nullOr types.package;
default = null;
description = "PostgreSQL package that we want to upgrade to. When set, an update script will be installed.";
};
};
};
config = {
# https://nixos.org/manual/nixos/unstable/#module-services-postgres-upgrading
environment.systemPackages = lib.mkIf (cfg.upgradeTargetPackage != null) [
(pkgs.writeScriptBin "upgrade-pg-cluster" ''
set -eux
# XXX it's perhaps advisable to stop all services that depend on postgresql
systemctl stop postgresql
export NEWDATA="/var/lib/postgresql/${cfg.upgradeTargetPackage.psqlSchema}"
export NEWBIN="${cfg.upgradeTargetPackage}/bin"
export OLDDATA="${config.services.postgresql.dataDir}"
export OLDBIN="${config.services.postgresql.package}/bin"
install -d -m 0700 -o postgres -g postgres "$NEWDATA"
cd "$NEWDATA"
sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
sudo -u postgres $NEWBIN/pg_upgrade \
--old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
--old-bindir $OLDBIN --new-bindir $NEWBIN \
"$@"
'')
];
};
}

View File

@@ -0,0 +1,153 @@
# taken from @jtojnar
{
config,
lib,
pkgs,
...
}: let
inherit (lib) types mkOption;
cfg = config.server.infra.postgresql;
database = {name, ...}: {
options = {
database = mkOption {
type = types.str;
description = "Database name";
};
extraUsers = mkOption {
type = types.listOf types.str;
default = [];
description = "List of extra users with access to this database.";
};
extensions = mkOption {
type = types.listOf types.str;
default = [];
description = "List of extensions to install and enable.";
};
};
};
in {
options = {
server.infra.postgresql = {
databases = mkOption {
type = types.listOf (types.submodule database);
default = [];
description = "List of databases to set up.";
};
};
};
config = lib.mkIf (cfg.databases != []) {
services.postgresql = {
enable = true;
package = pkgs.postgresql_17;
extensions = lib.filter (x: x != null) (
lib.concatMap (
{extensions, ...}: map (ext: config.services.postgresql.package.pkgs.${ext} or null) extensions
)
cfg.databases
);
authentication = lib.mkForce ''
local all postgres peer
local sameuser all peer
# local peer access for extra users
${lib.concatMapStringsSep "\n" (
{
database,
extraUsers,
...
}:
lib.concatMapStringsSep "\n" (user: "local ${database} ${user} peer") ([database] ++ extraUsers)
)
cfg.databases}
# host access (TCP) for databases and their users
${lib.concatMapStringsSep "\n" (
{
database,
extraUsers,
...
}:
lib.concatMapStringsSep "\n" (user: ''
host ${database} ${user} 127.0.0.1/32 trust
host ${database} ${user} ::1/128 trust
'') ([database] ++ extraUsers)
)
cfg.databases}
'';
ensureUsers = let
dbToUsers = {
database,
extraUsers,
...
}:
# we use same username as dbname
[database] ++ extraUsers;
in
map (name: {inherit name;}) (lib.unique (builtins.concatMap dbToUsers cfg.databases));
};
systemd.services = {
postgres-setup = let
pgsql = config.services.postgresql;
in {
after = ["postgresql.service"];
wantedBy = ["multi-user.target"];
path = [pgsql.package];
script =
lib.concatMapStringsSep "\n" (
{
database,
extensions,
extraUsers,
...
}: let
createExtensionsSql =
lib.concatMapStringsSep "; " (
ext: ''CREATE EXTENSION IF NOT EXISTS "${ext}"''
)
extensions;
createExtensionsIfAny = lib.optionalString (extensions != []) ''
$PSQL -d '${database}' -c '${createExtensionsSql}'
'';
in ''
set -eu
PSQL="${pkgs.util-linux}/bin/runuser -u ${pgsql.superUser} -- psql --port=${toString pgsql.settings.port} --tuples-only --no-align"
if ! $PSQL -c "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep --quiet 1; then
$PSQL -c 'CREATE DATABASE "${database}" WITH OWNER = "${database}"'
${createExtensionsIfAny}
fi
${
lib.optionalString (extraUsers != [])
"$PSQL '${database}' -c '${
lib.concatMapStringsSep "\n" (
user: "GRANT ALL ON ALL TABLES IN SCHEMA public TO ${user};"
)
extraUsers
}'"
}
''
)
cfg.databases;
serviceConfig = {
Type = "oneshot";
};
};
postgresql.serviceConfig = {
# Required by PLV8.
MemoryDenyWriteExecute = false;
SystemCallFilter = [
"@pkey"
];
};
};
};
}

View File

@@ -0,0 +1,26 @@
{
config,
lib,
self,
...
}:
with lib; let
cfg = config.server.infra.tailscale;
in {
options.server.infra.tailscale = {
enable = mkEnableOption "Enable tailscale server configuration";
};
config = mkIf cfg.enable {
age.secrets.sobotkaTsAuth.file = "${self}/secrets/sobotkaTsAuth.age";
services.tailscale = {
enable = true;
openFirewall = true;
useRoutingFeatures = "server";
authKeyFile = config.age.secrets.sobotkaTsAuth.path;
extraSetFlags = [
"--advertise-exit-node"
];
};
};
}

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

@@ -0,0 +1,116 @@
{
lib,
pkgs,
config,
...
}: let
unit = "unbound";
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:
if hostname == "ziggy"
then "192.168.88.12"
else if hostname == "sobotka"
then "192.168.88.14"
else throw "No IP defined for host ${hostname}";
in {
options.server.infra.${unit} = {
enable = lib.mkEnableOption {
description = "Enable ${unit}";
};
};
config = lib.mkIf cfg.enable {
services = {
# resolved.enable = lib.mkForce false;
unbound = {
enable = true;
enableRootTrustAnchor = true;
localControlSocketPath = "/run/unbound/unbound.ctl";
resolveLocalQueries = true;
package = pkgs.unbound-full;
settings = {
server = {
access-control = [
"127.0.0.0/8 allow"
"10.88.0.0/24 allow"
"::1 allow"
"192.168.88.0/24 allow"
];
aggressive-nsec = true;
cache-max-ttl = 86400;
cache-min-ttl = 300;
delay-close = 10000;
deny-any = true;
do-ip4 = true;
do-ip6 = true;
do-tcp = true;
do-udp = true;
prefer-ip6 = false;
edns-buffer-size = "1232";
extended-statistics = true;
harden-algo-downgrade = true;
harden-below-nxdomain = true;
harden-dnssec-stripped = true;
harden-glue = true;
harden-large-queries = true;
harden-short-bufsize = true;
infra-cache-slabs = 8;
interface = [
"127.0.0.1@5335"
"${hostIp config.networking.hostName}@5335"
"::@5335"
];
key-cache-slabs = 8;
msg-cache-size = "256m";
msg-cache-slabs = 8;
neg-cache-size = "256m";
num-queries-per-thread = 4096;
num-threads = 4;
outgoing-range = 8192;
prefetch = true;
prefetch-key = true;
qname-minimisation = true;
rrset-cache-size = "256m";
rrset-cache-slabs = 8;
rrset-roundrobin = true;
serve-expired = true;
so-rcvbuf = "2m";
so-reuseport = true;
so-sndbuf = "2m";
statistics-cumulative = true;
statistics-interval = 0;
tls-cert-bundle = "/etc/ssl/certs/ca-certificates.crt";
unwanted-reply-threshold = 100000;
use-caps-for-id = false;
verbosity = 1;
private-address = [
"10.0.0.0/8"
"169.254.0.0/16"
"172.16.0.0/12"
"192.168.0.0/16"
"fd00::/8"
"fe80::/10"
"192.0.2.0/24"
"198.51.100.0/24"
"203.0.113.0/24"
"255.255.255.255/32"
"2001:db8::/32"
];
local-data = generateLocalRecords srv.services;
local-data-ptr = [
"local-data: \"traefik.${srv.domain}. A ${srv.ip}\""
];
};
};
};
};
};
}

View File

@@ -0,0 +1,135 @@
{
lib,
config,
self,
...
}: let
inherit (lib) mkIf mkEnableOption mkOption types;
cfg = config.server.infra.www;
in {
options.server.infra.www = {
enable = mkEnableOption {
description = "Enable personal website";
};
url = mkOption {
default = "";
type = types.str;
description = ''
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;
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 = {
wwwCloudflared.file = "${self}/secrets/wwwCloudflared.age";
};
server.infra = {
fail2ban = {
jails = {
nginx-404 = {
serviceName = "nginx";
failRegex = ''^.*\[error\].*directory index of.* is forbidden.*client: <HOST>.*$'';
ignoreRegex = '''';
maxRetry = 5;
};
};
};
};
services = {
nginx = {
enable = true;
defaultListen = [
{
addr = "127.0.0.1";
port = 8283;
}
];
virtualHosts."webfinger" = {
forceSSL = false;
serverName = cfg.url;
root = "/var/www/webfinger";
locations."= /.well-known/webfinger" = {
root = "/var/www/webfinger";
extraConfig = ''
default_type application/jrd+json;
try_files /.well-known/webfinger =404;
'';
};
locations."= /robots.txt" = {
root = "/var/www/webfinger";
extraConfig = ''
default_type text/plain;
try_files /robots.txt =404;
'';
};
};
};
cloudflared = {
enable = true;
tunnels.${cfg.cloudflared.tunnelId} = {
credentialsFile = cfg.cloudflared.credentialsFile;
default = "http_status:404";
ingress."${cfg.url}".service = "http://127.0.0.1:8283";
};
};
};
environment.etc = {
"webfinger/.well-known/webfinger".text = ''
{
"subject": "acct:adam@${cfg.url}",
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://auth.${cfg.url}/application/o/tailscale/"
}
]
}
'';
"webfinger/robots.txt".text = ''
User-agent: *
Disallow: /
'';
};
services.traefik.dynamicConfigOptions.http = {
routers.webfinger = {
entryPoints = ["websecure"];
rule = "Host(`${cfg.url}`) && Path(`/.well-known/webfinger`)";
service = "webfinger";
tls.certResolver = "letsencrypt";
};
services.webfinger.loadBalancer.servers = [
{url = "http://127.0.0.1:8283";}
];
};
};
}