It asks 47 questions, all of them get and list.
Nothing is installed. Nothing runs after the command exits. The audit reads objects through the API server you already have credentials for, counts every request it makes, and prints that count in the reading it produces — so the read-only claim is a measurement, not a promise.
Every claim below names the file or the gate that holds it, because a page about trust that asks to be trusted is the wrong shape. The source is not public yet, so every check below is one you can run without it: the binary prints its own permissions, and your API server's audit log is a witness we do not control.
One ClusterRole, and the binary prints it
deploy/rbac.yaml, in every command-line download, is the exact access the collector needs — get and list, on 47
resource types, and no watch. An audit is one snapshot; a watch is a
subscription, and this tool has no time axis to spend one on. Apply that role and nothing else, and
the audit either completes or tells you which kind it was refused. It never turns a refusal into
a result that reads as nothing found.
The role and the collector cannot drift apart. Adding a collection step without adding it to the published role — or the reverse — fails the build.
gate: TestRBACContractMatchesCollector
source: internal/collect/permissions.go · deploy/rbac.yaml
A kind you decline is reported as declined, in the reading, by name — not folded into the findings and not counted as a pass.
source: the reading's own methodology section
and: every control it could not check reads not assessed
- nodes
- namespaces
- pods
- services
- endpointslices
- serviceaccounts
- resourcequotas
- limitranges
- persistentvolumes
- persistentvolumeclaims
- deployments
- statefulsets
- daemonsets
- poddisruptionbudgets
- clusterrolebindings
- rolebindings
- clusterroles
- roles
- networkpolicies · k8s
- networkpolicies · calico
- ingresses
- horizontalpodautoscalers
- cronjobs
- jobs
- customresourcedefinitions
- storageclasses
- ingressclasses
- priorityclasses
- certificates
- peerauthentications
- authorizationpolicies
- destinationrules
- servers
- applications
- kustomizations
- helmreleases
- ciliumnetworkpolicies
- ciliumclusterwidenetworkpolicies
- globalnetworkpolicies
- scaledobjects
- kafkas
- gateways
- httproutes
- servicemonitors
- podmonitors
- alertmanagerconfigs
- prometheuses
47 group/resource pairs, 46 distinct names — networkpolicies is asked twice, once of
Kubernetes and once of Calico, because they are different resources with the same word for a name.
The operator CRDs are read for spec and status. A cluster without them is not asked about them.
The role has no rule for these, so there is nothing to switch off
This is not a setting. The access simply is not granted, and a request the role does not cover is refused by your API server rather than by our good intentions.
- secretsNever requested. A Secret's value is the credential itself, and a tool that reads one has to be trusted with everything it protects.
- configmapsNever requested. Configuration is where people put connection strings when they run out of Secrets.
- pods/exec · pods/attachNo rule. Nothing runs inside your containers.
- pods/logNo rule. Logs carry request bodies, tokens and customer data.
- pods/portforwardNo rule for the audit. Reaching a Prometheus through a port-forward is a separate, explicit action you take.
- pod env valuesContainer environment is read for what is DECLARED — that a variable exists, and where it comes from — never for a value.
- create · update · patch
delete · deletecollectionNo rule for any of them, on any kind. There is no flag that adds one.
Where a finding depends on something inside a Secret or a ConfigMap, the reading says it could not be established rather than guessing from the name of the object.
source: internal/collect/rbac.go — “a name can never be trusted about”
Object writes are counted, and the count is in the document
Every request the process makes passes through a recorder installed on the transport. It classifies
each one, and a POST, PUT, PATCH or DELETE
against an object is a write. Each reading prints its own totals — how many reads,
how many writes, how many streams — so a run that wrote something would say so in the artifact it
produced.
The totals a reading of our kind test cluster printed on 18 October 2026.
No client can slip past the recorder. Three layers check it: which packages may build a client at all, which files may construct a transport, and — in the tests — that the recorder's count equals a real server's own count, exactly.
gate: transport-gate, in make ci
runtime: internal/collect and internal/prom assert 22 = 22 against an httptest server
A write to somewhere that is not your API server is counted separately, so “zero object writes” keeps meaning Kubernetes objects and cannot be diluted by anything else.
source: internal/trace/recorder.go — OffTargetWrite
A reading whose trace was not recorded says “no trace was recorded”. It does not print a zero, because an uncounted zero and a counted one are different facts.
type: TraceKnown, beside the count itself
To a file on the machine that ran the command
The audit talks to your API server, and — if a Prometheus is running in the cluster — to it through the
API server's service proxy (get services/proxy), or to one you name with --prometheus-url. The reading is written to disk beside its snapshot. Nothing is uploaded, and there is no
account to create before the tool works.
The reading is one self-contained HTML file. A copy you forward fetches nothing when it is opened — not a stylesheet, not a script, not a font.
gate: the browser corpus opens every rendered document offline
including: both typefaces, embedded
Beyond your cluster and your Prometheus, a default build makes one request that no audit makes — the update check — and sends nothing else unless you run ticket push, which carries a finding's text and object names to your tracker. shoalcheck upgrade — or the desktop app once a day, if you
leave that switch on — asks GitHub for the releases of Shoalcheck/shoalcheck. It carries nothing
about a cluster, and an update installs only if its manifest verifies under the release key and the
download's digest and version match.
source: internal/selfupdate/fetch.go — the address is a constant; asset URLs are pinned to the releases repository
off: the app's daily check has a switch, and SHOALCHECK_NO_UPDATE_CHECK=1 forces it off; the CLI asks only when you run upgrade
The reference-data fetcher, for signed bundles of prices and schedules, has no address in this build: a bundle source exists only if whoever builds the binary injects one. Prices and schedules come from tables that ship with the build, and each reading names which table it used and when that table was generated.
source: internal/refdata/fetch.go — empty by default; a build with no source refuses rather than fetches
gate: TestFetch_ACallerCannotSupplyAURL
Three ways, none of which involve believing this page
Print it with shoalcheck audit --show-permissions — it is the whole permission surface,
one line per resource, get and list only. Grant exactly that to its own ServiceAccount and run the
audit under it; anything beyond the role fails at your API server. Then read the reading's own request ledger, which lists
every call by verb and target.
The strongest check is your own audit log. Run Shoalcheck against a cluster whose API server audit log you can read, and compare it against the request count the reading prints. The two numbers are the same claim from two directions, and only one of them is ours.
If they disagree, that is a defect in the reading, and the two numbers side by side are the evidence for it.
Not a certified audit. Shoalcheck produces observations and potential gaps mapped to control frameworks for convenience — indicators for further review, not pass/fail attestations, and not issued by a QSA, CPA, or accredited auditor.