Replace Docker containers with native NixOS modules for nginx, Authelia, and go2rtc

- Native nginx with ACME wildcard cert (*.nordhammer.it) via Cloudflare DNS-01
- Native Authelia SSO with forward auth protecting homepage + camera
- Native go2rtc camera streaming (no more Docker)
- Auto-migration script for Authelia secrets and user database from Docker
- Homepage hrefs updated to use HTTPS domain names
- Fail2ban updated for native nginx log paths + new Authelia jail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ediblerope 2026-04-07 15:47:56 +01:00
parent cb8ecc1409
commit eadbc92126
6 changed files with 261 additions and 86 deletions

View file

@ -30,6 +30,7 @@
./services/bazarr.nix
./services/cloudflare-ddns.nix
./services/fail2ban.nix
./services/authelia.nix
./services/homepage.nix
./services/arr-interconnect.nix
];

103
services/authelia.nix Normal file
View file

@ -0,0 +1,103 @@
# services/authelia.nix — Native Authelia SSO with auto-migration from Docker
{ config, lib, pkgs, ... }:
let
# Migrates secrets + user DB from the old Docker Authelia setup
setupScript = pkgs.writeShellScript "authelia-setup" ''
set -euo pipefail
YQ="${pkgs.yq-go}/bin/yq"
DOCKER_CONFIG="/home/fred/docker/authelia/configuration.yml"
SECRETS_DIR="/var/secrets/authelia"
STATE_DIR="/var/lib/authelia-main"
mkdir -p "$SECRETS_DIR"
# Migrate secrets from Docker config if they haven't been extracted yet
if [ -f "$DOCKER_CONFIG" ]; then
if [ ! -f "$SECRETS_DIR/jwt_secret" ]; then
$YQ '.identity_validation.reset_password.jwt_secret' "$DOCKER_CONFIG" \
| tr -d '"' > "$SECRETS_DIR/jwt_secret"
echo "Migrated jwt_secret"
fi
if [ ! -f "$SECRETS_DIR/session_secret" ]; then
$YQ '.session.secret' "$DOCKER_CONFIG" \
| tr -d '"' > "$SECRETS_DIR/session_secret"
echo "Migrated session_secret"
fi
if [ ! -f "$SECRETS_DIR/storage_encryption_key" ]; then
$YQ '.storage.encryption_key' "$DOCKER_CONFIG" \
| tr -d '"' > "$SECRETS_DIR/storage_encryption_key"
echo "Migrated storage_encryption_key"
fi
fi
chmod 644 "$SECRETS_DIR"/*
# Migrate users database
if [ ! -f "$STATE_DIR/users_database.yml" ] && \
[ -f "/home/fred/docker/authelia/users_database.yml" ]; then
cp /home/fred/docker/authelia/users_database.yml "$STATE_DIR/"
chown authelia-main:authelia-main "$STATE_DIR/users_database.yml"
echo "Migrated users_database.yml"
fi
echo "Authelia setup complete."
'';
in
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
services.authelia.instances.main = {
enable = true;
secrets = {
jwtSecretFile = "/var/secrets/authelia/jwt_secret";
storageEncryptionKeyFile = "/var/secrets/authelia/storage_encryption_key";
sessionSecretFile = "/var/secrets/authelia/session_secret";
};
settings = {
theme = "dark";
server.address = "tcp://127.0.0.1:9091/";
log = {
level = "info";
format = "text";
};
authentication_backend.file.path = "/var/lib/authelia-main/users_database.yml";
access_control = {
default_policy = "deny";
rules = [
{ domain = "camera.nordhammer.it"; policy = "one_factor"; }
{ domain = "homepage.nordhammer.it"; policy = "one_factor"; }
];
};
session = {
cookies = [{
domain = "nordhammer.it";
authelia_url = "https://auth.nordhammer.it";
}];
expiration = "1h";
inactivity = "5m";
};
storage.local.path = "/var/lib/authelia-main/db.sqlite3";
notifier.filesystem.filename = "/var/lib/authelia-main/notification.txt";
};
};
# Auto-migrate Docker Authelia data on first deploy
systemd.services.authelia-setup = {
description = "Migrate Authelia secrets and user database from Docker";
before = [ "authelia-main.service" ];
requiredBy = [ "authelia-main.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = setupScript;
};
};
};
}

View file

@ -37,17 +37,29 @@
};
};
# Nginx Proxy Manager — watches Docker-mounted log files for 401/403s
nginx-proxy-manager = {
# Nginx — watches access log for HTTP auth failures
nginx = {
settings = {
enabled = true;
filter = "nginx-http-auth";
logpath = "/home/fred/docker/nginx-proxy-manager/data/logs/*.log";
logpath = "/var/log/nginx/access.log";
maxretry = 10;
bantime = "1h";
};
};
# Authelia — failed login attempts via journald
authelia = {
settings = {
enabled = true;
backend = "systemd";
journalmatch = "_SYSTEMD_UNIT=authelia-main.service";
filter = "authelia";
maxretry = 5;
bantime = "2h";
};
};
# Jellyfin auth failures — journald
jellyfin = {
settings = {
@ -140,6 +152,13 @@
ignoreregex =
'';
# Authelia filter
environment.etc."fail2ban/filter.d/authelia.conf".text = ''
[Definition]
failregex = ^.*Unsuccessful .* authentication attempt by user .* from <HOST>.*$
ignoreregex =
'';
# Jellyfin filter
environment.etc."fail2ban/filter.d/jellyfin.conf".text = ''
[Definition]

View file

@ -1,38 +1,18 @@
#/services/go2rtc.nix
{ config, pkgs, lib, ... }:
# services/go2rtc.nix — Native go2rtc camera streaming
{ config, lib, ... }:
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
virtualisation.oci-containers = {
backend = "docker";
# --- Authelia ---
containers."authelia" = {
image = "authelia/authelia:latest";
volumes = [
"/home/fred/docker/authelia:/config"
"/home/fred/docker/authelia/users_database.yml:/config/users_database.yml"
"/home/fred/docker/authelia/secrets:/secrets"
];
ports = [ "9091:9091" ];
};
# --- Go2RTC ---
containers."go2rtc" = {
image = "alexxit/go2rtc:latest";
volumes = [
"/home/fred/docker/go2rtc/config.yml:/config/go2rtc.yaml"
];
ports = [ "1984:1984" ];
services.go2rtc = {
enable = true;
settings = {
# NOTE: RTSP credentials end up in the nix store — same exposure as
# the old Docker bind-mount config. Acceptable for a local LAN camera.
streams.kids_bedroom = "rtsp://fredrik:12345678@192.168.4.39:554/stream1";
api.listen = ":1984";
webrtc.listen = ":8555";
};
};
# --- Create directories ---
systemd.tmpfiles.rules = [
# Local secrets & configs
"d /home/fred/docker/authelia/secrets 0700 fred users -"
"d /home/fred/docker/go2rtc 0755 fred users -"
];
};
}

View file

@ -87,7 +87,7 @@ in
# Allow access from anywhere on the LAN
# Add your domain here too if you expose it via Nginx Proxy Manager
allowedHosts = "localhost:8082,127.0.0.1:8082,192.168.4.74:8082";
allowedHosts = "localhost:8082,127.0.0.1:8082,homepage.nordhammer.it";
# API keys auto-extracted by homepage-extract-secrets.service
environmentFiles = [ "/etc/homepage-secrets" ];
@ -136,12 +136,12 @@ in
Media = [
{
Jellyfin = {
href = "http://192.168.4.74:8096";
href = "https://jellyfin.nordhammer.it";
description = "Media server";
icon = "jellyfin.png";
widget = {
type = "jellyfin";
url = "http://192.168.4.74:8096";
url = "http://127.0.0.1:8096";
key = "{{HOMEPAGE_VAR_JELLYFIN_KEY}}";
enableBlocks = true;
enableNowPlaying = true;
@ -150,24 +150,24 @@ in
}
{
Bazarr = {
href = "http://192.168.4.74:6767";
href = "https://bazarr.nordhammer.it";
description = "Subtitle management";
icon = "bazarr.png";
widget = {
type = "bazarr";
url = "http://192.168.4.74:6767";
url = "http://127.0.0.1:6767";
key = "{{HOMEPAGE_VAR_BAZARR_KEY}}";
};
};
}
{
Sonarr = {
href = "http://192.168.4.74:8989";
href = "https://sonarr.nordhammer.it";
description = "TV show management";
icon = "sonarr.png";
widget = {
type = "sonarr";
url = "http://192.168.4.74:8989";
url = "http://127.0.0.1:8989";
key = "{{HOMEPAGE_VAR_SONARR_KEY}}";
enableQueue = true;
};
@ -175,12 +175,12 @@ in
}
{
Radarr = {
href = "http://192.168.4.74:7878";
href = "https://radarr.nordhammer.it";
description = "Movie management";
icon = "radarr.png";
widget = {
type = "radarr";
url = "http://192.168.4.74:7878";
url = "http://127.0.0.1:7878";
key = "{{HOMEPAGE_VAR_RADARR_KEY}}";
enableQueue = true;
};
@ -192,7 +192,7 @@ in
Downloads = [
{
qBittorrent = {
href = "http://192.168.4.74:8080";
href = "https://torrent.nordhammer.it";
description = "Torrent client";
icon = "qbittorrent.png";
widget = {
@ -203,12 +203,12 @@ in
}
{
Prowlarr = {
href = "http://192.168.4.74:9696";
href = "https://prowlarr.nordhammer.it";
description = "Indexer manager";
icon = "prowlarr.png";
widget = {
type = "prowlarr";
url = "http://192.168.4.74:9696";
url = "http://127.0.0.1:9696";
key = "{{HOMEPAGE_VAR_PROWLARR_KEY}}";
};
};
@ -217,23 +217,16 @@ in
}
{
Infrastructure = [
{
"Nginx Proxy Manager" = {
href = "http://192.168.4.74:81";
description = "Reverse proxy";
icon = "nginx-proxy-manager.png";
};
}
{
Authelia = {
href = "http://192.168.4.74:9091";
href = "https://auth.nordhammer.it";
description = "SSO & 2FA";
icon = "authelia.png";
};
}
{
go2rtc = {
href = "http://192.168.4.74:1984";
href = "https://camera.nordhammer.it";
description = "Camera streams";
icon = "go2rtc.png";
};

View file

@ -1,34 +1,113 @@
#nginx.nix
{ config, pkgs, lib, ... }:
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
# services/nginx.nix — Native nginx reverse proxy with ACME wildcard cert
{ config, lib, ... }:
let
# Authelia forward-auth snippet injected into protected locations
autheliaAuthConfig = ''
auth_request /internal/authelia/authz;
auth_request_set $target_url $scheme://$http_host$request_uri;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
error_page 401 =302 https://auth.nordhammer.it/?rd=$target_url;
'';
# Nginx Proxy Manager
virtualisation.oci-containers = {
backend = "docker";
containers."nginx-proxy-manager" = {
image = "jc21/nginx-proxy-manager:latest";
ports = [
"80:80"
"81:81"
"443:443"
];
volumes = [
"/home/fred/docker/nginx-proxy-manager/data:/data"
"/home/fred/docker/nginx-proxy-manager/letsencrypt:/etc/letsencrypt"
];
# Remove the extraOptions with --restart, it conflicts with --rm
};
};
# Create directories
systemd.tmpfiles.rules = [
"d /home/fred/docker/nginx-proxy-manager/data 0755 root root -"
"d /home/fred/docker/nginx-proxy-manager/letsencrypt 0755 root root -"
];
# Open firewall
networking.firewall.allowedTCPPorts = [ 80 81 443 ];
};
# Internal location that queries Authelia's verification endpoint
autheliaLocation = {
"/internal/authelia/authz" = {
proxyPass = "http://127.0.0.1:9091/api/authz/forward-auth";
extraConfig = ''
internal;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Method $request_method;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-URI $request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Content-Length "";
proxy_set_header Connection "";
'';
};
};
ssl = {
useACMEHost = "nordhammer.it";
forceSSL = true;
};
# Simple reverse proxy vhost
proxy = port: ssl // {
locations."/" = {
proxyPass = "http://127.0.0.1:${toString port}";
proxyWebsockets = true;
};
};
# Reverse proxy protected by Authelia forward auth
protectedProxy = port: ssl // {
locations = autheliaLocation // {
"/" = {
proxyPass = "http://127.0.0.1:${toString port}";
proxyWebsockets = true;
extraConfig = autheliaAuthConfig;
};
};
};
in
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
# Wildcard TLS cert via Cloudflare DNS-01 challenge
security.acme = {
acceptTerms = true;
defaults.email = "fredrik@nordhammer.it";
certs."nordhammer.it" = {
domain = "*.nordhammer.it";
extraDomainNames = [ "nordhammer.it" ];
dnsProvider = "cloudflare";
credentialFiles = {
"CF_DNS_API_TOKEN_FILE" = "/var/secrets/cloudflare-token";
};
};
};
users.users.nginx.extraGroups = [ "acme" ];
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
# File-based access log for fail2ban
appendHttpConfig = ''
access_log /var/log/nginx/access.log;
'';
virtualHosts = {
# --- Authelia portal (not behind auth itself) ---
"auth.nordhammer.it" = proxy 9091;
# --- Media ---
"jellyfin.nordhammer.it" = proxy 8096;
"bazarr.nordhammer.it" = proxy 6767;
"sonarr.nordhammer.it" = proxy 8989;
"radarr.nordhammer.it" = proxy 7878;
# --- Downloads ---
"prowlarr.nordhammer.it" = proxy 9696;
"torrent.nordhammer.it" = proxy 8080;
# --- Other ---
"games.nordhammer.it" = proxy 8787;
"search.nordhammer.it" = proxy 8087;
# --- Protected by Authelia ---
"camera.nordhammer.it" = protectedProxy 1984;
"homepage.nordhammer.it" = protectedProxy 8082;
};
};
networking.firewall.allowedTCPPorts = [ 80 443 ];
};
}