Port to NixOS: replace Helm chart with flake-based NixOS config
Replaces the Helm/k3s setup with a declarative NixOS configuration targeting
a Raspberry Pi 4. Services run as podman containers under systemd, with data
on an external HD at /mnt/data. Key components:
- flake.nix: multi-host flake with pi-main (aarch64) and a placeholder for a
second machine
- modules/common.nix: shared system config (nix, podman, sops, SSH)
- modules/storage.nix: external HD mount with per-service subdirs
- modules/caddy.nix: Caddy with cloudflare DNS-01 ACME + authelia forward_auth
- modules/cloudflared.nix: Cloudflare tunnel for remote access
- modules/backup.nix: restic daily backups with NC maintenance mode pre-hook
- modules/services/{openldap,authelia,gitea,nextcloud,phpldapadmin}.nix: core services
- modules/services/{jellyfin,transmission}.nix: media services (disabled by default)
- secrets/: sops-nix scaffold with .sops.yaml age key config
- hosts/pi-main/: hardware config + service selection for the Pi
- PORTING.md: step-by-step migration guide (SD card → data restore → verify)
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
{ config, lib, pkgs, homeyConfig, ... }:
|
||||
|
||||
# Caddy reverse proxy.
|
||||
#
|
||||
# Features:
|
||||
# - DNS-01 ACME via Cloudflare API → real wildcard cert for *.home.zakobar.com
|
||||
# - forward_auth to Authelia for protected vhosts
|
||||
# - Plain reverse_proxy for public vhosts (authelia itself, nextcloud)
|
||||
# - Listens on :80 (redirect) and :443 (TLS)
|
||||
#
|
||||
# Because nixpkgs ships Caddy without the cloudflare DNS plugin by default,
|
||||
# we build a custom Caddy with it using the xcaddy wrapper from nixpkgs.
|
||||
#
|
||||
# Secrets consumed from sops:
|
||||
# cloudflare/api_token
|
||||
|
||||
let
|
||||
cfg = config.homey.caddy;
|
||||
domain = homeyConfig.domain;
|
||||
|
||||
# Build Caddy with the Cloudflare DNS plugin.
|
||||
# This compiles on the Pi (slow once, cached after).
|
||||
caddyWithCloudflare = pkgs.caddy.override {
|
||||
externalPlugins = [
|
||||
{
|
||||
name = "github.com/caddy-dns/cloudflare";
|
||||
version = "89f16b99c18ef49c8bb470a82f895bce01cbaece";
|
||||
}
|
||||
];
|
||||
vendorHash = lib.fakeHash; # replace with real hash after first build
|
||||
};
|
||||
|
||||
# Reusable Authelia forward_auth snippet
|
||||
# Returns a Caddyfile snippet block that applies forward_auth.
|
||||
# copy_headers makes Authelia's Remote-* headers available downstream.
|
||||
autheliaForwardAuth = ''
|
||||
forward_auth localhost:9091 {
|
||||
uri /api/verify?rd=https://auth.${domain}
|
||||
copy_headers Remote-User Remote-Name Remote-Groups Remote-Email
|
||||
# On auth failure, redirect to the authelia login page
|
||||
@goauth status 401
|
||||
handle_response @goauth {
|
||||
redir https://auth.${domain}?rm={method} 302
|
||||
}
|
||||
}
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
options.homey.caddy = {
|
||||
enable = lib.mkEnableOption "Caddy reverse proxy";
|
||||
|
||||
acmeEmail = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "admin@zakobar.com";
|
||||
description = "Email for Let's Encrypt ACME registration.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# -----------------------------------------------------------------------
|
||||
# Secrets
|
||||
# -----------------------------------------------------------------------
|
||||
sops.secrets."cloudflare/api_token" = {
|
||||
owner = config.services.caddy.user;
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Caddy service
|
||||
# -----------------------------------------------------------------------
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
package = caddyWithCloudflare;
|
||||
|
||||
# Global options
|
||||
globalConfig = ''
|
||||
email ${cfg.acmeEmail}
|
||||
# Use Cloudflare DNS-01 challenge for wildcard cert
|
||||
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||
'';
|
||||
|
||||
# Each virtual host
|
||||
virtualHosts = {
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Authelia — public, no auth gate (it IS the auth gate)
|
||||
# ------------------------------------------------------------------
|
||||
"auth.${domain}" = {
|
||||
extraConfig = ''
|
||||
reverse_proxy localhost:9091
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Gitea — protected behind one_factor Authelia
|
||||
# ------------------------------------------------------------------
|
||||
"git.${domain}" = {
|
||||
extraConfig = ''
|
||||
${autheliaForwardAuth}
|
||||
reverse_proxy localhost:3000
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Nextcloud — public auth (Nextcloud manages its own users + LDAP)
|
||||
# Authelia is not gating nextcloud directly because NC has its own
|
||||
# login flow. We still want HTTPS.
|
||||
# ------------------------------------------------------------------
|
||||
"nextcloud.${domain}" = {
|
||||
extraConfig = ''
|
||||
# Redirect CardDAV/CalDAV discovery
|
||||
redir /.well-known/carddav /remote.php/dav/ 301
|
||||
redir /.well-known/caldav /remote.php/dav/ 301
|
||||
|
||||
# Large uploads (5 GB)
|
||||
request_body {
|
||||
max_size 5GB
|
||||
}
|
||||
|
||||
reverse_proxy localhost:8080
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# phpLDAPadmin — two_factor, admins only (enforced by authelia policy)
|
||||
# ------------------------------------------------------------------
|
||||
"ldapadmin.${domain}" = {
|
||||
extraConfig = ''
|
||||
${autheliaForwardAuth}
|
||||
reverse_proxy localhost:8081
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Jellyfin — one_factor (added when enabled)
|
||||
# ------------------------------------------------------------------
|
||||
"jellyfin.${domain}" = {
|
||||
extraConfig = ''
|
||||
${autheliaForwardAuth}
|
||||
reverse_proxy localhost:8096
|
||||
'';
|
||||
};
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Transmission — two_factor, admins only (enforced by authelia policy)
|
||||
# ------------------------------------------------------------------
|
||||
"torrent.${domain}" = {
|
||||
extraConfig = ''
|
||||
${autheliaForwardAuth}
|
||||
reverse_proxy localhost:9091_transmission
|
||||
'';
|
||||
# NOTE: transmission uses 9091 too; we'll bind it to 9092 in its
|
||||
# module to avoid a clash with authelia.
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Pass Cloudflare token as env var to the caddy systemd unit
|
||||
# -----------------------------------------------------------------------
|
||||
systemd.services.caddy = {
|
||||
serviceConfig = {
|
||||
EnvironmentFile = pkgs.writeText "caddy-cf-env"
|
||||
"CLOUDFLARE_API_TOKEN_FILE=${config.sops.secrets."cloudflare/api_token".path}";
|
||||
# Caddy supports _FILE suffix for env vars via its secret file reader,
|
||||
# but cloudflare plugin reads CLOUDFLARE_API_TOKEN directly.
|
||||
# We write a wrapper ExecStartPre to populate the env var from the file:
|
||||
ExecStartPre = [
|
||||
(pkgs.writeShellScript "caddy-inject-cf-token" ''
|
||||
export CLOUDFLARE_API_TOKEN=$(cat ${config.sops.secrets."cloudflare/api_token".path})
|
||||
systemctl set-environment CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN"
|
||||
'')
|
||||
];
|
||||
};
|
||||
after = lib.mkAfter [ "podman-authelia.service" ];
|
||||
wants = lib.mkAfter [ "podman-authelia.service" ];
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Firewall — open HTTP + HTTPS (already in common.nix, explicit here too)
|
||||
# -----------------------------------------------------------------------
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user