TLS certificate setup on NixOS
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.
No webmentions were found.