A ctlplne studio product
trstctl /docs GitHub ↗ Live demo

Vulnerability management

How trstctl receives, triages, fixes, and discloses security vulnerabilities. This is the process an enterprise vendor-risk review expects to find; it complements the private reporting policy and the architectural threat model.

Coordinated disclosure

We practise coordinated disclosure. Reports arrive privately (GitHub private vulnerability reporting, or email to the maintainer — see SECURITY.md), we agree a remediation and disclosure timeline with the reporter, and we publish an advisory once a fix is available. We credit reporters who want to be credited and ask for a reasonable embargo while we remediate. We never penalise good-faith research conducted within the documented scope.

Triage

Every report is triaged on intake and assigned a severity that drives the SLA below.

  1. Acknowledge the report to the reporter and open a private tracking issue (a GitHub Security Advisory draft — GHSA).
  2. Reproduce against the latest main and the reported version/commit; capture a minimal proof of concept.
  3. Assess severity using CVSS v3.1 as the baseline, adjusted for trstctl's trust boundaries — anything that crosses a tenant boundary (per-tenant database isolation), extracts or misuses key material (the isolated crypto path, the isolated signer process, or wipeable secret memory), forges or breaks the audit chain, or bypasses authentication/authorization is treated as at least High, and a remote, unauthenticated instance of any of those as Critical.
  4. Decide scope: confirm whether served subsystems are affected, identify the fix, and request a CVE ID (via GitHub) for anything Medium or above.
  5. Track the advisory through fix, release, and publication.

Patch SLA (by severity)

Targets are measured from triage confirmation. "Fix" means a patched main and, once releases are tagged, a patched release; "disclose" means publishing the advisory.

Severity Acknowledge Triage & assess Fix target Public disclosure
Critical 1 business day 2 business days 7 days with the fix (coordinated)
High 2 business days 3 business days 30 days with the fix
Medium 3 business days 5 business days 90 days with the fix or next release
Low 5 business days 10 business days best effort / next release next release notes

Critical issues may ship an out-of-band security release; lower severities are rolled into the normal release cadence. If a target will slip, we tell the reporter and agree a revised date rather than letting the embargo lapse silently.

Security releases & advisories

  • Fixes land on main first. A security release is tagged for Critical/High issues (and bundled for Medium/Low), with the cosign-signed, reproducible image and SBOM the normal pipeline produces.
  • We publish a GitHub Security Advisory (GHSA) with the CVE, affected versions/commits, severity, impact, remediation, and credit. The release notes link the GHSA.
  • Downstream forks and plugin authors can watch advisories; the conformance suites let them self-validate after pulling a fix.

Known dependency advisories (assessed not-reachable)

Run make vuln locally to install the repository-pinned govulncheck and inspect the current source. CI repeats this reachability check. When evaluating Linux artifacts from another host OS, also scan with GOOS=linux and repeat with govulncheck -tags trstctl_core ./... for the core build. A dependency appearing in a module list does not by itself prove that a vulnerable package or function ships or can execute. Record that distinction and re-check it whenever dependencies, build tags, or imports change; do not suppress the advisory in the scanner.

Advisory Dependency Status Why not reachable Plan
GO-2026-5932 golang.org/x/crypto v0.56.0 Module only; affected package not imported in the local 2026-09-12 standard and core Linux scans The advisory concerns the unmaintained golang.org/x/crypto/openpgp package. govulncheck reports no affected imported package or reachable symbol. Other packages in the same module remain required. No fixed version is published. Keep OpenPGP out of the dependency closure and re-run the reachability checks on changes.

The earlier OPA v0.69.0 assessment is historical: the repository now requires OPA v1.4.0, and the fresh scan no longer reports GO-2025-3660. This source assessment does not establish that every deployment image is free of vulnerable libraries.

Local runtime image checks

