Add DNS accounts, Let's Encrypt, password change, reboot, HTTP redirect

- 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
This commit is contained in:
2026-10-30 19:27:49 +01:00
co-authored by Claude Sonnet 5.5
parent a32bf42777
commit caf2c6b26d
33 changed files with 1985 additions and 224 deletions
+108 -20
View File
@@ -580,6 +580,111 @@ in
);
};
dnsProviders = mkOption {
default = { };
description = ''
Accounts at DNS providers, by name. Dynamic DNS (`ddns.provider`) and
certificates (`acme.provider`) use them, so a token is kept once.
'';
type = types.attrsOf (
types.submodule {
options = {
type = mkOption {
type = types.str;
example = "cloudflare";
description = ''
The provider. For dynamic DNS the ddclient protocol
(<https://ddclient.net/protocols.html>); for certificates the
provider code of lego (<https://go-acme.github.io/lego/dns>).
The names are the same where both know the provider.
The lists are in `pkgs/router-webui/providers.json`.
'';
};
username = mkOption {
type = types.str;
default = "token";
description = "Login for dynamic DNS; `token` for an API token (Cloudflare).";
};
server = mkOption {
type = types.nullOr types.str;
default = null;
example = "dynupdate.no-ip.com";
description = "The provider's update server, for protocols that need one (`dyndns2`).";
};
secretFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "/var/lib/router/dns-secrets/cloudflare";
description = ''
File with the password or API token (dynamic DNS; for
certificates through cloudflare, desec, digitalocean, duckdns
and hetzner the same token). Never a path in the Nix store.
'';
};
environmentFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "/var/lib/router/dns-secrets/route53.env";
description = ''
File with `VARIABLE=value` lines for lego, for certificates
through providers other than those above; the variables are
listed at <https://go-acme.github.io/lego/dns>. Never a path in the Nix store.
'';
};
};
}
);
};
acme = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
A certificate from Let's Encrypt for the web UI, proven by a DNS
record (DNS-01, no port open to the internet), renewed automatically.
'';
};
provider = mkOption {
type = types.nullOr types.str;
default = null;
example = "cloudflare";
description = "The name of a connection in `dnsProviders` that can change the records of the domains.";
};
domains = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "router.example.org" ];
description = "Names in the certificate; the first is its name. A wildcard like `*.home.example.org` is possible.";
};
email = mkOption {
type = types.nullOr types.str;
default = null;
description = "Where Let's Encrypt writes about expiring certificates.";
};
dnsResolver = mkOption {
type = types.nullOr types.str;
default = "1.1.1.1:53";
example = "9.9.9.9:53";
description = ''
Resolver (`address:port`) that lego asks to find the zone's name
servers and to check that the challenge record has spread. Without
one it would use the router's own Unbound, which answers the local
domain itself and caches. `null`: the router's resolver.
'';
};
staging = mkOption {
type = types.bool;
default = true;
description = "Use the staging service: certificates that browsers do not trust, for trying it out.";
};
acceptTerms = mkOption {
type = types.bool;
default = false;
description = "Accept the Let's Encrypt subscriber agreement.";
};
};
ddns = {
enable = mkOption {
type = types.bool;
@@ -590,28 +695,11 @@ in
uplink address changes.
'';
};
protocol = mkOption {
type = types.str;
default = "cloudflare";
example = "dyndns2";
description = "ddclient protocol of the provider, see <https://ddclient.net/protocols.html>.";
};
server = mkOption {
provider = mkOption {
type = types.nullOr types.str;
default = null;
example = "dynupdate.no-ip.com";
description = "The provider's update server, for protocols that need one (`dyndns2`).";
};
username = mkOption {
type = types.str;
default = "token";
description = "Login; `token` for a Cloudflare API token.";
};
passwordFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "/var/lib/router/ddns-secret";
description = "File with the password or API token. Never a path in the Nix store.";
example = "cloudflare";
description = "The name of a connection in `dnsProviders` to update the records through.";
};
zone = mkOption {
type = types.nullOr types.str;