DigressEd.net

Ocasional Digressions

NixOS/nginx/acme with mutliple vhosts

(TL;DR: skip to the config)

I recently migrated my VPS from arch to nixos which also meant migrating my apache2 conf to nginx since it's what seems to be most commonly used. I got 90% of the way with my various domains (all of static content) but the last bit took a lot of patience as I couldn't find examples that handled all my cases:

  • domain 1 (this blog):
    • http redirected to https.
    • www. redirected to root domain, hosting the about page.
    • pages served from a separate location for the blog. subdomain.
    • a redirect for another subdomain to an external site.
  • domain 2, another personal site, nothing unexpected:
    • http redirected to https.
    • www. redirected to root domain.
  • domain 3, a domain not hosted on the VPS, just a redirect to an external site.
    • needs http and https redirected to the external site.

I thought tweaking the conf a bit would be trivial but, while trying suggestions from other posts and even NixOS's Wiki all I ended up doing was :

  • arbitrary subdomains redirected to another vhost,
  • broken https redirects,
  • getting rate-limited by Let's Encrypt because I didn't realize the implications of changing the acme config (newbie error).

I got all frustrated and commented out the entire acme / nginx config and focusing on other things while the Let's Encrypt restriction passed.

I determined the main source of my problems were the catch-all vhosts suggested on the Wiki and other places plus some missing forceSSL = true which turns out are really needed. They catch-all hosts seem ideal but it is not straightforward to follow as soon as you have different sub-domains that should not all be redirected to the same base host. I finally started re-enabling one by one, using staging certificates, and got it all working. Surely it could be simplified but that's for another time.

Sample Config

# set up files / permissions
systemd = {
  # for nginx to write logs
  services.nginx.serviceConfig.ReadWritePaths = [ "/var/log/nginx" ];

  # where all files for the sites are in the filesystem - referenced
  # later in the nginx config
  tmpfiles.settings = {
    www = {
      "/var/www/domain-1.net" = {
        f = { user = "<your user id>"; group = "nginx"; };
      };
      "/var/www/domain-2.com" = {
        f = { user = "<your user id>; group = "nginx"; };
      };
    };
  };
};

# let's encrypt certificates
security = {
  acme = {
    acceptTerms = true;
    defaults = {
      email = "<your contact email>";
      group = "nginx";

      # Be aware there are rate-limits so too many changes to your
      # certs config may result in rejected requests for 24-hours.
      # Instead, uncomment the server option to enable use of the
      # staging environment
      #server = "https://acme-staging-v02.api.letsencrypt.org/directory";
      #
      # If you need to force-regenerate certificates, you can do so
      # with:
      #   sudo systemctl clean --what=state acme-domain-1.net.service
      #
    };

    certs = {
      "domain-1.net" = {
        webroot = "/var/lib/acme/acme-challenge";
        # list all subdomains that must share the certificate
        extraDomainNames = [ "www.domain-1.net" "sub.domain-1.net" "redir.domain-1.net" ];
      };
      "domain-2.com" = {
        webroot = "/var/lib/acme/acme-challenge";
        extraDomainNames = [ "www.domain-2.com" ];
      };
      "domain-3.com" = {
        webroot = "/var/lib/acme/acme-challenge";
        extraDomainNames = [ "www.domain-3.com" ];
      };
    };
  };
};

# finally the web server config
services.nginx = {
  enable = true;
  logError = "stderr emerg";
  package = pkgs.nginxStable.override { openssl = pkgs.libressl; };

  virtualHosts = {

    domain-1 = {
      serverName = "domain-1.net";         # matches domain used in security.acme.certs
      forceSSL = true;                     # needed to handle http://domain-1.net -> https://domain-1.net redirection
      enableACME = true;
      root = "/var/www/domain-1.net/html"; # where the site's files are found

      # acme path for request validation. `root` must match
      # `webroot` in security.acme.certs.<domain>
      locations."/.well-known/acme-challenge" = {
        root = "/var/lib/acme/acme-challenge";
      };

      extraConfig = ''
         charset utf-8;
         # error_log /var/log/nginx/domain-1_error_log emerg; # uncomment if you want separate error log file
         access_log /var/log/nginx/domain-1_access_log;
      '';
    };

    # www version of domain-1.net. Needs the acme location for cert
    # validation. All other requests are dedirected to https://domain-1.net/<path>
    "www.domain-1.net" = {
      serverName = "www.domain-1.net";
      forceSSL = true;
      useACMEHost = "domain-1.net";

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };

        "/" = {
          return = "301 https://domain-1.net$request_uri";
        };
      };
    };

    # separate subdomain served from a different location
    blog = {
      serverName = "blog.domain-1.net";
      forceSSL = true;
      useACMEHost = "domain-1.net";
      root = "/var/www/domain-1.net/blog";

      locations."/.well-known/acme-challenge" = {
        root = "/var/lib/acme/acme-challenge";
      };

      # optional, if you want separate logs
      extraConfig = ''
         charset utf-8;
         # error_log /var/log/nginx/blog_domain-1_error_log emerg; # uncomment if you want separate error log file
         access_log /var/log/nginx/blog_domain-1_access_log;
      '';
    };

    # separate subdomain, redirected to external host
    sub = {
      serverName = "sub.domain-1.net";
      forceSSL = true;
      useACMEHost = "domain-1.net";

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };
        "/" = {
          return = "301 https://externalhost.ext;
        };
      };
    };

    # second vhost, with only www. and non-www. versions. www. is
    # redirected to non-www. Defines aliases for resources outside
    #  of root path
    domain-2 = {
      serverName = "domain-2.com";
      forceSSL = true;
      enableACME = true;
      root = "/var/www/domain-2.com/html"; # / is served from .../html/ path

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };
        # point to a resources/ not within /html/
        "/css/" = {
          alias = "/var/www/domain-2.com/resources/css/";
        };
        "/js/" = {
          alias = "/var/www/domain-2.com/resources/js/";
        };
        # redirect domain-2.com/info to domain-2.com/about
        "/info" = {
          return = "301 https://domain-2.com/about/";
        };
      };

      extraConfig = ''
         charset utf-8;
         # error_log /var/log/nginx/domain-2_error_log emerg; # uncomment if you want separate error log file
         access_log /var/log/nginx/domain-2_access_log;
      '';
    };

    # www version of domain-2.com. Needs the acme location for cert
    # validation. All other requests are dedirected to https://domain-2.com/<path>
    "www.domain-2.com" = {
      serverName = "www.domain-2.com";
      forceSSL = true;
      useACMEHost = "domain-2.com";

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };

        "/" = {
          return = "301 https://domain-2.com$request_uri";
        };
      };
    };

    # a third domain - No files are hosted.
    # only here to handle ssl certificate and redirect both
    # domain-3.com and www.domain-3.com to an external site (e.g., soundcloud)
    "domain-3.com" = {
      serverName = "domain-3.com";
      serverAliases = [ "www.domain-3.com" ];
      forceSSL = true;
      enableACME = true;

      locations."/.well-known/acme-challenge" = {
        root = "/var/lib/acme/acme-challenge";
      };

      locations."/" = {
         return = "301 https://soundcloud.com/myband";
      };
    };
  };
};

