Skip to content

feat: expiry warnings, per-site off-site backups, per-site fail2ban, verified node→master TLS - #56

Merged
nechodom merged 1 commit into
mainfrom
feat/finish-backlog
Aug 5, 2026
Merged

feat: expiry warnings, per-site off-site backups, per-site fail2ban, verified node→master TLS#56
nechodom merged 1 commit into
mainfrom
feat/finish-backlog

Conversation

@nechodom

@nechodom nechodom commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Four things the product promised and the software did not do.

1. Expiry warnings were never sent

run_action's notify branch only logged. Its own comment admitted it: "real SMTP integration ships with the controller (sub-project 4)" — which never happened. Meanwhile the UI labels owner_email "for warning notifications" and profiles expose warning offsets. So a site was auto-suspended with no prior word to the customer.

Now it sends, over the same path the billing sweep uses, in Czech, stating the real behaviour read off the surrounding code: suspend on the expiry date, delete after the grace period — with the delete date computed the same .max(1) way the scheduler actually queues it. Days remaining come from the clock, not from the offset the row was queued for, so a late tick says what is really left. When it cannot send (no relay, no owner_email, no expiry) it says so loudly and audits the row skipped rather than logging success. The letter is a pure function, tested for the 30/7/1-day and same-day variants.

2. Off-site backups were node-global

So "backups to secure off-site storage" could not be sold per client. A hosting can now pin its own target. Resolution order: hosting pin → node default, so an unset hosting behaves byte-identically to before — a pure addition.

A pin naming a target that is missing, disabled, or has no readable secret uploads nothing and says so, instead of silently falling back to the node default. A customer paying for off-site backups must not have their data quietly go somewhere else — or nowhere.

3. fail2ban had no per-site switch

So hardening could not be scoped to the customers who pay for it. The per-hosting HTTP brute-force scan is now opt-out (default ON — nothing changes for existing sites), and the [fail2ban] section is finally editable in Settings instead of by hand in TOML.

Node-wide sources (sshd/ftp/mail) stay node-wide: they are not attributable to one site. The auto_ban guards are untouched — it still refuses non-public IPs, and the panel login still bans the real TCP peer, never a request header.

4. node→master TLS is now verified

This is the leg that carries the node's per-node secret in the clear on every heartbeat, and the leg that can be verified: the master normally holds a CA-issued certificate while the worker does not.

[enrollment] verify_tls becomes tri-state — absent = verify whenever the master URL is https:// with a DNS hostname; true = always; false = the documented escape hatch for a self-signed master. A verification failure is never retried with -k — silently downgrading is precisely the outcome an attacker wants, so the agent aborts and logs the fix.

Separately, the worker TLS pin is now write-once with refuse-and-warn on change, mirroring the response-signing key. The audit's objection to pin enforcement was never the enforcement — it was that the pin arrived over an unauthenticated channel and was stored last-write-wins. Enforcement stays OFF by default: this makes it safe to turn on, it does not turn it on.

Tests

926 pass (+25), fmt clean.

🤖 Generated with Claude Code

…verified node→master TLS

Four things the product promised and the software did not do.

EXPIRY WARNINGS WERE NEVER SENT. run_action's notify branch only logged;
its own comment admitted "real SMTP integration ships with the controller
(sub-project 4)", which never happened. Meanwhile the UI labels owner_email
"for warning notifications" and profiles expose warning offsets — so a site
was auto-suspended with no prior word to the customer. Now it sends, over
the same path the billing sweep uses, in Czech, stating the real behaviour
read off the surrounding code: suspend on the expiry date, delete after the
grace period, with the delete date computed the same `.max(1)` way the
scheduler actually queues it. Days remaining come from the clock, not from
the offset the row was queued for, so a late tick says what is really left
instead of counting down from a stale number. When it CANNOT send — no
relay, no owner_email, no expiry — it says so loudly and audits the row as
"skipped" rather than logging success. The letter is a pure function with
tests for the 30/7/1-day and same-day variants.

OFF-SITE BACKUPS WERE NODE-GLOBAL, so "backups to secure off-site storage"
could not be sold per client. A hosting can now pin its own target; the
resolution order is hosting pin, else node default, so an unset hosting
behaves byte-identically to before — a pure addition. A pin naming a target
that is missing, disabled, or has no readable secret uploads NOTHING and
says so, instead of silently falling back to the node default: a customer
paying for off-site backups must not have their data quietly go somewhere
else, or nowhere.

FAIL2BAN HAD NO PER-SITE SWITCH, so hardening could not be scoped to the
customers who pay for it. The per-hosting HTTP brute-force scan is now
opt-out (default ON — nothing changes for existing sites), and the
[fail2ban] section is finally editable in Settings instead of by hand in
TOML. Node-wide sources (sshd/ftp/mail) stay node-wide: they are not
attributable to one site. The auto_ban guards are untouched — it still
refuses non-public IPs, and the panel login still bans the real TCP peer,
never a request header.

NODE→MASTER TLS IS NOW VERIFIED. This is the leg that carries the node's
per-node secret in the clear on every heartbeat, and it is the leg that CAN
be verified: the master normally holds a CA-issued certificate while the
worker does not. `[enrollment] verify_tls` becomes tri-state — absent means
verify whenever the master URL is https with a DNS hostname, true always
verifies, false is the documented escape hatch for a self-signed master. A
verification failure is never retried with `-k`; silently downgrading is
precisely the outcome an attacker wants, so the agent aborts and logs the
fix. Separately, the worker TLS pin is now WRITE-ONCE with refuse-and-warn
on change, mirroring the response-signing key: the audit's objection to pin
enforcement was never the enforcement, it was that the pin arrived over an
unauthenticated channel and was stored last-write-wins. Enforcement stays
OFF by default — this makes it safe to turn on, it does not turn it on.

926 tests pass.
@nechodom
nechodom merged commit db03414 into main Aug 5, 2026
1 check passed
@nechodom
nechodom deleted the feat/finish-backlog branch August 5, 2026 18:35
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