Skip to content

Personal Edition & Security

Latest

Choose a tag to compare

@btouchard btouchard released this 12 Aug 13:11
· 1 commit to main since this release

Introduces Personal, a third edition between Community and Pro, bought once, which lifts every cap and opens multi-host monitoring up to 20 remote machines. Authorization moves from a binary "is this Pro" check to an order, Community < Personal < Pro, resolved per capability. Also ships a security round on the agent gRPC plane and the HTTP surface, and fixes stale Swarm update findings, pods killed mid-migration, and two navigation regressions.

The Personal edition

Discussion #31 converged on the same finding from three users: Maintenant is useful in a homelab, Community gets narrow quickly there, and 29€/month makes no sense for a machine hosted at home. One of them was explicit: without an intermediate option, they would fork the project to lift the limits themselves.

Community caps never protected the business model. A company does not buy Pro because it reached ten endpoints, it buys Pro because it has to alert a team, run an on-call rotation and show a status page to its customers. The caps only created friction on personal use. Personal removes that friction without taking anything away from Pro, following a single rule: Personal sees everything, Pro warns others.

Personal (149€ once, for life, non-commercial) opens:

  • every cap lifted: endpoints, heartbeats, certificate monitors, status components
  • multi-host, up to 20 remote machines
  • Email channel and advanced trigger filters
  • CVE enrichment, per-container risk scoring, unified security posture
  • incident management, changelog, resource history, OCSP stapling
  • €59 per year for additional updates

Pro keeps what serves a team or addresses third parties: unlimited hosts, Slack and Microsoft Teams, alert escalation, per-entity routing, maintenance windows, subscriber notifications, status page branding, and the right to use Maintenant on behalf of others.

Community is unchanged in what it already did, and gains the Swarm and Kubernetes node and cluster views, which were the only gated part of a feature otherwise entirely open (discovery, services, workloads and pods were already free). It stays standalone: no remote agent enrollment.

Existing Pro keys are unaffected. An active license carrying no edition information resolves to Pro, exactly as before.

Ordered gating instead of a Pro flag

requireCapability replaces requirePro on the 65 guarded routes: 26 open at Personal, 29 at Pro, and the 10 Swarm and Kubernetes routes lose their guard entirely. A single capability registry (20 capabilities) and a single quota registry (5 capped resources, unlimited being -1) are read by the REST middleware, the MCP tools and GET /api/v1/edition alike, which is what keeps the three surfaces agreeing. A conformance suite walks the three editions against the twenty capabilities on both surfaces and asserts they match.

Refusals are now machine-readable and carry the capability, the resource, the limit and the edition that grants it:

  • 403 EDITION_REQUIRED naming the edition that opens the feature, so a Community user blocked on incidents is told Personal, not Pro
  • 403 QUOTA_EXCEEDED on the four capped resources, heartbeats included, which used to answer with a different status and code
  • 409 HOST_LIMIT_REACHED on agent enrollment

The interface no longer matches on an English sentence to build its message. Enrollment stays capped atomically, so concurrent enrollments can never exceed the limit, and the machine running the application is never counted.

The /pro-edition page becomes /editions, a three-column comparison highlighting the active edition (the old route redirects). Upsell banners are now shown to Community users only.

Personal update window

A Personal license is bought once and never expires. What is bounded is the right to new versions: one year, then ?59 per extra year.

  • a build released inside the window stays licensed for life
  • a build released after it keeps its edition for 30 days of grace, then falls back to Community for that binary alone
  • going back to a covered version restores everything immediately, and costs nothing

The grace clock is anchored on the window end rather than on the build, so updating every month cannot restart it. Its state lives in <dataDir>/.maintenant-update-window (0600). Two statuses, update_window_grace (warning) and update_window_ended (critical), reach GET /api/v1/license/status and the interface. A throttled instance running in server mode still starts: refusing would take a whole fleet's monitoring down over an unpaid renewal.

Security

