Skip to content

feat(extensions): add Enable HTTPS extension (Let's Encrypt / certbot DNS-01)#806

Open
lexfrei wants to merge 10 commits into
dw-0:masterfrom
lexfrei:feat/enable-https-certbot-extension
Open

feat(extensions): add Enable HTTPS extension (Let's Encrypt / certbot DNS-01)#806
lexfrei wants to merge 10 commits into
dw-0:masterfrom
lexfrei:feat/enable-https-certbot-extension

Conversation

@lexfrei

@lexfrei lexfrei commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

What

Adds an optional "Enable HTTPS" extension. It obtains a Let's Encrypt certificate via certbot's DNS-01 challenge and reconfigures the Mainsail/Fluidd nginx site into an HTTP→HTTPS redirect plus a TLS server block on port 443.

DNS-01 is used instead of HTTP-01 so it works on a printer that is only reachable on the LAN: HTTP-01 needs the CA to reach http://<fqdn>/.well-known/... from the public internet, which a private-IP host cannot satisfy. DNS-01 only needs an API token for a domain whose DNS is hosted by a supported provider. Supported here: Cloudflare, DigitalOcean and Linode; the provider model is a small dataclass, so adding more is a single entry.

Why

KIAUH installs Mainsail/Fluidd behind nginx on port 80, HTTP only, with no built-in TLS option. Every LAN-only user currently hand-rolls a certbot DNS-01 issuance plus an nginx rewrite by hand. This turns it into a guided, reversible action.

Behaviour

  • Enable: gate on a Debian-based host, pick the client and DNS provider, prompt for the FQDN, email and credentials, issue the certificate, install a renewal reload hook, then apply the nginx rewrite transactionally — validate with nginx -t and restore the previous config if it fails, so nginx is never left unable to load.
  • Disable: regenerate the original plain-HTTP site from KIAUH's own template (restoring the original listen port, read back from the live config) and reload. The certificate and credentials are kept; the manual cleanup commands are printed, not run.
  • The provider token is written only to /root/.secrets/<provider>.ini (chmod 600 in a 700 dir), passed to the file via stdin, never as a command argument, never logged, never in KIAUH config. The prompt does not echo.

Also included

  • Fix get_nginx_listen_port: it took the last token of a listen line, so listen 443 ssl http2; parsed to http2 and the port dropped to None. It now reads the address token after listen, so ssl / IPv6 / host:port forms resolve to the numeric port. Independent of the feature; happy to split it out.
  • "Reconfigure Listen Port" now refuses to run on an HTTPS site (it would otherwise strip the TLS block and orphan the certificate).
  • A no-echo secret-input helper (get_secret_input, via getpass) for credentials.

Notes

  • Maintainership: I'm happy to own this extension and act as its code owner going forward. For now maintained_by is set to dw-0 by analogy with the other bundled extensions — glad to switch it to my username if you'd prefer I maintain it.
  • Moonraker cors_domains is left unchanged: nginx is a same-origin reverse proxy, so the normal UI flow is not subject to CORS. The success dialog mentions the manual step only for the cross-origin case.
  • Route53 / Google DNS are deferred — they use a different auth model (env/IAM, JSON key) rather than a single-token credentials file.

Testing

86 new tests: pure transform with a golden snapshot, provider command/credentials assembly, secret handling, transactional apply + rollback, port round-trip, distro gate, listen-port parser edge cases, and the port-reconfigure guard. Full suite passes; ruff and mypy clean on the new modules.

lexfrei added 10 commits June 8, 2026 22:58
Introduce the side-effect-free core of an optional HTTPS feature: helpers
that rewrite a KIAUH-generated Mainsail/Fluidd nginx site into an
HTTP->HTTPS redirect server block plus a TLS server block carrying the
original body. The body is taken from the on-disk config rather than a
duplicate template, so it always reflects what KIAUH actually generated.

Keeping this logic pure (no subprocess, sudo or certbot) makes the config
transformation fully unit-testable without a running nginx or a Debian
host. Covered by structural tests plus a golden-output snapshot.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
get_nginx_listen_port took the last whitespace token of a listen line, so
"listen 443 ssl http2;" parsed to "http2" and the port silently dropped to
None. That excluded such ports from read_ports_from_nginx_configs and any
port-conflict check. Take the address token right after the listen keyword
and keep the part after the last colon, so both "listen 443 ssl http2;" and
"listen [::]:80;" resolve to the numeric port.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
get_string_input reads via input(), which echoes to the terminal and leaves
the value in scrollback - unsafe for credentials such as DNS provider API
tokens. Add get_secret_input, which reads through getpass so the secret is
never displayed. The value is returned verbatim (only blank input is
rejected) so a secret is never silently altered.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Describe each supported certbot DNS plugin as pure data (apt package, certbot
flags, credentials-file fields) and assemble the certbot command and the
credentials-file body generically from it, so adding a provider is one dict
entry. Ship Cloudflare, DigitalOcean and Linode. The credentials and
propagation flags are optional, leaving room for a future env/IAM provider
with no credentials file. The assembled command references only the
credentials file path, never the secret, and uses long-form flags throughout.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Thin, mockable wrappers for the privileged side of HTTPS issuance: install
certbot and the DNS plugin, write the provider credentials to a root-owned
0600 file, run certbot certonly, and install a renewal deploy hook that
reloads nginx. The provider secret reaches the credentials file only through
tee's stdin (stdout discarded) and never appears as a command argument or a
log line. Certbot output is shown live and never captured, so a secret cannot
leak through its stderr. The deploy hook is idempotent and reloads nginx only
when a certificate is actually renewed.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Wrap the privileged nginx steps needed to apply an HTTPS rewrite safely:
back up the current site, write the new one, validate with nginx -t, and
reload or restore the backup. This lets the orchestration switch a site to
HTTPS without ever leaving nginx holding a configuration it cannot load -
critical when nginx is the only way to reach the printer.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Wire the HTTPS feature into the Extensions menu. Enabling gates on a
Debian-based host, selects the Mainsail/Fluidd site and a DNS provider,
prompts for the domain, email and credentials (the token via the no-echo
input), obtains a certificate, installs the renewal hook and applies the
HTTPS rewrite transactionally - reverting nginx if the new config fails
nginx -t. Disabling regenerates the original HTTP site from KIAUH's own
template and keeps the certificate and credentials, printing the manual
cleanup commands instead of deleting them.

A same-origin reverse proxy does not need Moonraker cors_domains changes,
so that is surfaced as a conditional note rather than applied automatically.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
An HTTPS site carries a dedicated 443 TLS block alongside an :80 redirect.
Running "Reconfigure Listen Port" on it would regenerate a plain-HTTP config,
stripping the TLS directives and orphaning the certificate. Detect a TLS
block and refuse, pointing the user at the HTTPS extension to disable it
first. Also pin that the listen-port parser resolves such a dual-block site
to 443 - the value the HTTPS port-conflict guard depends on.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Make enabling then disabling HTTPS preserve a non-default port: the redirect
block now listens on the port the site is currently served on (read from the
live config), and disabling reads the port back from that redirect block
rather than from KIAUH settings, so a custom port survives the round-trip.

Harden the two issuance failure paths: a certbot/plugin apt install failure
is caught and reported instead of crashing the menu, and a failed certbot
run removes the credentials file it wrote moments earlier so a token is not
left behind for an operation that aborted. Also document that the rewrite
only carries the first server block.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…ields

A non-secret DNS credential field with no default was read through the
alphanumeric-only input path, which would reject any value containing '-',
'_', '.' or '/' - common in zone ids, account names and API versions. Read
all non-secret fields with special characters allowed and the field default
offered, so a future provider that adds such a field works without a silent
"invalid choice" rejection.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei lexfrei marked this pull request as ready for review June 8, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant