v26.5.1 ported modules

May 15, 2026 · View on GitHub

This file is a living reference table tracking the v26.2.7 → v26.5.1 module merge. Each module session (01–12) updates the row for the module it lands. Reviewers use this table to see at a glance which modules are in flight, which migrations have already been renumbered, and where the PR lives.

Per-feature detail lives in CHANGELOG.md under [v26.5.1] — 2026-05-15 and in release-notes-v26.5.1.md.

Status legend

  • TODO — not started; no branch yet.
  • IN PROGRESS — branch open, code in flight, not yet merged.
  • REVIEW — PR open as draft or ready-for-review.
  • MERGED — module on main, tests green, smoke verified.
  • BLOCKED — module held up by a dependency or open question; see notes.

Migration renumbering map

v26.5.0 main occupies migrations 044 (recon_module) and 045 (recon_retention). Every v26.2.7 module migration shifts up by 2 to avoid the collision. dockerconfig has no migration in v26.2.7 (config-file service) so no number is assigned.

ModuleSource migration (v26.2.7)Target migration (v26.5.1)SessionStatusPRNotes
firewall048_firewall050_firewall01MERGED#56UFW/nftables wrapper. Biz gate dropped. New REST API at /api/v1/firewall/* (v26.2.7 was web-only). Service input validation hardened.
crontab047_crontab049_crontab02MERGED#57Managed cron jobs (shell/docker/http) via robfig/cron/v3. Biz gate dropped. New REST API at /api/v1/crontab/* with paginated executions and WebSocket tail — v26.2.7 was web-only. Docker command type now executes; HTTP body capped at 64 KiB; executions paginated at 100/page.
backup-verify050_backup_verification052_backup_verification03MERGED#58Scheduled + on-demand restore tests for existing backups. Biz gate dropped. New REST API at /api/v1/backup-verify/* (v26.2.7 was web-only). Sandboxed execution via recon launcher (read-only rootfs, dropped caps). Encryption key passed via tmpfs file mount — never an env var. Real checksum + extract via existing backup-service Verify; v26.2.7 returned heuristic "1 file per 10 KiB" file counts.
rollback052_automated_rollback054_automated_rollback04MERGED#59Automated revert to last-known-good stack version on failed deploy. Biz gate dropped. New REST API at /api/v1/rollback/* (CRUD + dry-run, no API existed in v26.2.7). Event-driven via the new changes.Service.Subscribe API instead of polling. last_healthy strategy walks version history for is_deployed && deployed_at (v26.2.7 used naive "previous"). Per-stack sync.Mutex lock prevents racing manual deploys. Audit table append-only via Postgres trigger + static guard test mirroring recon. Service unit tests at 69.8 % coverage.
ssl-observatory049_ssl_observatory051_ssl_observatory05MERGED#60TLS cert health monitor + grading. Biz gate dropped. New REST API at /api/v1/ssl/* (v26.2.7 had no API). Repository extracted from inline service SQL. Per-target alert thresholds (default 30/14/7/3/1 days) and SNI virtual-host scans (one scan_result row per (target, hostname)). ScanAll enforces a per-target concurrency cap (default 4). Every TLS dial has a hard 10s timeout. Cert-expiry alerts route through the notification service via a narrow Notifier interface. Daily 04:00 UTC scheduled sweep via the new JobTypeSSLScan worker. Service unit tests at 79.9 % coverage with a real-handshake SNI scenario.
docker-config(no migration)(no migration)06MERGED#61Edits daemon.json with atomic writes (temp+fsync+rename), snapshot history, and reload-with-rollback (60 s hard timeout). Biz gate dropped. New REST API at /api/v1/docker-engine/* (no API existed in v26.2.7). v26.2.7's 898-LoC monolithic service split into reader/writer/applier/service files. Dropped the v26.2.7 nsenter-via-docker exec self-exec path — operators volume-mount the host's /etc/docker into the container as :rw instead. Registry mirror credentials redacted from logs (asserted by unit test). Web UI replaces v26.2.7's seven-tab form with a Monaco diff editor + history page. Service unit tests at 65.9 % statement coverage including the explicit rollback path.
wireguard053_wireguard_vpn055_wireguard_vpn07MERGED#62WireGuard peer + interface manager extended into a master→agent mesh. Biz gate dropped (v26.2.7 was unconditional; keep it that way). New REST API at /api/v1/wireguard/* (no API existed in v26.2.7). New wireguard_mesh_links table tracks (agent_host_id, peer_id, status, last_handshake) for the new Mesh status page; rows are upserted on every push attempt via the existing NATS transport (wireguard.apply_peer/remove_peer/status/probe commands added to the gateway protocol). Private + preshared keys and the rendered client config are AES-256-GCM-encrypted at rest with the installation data encryption key (USULNET_ENCRYPTION_KEY); the service is intentionally NOT constructed when the encryptor is unavailable so cleartext keys never land in the DB. v26.2.7's placeholder XOR public key replaced by real curve25519.X25519 derivation. One-time QR endpoint with 5 min TTL (/peers/{id}/qr/issue/peers/{id}/qr?token=…); the peer detail page renders the QR client-side via the already-vendored qrcodejs library. Wg/wg-quick are probed at startup and per-agent on demand; a non-fatal log line and a yellow banner in the list page surface missing binaries. Hard timeouts on every wg invocation; preshared keys are piped on stdin so they never appear in argv. Service unit tests reach 72.5 % statement coverage.
image-builder051_image_builder053_image_builder08MERGED#63Local Dockerfile build pipeline. Biz gate dropped (v26.2.7 was unconditional too — kept that way). New REST API at /api/v1/builds/* and /api/v1/build-templates/* (v26.2.7 was web-only). Build logs stream live through Redis pub/sub on imagebuilder:logs:<id> channels — a per-build subscriber bridges the daemon's NDJSON output into the API's WebSocket and SSE endpoints; the v26.2.7 service buffered every log line in memory and stubbed the actual docker call, so the live-tail capability is wholly new. Build-context uploads are capped at a configurable 256 MiB (image_builder.max_context_bytes); oversized payloads return a 413 before reaching the daemon. The trailing 64 KiB window of the build log is persisted to image_build_jobs.output so the table view stays bounded regardless of how chatty a build was. The starter-template seeder ships seven AGPL-compatible Dockerfile snippets (alpine-minimal, static-web-nginx, node-app, python-app, go-app, postgres-extension, background-worker), all authored fresh for usulnet — no encumbered upstream content. Built-in templates are protected by a service-level guard that returns ErrBuiltinDelete (mapped to 403). Optional cosign hook (image_sign.enabled=true) signs the resulting image via the existing imagesign service on success; signing failures are logged but never flip the build to failed. New columns signed/signature_ref track the cosign result. Service unit tests at 79.6 % coverage; smoke E2E at tests/e2e/image_builder/ builds FROM alpine:3.21 against a real daemon.
proxy-extended045_proxy_extended047_proxy_extended09MERGED#64Access lists, dead hosts, locations, redirections, streams. Biz gate already absent. Migration 045→047 renumbered; no column collisions with v26.5.0's proxy_* (the only extra column added on proxy_hosts is access_list_id plus four nginx-flavoured opt-ins). New REST API at /api/v1/proxy/{access-lists,dead-hosts,redirections,streams,hosts/{id}/locations}/* plus GET /api/v1/proxy/support for the backend matrix — v26.2.7 had no API for these. usulnet now holds the authoritative state in PostgreSQL; proxysvc.Service translates it to the active backend on apply via the new ExtendedSyncBackend interface. The Caddy backend reports the support matrix {access_lists, dead_hosts, locations, redirections: true; streams: false}; nginx reports full support. Streams against Caddy surface ErrFeatureNotSupported, mapped to HTTP 422 by the API and a clear "switch to nginx" banner in the UI (not 500). Access-list rule evaluator separated into EvaluateClientAccess() and pinned by table-driven tests covering the explicit-deny > explicit-allow > default precedence contract for IPv4 literals, IPv4 CIDR, IPv6 literal, IPv6 CIDR and the literal "all". Repository files split per sub-entity (proxy_access_lists_repo.go, proxy_dead_hosts_repo.go, proxy_locations_repo.go, proxy_redirections_repo.go, proxy_streams_repo.go) using the v26.5.0 _repo.go suffix convention. Service unit tests assert idempotency (replay apply → same data, no spurious change) and a full host smoke (host + location + access list + redirection in one fixture, last backend payload pinned).
dns046_dns048_dns10MERGED#65DNS provider plugins for ACME DNS-01 + proxy record automation. v26.2.7's embedded miekg/dns server is out of scope per session-10; this rewrite is a thin plugin layer over Cloudflare, AWS Route 53, DigitalOcean (direct HTTP, no vendor SDK per session risk note), and any RFC 2136 nameserver (TSIG-authenticated UPDATE messages). Plugin registration is explicit via dns/providers.RegisterAll — no init() drift. New REST API at /api/v1/dns/* (v26.2.7 had no API). Provider credentials AES-256-GCM encrypted at rest with the installation data encryption key (USULNET_ENCRYPTION_KEY); the column is base64 ciphertext, JSON-redacted from API responses. ACME DNS-01 flow is a persistent state machine (pending → dropping → propagating → ready → completing → completed, with failed as the sad terminal); orders survive restarts via (*Service).ResumeInFlightOrders. Per-user prefs.IsHidden("dns") sidebar gate (no edition gate). New permissions dns:view/dns:write. Tests use only httptest servers and a fake miekg/dns server — no real-API calls anywhere. Service+plugin unit tests at 60–76 % per package. Smoke E2E mocks Cloudflare and asserts TXT-drop/clean.
calendar044_calendar046_calendar11MERGED#66Operations calendar with manual event entry plus a read-only EventSource aggregator (backup runs, scheduled jobs). REST CRUD at /api/v1/calendar/events, paginated ?from/to range, RFC 5545 iCalendar export at /export.ics. Migration 046 indexes (starts_at, ends_at) for range queries; the kind column is constrained to a strict allow-list so the table cannot become a junk drawer. Aggregator failures are logged and skipped. Service unit tests at 89.9 % coverage including a CRLF/folded .ics validator.
marketplace054_marketplace056_marketplace12MERGED#67Curated app marketplace with installation tracking and reviews. Biz gate absent (v26.2.7 was already AGPL). New REST API at /api/v1/marketplace/* (v26.2.7 had no API). Catalogue source per merge-plan open question 2 is offline static (embedded): app templates live under internal/templates/marketplace/<slug>/{manifest.yaml,compose.yaml,icon.svg}, baked into the binary via go:embed. The service hydrates the embedded entries into marketplace_apps on first boot, then idempotently bumps rows whose manifest_version increases on subsequent releases; user-submitted apps (built_in=false) survive hydration, and built-in apps are protected from deletion. There is zero outbound HTTP at runtime — a unit test (TestStart_NoOutboundCalls) wires http.DefaultTransport to fail every dial and asserts HydrateCatalog issues none. Upstream image licenses for every shipped template are documented in internal/templates/marketplace/LICENSES.md (AGPL-compatible only: nginx BSD-2-Clause, traefik/whoami MIT, Gitea MIT, Uptime Kuma MIT). Install action wires through the existing stack service: the compose template is rendered with {{KEY}} substitution and a stack is created on the active host; the resulting stack_id is stored on the installation row. Reviews are local-only — they never leave the instance — and the unique (user_id, app_id) DB constraint is paired with a repository-level Upsert so a user revising their review collapses to one row instead of racing. New permissions marketplace:view/marketplace:write added to the legacy role map. The sidebar item is registered unconditionally (no isEditionAvailable/navItemLocked). Service unit tests at 70.0 % coverage (≥ session bar); embedded-catalogue tests at 80.3 %. The pre-existing internal/templates/catalog/ tree (container templates, unrelated feature) is intentionally left alone — marketplace ships under a separate internal/templates/marketplace/ tree so the two surfaces stay independent.
sidebar/edition cleanup(no migration)(no migration)13MERGED#68Drops the last edition gating callsites. isEditionAvailable / navItemLocked removed from internal/web/templates/partials/sidebar.templ. requireFeature web middleware + its 27 callsites removed. API enforcement middleware RequireFeature / RequirePaid / RequireEnterprise / RequireLimit removed. Service-level limitProvider enforcement dropped in team, user, host, git, storage, notification, backup. License-tier comparison table on the in-product License page removed. license.CELimits()license.OpenLimits() (zero value of Limits); AllBusinessFeatures() + AllEnterpriseFeatures() collapse into AllFeatures(). LICENSE_REQUIRED / LICENSE_EXPIRED / FEATURE_DISABLED / LIMIT_EXCEEDED error codes retired; only LICENSE_INVALID survives for JWT cryptographic failures. Edition constants preserved so previously-issued JWTs remain parseable.
infra parity / bootstrap(no migration)(no migration)14MERGED#69Splits the ~2,700-line startStandalone into phased init_*.go files (server, auth, docker, services, scheduler, api, web) backed by a shared initContext. Strengthens scripts/verify-migrations.sh to reject gaps, duplicates, and unpaired up.sql/down.sql files. Opt-in USULNET_TLS_LOCAL_SERVICES=true wires self-signed ECDSA P-256 certs onto Postgres/Redis/NATS; defaults unchanged. make dev-certs re-added from v26.2.7. The managed usulnet-nginx container from v26.2.7 is not re-introduced.
security hardening(no migration)(no migration)15MERGED#70Full hardening audit recorded in docs/v26.5/security-review-v26.5.1.md. Dependency bumps for govulncheck readiness (Go 1.25.7→1.25.10; pgx, NATS, go-redis, go-chi, jwt, go-oidc, go-ldap, docker). .github/workflows/govulncheck.yml runs scripts/govulncheck.sh on every push to main and every PR. Two unfixed Moby findings (GO-2026-4883, GO-2026-4887) documented in the allowlist with inline justifications — daemon-side defects in plugin install paths usulnet does not exercise. golangci-lint triage clears whitespace/unconvert/ineffassign/staticcheck/nilerr/gocritic/gofmt/misspell/govet/errcheck categories.

Per-module conventions

For every row above, the session PR must satisfy:

  • The renumbered migration up.sql / down.sql pair passes scripts/verify-migrations.sh (no gap, no duplicate, no orphan).
  • The migration does not name a table or column that collides with the v26.5.0 recon_* schema (044_recon_module.up.sql, 045_recon_retention.up.sql).
  • The sidebar entry for the module is registered without isEditionAvailable or navItemLocked (one AGPL build, all features — see CHANGELOG.md [v26.5.0] "Open and unlimited").
  • The service has no call-home, no runtime caps, no closed-source extension points.
  • The AGPL SPDX header is on every new Go file.
  • A manual UI smoke is attached to the PR as at least one screenshot.

How to update this file

Each session PR appends to its own row only. Do not modify other modules' rows. The release-engineering session (16) reconciles the board with the final [v26.5.1] CHANGELOG entry and bumps every row to MERGED before the release tag.

Release status

Session 16 (release engineering) flipped every row above to MERGED on 2026-05-15 as part of cutting v26.5.1. PR column points at fran-olivares/usulnetdevbeta04 (the work mirror); per-PR detail is visible there. The release engineering PR itself is #72, and a follow-up release polish PR backfilled the PR-link column on 2026-05-15.