Everything We've Hardened This Year

Six rounds of security work, laid out end to end: a chokepoint audit, VPS lockdown, a full 8-domain review, and today's fix for a fail-open tenant-identity gap.

T
The Plenix Team·21 September 2026·4 min read

Security work is rarely one big fix — it's a series of audits, each one finding a few real things, each fix making the next audit's job easier. Over the last few months we've run more of these than most platforms will admit to in public, so here's the full ledger, not just the highlight reel.

Round 1 — The chokepoint audit (August)

A focused pass looking for places where one piece of shared code was making an access-control decision for many endpoints at once — because a bug in a chokepoint is worse than a bug in one endpoint, it's a bug in all of them. This round found and fixed three separate staff-leak issues (internal Plenix employee data surfacing where only tenant data should appear), a credentials-hub leak, and a gap in how the public blog's sitemap was generated. Existing RBAC role-checking was reviewed against the findings and confirmed already correctly mitigated — not every audit finding is a new bug; some are "checked, and it's fine."

Round 2 — VPS infrastructure hardening

Root SSH login was disabled platform-wide — all VPS access now goes through a dedicated, non-root plenixops user with passwordless sudo for the specific operations that need it (PM2 restarts, the deploy script). fail2ban was deployed across the box, with jail configuration validated as an atomic batch — a lesson learned the hard way when one filter definition missing a capture group took down every jail, including the one guarding SSH itself, until we caught it. A full security dashboard hub (five pages) shipped alongside this round, plus dedicated IMAP wiring for the support inbox.

Round 3 — Backend plan-gating audit

A systematic pass checking that every module a tenant could theoretically reach actually matched what their plan entitled them to — 19 frontend gaps and 13 backend gaps closed in one sweep. This is the same category of bug as the OKR-on-Starter issue fixed in this exact release: plan enforcement drift is subtle, it accumulates one feature at a time, and it needs a dedicated sweep rather than hoping each new feature remembers to gate itself correctly.

Round 4 — The full 8-domain review

The most comprehensive pass yet: authentication, authorization, input validation, secrets handling, transport security, dependency exposure, infrastructure configuration, and audit logging, each reviewed as its own domain rather than folded into a general "security check." This is the round that shipped the CSP nonce migration — moving Content-Security-Policy enforcement from a static allowlist to per-request nonces — and patched a denial-of-service vector in a query-string parsing dependency (qs) used platform-wide.

Round 5 — The small stuff that adds up

Not every fix needs its own headline. A deploy-script bug that was leaving plaintext .env backups in /tmp on every deploy got closed the same day it was found. A stray .bak file sitting in nginx's sites-enabled — which has no extension filter, so any file there becomes a live server block — caused a real duplicate-vhost bug, caught and fixed at the nginx layer. Small, unglamorous, and exactly the kind of thing that only gets caught by someone actually looking.

Round 6 — Today: closing the impersonation gap

This release ships the fix for the deepest issue in this list: super-admin impersonation and read-only tenant scoping could both silently fail to resolve which tenant a request was actually acting as — and the fallback behavior in both the frontend module-access hook and the backend feature-gating guard was to allow access rather than deny it when that resolution failed. That's a fail-open default in exactly the two places responsible for enforcing plan boundaries, and it's the real root cause behind modules appearing where they shouldn't. The fix makes both paths fail closed for any authenticated request whose tenant can't be resolved, while deliberately leaving genuinely public, unauthenticated routes (like the customer-facing chatbot widget) untouched — those were never the problem, and a blanket fail-closed change would have broken them for no security benefit.

Why we're publishing all of this

None of these were breaches. None of them were exploited, as far as any of our monitoring shows. That's the point of publishing them together: this is what proactive security work looks like when nobody's forcing you to disclose anything — audits that go looking for problems before an attacker does, and a track record you can actually check against commit history instead of a vague "we take security seriously" paragraph.