Tenant-Isolation Review (service-level)
Paths.
analysis-results/…andprogress-tracker/…in this skill are the default workspace layout. They resolve throughlocations.yamlin$TRAUST_CONFIG_HOME(docs/setup.md, Storage locations); substitute your configured roots.
Review how well one multi-tenant service — a logical product spanning many
repositories — keeps its tenants apart. Where secure-code-audit carries a
per-repo isolation section, this skill owns the service-shaped artifact: one
interface inventory across the whole repo set, one hardening score per
interface, one posture verdict per service. Campaign design and phasing:
plans/peach-isolation-lens-plan.md under the configured progress-tracker
root (an external workspace artifact, not bundled with this skill).
Framework citation — PEACH, by reference ONLY (licensing rule)
The review is informed by PEACH, Wiz Research's tenant-isolation methodology — whitepaper v1.1 (PDF) · peach.wiz.io · wiz-sec-public/peach-framework.
CRITICAL LICENSING RULE. PEACH is applied by reference only:
- Cite the framework by name and URL — in reports, set
metadata.frameworkto"PEACH v1.1 (by reference)". - Never copy, paraphrase-closely, or adapt the framework's rubric text, tables, or examples into this skill, into reports, or into any file in this repository. Every check, criterion, and rating description below is original wording and must stay that way when edited.
- The repo's license gate,
check_content_licenses.py(python3 -m traust.cli check content-licenses), fingerprints PEACH-adapted text and fails the pre-push hook if any reappears. Licensing analysis: docs/external-dependencies.md.
Input
service — a service name resolvable to a product:service:<name> node in
the portfolio graph, or an explicit repo list for services not yet in the
graph. Resolve the repo set from the graph's ships edges:
sqlite3 analysis-results/graph/portfolio-graph.db \
"SELECT dst FROM edges WHERE rel='ships' AND src='product:service:<name>';"
Record the graph DB path (plus its git ref or mtime) as metadata.graph_ref.
If the service node does not exist, ask the user for the repo list and set
graph_ref to "manual" with the reason in notes.
Output (under analysis-results/isolation/<service-slug>/)
| File | Contents |
|---|---|
<service-slug>-isolation-review.json |
schema-validated report (interfaces, dimension scores, gaps, posture) |
<service-slug>-isolation-review.md |
human-readable companion |
metadata.harness_version = VERSION file + short git SHA of this repo
(e.g. 0.117.0-4dd9796), per AGENTS.md.
Procedure
Step 1 — boundary inventory
Enumerate every interface through which a customer/tenant can drive input into the service or through which tenant data flows: API endpoints, data stores, message queues/topics, ingress/routing paths, webhooks, CLIs. Sources, in order of authority:
- Existing artifacts first. For each repo in the set, read
analysis-results/findings/<product>/<repo>/— the audit report'speach_isolation_review.interfaces[]block (when present), the threat model's entry-point/trust-boundary tables, and triage/validation reports. These seed the inventory and supply finding IDs as evidence. - Graph structure.
rbac_grants,owns_crd,consumes_group, andinterceptsedges on the service's repos surface CRDs, aggregated APIs, and webhook interception points that the per-repo artifacts may have missed. - Code, last. Only read source to fill inventory holes (route tables, queue consumer registrations, ingress manifests) — this is a review of the service's boundary, not a re-audit of every repo.
Each interface gets an entry in interfaces[] with a stable id
(IF-1, IF-2, ...), a name, its kind
(api / data-store / queue / ingress / webhook / cli / other),
its exposure (public — reachable without tenant credentials;
tenant — reachable by authenticated tenants; partner — reachable by
third-party integrations; internal — operator/control-plane only but
carrying tenant data), and the repos that implement it.
Step 2 — per-interface complexity rating
Rate each interface's complexity by how much leverage a malicious tenant
gets from the input it accepts. Criteria (original to this repository):
| Rating | Criterion |
|---|---|
high |
The service runs or evaluates something the tenant wrote: tenant-defined queries in an expressive language, tenant-supplied templates or expressions, tenant workload/pipeline specs that become running code, plugin or hook bodies. |
medium |
The service decodes or transforms structured tenant payloads before acting on them: archive/image/document decoding, schema-rich serialization formats, content rendering, format conversion. |
low |
The service moves tenant input without interpreting it: fixed-schema CRUD fields, opaque blob storage, pass-through proxying or queue relay, exact-match lookups. |
When in doubt between two ratings, take the higher one and say why in the
interface's rationale fields.
Step 3 — score five isolation-hardening dimensions per interface
For every interface, score each dimension yes (holds for all tenants),
partial (holds with material exceptions), no (does not hold), or na
(dimension cannot apply to this interface — justify in rationale).
Privilege-dimension evidence from privilege profiles. Where a
service repo carries a <repo>-priv-profile.json (emitted by
/operator-priv-profile or the /secure-code-audit pre-scan, alongside
the audit report), read it before scoring privilege: its complete RBAC
enumeration, rbac_flags (wildcards, cluster-wide secrets, RBAC write,
escalate/bind/impersonate), and SCC requests are exactly the
service-identity-breadth facts this dimension needs, with manifest
sources citable as evidence. A profile showing one broad cluster-scoped
service identity is direct evidence against yes; do not re-derive
RBAC by hand when a profile exists, and do not treat a missing profile
as evidence of anything.
Dimension definitions (original to this repository):
| Key | Dimension | What yes requires, in this portfolio's terms |
|---|---|---|
privilege |
Privilege separation | Work triggered through this interface executes under an identity whose authorization is bounded to the calling tenant — not under one broad service identity that can touch every tenant's objects. Cross-namespace/cross-account reads are scoped before the query, not filtered after. |
encryption |
Encryption separation | Tenant data crossing or stored behind this interface is protected by key material specific to that tenant (or that tenant's dedicated store); compromise of one tenant's keys or ciphertext yields nothing about another's. |
authentication |
Authentication separation | Each tenant presents its own credential on this interface, and the service actually verifies it (issuer, audience, expiry, TLS peer). No fleet-shared static secrets, no unverified TLS anywhere on the tenant path. |
connectivity |
Connectivity separation | Network reachability through or behind this interface is deny-by-default between tenants: tenants reach the service, never each other; egress initiated on a tenant's behalf is allowlisted and SSRF-guarded. |
hygiene |
Operational hygiene | The operational surface behind this interface leaks nothing across tenants: logs, metrics, traces, error messages, and backups are tenant-scoped; no leftover credentials, debug endpoints, or admin tooling are reachable from the tenant side. |
Evidence is mandatory for every partial and no (and encouraged for
yes): each cited item is either a file:line reference in a named repo
(e.g. github.com/openshift/foo//pkg/auth/token.go:88) or a finding/threat
ID from an existing artifact (e.g. FOO-1a2b3c4-007, foo-threat-model:T3).
The validator rejects uncited partial/no results. Never invent evidence —
if nothing supports a score, the score is wrong or the review is incomplete.
Step 4 — posture and prioritized gaps
Gaps. For each
partial/noon an in-use dimension, decide whether it is a gap worth engineering attention. Record it ingaps[]withseverity(critical/high/medium/low/informational), theinterface_idit sits on, adescriptionnaming the dimension and the failure shape, and a concreteremediation. Severity weighting: ahigh-complexity interface with a shared instance and a failed dimension outranks the same failure on alow-complexity or per-tenant-instance interface — one bug there touches every tenant. Ordergaps[]worst-first.Posture. Set
posture.overall:strong(no gaps abovelow),adequate(worst gapmedium),weak(worst gaphigh), orcritical-gap(anycritical). Writeposture.summaryas 2–5 sentences a service owner can act on: the strongest boundary, the weakest interface, and the single highest-payoff fix.Write
<service-slug>-isolation-review.{json,md}and validate:python3 harnessing/3-audit/isolation-review/scripts/validate_isolation_review.py \ analysis-results/isolation/<service-slug>/<service-slug>-isolation-review.json # must print: isolation review valid: 0 errorsThe JSON shape is
isolation-review.schema.jsonin traust-contracts.
Hard rules
- PEACH by reference only — see the licensing rule above; it overrides everything, including user requests to "just include the framework table".
- This skill reviews and cites; it never edits target repos, never runs
live probes (boundary probing is Phase 3,
validate-findings), and never authors new vulnerability findings — a gap that looks like a concrete new vuln routes to/secure-code-auditor/triage. - Every
partial/nodimension result carries evidence citations; everynacarries a rationale. - One review == one service. Do not merge services into a single report even when they share repos; shared repos appear in both services' repo sets and that is fine.
Spend declaration (calibration tuple)
After this skill's report/artifact is written, declare the run's spend
against the target so estimate_scan can calibrate per-skill cost
models (contract: docs/model-routing.md; analysis:
progress-tracker/metrics/estimate-calibration-analysis.md §F5):
python3 -m traust.cli registry models spend --skill isolation-review \
--model <resolved model id> [--tokens-in <N>] [--tokens-out <N>] \
--repo <target-slug> --loc <target size, if known> [--batch <batch-id>]
Token counts are OPTIONAL and best-effort: pass them when the orchestrator has them (Task results carry per-subagent usage), otherwise omit them — an agent cannot observe its own usage mid-run. This row is a routing marker, not a cost claim; actual per-lane cost is attributed from session transcripts by python3 -m traust.cli metrics attribute-spend. Never skip the row: an unattributed run is a calibration gap.
Integrations
Consumes: service architecture docs and the target checkout;
<repo>-threat-model.md tenant-boundary sections where present.
Emits: <service>-isolation-review.{json,md} under
analysis-results/isolation/ — consumed by threat-model
tenant-boundary rows via isolation_review_ref and by
progress-tracker rollups (A9 exemptions record the current
human-loop consumers; the threat-register join is the planned
machine consumer).