aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorKaran Jayachandra <mail@karanjayachandra.com>2026-07-18 23:04:53 +0200
committerKaran Jayachandra <mail@karanjayachandra.com>2026-07-18 23:04:53 +0200
commit9ceddb33272e5fca6382c1b4dec2074bd1167738 (patch)
treecac68c1bdc90c9ab618e4fdb4c8eb65d27d9a647 /modules
parent718a79f8de2a55f2ab83cac7e8595cd3916ed486 (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.nix32
-rw-r--r--modules/actual.nix32
-rw-r--r--modules/adguard.nix36
-rw-r--r--modules/caddy.nix25
-rw-r--r--modules/cgit.nix14
-rw-r--r--modules/common.nix14
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" ];
};
}