diff options
| author | Karan Jayachandra <mail@karanjayachandra.com> | 2026-07-18 23:04:53 +0200 |
|---|---|---|
| committer | Karan Jayachandra <mail@karanjayachandra.com> | 2026-07-18 23:04:53 +0200 |
| commit | 9ceddb33272e5fca6382c1b4dec2074bd1167738 (patch) | |
| tree | cac68c1bdc90c9ab618e4fdb4c8eb65d27d9a647 /modules | |
| parent | 718a79f8de2a55f2ab83cac7e8595cd3916ed486 (diff) | |
Simplify config and add AdGuard DoH/DoT support
- Replace the hand-rolled Podman OCI container for Actual Budget with the
native services.actual module (available in the pinned nixpkgs release);
the container never actually had a backend enabled, so it likely never ran.
- Collapse cgit's Caddy routing to a single reverse proxy - the smart-HTTP
git backend was already served on the same nginx vhost/port as cgit
itself, so the separate /git/* -> 8085 route was dead and pointed at a
port nothing listened on.
- Drop the unused kvm-intel kernel module from the guest hardware profile,
and rename disko's misleadingly-named ESP partition (it's ext4, not a
real EFI System Partition).
- Point common.nix's disabled autoUpgrade flake URL at this repo's own
cgit hosting instead of a generic GitHub placeholder.
- Add AdGuard Home DNS-over-TLS (853) and DNS-over-HTTPS (8443) support,
backed by an independent ACME certificate (modules/acme.nix) issued via
a webroot Caddy serves on port 80.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/acme.nix | 32 | ||||
| -rw-r--r-- | modules/actual.nix | 32 | ||||
| -rw-r--r-- | modules/adguard.nix | 36 | ||||
| -rw-r--r-- | modules/caddy.nix | 25 | ||||
| -rw-r--r-- | modules/cgit.nix | 14 | ||||
| -rw-r--r-- | modules/common.nix | 14 |
6 files changed, 108 insertions, 45 deletions
diff --git a/modules/acme.nix b/modules/acme.nix new file mode 100644 index 0000000..23c995f --- /dev/null +++ b/modules/acme.nix @@ -0,0 +1,32 @@ +{ ... }: + +# Shared ACME (Let's Encrypt) infrastructure for services that need their own +# certificate rather than being fronted by Caddy - currently just AdGuard +# Home's DNS-over-HTTPS/DNS-over-TLS listener (modules/adguard.nix), which +# terminates TLS itself on its own ports rather than through Caddy. +# +# Challenge type: HTTP-01 via a shared webroot that Caddy serves on port 80 +# for dns.karanj.com (see modules/caddy.nix) - no DNS provider API +# credentials needed. This is a separate Let's Encrypt certificate from the +# one Caddy obtains for its own reverse-proxied HTTPS on the same domain; +# issuing two independent certs for one domain is fine and well within +# Let's Encrypt's rate limits. +{ + security.acme = { + acceptTerms = true; + defaults.email = "me@karanj.com"; + + certs."dns.karanj.com" = { + webroot = "/var/lib/acme/acme-challenge"; + # Owns the issued cert/key files by the adguardhome group so the + # service can read them without running as root. + group = "adguardhome"; + # Restart (not just reload) so AdGuard re-reads the cert on renewal. + reloadServices = [ "adguardhome.service" ]; + }; + }; + + # Make sure Caddy (which serves the HTTP-01 webroot) is already up before + # the first issuance attempt. + systemd.services."acme-dns.karanj.com".after = [ "caddy.service" ]; +} diff --git a/modules/actual.nix b/modules/actual.nix index 62593a1..95bbc7d 100644 --- a/modules/actual.nix +++ b/modules/actual.nix @@ -1,32 +1,22 @@ -{ pkgs, ... }: +{ ... }: # Actual Budget - local-first personal finance application. # -# Runs as a Podman OCI container because the native services.actual NixOS -# module is not available in nixos-24.11. The container listens on -# 127.0.0.1:5006; Caddy handles public HTTPS termination. +# Native NixOS module (services.actual, available since this flake's pinned +# nixpkgs release). Listens on 127.0.0.1:5006; Caddy handles public HTTPS +# termination. # # On first visit to https://budget.karanj.com the app prompts you to set a # server password in the browser - no pre-configuration needed. # -# All budget data is persisted in /var/lib/actual on the host. +# All budget data is persisted in /var/lib/actual on the host (the module's +# own StateDirectory). { - # Ensure the data directory exists before the container starts - systemd.tmpfiles.rules = [ - "d /var/lib/actual 0750 root root -" - ]; - - virtualisation.oci-containers.containers.actual = { - image = "docker.io/actualbudget/actual-server:latest"; - - ports = [ "127.0.0.1:5006:5006" ]; - - volumes = [ - "/var/lib/actual:/data" - ]; - - environment = { - ACTUAL_PORT = "5006"; + services.actual = { + enable = true; + settings = { + hostname = "127.0.0.1"; + port = 5006; }; }; } diff --git a/modules/adguard.nix b/modules/adguard.nix index 85c5f5c..e66d072 100644 --- a/modules/adguard.nix +++ b/modules/adguard.nix @@ -2,8 +2,20 @@ # AdGuard Home - DNS resolver + optional network-wide ad blocker. # -# DNS listens on port 53 (udp/tcp) directly on the public IP. -# The web UI listens on 127.0.0.1:3000 and is fronted by Caddy. +# Plain DNS listens on port 53 (udp/tcp) directly on the public IP. +# DNS-over-TLS (853/tcp) and DNS-over-HTTPS (8443/tcp, path /dns-query) are +# also served directly by AdGuard itself, terminating TLS with their own +# independent Let's Encrypt cert (modules/acme.nix) - NOT via Caddy, since +# Caddy already owns port 443 for its own reverse-proxied vhosts and +# AdGuard's HTTPS/DoH listener needs its own port. +# +# AdGuard's HTTPS/DoH listener binds to the same host as the plain `http` +# listener below (confirmed in AdGuardHome's source: both come from +# HTTPConfig.Address) - that's why `http.address` binds 0.0.0.0 rather than +# 127.0.0.1 even though the plain-HTTP web UI is still meant to be reached +# only via Caddy's reverse proxy: port 3000 itself is never opened in the +# firewall (modules/common.nix), so it's still unreachable directly. +# DNS-over-TLS reuses `dns.bind_hosts` below, so it needs no such change. # # Admin password is seeded from a sops secret containing a bcrypt hash. # To generate the hash on your local machine: @@ -16,7 +28,19 @@ settings = { http = { - address = "127.0.0.1:3000"; + address = "0.0.0.0:3000"; + }; + + tls = { + enabled = true; + server_name = "dns.karanj.com"; + port_https = 8443; + port_dns_over_tls = 853; + # DNS-over-QUIC and DNSCrypt were not requested - keep them off. + port_dns_over_quic = 0; + port_dnscrypt = 0; + certificate_path = "/var/lib/acme/dns.karanj.com/fullchain.pem"; + private_key_path = "/var/lib/acme/dns.karanj.com/key.pem"; }; dns = { @@ -25,6 +49,7 @@ # Clients configure this server's public IPv4 address (not # dns.karanj.com - that hostname only resolves to the HTTPS web UI # via Caddy) as their plain DNS resolver, e.g. <server-ipv4>:53. + # For encrypted DNS, see the client setup notes in README.md. # Upstream DNS resolvers (privacy-respecting) upstream_dns = [ "https://dns.quad9.net/dns-query" @@ -96,6 +121,11 @@ # `sed -i`: sed -i creates a temp file and chowns/renames it in place, and # this unit's hardened SystemCallFilter (~@privileged) blocks the chown # syscall, killing sed with SIGSYS ("Bad system call"). + # Don't attempt the first start until the ACME cert referenced by + # tls.certificate_path/private_key_path above actually exists. + systemd.services.adguardhome.after = [ "acme-dns.karanj.com.service" ]; + systemd.services.adguardhome.wants = [ "acme-dns.karanj.com.service" ]; + systemd.services.adguardhome.serviceConfig = { DynamicUser = lib.mkForce false; User = "adguardhome"; diff --git a/modules/caddy.nix b/modules/caddy.nix index c2c41e4..f82c7f0 100644 --- a/modules/caddy.nix +++ b/modules/caddy.nix @@ -9,10 +9,17 @@ virtualHosts = { - # AdGuard Home web UI + # AdGuard Home web UI, plus the HTTP-01 webroot for the independent + # ACME cert AdGuard uses for its own DoH/DoT TLS (modules/acme.nix). "dns.karanj.com" = { extraConfig = '' - reverse_proxy 127.0.0.1:3000 + handle /.well-known/acme-challenge/* { + root * /var/lib/acme/acme-challenge + file_server + } + handle { + reverse_proxy 127.0.0.1:3000 + } ''; }; @@ -30,17 +37,13 @@ ''; }; - # cgit - public read-only git viewer + smart HTTP for git clone/pull + # cgit - public read-only git viewer + smart HTTP for git clone/pull. + # Both cgit browsing and git-http-backend (clone/pull) are served by + # the same nginx vhost on 8086 - nginx itself routes between them by + # matching the request path, so a single proxy here covers both. "git.karanj.com" = { extraConfig = '' - # Smart HTTP git (clone/pull only - no push exposed) - handle /git/* { - reverse_proxy 127.0.0.1:8085 - } - # cgit web UI - handle { - reverse_proxy 127.0.0.1:8086 - } + reverse_proxy 127.0.0.1:8086 ''; }; diff --git a/modules/cgit.nix b/modules/cgit.nix index 058fc79..de15e08 100644 --- a/modules/cgit.nix +++ b/modules/cgit.nix @@ -8,12 +8,14 @@ # - git clone/pull over HTTPS: public, read-only via git-http-backend # - git push: SSH only, using the "git" user + your authorized keys # -# Caddy routes: -# /git/* -> fcgiwrap serving git-http-backend (port 8085 via nginx shim) -# /* -> cgit (port 8086 via nginx shim) -# -# Both cgit and git-http-backend are served through a minimal nginx instance -# bound to localhost, which Caddy then reverse-proxies. +# The NixOS cgit module serves both cgit browsing and git-http-backend +# (clone/pull, via gitHttpBackend.enable which defaults to true) on the +# *same* nginx vhost/location - nginx tells them apart by matching the +# request path against a regex (.../info/refs|git-upload-pack for the +# smart-HTTP protocol), not by a separate port. That combined vhost is +# bound to localhost:8086 below, and Caddy reverse-proxies everything +# for git.karanj.com straight to it. Only git-upload-pack is wired up, so +# push over HTTP is impossible regardless - push stays SSH-only. { # Dedicated git user for SSH push access users.users.git = { diff --git a/modules/common.nix b/modules/common.nix index e5c38d4..e27d276 100644 --- a/modules/common.nix +++ b/modules/common.nix @@ -15,10 +15,13 @@ }; }; - # Firewall: only allow SSH, HTTP, HTTPS, and DNS (for AdGuard) + # Firewall: SSH, HTTP/HTTPS (Caddy), plain DNS, and AdGuard's own + # DNS-over-TLS (853) + DNS-over-HTTPS (8443) listeners. + # Port 3000 (AdGuard's plain-HTTP web UI) is deliberately NOT opened here - + # see modules/adguard.nix for why it still binds 0.0.0.0 anyway. networking.firewall = { enable = true; - allowedTCPPorts = [ 22 80 443 53 ]; + allowedTCPPorts = [ 22 80 443 53 853 8443 ]; allowedUDPPorts = [ 53 ]; }; @@ -47,10 +50,13 @@ sops ]; - # Automatic security updates for the OS + # Automatic security updates for the OS. + # This repo is self-hosted via cgit (modules/cgit.nix), so the flake URL + # below points at this same server rather than GitHub - update the repo + # name if you push this config somewhere else. system.autoUpgrade = { enable = false; # set to true once you are comfortable with unattended reboots - flake = "github:YOUR_USERNAME/nix#eurovm"; # update to your actual flake URL + flake = "git+https://git.karanj.com/feynman.git#eurovm"; flags = [ "--update-input" "nixpkgs" ]; }; } |
