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:
parent
cb8ecc1409
commit
eadbc92126
6 changed files with 261 additions and 86 deletions
|
|
@ -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
103
services/authelia.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 -"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue