Info dump on how to set up a HTTPS-enabled service on NixOS.

Prerequisites:

  • API access to change DNS entries for your own domain
  • NixOS
  • Static IP or dynamic DNS setup

Starting off configuration.nix:

{
  modulesPath,
  config,
  lib,
  pkgs,
  ...
}:
let
  certName = "${config.networking.hostName}-dot-${config.networking.domain}-wildcard";
in
{
  # TODO: See below
}

Networking:

networking = {
  domain = "example.org"; # TODO: Replace with your own domain
  firewall.allowedTCPPorts = [
    22 # TODO: Configure SSH (not shown)
    # I've intentionally left out port 80 since all modern clients support TLS
    443
  ];
  hostName = "your-hostname"; # TODO: Replace with the name you want for your server
};

Let’s Encrypt (ACME) setup:

security.acme = {
  acceptTerms = true;
  certs."${certName}" = {
    # TODO: Replace with a real path or something like `config.sops.secrets.acmeCredentials.path` if you enable SOPS
    credentialsFile = /path/to/acme/credentials;
    # TODO: Replace with a value from https://search.nixos.org/options?query=security.acme.certs.%3Cname%3E.dnsProvider
    dnsProvider = "some-provider";
    domain = "*.${config.networking.hostName}.${config.networking.domain}";
  };
  defaults.email = "jdoe@example.org"; # TODO: Replace with your email address
};

Enable any HTTP service. In this case I’ll be setting services.audiobookshelf.enable = true; to serve audiobooks locally. Then I’ll use nginx to serve audiobookshelf to the world:

nginx = {
  clientMaxBodySize = "4G"; # Allow uploading big audiobooks
  enable = true;
  recommendedGzipSettings = true;
  recommendedOptimisation = true;
  recommendedProxySettings = true;
  recommendedTlsSettings = true;
  virtualHosts = {
    "audiobookshelf.${config.networking.fqdn}" = {
      forceSSL = true;
      locations."/" = {
      proxyPass = "http://127.0.0.1:${builtins.toString config.services.audiobookshelf.port}";
      proxyWebsockets = true;
      extraConfig = ''
        proxy_redirect http:// $scheme://;
      '';
    };
    useACMEHost = certName;
    };
  };
};

Using sops-nix is optional, but it is a good way to automate secrets management. The Nix part of the configuration should look something like this:

sops = {
  age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
  defaultSopsFile = ./secrets/default.yaml;
  secrets = {
    acmeCredentials = { };
  };
};

Then there’s the file configuration in .sops.yaml:

keys:
  # TODO: Use `age-keygen --output ~/.config/sops/age/keys.txt` to replace the value below
  - &admin age125nlnhal9c90u8vwtveurccf5emtdk2u5nr3vv3yyu5kfmldgsusre2n8k
  # TODO: Use `ssh-keyscan HOST | ssh-to-age` to replace the value below
  - &host_audiobookshelf age1hr9vcf9jlfgxf4f6c3f9yq2kklzj7tplxv7cyulhqzfvhx42le4ssnq89j
creation_rules: # sops updatekeys secrets/*
  - path_regex: audiobookshelf/secrets/[^/]+\.(yaml|json|env|ini|sops)$
    key_groups:
      - age:
          - *admin
          - *host_audiobookshelf

You can then add encrypted credentials in audiobookshelf/secrets/default.yaml, which would look something like this:

acmeCredentials: ENC[AES256_GCM,data:…,type:str]
sops:
  kms: []
  gcp_kms: []
  azure_kv: []
  hc_vault: []
  age:
    - recipient: age125nlnhal9c90u8vwtveurccf5emtdk2u5nr3vv3yyu5kfmldgsusre2n8k
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        
        -----END AGE ENCRYPTED FILE-----
    - recipient: age1hr9vcf9jlfgxf4f6c3f9yq2kklzj7tplxv7cyulhqzfvhx42le4ssnq89j
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2024-11-20T04:20:13Z"
  mac: ENC[AES256_GCM,data:…,type:str]
  pgp: []
  unencrypted_suffix: _unencrypted
  version: 3.9.1

Finally, to allow nginx to read the TLS certificate:

users.users.nginx.extraGroups = [ "acme" ];

Apply this configuration, and https://audiobookshelf.your-hostname.example.org should be accessible online.