feat: expiry warnings, per-site off-site backups, per-site fail2ban, verified node→master TLS - #56
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 labelsowner_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, noowner_email, no expiry) it says so loudly and audits the rowskippedrather 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_banguards 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_tlsbecomes tri-state — absent = verify whenever the master URL ishttps://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