2026.09
Private registries, retention, a second factor, and a database that enforces the tenant boundary
- added
- security
- fixed
- changed
Added
Private registry scanning. Store a credential against a registry host —
either a username with a password or personal access token, or a bearer
token — and scans of images from that host present it. Available on Pro and
Enterprise; the private_registries entitlement is checked when the credential
is created.
There is deliberately no endpoint that reads a stored secret back, at any role. A secret that can be fetched is one that ends up in a screenshot; if you lose it, rotate it. The secret is encrypted under its own derived subkey, so a ciphertext from this column can never be decrypted as a webhook signing secret, and the scan carries a reference rather than the secret — the plaintext exists only in the few lines that hand it to a registry client, never in a job payload, a log line or an error message.
A scan whose credential cannot be resolved fails rather than falling back to an anonymous pull. The fallback risks pulling a public image that happens to share the name and reporting its findings as yours, which is the one failure a vulnerability scanner must never have.
Two-factor authentication. TOTP with single-use recovery codes. A password that is correct but unaccompanied produces a challenge and no session at all — not a partial one — and a step already spent on an account is refused even while it is still inside its time window. Enrolment revokes every other live session. Removing the factor requires proof of holding it, because a session alone being enough is exactly what turns a stolen session back into a stolen account.
Consumer OAuth logins are challenged too, since every provider callback funnels through one handoff. Enterprise SSO deliberately defers to your own identity provider. Two-factor is per-user and opt-in; an organization cannot yet require it of its members.
Retention windows are enforced. A sweep now expires each tenant’s history against the window their plan grants — 30 days on Free, a year on Pro, two years on Enterprise. These numbers were in the plan tables from the beginning and nothing read them.
One rule governs the whole thing: retention expires history, never current state. A finding that is still open is never deleted at any age — an unfixed vulnerability does not stop existing because it is old. A suppressed finding is never deleted either, because a suppression is a decision somebody made and deleting the finding would let the next scan re-create it as new, silently reversing an accepted risk. What expires is resolved findings, superseded timeline entries, and metering history. The identity audit trail is append-only and is not swept on any plan.
Security
The database now enforces the tenant boundary too. Every statement on the request path runs inside a transaction carrying the tenant, and PostgreSQL row-level security holds the same boundary the application’s scope type already did. The two fail differently, which is the reason for having both: the scope cannot catch a query that holds one and forgets to use it in the SQL text, and row-level security does not care what the query says.
The worker connects as a second role with a narrow, per-transaction cross-tenant
policy rather than BYPASSRLS. BYPASSRLS is not scoped to a table or a
statement — it would also strip the boundary from the worker’s ordinary
per-tenant writes, so one mistake in one query would become a cross-tenant write
with nothing behind it. Neither role holds it.
This is opt-in per deployment: set JOB_DATABASE_URL to a distinct job-plane
role and the second layer is armed, and the process says which configuration it
is in at startup. Unset, both planes share one role and isolation rests on the
application boundary alone, exactly as before.
Registry credential validation is dialled through the SSRF guard. Testing a
credential connects to a host the customer named and presents a secret to it,
which is the same shape as a webhook destination. It now uses the guarded
transport that inspects every resolved address at connect time and ignores an
inherited HTTP_PROXY, so an administrator cannot have TRUSTIVAN authenticate
against a cloud metadata endpoint. A deployment that has set
SCAN_ALLOW_PRIVATE_REGISTRIES still validates against its own internal
registry — that setting is what the guard is bound to.
Two API-key crashes closed, and a bug that made recovery codes unreadable after an encryption-key rotation.
Fixed
Four scan reliability defects. A scan whose lease expired reached a terminal
failure without ever emitting scan.completed, so a pipeline waiting on that
event waited forever for a scan the API already showed as failed. A worker
shutting down mid-scan consumed one of the scan’s retry attempts, so three
rolling restarts hitting the same scan could exhaust its budget and fail it
permanently having never once actually run it to completion. Finding lifecycle
accounting and the scan idempotency fingerprint were also corrected.
Failures that cannot succeed no longer retry. A reference that does not resolve, a credential the registry rejected, or a request refused before any network call now fails immediately instead of spending three attempts and several minutes on an answer that was knowable on the first try. Everything unrecognised keeps retrying exactly as before.
Two-factor no longer locks you out of the dashboard. A user with a factor enrolled was answered with a challenge and no session, which the dashboard read as a successful sign-in — landing on a page with no session and bouncing back to the login screen indefinitely. The callback now presents the verification step, and two-factor can be set up, inspected and turned off from Settings → Security rather than only through the API.
Changed
Scans have never been cancellable, and the documentation said otherwise: a
canceled status was listed as one a scan can reach. Nothing has ever written
it. The status tables now say so.