Published: 2024-09-07

Tagged: nixos vps vhosts acme nginx

Dates with babashka

I seldomly do date/time manipulation with babashka and, while working on a recent personal project, I had to look it all up and it took much longer than it should. Ergo, for future reference:

  1. Imports:
    (ns mybb
      (:import
       (java.time Instant Duration ZonedDateTime ZoneId)
       (java.time.format DateTimeFormatter)))
    
  2. Common defs:
    (def now (Instant/now))
    (def zone-id (ZoneId/systemDefault)) ; => #object[java.time.ZoneRegion 0x71cb68be "Europe/Paris"]
    (def local-date-pattern (DateTimeFormatter/ofPattern "dd/MM/yyyy"))
    (def tz-datetime-pattern (DateTimeFormatter/ofPattern "dd/MM/yyyy HH:mm:ss z"))
    
  3. Formatting a date instance as a string:
    (.format local-date-pattern date)
    
  4. Get the system zone-id and determine the time-offset.
    (def tz-offset (.getOffset (.getRules zone-id) now)) ; => #object[java.time.ZoneOffset 0x57d84226 "+02:00"]
    (.toString tz-offset) ; => "+02:00"
    
  5. Parse a date to create a ZonedDateTime.
    (ZonedDateTime/parse "09/11/2023 12:01:04 +02:00" tz-datetime-pattern) ; => #object[java.time.ZonedDateTime 0x70411fb8 "2023-11-09T12:01:04+02:00"]
    
  6. date +/- number of days
    (.minusDays zoned-datetime 10)
    (.plusDays zoned-datetime 10)
    
  7. Difference between two date-times, in seconds. NOTE: to convert to instants, you must be using ZonedDateTime.
    (-> (Duration/between (.toInstant zoned-datetime-1)
                          (.toInstant zoned-datetime-2))
        (.getSeconds))
    

Published: 2024-08-19

Tagged: clojure java-time babashka

Pointless SNCF notifications

I really wish the SNCF's notifications I receive 20 minutes before my departure would skip the redundant information to ensure what I need to know is actually presented.

Redundant Information

What I want to see:

  • train number ✅
  • expected departure time ✅
  • platform ❌

What I don't need to see:

  • the station I'm leaving from (because if I'm not there already, I'm pretty sure I know where I need to go).

Published: 2024-01-31

Tagged: mobile notifications

Archive