summary refs log tree commit diff
path: root/modules/foundation/www
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2025-04-16 12:49:53 +0200
committerMel <mel@rnrd.eu>2025-04-16 12:49:53 +0200
commit5e1a9f0179c0beb73b63140a3ee20c4acb04fcc4 (patch)
treee2d3c83e761dc2bec056d81c268bc500ad148a6c /modules/foundation/www
parent6c9f992808b6cf8b079f4c8cfa5625de1e624618 (diff)
downloadnetwork-5e1a9f0179c0beb73b63140a3ee20c4acb04fcc4.tar.zst
network-5e1a9f0179c0beb73b63140a3ee20c4acb04fcc4.zip
Move WWW configuration into foundation module, and make it configurable
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'modules/foundation/www')
-rw-r--r--modules/foundation/www/default.nix130
-rw-r--r--modules/foundation/www/tailnet.nix121
2 files changed, 251 insertions, 0 deletions
diff --git a/modules/foundation/www/default.nix b/modules/foundation/www/default.nix
new file mode 100644
index 0000000..2e2b662
--- /dev/null
+++ b/modules/foundation/www/default.nix
@@ -0,0 +1,130 @@
+{
+  me,
+  config,
+  pkgs,
+  lib,
+  util,
+  ...
+}:
+
+let
+  inherit (lib)
+    mergeAttrsList
+    mkIf
+    mkEnableOption
+    mkOption
+    ;
+  inherit (config.age) secrets;
+
+  cfg = config.foundation.www;
+
+  rnrdUrl = if me.is.renard then "rnrd.eu" else "${me.name}.rnrd.eu";
+
+  default-page-index = pkgs.substituteAll {
+    src = ../../../assets/base.html;
+    env.me = util.titleCase me.name;
+  };
+
+  default-page = pkgs.linkFarm "www-base" {
+    "index.html" = default-page-index;
+    "favicon.png" = ../../../assets/favicon.png;
+  };
+
+  certificate = domain: {
+    ${domain} = {
+      domain = "*.${domain}";
+      extraDomainNames = [ domain ];
+
+      dnsProvider = "cloudflare";
+      credentialFiles = {
+        CLOUDFLARE_DNS_API_TOKEN_FILE = secrets.cloudflare-dns.path;
+      };
+    };
+  };
+
+  defaultHost = domain: certificate: base: log: {
+    default = true;
+    serverName = domain;
+    forceSSL = true;
+    useACMEHost = certificate;
+
+    root = base;
+    extraConfig = ''
+      access_log /var/log/nginx/${log}.access.log json_combined;
+    '';
+  };
+
+in
+{
+  imports = [ ./tailnet.nix ];
+
+  options.foundation.www = {
+    enable = mkEnableOption "www server";
+    public = mkEnableOption "public access through rnrd.eu url";
+
+    defaultPage = mkOption {
+      type = lib.types.package;
+      default = default-page;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    age.secrets = {
+      cloudflare-dns.file = ../../../secrets/cloudflare-dns.age;
+    };
+
+    security.acme = {
+      acceptTerms = true;
+      # causes issues with tailscale certificates
+      preliminarySelfsigned = false;
+      defaults = {
+        email = "mel@rnrd.eu";
+        # our certificates are really only used with Nginx
+        group = config.services.nginx.group;
+        reloadServices = [ "nginx.service" ];
+      };
+
+      # yes, we generate both certificates, even if they are not
+      # used by every machine, but as long as it doesn't cause
+      # any problems... :)
+      certs = mergeAttrsList [
+        (certificate "rnrd.eu")
+        (certificate "rnrd.fyi")
+      ];
+    };
+
+    services.nginx = {
+      enable = true;
+      recommendedGzipSettings = true;
+      recommendedOptimisation = true;
+      recommendedProxySettings = true;
+      recommendedTlsSettings = true;
+
+      statusPage = true;
+
+      commonHttpConfig = ''
+        log_format json_combined escape=json '{'
+          '"time_local":"$time_local",'
+          '"remote_addr":"$remote_addr",'
+          '"remote_user":"$remote_user",'
+          '"request":"$request",'
+          '"status": "$status",'
+          '"body_bytes_sent":"$body_bytes_sent",'
+          '"request_length":"$request_length",'
+          '"request_time":"$request_time",'
+          '"http_referrer":"$http_referer",'
+          '"http_user_agent":"$http_user_agent",'
+          '"upstream_response_time":"$upstream_response_time",'
+          '"upstream_addr":"$upstream_addr",'
+          '"upstream_status":"$upstream_status"'
+        '}';
+        access_log /var/log/nginx/access.log json_combined;
+        error_log /var/log/nginx/error.log warn;
+      '';
+
+      virtualHosts = {
+        base = mkIf cfg.public (defaultHost rnrdUrl "rnrd.eu" cfg.defaultPage "base");
+      };
+    };
+  };
+}
diff --git a/modules/foundation/www/tailnet.nix b/modules/foundation/www/tailnet.nix
new file mode 100644
index 0000000..ff2410c
--- /dev/null
+++ b/modules/foundation/www/tailnet.nix
@@ -0,0 +1,121 @@
+# NOTE: the tailnet virtual host and it's certificate management
+# has been mostly superseded by the `rnrd.fyi` domain, allowing
+# for both vastly simpler certificate requesting and subdomains,
+# which tailscale does not support for their magicdns product.
+{
+  me,
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+let
+  cfg = config.foundation.www;
+
+  rnrdInternalUrl = if me.is.renard then "rnrd.fyi" else "${me.name}.rnrd.fyi";
+
+  oneWeekInSeconds = 7 * 24 * 60 * 60;
+
+  tailscaleRenewScript = pkgs.writeShellScript "tailscale-cert-renew" ''
+    set -euxo pipefail
+
+    check_validity() {
+      pem=$1
+      ${pkgs.openssl}/bin/openssl x509 \
+    -checkend ${toString oneWeekInSeconds} \
+    -noout <$pem
+    }
+
+    try_renew() {
+      ${pkgs.tailscale}/bin/tailscale cert \
+    --cert-file certificates/fullchain.pem \
+    --key-file certificates/key.pem \
+    ${me.tailscale.domain}
+    }
+
+    cut_out_certificate_authority() {
+      fullchain=$1
+      buf=""
+      while read LINE; do
+    if [[ $LINE == *"BEGIN CERTIFICATE"* ]]; then
+      buf=""
+    fi
+    buf="$buf$LINE"$'\n'
+      done < $fullchain
+      echo "$buf"
+    }
+
+    install_certificates() {
+      touch out/renewed
+      cp -vp 'certificates/fullchain.pem' out/fullchain.pem
+      cp -vp 'certificates/key.pem' out/key.pem
+      ln -sf fullchain.pem out/cert.pem
+      cat out/key.pem out/fullchain.pem > out/full.pem
+      cut_out_certificate_authority out/fullchain.pem > out/chain.pem
+      chown 'acme:nginx' out/*
+      chmod 640 out/*
+    }
+
+    if [[ ! -e 'out/fullchain.pem' ]] || ! check_validity out/fullchain.pem; then
+      echo 1>&2 "attempting tailscale certificate renewal..."
+      if ! try_renew; then
+    echo 1>&2 "renewal failed :("
+    exit 1
+      fi
+      install_certificates
+      echo 1>&2 "successfully renewed certificate :)"
+    else
+      echo 1>&2 "renewal not yet necessary."
+    fi
+  '';
+
+in
+{
+  options.foundation.www = {
+    tailnet = lib.mkEnableOption "tailnet internal host";
+  };
+
+  config =
+    lib.mkIf (cfg.enable && cfg.tailnet) {
+      # overwrite default acme behaviour with tailscale
+      systemd.services."acme-${me.tailscale.domain}" = {
+        after = [ "tailscaled.service" ];
+        requires = [ "tailscaled.service" ];
+        serviceConfig = {
+          ExecStart = lib.mkForce "+${tailscaleRenewScript}";
+        };
+      };
+
+      # tailnet internal vhost
+      services.nginx.virtualHosts = {
+        # mostly superceded
+        tailnet = {
+          forceSSL = true;
+          enableACME = true;
+          serverName = me.tailscale.domain;
+          listenAddresses = [ me.tailscale.ip ];
+          # point to the default page, for now!
+          locations."/" = {
+            alias = "${cfg.defaultPage}/";
+          };
+          extraConfig = ''
+            access_log /var/log/nginx/tailnet.access.log json_combined;
+          '';
+        };
+
+        # default page for the `rnrd.fyi` internal domain
+        ${rnrdInternalUrl} = {
+          useACMEHost = "rnrd.fyi";
+          forceSSL = true;
+          listenAddresses = [ me.tailscale.ip ];
+          locations."/" = {
+            alias = "${cfg.defaultPage}/";
+          };
+          extraConfig = ''
+            access_log /var/log/nginx/tailnet.access.log json_combined;
+          '';
+        };
+      };
+    };
+}