checkId: check.core.plugins.trustroot plugin: stellaops.doctor.core severity: fail tags: [quick, core, plugins, security]
Plugin trust-root present
Doctor check
check.core.plugins.trustroot— turns a silent plugin trust-root failure into a visible one (ADR-026 Decision 6). It verifies the trust-root (cosign.pub) used to admit mounted Doctor runtime plugins is present and readable; a missing/invalid root silently rejects every signed plugin.
What It Checks
This is a core check: it is contributed by the in-process Core plugin, so it always runs independent of mounted-plugin admission — it cannot itself vanish when a mounted plugin is rejected.
It reads the trust-root state through the engine-lib seam IMountedPluginAdmissionStatus, which the Doctor WebService backs with the same mounted-bundle loader options /readyz uses (Doctor:RuntimePlugins:TrustRootPath / AllowUnsigned). So a Doctor RUN and /readyz agree about the trust-root state. The probe inspects a local file only — no network, no external callback (air-gap invariant).
| Result | Condition |
|---|---|
| Pass | the trust-root file is present and readable at the configured path |
| Fail | the trust-root is missing, unreadable, or its path is not configured |
| Skip | the AllowUnsigned dev escape hatch is enabled (the only loader configuration where a missing root is acceptable — the loader then admits bundles without a trust-root), or there is no mounted-plugin host (the seam is not registered, e.g. the CLI) |
It does not Pass-by-default: when there is no host to evaluate, or when the dev hatch removes the requirement, it Skips with a reason rather than reporting a false green. The evidence emits TrustRootPresent, TrustRootOutcome (PresentAndReadable / Missing / Unreadable / NotConfigured), TrustRootPath, and AllowUnsigned.
Why It Matters
The mounted bundle loader is fail-closed: each mounted Doctor plugin (Release, Environment, Scanner, Compliance, BinaryAnalysis, Timestamping) must clear detached RSA/SHA-256 signature verification against the trust-root before any code runs. If the trust-root (cosign.pub) is missing or unreadable, the loader rejects every signed bundle — and those plugins’ checks silently disappear from the run while Doctor still reports all-green. The trust-root is therefore a platform-wide single point of failure for plugin admission. This check makes its absence a visible Fail instead of a silent gap, and is the upstream companion of check.core.plugins.loaded (which reports the resulting rejected plugins).
Common Causes
- The trust-root file (
cosign.pub) is absent at the configured path (default/app/etc/certificates/trust-roots/plugins/doctor/cosign.pub) — e.g. the mount/secret was not provisioned. - The trust-root path is misconfigured (
Doctor:RuntimePlugins:TrustRootPath) and points at a non-existent file. - The trust-root file exists but is unreadable by the service user (permissions, or a corrupt/empty mount).
How to Fix
Place the cosign public-key trust-root at the configured path so the loader can verify mounted bundle signatures, then redeploy the Doctor service. The trust-root is produced/distributed alongside the signed bundles by devops/build/package-runtime-plugins.ps1.
Docker Compose
The trust-root is a read-only mount in devops/compose/docker-compose.plugins.doctor.yml (with AllowUnsigned=false). Confirm cosign.pub exists on the host at the mounted source path, then recreate the service:
# Verify the trust-root is present on the host at the path mounted to
# /app/etc/certificates/trust-roots/plugins/doctor/cosign.pub, then:
docker compose -f devops/compose/docker-compose.plugins.doctor.yml up -d --force-recreate doctor-web
Bare Metal / systemd
Ensure the trust-root exists and is readable by the service user:
ls -l /app/etc/certificates/trust-roots/plugins/doctor/cosign.pub
# If missing, copy the cosign.pub produced by package-runtime-plugins.ps1 into place, then:
sudo systemctl restart stellaops-doctor
Kubernetes / Helm
The trust-root is typically mounted from a Secret. Confirm the Secret exists and is keyed/mounted at the configured path, then roll out:
kubectl get secret doctor-plugin-trust-root -o jsonpath='{.data.cosign\.pub}' | base64 -d | head -1
kubectl rollout restart deployment/doctor
Dev only: setting
Doctor:RuntimePlugins:AllowUnsigned=truemakes the loader admit bundles without a trust-root and makes this checkSkip. Never enable it in production — admission is a supply-chain boundary (the loader stays fail-closed withAllowUnsigned=false).
Verification
stella doctor run --check check.core.plugins.trustroot
A Pass means the trust-root is present and readable. A Fail reports the missing/unreadable path; a Skip means either the dev hatch is enabled or this host has no mounted plugins.
Related Checks
check.core.plugins.loaded— reports the mounted plugins that were rejected (their checks vanished) when, for example, this trust-root is missing.check.core.integration-coverage— countsintegration.trust.cosign-rootas covered once this check is live (DOC-CLAUSE-005).
See the Doctor reference for the full check catalog, CLI usage, and export bundles.
