summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--hosts/ahmed/configuration.nix2
-rw-r--r--hosts/ahmed/dns/default.nix33
-rw-r--r--hosts/ahmed/local-dns/certificates.nix52
-rw-r--r--hosts/ahmed/local-dns/default.nix41
-rw-r--r--hosts/ahmed/local-dns/dns-resolver.nix57
-rw-r--r--hosts/ahmed/torrenting/reverse-proxy.nix23
-rw-r--r--secrets/cloudflare-acme-token.env.agebin0 -> 1038 bytes
-rw-r--r--secrets/cloudflare-acme-token.env.example2
-rw-r--r--secrets/secrets.nix1
9 files changed, 161 insertions, 50 deletions
diff --git a/hosts/ahmed/configuration.nix b/hosts/ahmed/configuration.nix
index c36f607..4386460 100644
--- a/hosts/ahmed/configuration.nix
+++ b/hosts/ahmed/configuration.nix
@@ -23,7 +23,7 @@
./dyndns
./minecraft
./nginx
- ./dns
+ ./local-dns
];
# Create the main user.
diff --git a/hosts/ahmed/dns/default.nix b/hosts/ahmed/dns/default.nix
deleted file mode 100644
index f590f2f..0000000
--- a/hosts/ahmed/dns/default.nix
+++ /dev/null
@@ -1,33 +0,0 @@
-{metadata, ...}: {
- services.dnscache = {
- enable = true;
- clientIps = [
- "192.168" # LAN
- "127.0.0.1" # Local connections
- ];
-
- domainServers = {
- "internal" = ["127.0.0.1"];
- };
- };
-
- services.tinydns = {
- enable = true;
-
- # We will only listen for internal queries from the DNS cache.
- ip = "127.0.0.1";
-
- data = ''
- .internal:127.0.0.1:a
- =ahmed.internal:${metadata.hosts.ahmed.ipAddress}
- =muhammed.internal:${metadata.hosts.muhammed.ipAddress}
- =jellyfin.internal:${metadata.hosts.ahmed.ipAddress}
- =qbittorrent.internal:${metadata.hosts.ahmed.ipAddress}
- '';
- };
-
- networking.firewall = {
- allowedTCPPorts = [53];
- allowedUDPPorts = [53];
- };
-}
diff --git a/hosts/ahmed/local-dns/certificates.nix b/hosts/ahmed/local-dns/certificates.nix
new file mode 100644
index 0000000..25784c1
--- /dev/null
+++ b/hosts/ahmed/local-dns/certificates.nix
@@ -0,0 +1,52 @@
+# Getting HTTPS to work for local domains is pretty hard. The approach I've
+# gone with is to request a wildcard domain for `*.rumpenettet.linus.onl`. We
+# can do this because `linus.onl` is a public domain which we have control
+# over.
+#
+# This module requests a certificate from letsencrypt using DNS-01
+# verification. I have an API token which can modify DNS records for
+# `linus.onl`. This is how Lego (i.e. `security.acme`) proves domain ownership
+# when renewing the certificate.
+#
+# Any services running under `rumpenettet.local.onl` and use this certificate.
+# For NGINX that happens via `useACMEHost` and one of the options that enable
+# HTTPS.
+{
+ lib,
+ config,
+ ...
+}: {
+ security.acme = {
+ certs.${config.linus.local-dns.domain} = {
+ dnsProvider = "cloudflare";
+ dnsResolver = "1.1.1.1:53";
+ environmentFile = config.age.secrets.cloudflare-acme-token.path;
+ dnsPropagationCheck = true;
+ domain = "*.${config.linus.local-dns.domain}";
+
+ group = config.services.nginx.group;
+ reloadServices = ["nginx"];
+ };
+ };
+
+ # This file contains the variables that Lego needs to authenticate to
+ # Cloudflare. This is how we prove ownership of the domain.
+ #
+ # See: https://go-acme.github.io/lego/dns/cloudflare/
+ # See: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#EnvironmentFile=
+ age.secrets.cloudflare-acme-token.file = ../../../secrets/cloudflare-acme-token.env.age;
+
+ # Use the certificate for each subdomain in NGINX. Luckily, we can be pretty
+ # opinionated since this isn't reusable logic.
+ #
+ # NOTE: This assumes that each subdomain *has* an NGINX virtual host, which
+ # may not be the case in the future.
+ services.nginx.virtualHosts = let
+ virtualHostConfig = subdomain:
+ lib.nameValuePair "${subdomain}.${config.linus.local-dns.domain}" {
+ forceSSL = true;
+ useACMEHost = config.linus.local-dns.domain; # Same as security.acme.certs.${...} above.
+ };
+ in
+ builtins.listToAttrs (map virtualHostConfig config.linus.local-dns.subdomains);
+}
diff --git a/hosts/ahmed/local-dns/default.nix b/hosts/ahmed/local-dns/default.nix
new file mode 100644
index 0000000..6ac96e9
--- /dev/null
+++ b/hosts/ahmed/local-dns/default.nix
@@ -0,0 +1,41 @@
+# This module sets up local DNS so that services on this host become visible to devices on LAN.
+# The work is split in submodules, coordinated via the options set in this module:
+#
+# - certificates.nix: Get certs for HTTPS (surprisingly hard)
+# - dns-resolver.nix: Make local domains visible to devices
+#
+# See the files for more info on how each part works.
+{lib, ...}: {
+ imports = [
+ ./certificates.nix
+ ./dns-resolver.nix
+ ];
+
+ options = {
+ linus.local-dns = {
+ domain = lib.mkOption {
+ description = ''
+ A (sub)domain we have ownership over.
+
+ To devices using our DNS cache (on port 53), it will look like this
+ computer has the authority over that domain. It should not be used to
+ server anything public, as that would then be overwritten.
+ '';
+ type = lib.types.nonEmptyStr;
+ };
+
+ # TODO: This assumes that all subdomains are located on this host. What about our NAS? Be more flexible.
+ subdomains = lib.mkOption {
+ description = ''
+ List of subdomains that to {option}`domain` which are in use.
+ '';
+ type = with lib.types; listOf nonEmptyStr;
+ default = [];
+ };
+ };
+ };
+
+ config = {
+ linus.local-dns.domain = "rumpenettet.linus.onl";
+ };
+}
diff --git a/hosts/ahmed/local-dns/dns-resolver.nix b/hosts/ahmed/local-dns/dns-resolver.nix
new file mode 100644
index 0000000..1954a52
--- /dev/null
+++ b/hosts/ahmed/local-dns/dns-resolver.nix
@@ -0,0 +1,57 @@
+# This module creates a local DNS server which provides "split horizon DNS".
+#
+# It only serves devices on the LAN (see `services.dnscache.clientIps`) and for
+# those, it claims to have authority over the domain set in `config.linus.local-dns.domain`.
+#
+# See: https://www.fefe.de/djbdns/split-horizon
+{
+ config,
+ metadata,
+ lib,
+ ...
+}: {
+ services.dnscache = {
+ enable = true;
+ clientIps = [
+ "192.168" # LAN
+ "127.0.0.1" # Local connections
+ ];
+
+ domainServers = {
+ # Forward any requests to the split domain to our local, authoritative name server.
+ ${config.linus.local-dns.domain} = ["127.0.0.1"];
+ };
+ };
+
+ # Authoritative name server which claims ownership of the split domain.
+ services.tinydns = {
+ enable = true;
+
+ # We will only listen for internal queries from the DNS cache.
+ ip = "127.0.0.1";
+
+ # Here we publish all the services we want.
+ data = let
+ subdomainToARecord = subdomain: "=${subdomain}.${config.linus.local-dns.domain}:${metadata.hosts.ahmed.ipAddress}";
+ ARecords = lib.concatMapStringsSep "\n" subdomainToARecord config.linus.local-dns.subdomains;
+ in ''
+ # We are authoritative over ${config.linus.local-dns.domain}.
+ # Here we simply identify as localhost, as only the local dnscache instance will ever see this (I think).
+ .${config.linus.local-dns.domain}:127.0.0.1:a
+ # Next, we link all the subdomains to our LAN IP.
+ ${ARecords}
+ '';
+ };
+
+ # Allow other devices on LAN to interact with us. In the router's DHCP
+ # settings, I have set ahmed's IP as the primary DNS server. This will make
+ # all clients (which respect DNS from DHCP) use ahmed if he's online.
+ #
+ # Notably, the NAT on the router does not route external trafic here; we are
+ # a non-authoritative DNS resolver, so we don't want to service the global
+ # internet.
+ networking.firewall = {
+ allowedTCPPorts = [53];
+ allowedUDPPorts = [53];
+ };
+}
diff --git a/hosts/ahmed/torrenting/reverse-proxy.nix b/hosts/ahmed/torrenting/reverse-proxy.nix
index 1cee18f..7f8db3e 100644
--- a/hosts/ahmed/torrenting/reverse-proxy.nix
+++ b/hosts/ahmed/torrenting/reverse-proxy.nix
@@ -1,20 +1,8 @@
# This module configures a reverse proxy for the various services that are
# exposed to the internet.
-{
- pkgs,
- config,
- lib,
- ...
-}: let
- baseDomain = "internal";
- qbDomain = "qbittorrent.${baseDomain}";
- jellyfinDomain = "jellyfin.${baseDomain}";
-
+{config, ...}: let
# The internal port where qBittorrents web UI will be served.
qbWebUiPort = 8082;
-
- # Whether to use ACME/Letsencrypt to get free certificates.
- useACME = true;
in {
services.qbittorrent = {
openFirewall = false;
@@ -32,14 +20,14 @@ in {
# Use NGINX as a reverse proxy.
services.nginx = {
- virtualHosts.${qbDomain} = {
+ virtualHosts."qbittorrent.${config.linus.local-dns.domain}" = {
locations."/" = {
proxyPass = "http://localhost:${toString qbWebUiPort}";
recommendedProxySettings = true;
};
};
- virtualHosts.${jellyfinDomain} = {
+ virtualHosts."jellyfin.${config.linus.local-dns.domain}" = {
locations."/" = {
# This is the "static port" of the HTTP web interface.
#
@@ -62,5 +50,8 @@ in {
};
};
- # See also `hosts/ahmed/dns/default.nix`.
+ linus.local-dns.subdomains = [
+ "qbittorrent"
+ "jellyfin"
+ ];
}
diff --git a/secrets/cloudflare-acme-token.env.age b/secrets/cloudflare-acme-token.env.age
new file mode 100644
index 0000000..cd761fe
--- /dev/null
+++ b/secrets/cloudflare-acme-token.env.age
Binary files differ
diff --git a/secrets/cloudflare-acme-token.env.example b/secrets/cloudflare-acme-token.env.example
new file mode 100644
index 0000000..c8c10d7
--- /dev/null
+++ b/secrets/cloudflare-acme-token.env.example
@@ -0,0 +1,2 @@
+CF_DNS_API_TOKEN=7qv9jF93ls9Vww8wi3d7yRtWtki8FLbRQj2-OKSX
diff --git a/secrets/secrets.nix b/secrets/secrets.nix
index a747bcb..45a40a5 100644
--- a/secrets/secrets.nix
+++ b/secrets/secrets.nix
@@ -6,6 +6,7 @@ let
muhammedKey = metadata.hosts.muhammed.sshPubKey;
in {
"cloudflare-ddns-token.env.age".publicKeys = [muhammedKey ahmedKey];
+ "cloudflare-acme-token.env.age".publicKeys = [muhammedKey ahmedKey];
"duksebot.env.age".publicKeys = [muhammedKey ahmedKey];
"mullvad-wg.key.age".publicKeys = [muhammedKey ahmedKey];
"wraaath-sftp-password.txt.age".publicKeys = [muhammedKey ahmedKey];