Remediation of an audit of the agent gRPC plane and the HTTP surface:

  • Unauthenticated remote DoS. A crafted handshake against the NULL-keyed local sentinel reached ed25519.Verify with a malformed key, which panics, taking the whole process down. The sentinel and any key that is not 32 bytes are rejected up front, and panic-recovery interceptors now return Internal instead of killing the server.
  • agent_id spoofing. Events are dispatched under the authenticated agent identity instead of the client-controlled wire field, and any mismatch is rejected. A compromised agent can no longer forge or wipe another host's data.
  • Enrollment tokens were stored in clear, so any copy of the SQLite file handed over replayable tokens. The column becomes a sha256 hash plus a 14-character prefix kept for display. Existing installs are rewritten in place on first boot, so outstanding tokens stay valid and nothing has to be reissued.
  • MAINTENANT_MCP=true without OAuth credentials now refuses to start instead of serving /mcp unauthenticated, since the reverse proxy is documented as letting that route bypass auth. Opt out with MAINTENANT_MCP_ALLOW_UNAUTHENTICATED=true. --mcp-stdio, which never listens, is unaffected.
  • Two hardcoded Access-Control-Allow-Origin: * on the log streams are removed; they let any origin an operator visited read container logs on an unauthenticated deployment.
  • Security headers on every response: nosniff, Referrer-Policy, frame refusal, and a CSP whose script-src carries the sha256 of the inline theme bootstrap, computed at startup from the embedded index. The public status page stays embeddable. /api/ gets its own rate bucket (50 rps, burst 200), the public bucket being too tight for an ordinary dashboard load.
  • Go toolchain bumped to 1.26.5, and the frontend lockfile refreshed to clear six advisories, all in build and test tooling, none of it shipped in the bundle.

Bug fixes

  • Stale update findings on Swarm. Cleanup only matched findings whose container name appeared in the current scan, and Swarm renames the task container on every docker stack deploy, so a replaced task left a dead row on the Updates page for good, one per service per upgrade. Findings are now shed by container identity, update.resolved is emitted so the alert engine resolves the right alert, and reads are filtered immediately rather than at the next scan. Purging archived containers also clears the tables keyed by external_id (image updates, CVEs, version pins, risk score history), which had no foreign key to cascade through. (#50)
  • Kubernetes pods killed mid-migration. Migrations run before the HTTP listener starts, so /api/v1/health stays silent for their whole duration and kubelet killed the pod after roughly 95 seconds, leaving the schema marked dirty and repeating on every restart. A startupProbe (10 minutes of grace, configurable in the chart) is added to the raw manifest and the Helm chart, and the default PVC goes from 1Gi to 10Gi, since table rebuilds copy a table before dropping the original.
  • Containers entry missing on a Swarm node. The sidebar hid it as soon as one service was deployed, although the host still runs plain containers and the page worked by direct URL.
  • Navigation silently emptied by one failed request. A single failed runtime status fetch left the store unloaded for the rest of the session, and every runtime-specific entry disappeared with no error shown. It is retried with exponential backoff, and the error surfaced.
  • --mode=server exited at boot whatever the license said, so the documented multi-host setup could not be started that way. The check now runs after the license manager has resolved the edition.
  • Agent stream cut periodically behind a reverse proxy. The stream is a single gRPC request whose body never ends, so a proxy capping request duration severs it (Traefik v3: respondingTimeouts.readTimeout, 60s by default). Documented in the agent setup guide with a matching troubleshooting entry.

Upgrade notes

  • Automatic DB migration (27) indexes containers(external_id), plus a one-time in-place rewrite of enrollment tokens into hashes. Tokens already issued stay valid. Back up the .db first as usual.
  • MCP with MAINTENANT_MCP=true and no client id/secret no longer starts. Set the credentials, or set MAINTENANT_MCP_ALLOW_UNAUTHENTICATED=true if the route is genuinely on a trusted network.
  • Kubernetes: apply the updated manifest or chart before upgrading, so the startupProbe is in place, and check that the volume has room for a table rebuild.
  • Nothing to do for existing Pro keys. Editions are resolved from the license as before; a key with no edition information remains Pro.

Pull the new image to upgrade.