- router.dnsProviders: named accounts at DNS providers, used by dynamic DNS and certificates (ddclient and lego provider lists in pkgs/router-webui/providers.json). Schema 2 migrates router.ddns. - router.acme: a Let's Encrypt certificate for the web UI via security.acme (DNS-01, resolver 1.1.1.1), served and reloaded by the web UI; Certificate page. - Web UI: password change (web UI and root), reboot button, HTTP to HTTPS redirect (router.webui.httpPort), grouped menu. - Latest kernel (linuxPackages_latest); 7.3 and 7.3.0 are the same kernel for the reboot hint. - SPECS.md: wanted later (REST API, GraphQL, MCP). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
74 lines
2.7 KiB
Nix
74 lines
2.7 KiB
Nix
# A Let's Encrypt certificate for the web UI, proven with a DNS record (DNS-01):
|
|
# no port needs to be open to the internet, and wildcards work. NixOS's
|
|
# security.acme (lego) orders and renews it; the web UI picks up the files.
|
|
{
|
|
config,
|
|
lib,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.router;
|
|
acme = cfg.acme;
|
|
|
|
# Shared with the web UI: the providers lego knows, and those with a single
|
|
# token (the variable names the file holding it). The others take an
|
|
# environment file with whatever variables they need.
|
|
providers = builtins.fromJSON (builtins.readFile ../../pkgs/router-webui/providers.json);
|
|
tokenVariables = providers.acmeToken;
|
|
|
|
prov =
|
|
cfg.dnsProviders.${if acme.provider == null then "" else acme.provider} or {
|
|
type = "none";
|
|
secretFile = null;
|
|
environmentFile = null;
|
|
};
|
|
token = tokenVariables ? ${prov.type};
|
|
secret = if token then prov.secretFile else prov.environmentFile;
|
|
has = acme.provider != null && cfg.dnsProviders ? ${acme.provider};
|
|
name = if acme.domains == [ ] then "unset" else lib.head acme.domains;
|
|
in
|
|
{
|
|
config = lib.mkIf (cfg.enable && acme.enable) {
|
|
assertions = [
|
|
{
|
|
assertion = acme.domains != [ ];
|
|
message = "router.acme: no domains for the certificate.";
|
|
}
|
|
{
|
|
assertion = has;
|
|
message = "router.acme.provider must name a connection in router.dnsProviders.";
|
|
}
|
|
{
|
|
assertion = !has || lib.elem prov.type providers.acme;
|
|
message = "router.acme: lego has no DNS provider ${prov.type}.";
|
|
}
|
|
{
|
|
assertion = !has || !lib.elem prov.type providers.acme || (secret != null && !lib.hasPrefix "/nix/store" secret);
|
|
message = "router.dnsProviders.${toString acme.provider} needs ${if token then "secretFile" else "environmentFile"} for certificates, a file outside the Nix store (the secret must not be copied into it).";
|
|
}
|
|
{
|
|
assertion = acme.email != null;
|
|
message = "router.acme.email: Let's Encrypt needs a contact address.";
|
|
}
|
|
{
|
|
assertion = acme.acceptTerms;
|
|
message = "router.acme.acceptTerms: the Let's Encrypt subscriber agreement must be accepted.";
|
|
}
|
|
];
|
|
|
|
security.acme = {
|
|
# Ours is the assertion above.
|
|
acceptTerms = true;
|
|
defaults.email = acme.email;
|
|
certs.${name} = {
|
|
extraDomainNames = lib.tail acme.domains;
|
|
dnsProvider = prov.type;
|
|
inherit (acme) dnsResolver;
|
|
credentialFiles = lib.optionalAttrs token { ${tokenVariables.${prov.type}} = prov.secretFile; };
|
|
environmentFile = lib.mkIf (!token) prov.environmentFile;
|
|
server = lib.mkIf acme.staging "https://acme-staging-v02.api.letsencrypt.org/directory";
|
|
};
|
|
};
|
|
};
|
|
}
|