Scan the actual built image IDs for the control plane, signer, database, event store, and evaluation helpers with a current vulnerability database. Record the scanner version, database timestamp, image ID, native findings, and available fixes. A digest pin prevents an image changing unexpectedly; it does not make an old image safe.

Evaluation and demo helper services build deploy/docker/Dockerfile.node. They use Node's built-in modules, so the runtime removes npm, Yarn, and Corepack and their unused dependency trees. Node 24.21.0 contains OpenSSL 3.5.8; separate checksum-pinned Alpine packages update the system OpenSSL libraries on AMD64 and ARM64. Check process.versions.openssl as well as the package scan: updating the system libraries does not update the OpenSSL copy compiled into Node. After an image change, verify cold key generation, retained login keys, PKCE sign-in, and the bridge services in the running evaluation. The web build stage still needs its package manager and is a separate dependency surface.

The separate demo-seed image follows the same Node 24.21.0 and OpenSSL 3.5.8 runtime policy in deploy/demo/Dockerfile.seed. It retains the OpenSSL command-line tool needed to create local demo certificates, installs its library and trust bundle packages from checksum-pinned inputs without network access, and removes unused package managers from the final image. Validate the seed as its configured nonroot user and through a fresh demo startup; a successful image scan alone does not prove bootstrap or restart behavior.

The evaluation, demo, and restore rehearsal build external NATS with deploy/docker/Dockerfile.nats. It compiles the NATS server selected by the repository's go.mod and go.sum using the same patched Go toolchain as the control plane. This keeps external and embedded NATS on the same dependency graph. Its Alpine runtime and TLS trust bundle are content-pinned separately. For an existing evaluation volume, preserve a cold snapshot and verify migration, event contents, durable consumer state, and recovery on a copy before replacing the event store. A rollback to the old engine must use the original snapshot; do not assume an upgraded data directory can safely be opened by an older server. See the upstream NATS upgrade and downgrade considerations.

Keep raw binary-scanner results alongside the source assessment. In govulncheck v1.6.0, a stripped binary without extracted package symbols triggers a conservative module-level fallback; placeholder wildcard symbols in that output do not establish that those packages were linked. For the local NATS 2.12.15 build, the exact main package scan and its 245-package Linux dependency closure contain no affected OpenPGP package. The raw binary scan still exits nonzero for GO-2026-5932. Record both results and repeat the assessment after changes; do not describe that binary scan as passing or suppress its output.

Triage dry-run (worked example)

A dry-run of the process end to end against a sample advisory, to prove the workflow holds before a real report arrives. (Illustrative — not a real vulnerability.)

Sample report: "An authenticated tenant A user can read tenant B's issued certificates by passing a crafted tenant_id to GET /api/v1/certificates."

  1. Acknowledge (day 0): reply to the reporter; open a private GHSA draft GHSA-xxxx-sample.
  2. Reproduce (day 0–1): stand up a two-tenant instance, issue a cert under tenant B, and confirm whether tenant A can read it. In this sample, suppose it reproduces.
  3. Assess: a cross-tenant read bypasses row-level security — Critical (remote, authenticated, tenant-boundary crossing). CVSS ≈ 8.1. Request a CVE.
  4. Scope the fix: the offending handler trusted a request-supplied tenant_id instead of the authenticated context; the fix forces the tenant from the session and relies on the RLS policy. Add a regression: a read-model integration test asserting tenant A receives 404/empty for tenant B's cert, plus an architecture-linter check that the query path is tenant-filtered.
  5. Fix (within the 7-day Critical target): patch main, tests green (make lint test), tag a security release.
  6. Disclose: publish GHSA-xxxx-sample with the CVE, affected commits, impact, the fixed version, and reporter credit; link it from the release notes.
  7. Retrospective: confirm the architecture linter or a new rule would have caught the class of bug, and file follow-ups if not.

This dry-run is re-run whenever the triage process or the served surface changes, so the process stays exercised rather than aspirational.

Rendered live from github.com/ctlplne/trstctl — found a mistake? edit this page.