This is the Backups + REST API release. Backups now run on a built-in cron scheduler, with credentials sealed at rest, a job-handle pattern keeping long-running ops alive past the WS timeout, and a HTTPS+auth-enforcing receiver. The engine's JSON-RPC dispatcher now also speaks REST at
/api/v1/{...}with OpenAPI 3.1 and a built-in Swagger UI. Disk health gets first-class NVMe, SAS, ATA and PCIe-link panels; alerts fire on critical SMART attributes before the drive's own self-assessment flips. Plus a security pass — input validators, configfs secret redaction, systemd restart hardening, scanner-flagged response headers, two new destructive-confirm dialogs.
Headline changes
-
Cron-driven backup scheduler (#402). Backup profiles run on their configured cron expression end-to-end. A new
nasty_backup::schedulerparses cron, ticks every 60s, fires each due profile in its owntokio::spawnso a slow run can't starve the scheduler, and seedslast_attemptedto now on engine start to avoid a thundering-herd catch-up after a long downtime. Long-running RPCs (backup.repo.init,backup.run,backup.repo.check) return aBackupJobhandle that the WebUI polls every 2s, keeping multi-hour init / backup / check operations alive across reloads and well past the WS timeout (#404). The Backups page rehydrates in-flight job badges across reloads, the edit form has the same Hourly/Daily/Weekly chip group as create, and the target is editable post-creation (#405, #406, #410, #416). -
Backup credentials sealed at rest (#401, #403). Repository passwords + S3/B2 cloud secrets are sealed via
systemd-creds --with-key=auto(TPM2+host when available, host-only fallback) before they hit/var/lib/nasty/backups.json. A newnasty_common::secretsmodule owns the seal/unseal path; on first start after upgrade a boot phasebackups.migrate_secrets(30s budget) walks existing profiles and encrypts in place, idempotently. Wire shape stays backward-compatible — plaintext on input still works; output is redacted to"***"or absent. Abackup.secrets_statusRPC drives a Secrets: TPM-protected / encrypted at rest / plaintext pill in the WebUI so operators see at a glance which protection level applies. Filesystem encryption keys stay on the existing PCR-7 path (Phase 2, separate review). -
Backup Server: HTTPS, required auth, paste-the-CA flow (#407, #408, #409, #411, #412, #417). The bundled
restic-rest-serverreads Caddy's already-issued cert for the box'stls_domainand serves HTTPS for free (#407). Authentication is HTTP basic backed by a per-box 32-char alphanumeric password sealed via systemd-creds, with a Receiver credentials card on /services exposing Show / Copy / Rotate buttons (#408). Source-side profiles get first-class Username + Password fields that round-trip as"***"and seal to disk via the same systemd-creds path asBackupProfile.password(#417) — credentials live in dedicated fields instead of riding inside the URL, so they stay out of list responses. Source-side profiles can also paste their own trust anchor PEM into a Trusted CA certificate field (#409); the receiver's local CA is exposed inline on /services so the operator doesn't have to bounce through /tls to copy it (#412). -
In-engine REST API + Swagger UI (#350, #351, #352, #353). The method registry moved into
nasty-engineand was backfilled from 106 → 256 methods — every RPC the dispatcher actually accepts now has typed params, role, and result schemas. Six docs-vs-runtime drift bugs fixed in passing. A new REST gateway at/api/v1/{path}translates dotted method names to slash paths with HTTP verbs inferred from the last segment (*.get/.list/.status→ GET,*.delete→ DELETE,*.set/.update→ PUT, else POST). Auth matches/ws:nasty_sessioncookie orAuthorization: Bearer./api/openapi.jsonemits a ~1.1 MB OpenAPI 3.1 doc covering all 256 methods + 104 component schemas;/api/docsmounts Swagger UI v5.17.14 vendored viainclude_dir!— no CDN, works air-gapped, Try it out carries the same session cookie as the WebUI. -
Disk health: full per-protocol detail (#366, #367, #368, #369, #373, #375, #376, #378, #380, #381, #382). NVMe drives show a full panel reading the controller's health log — endurance %, spare-vs-threshold, critical-warning bits, media errors, host R/W totals, unsafe shutdowns, time above warning/critical temps (#367). SAS drives get an equivalent SCSI panel including a health override that flips
health_passedto false when any uncorrected R/W/V counter is non-zero — caught a real failing Seagate that smartctl still reported PASSED (#369). ATA drives get a summary panel with interface-speed downgrade detection, reallocated/pending/uncorrectable tiles, SATA SSD endurance via smartctl 7.5 (#373, #378). Topology shows each controller's PCIe gen × width × computed MB/s including for RAID-tunneled drives via/sys/class/scsi_host/(#380, #381), with chipset-integrated controllers annotated (#382). Enumeration viasmartctl --scan-open -jcovers drives behind MegaRAID and similar SAS HBAs (#368). SMART attribute names + descriptions + critical flags come from a 79-row Backblaze-evidence-backed table adapted from Scrutiny, and a newSmartAttributealert fires the moment any of the 9 statistically-predictive attributes goes non-zero (#375, #376).
Backups
- Cron-driven scheduler (#402); long-running RPCs return job handles, WebUI polls (#404).
- Repo passwords + S3/B2 secrets sealed via systemd-creds; boot-time migration converts existing plaintext (#401, #403).
- Edit the backup target after creation (#405) — fix a typo in a REST URL, swap to a new bucket, or move from local to remote in place.
- Backup Server: HTTPS via Caddy's cert (#407) + required HTTP basic auth, 32-char generated password sealed at rest (#408).
- Source-side REST profiles: separate Username + Password fields, sealed-at-rest, same shape as S3/B2 secrets (#417). Pre-existing profiles with userinfo in the URL keep working unchanged — the field-based path keeps passwords out of list responses.
- Operator-supplied trust anchor field on source profiles for private-CA / self-signed receivers (#409).
backup.secrets_statuspill on Backups; CA-cert inline on the Backup Server /services panel (#406, #412).- Edit form: same chip group as create (#416); no-op edits stop falsely firing the change-target confirm modal (#410); cleaner Save-row + Copy-feedback toast (#411).
REST API & docs
/api/v1/{method-as-path}for every registered RPC; verb inferred from the trailing segment; GET params via query string, others via JSON body (#352)./api/openapi.json— OpenAPI 3.1, 256 methods, 104 schemas, built once and cached process-lifetime (#352)./api/docs— Swagger UI vendored into the binary; Try it out shares the WebUI session cookie (#353).- Method registry moved into
nasty-engine; six runtime-vs-docs drift bugs fixed;nasty-apidocretired (#350). - 150 previously-undocumented methods backfilled — vm., apps., backup., smb.user., system.hardware., system.secure_boot., system.acme.*, etc. (#351).
Disks & SMART
smartctl --scan-openenumeration surfaces drives behind RAID controllers; per-physical-drive identity keyed on(device, transport)so two failing megaraid slots don't dedup into one alert (#368).- NVMe health log: endurance, spare-vs-threshold, critical-warning bits, media errors, unsafe shutdowns, time above temp thresholds; Prometheus metrics exposed (#367).
- SAS/SCSI panel: error counters, grown defects, drive-trip temp, manufacture date, self-test results, plus the uncorrected-error override (#369).
- ATA summary panel with interface-speed downgrade and Helium_Level tile for HGST/WD He10/He12 drives (#373).
- SATA SSD endurance via smartctl 7.5's
endurance_used.current_percent(#378). - Topology PCIe gen × width × MB/s next to each controller (#380); resolved for RAID-tunneled drives via
/sys/class/scsi_host/(#381); chipset-integrated controllers annotated (#382). - Drive class
sasis its own badge, no longer disguised as hdd/ssd (#366). - All disks always appear in SMART + Topology with status
UNAVAILABLEwhen smartctl returns no usable envelope; alerts skip that state so unformatted disks stay quiet (#358, fixes #349). - ATA attribute metadata adapted from Scrutiny: normalized names, descriptions, ideal-direction markers, evidence-backed
criticalflag drives row highlight (#375). SmartAttributealert firesWarningon any of the 9 Scrutiny-critical attributes going non-zero, stable per-(drive, attribute) source (#376).
Networking & sharing
- Dual-stack defaults: iSCSI target creation on a v6-capable host lists both
0.0.0.0:3260and[::]:3260; NVMe-oF auto-port adds a probed v6 sibling (#391). The v6 leg is best-effort — LIO IPv6 misconfig logs a warn and skips, keeping nasty-csi happy (#400). - iSCSI portal management: per-target add/remove via WebUI + RPC; configfs paths wrap v6 in brackets; last-portal removal refused (#390).
- Listen-address picker on Add Portal / Add Port: drop-down of host interface addresses (filters down/loopback/link-local), wildcards offered for iSCSI only (#392).
- IPv6-aware client-side validators on NVMe-oF Add Port + NFS Add Client; engine's
validate_nfs_hostmirrored in JS (#389). - Bridge creation accepts a live-DHCP NIC as a member on first attempt — the structural validator now synthesizes undeclared bond/bridge members as Physical links (#357, fixes #348).
- udev rule disables
multicast_snoopingon engine-createdbr[0-9]*bridges — Avahi / WS-Discovery stay reachable past the ~5-minute IGMP membership timeout on querier-less LANs (#356, fixes #291).
Security & hardening
- Input validators tightened: subvolume name (
@, path-traversal, control chars; volsize capped at 256 TiB at create + resize), iSCSI target name (configfs-escape), NVMe-oF subsystem + host NQN (nqn.prefix + 223-byte limit), SMB share path (newline / CR / NUL /"/#) (#393). - iSCSI CHAP password redacted in
configfs_writeerror logs via a newconfigfs_write_secrethelper — username writes still show the failing IQN for diagnostics (#398). - Webhook notifications: structured
event_type/event_id/datapayload (legacysubject/body/source/timestampkept), optional HMAC-SHA256 signing viaX-NASty-Signature, 3-attempt retry with 5s+30s backoff on 5xx + network (#383). Plusalert.resolvedevents fired symmetrically when an active alert clears, sharing the originalevent_idso receivers auto-close incidents (#384). - Subvolume delete: child-delete +
losetup -dfailures surface asSubvolumeError::ChildrenStuck/LoopDetachFailednaming the specific child //dev/loopN(#395). - Two new Caddy response headers:
Permissions-Policy(denies camera/mic/geo/payment/usb/sensors/interest-cohort),Cross-Origin-Opener-Policy: same-origin(#396). - Confirm dialog before removing SSH keys (names the key, calls out self-lockout risk) and before unchecking SMB group memberships (reverts the checkbox on cancel) (#397).
- systemd
StartLimitIntervalSec=60+StartLimitBurst=5onnasty-engine,nasty-metrics,nasty-rest-server,nasty-tailscale— deterministic-panic loops now land infailedafter 25s; escape viasystemctl reset-failed(#394). - Security CI: weekly cargo-deny (advisories + license allow-list + crates.io-only sources + wildcard-dep ban), npm audit at
high+, cargo-geiger as artifact-only (#364).
VMs
- New VMs land at
/fs/<filesystem>/vms/<name>instead of top-levelvm-<name>— matches theapps/layout and the existingvms/images/sibling (#360). Existing VMs unaffected (VmConfig stores absolute disk paths); only new ones use the layout.
WebUI fixes & polish
- Login page hides the Sign in with security key button until at least one credential is registered — new unauthenticated
/api/auth/webauthn/availableendpoint mirroring the OIDC pattern (#346). - Restart spinner safety-net: 5s timer promotes
reconnectingif the disconnect callback didn't fire; debug logging on every reconnect state-machine transition behind thenasty-debuglocalStorage flag (#347). - Chromium-on-Linux dark mode:
<select>options were white-on-white; onecolor-scheme: light/darkper theme block (#379, fixes #377).
Build / CI / packaging
nixpkgspin moved from rollingnixos-unstabletonixos-26.05stable; wrapper template'snixpkgs.follows = "nasty/nixpkgs"carries every operator box forward on next apply (#355).- Bundled
nasty-topbumped to v0.0.7 — fixes the0.0/0.0 GiBtop-bar display on bcachefs pools with sparsedev-Nnumbering (#372). nasty-topderivation switched fromcargoHashtocargoLock.lockFile(#362).- Installer wrapper-lock idempotency gate: renders the template + runs
nix flake locktwice; second pass must be a no-op (#345). - magic-nix-cache-action bumped to v14 (Node 24);
use-flakehub: falsesilences the FlakeHub-registration error (#359). - Weekly-bump workflow resolves the kernel version via
nix eval pkgs.linuxPackages.kernel.versionrather than hardcoded"6.18"(#371). cron0.15 → 0.16 (backup scheduler dep); routinenpm update+ safe cargo bumps (#385, #414, #415).- README: Community section with WarlockSyno's Proxmox storage plugin (#399); v0.0.9 features (TPM2 / WebAuthn / Secure Boot) backfilled into Features (#361).
Upgrade notes
- Backup credentials migrate automatically. First boot after upgrade runs
backups.migrate_secrets(~150ms typical, 30s budget). On-disk format flips frompassword: "..."topassword_encrypted: <systemd-creds blob>. JSON-RPC output is now redacted to"***"or absent. - Backup Server now requires HTTP basic auth. Operators with existing source-side REST profiles will get HTTP 401 after the upgrade until they fill in credentials. Open each source profile in /backups → Edit, paste the receiver's username + password from the receiver's /services → Backup Server → Show panel into the new Username + Password fields, save. Pre-existing profiles that already inlined creds into the URL (
https://user:pass@host:8000/path) keep working without changes — the new fields are the cleaner path for everything else. For private-CA / self-signed setups, paste the receiver's CA cert (also exposed inline on the same panel) into the Trusted CA certificate field. ACME-served receivers don't need that step. - Backup target now editable. A field-level destination change on an already-initialized profile prompts a confirm, then resets
repo_initialized=falseso the next run / Init Repo materializes the new target. The old rustic repo at the previous destination is left where it was — operator handles cleanup. - VM disk layout changed for new VMs only. WebUI creates new VM subvolumes at
vms/<name>instead of top-levelvm-<name>. Existing VMs unchanged (paths stored absolute). - iSCSI default portals now dual-stack on v6-capable hosts. New targets get both
0.0.0.0:3260and[::]:3260by default; existing targets unchanged. v6 leg is best-effort. - Webhook payload gained fields; legacy fields preserved. New
event_type/event_id/nasty_version/datafields are additive; HMAC signing viaX-NASty-Signatureonly fires when the operator sets asecreton the channel.alert.resolvedis a new event type. - REST API + Swagger UI at
/api/v1/{...}and/api/docs. Auth uses the same session cookie orAuthorization: Bearertoken as/ws. Streaming endpoints (log, terminal, VM console, telemetry, events) stay WebSocket-only. - systemd service restart cap. Engine + metrics + rest-server + tailscale now allow at most 5 restarts in 60s before landing in
failed. Recovery issystemctl reset-failed <unit>.
Proxmox users: NASty requires UEFI. Switch the VM firmware from SeaBIOS to OVMF before installing.