Graph, query & AI — see how everything connects, and ask in plain language
What it is
trstctl builds a dependency graph instead of holding only a flat credential list. The graph shows how credentials connect (who owns which key, who issued it, what it can reach), exposes a unified query layer to ask questions across all its data safely, and layers AI on top: a pluggable model adapter, grounded root-cause analysis with natural-language questions, and an MCP server so external AI agents can query trstctl through a safe, read-only interface.
The mental model: the graph maps roads between credential "buildings"; the query layer is the one inspector's desk every question must pass through. Product help uses that desk for exact questions it understands and shows cited, scoped evidence for other questions without calling it a complete answer.
Why it exists
Security questions are rarely about one credential — they're about relationships: "if this key leaks, what's exposed?", "what does this AI agent actually have access to?", "why did this renewal fail?" Answering those needs a graph and a safe way to query it, through one rigorously scoped path instead of each feature reinventing access control. The AI layer then makes it approachable — ask in English, get a cited answer — without letting a model invent facts or leak across tenants.
How it works
The credential graph (F21)
The graph models your inventory as nodes (workloads, credentials, issuers, resources,
crypto assets, attestations) and impact-oriented edges (ISSUED, OWNS, DEPLOYED_TO,
GRANTS_ACCESS, CONNECTS_TO, EXHIBITS, BOUND_TO_IDENTITY), where edge A→B means "compromising A puts B
at risk." It's built on demand, and every read is isolated to the caller's tenant at the
database layer, so a traversal can never escape the tenant boundary. On top: Reachable
(breadth-first reach), BlastRadius (compromise impact by kind), and a minimal
Cypher-style Query.
Reachable and BlastRadius return more than a flat list. For every affected node,
the server returns one deterministic shortest evidence path: the ordered nodes and the
exact edges, including each relationship's source and confidence when reported. The
console can therefore say “credential → grants access → database” and link the affected
object to risk, lifecycle, and audit evidence without reconstructing security-sensitive
impact in the browser. Subject-only trust candidates stay visible as explicitly
unverified graph evidence but are excluded from these authoritative paths unless a
specialized caller asks for that relationship type.
BOUND_TO_IDENTITY links a certificate to each lifecycle identity named in its
retained issuance or successful delivery evidence. This lets a certificate impact
preview reach the identity's declared destinations. Matching names, owners, or SANs
do not establish a binding. Shared certificates retain every exact identity link,
and the evidence path keeps each relationship's source and confidence. The binding
is historical evidence; it does not prove that a listener currently serves that
certificate. Use the deployment verification result for that observation.
CONNECTS_TO is not guessed from a test fixture. A host/network agent reports an exact
metadata-only service_dependency observation over its mTLS inventory channel. The
event-projected row binds the observing agent, an owner-model workload name, and one
target resource; graph.Build emits the edge only when that workload maps exactly inside
the same tenant. Credential owners are recovered by walking the incoming canonical
workload → credential OWNS edge. An unmapped workload or mismatched target makes the
graph unavailable instead of turning missing topology into a reassuring zero. This is
observed dependency truth, not passive traffic inference: unreported connections remain
unknown.
Served — GET /api/v1/graph, /graph/reachable/{id}, /graph/blast-radius/{id},
POST /api/v1/graph/query, plus the graph CLI group.
The unified semantic query layer (F75)
This is the one security boundary every advanced consumer (AI, MCP, compliance) routes
through, so scoping is never reinvented. Callers submit a typed Spec — allow-listed
surfaces (log, graph, inventory, owners, CBOM), fields, and operators, bound values, never
raw SQL or Cypher. The engine enforces tenant first (always the caller's, non-overridable,
database-layer-enforced), then RBAC (holding the permission for every selected surface, or
the query is denied before execution, not post-filtered). It runs in its own bounded lane
with a wall-clock deadline and row caps, pins results to a position in the immutable event
history, and returns deliberately coarse errors so a caller can't tell "out of scope" from
"not found."
Served through the read-only AI/RCA routes when ai.enable_api is on
(POST /api/v1/ai/query, POST /api/v1/ai/rca) and by POST /api/v1/graph/query, and
used by MCP tools; the standalone Go API stays available for embedded consumers. Saved
prompts and richer analysis workspaces remain roadmap residuals, not hidden GA scope.
An AI query answer is returned only after its ai.query.answered event is stored in
the immutable audit log. Operators can read that receipt through
GET /api/v1/audit/events?type=ai.query.answered&window=latest. The v2 receipt records
row and citation counts, sufficiency, truncation, and the exact interpreted expiry
window; retained v1 receipts keep their original smaller shape. If the audit append
is unavailable, the query returns a retryable 503 instead of an unaudited answer.
The pluggable AI model adapter (F76)
trstctl's AI features are model-agnostic: a thin adapter routes reasoning to a cloud LLM
gateway or a local Ollama/vLLM endpoint, for air-gapped deployments. ai.model.mode is
off (default), local (an operator-owned completion endpoint), or cloud (requires
explicit allow_egress=true); GET /api/v1/ai/status reports the live mode, endpoint,
egress class, and pii_egress posture. A secret redactor strips PEM blocks, secret/token
assignments, and long base64 runs before any prompt leaves the process, so key material
cannot reach a model or its logs; a residual-entropy gate blocks the send if any
high-entropy run survives.
The redactor deliberately preserves personal/identifying data (emails, certificate subjects, graph node names, SPIFFE/OIDC subjects, IPs, hostnames) as useful in-house context — personal-data egress is default-private. Since a configured cloud model is a third party, a second PII-aware boundary runs after secret redaction:
pii_egress: redact(the default): emails, IP addresses, OIDC/SPIFFE subjects, hostnames, and person names are stripped before egress.pii_egress: block(ai.model.block_pii=true): a prompt still carrying personal data after redaction is refused (strict fail-closed).pii_egress: allow(ai.model.allow_pii=true): an operator has explicitly consented to sending personal data to the model; PII is preserved.
Cloud egress needs two deliberate choices — allow_egress=true to reach a cloud model at
all, and allow_pii=true to include personal data. Provider data-retention/training-use
policies are outside trstctl's control in cloud mode; review them first, and keep
allow_pii=false (the default) unless permitted. Served as an optional adapter behind
ai.enable_api; no model is configured by default.
Grounded RCA & natural-language query (F77)
You ask a question in plain language ("what's the blast radius of the payments cert?");
trstctl gathers evidence through the query layer (inheriting its tenant+RBAC scoping),
then answers using only that evidence — every claim carries a citation (source#id), and
with no evidence it says "insufficient evidence" rather than inventing one. Retrieved data
is treated as untrusted (a hostile string in a SAN can't become an instruction), the
pipeline is strictly read-only, and every gather is recorded as an immutable audit event.
For a cause question, event IDs or event types alone are context, not a cause: the
answer is marked sufficient only when a cited record contains an explicit failure
reason. Renewal-failure reasons are read from the tenant's allowlisted lifecycle
events and redacted before they enter an answer. If no reason is present, the
response asks the operator to inspect the exact identity's renewal job and audit
history; it does not infer a cause from unrelated certificate events.
Served at POST /api/v1/ai/rca when ai.enable_api is on.
The trstctl MCP server (F78)
The Model Context Protocol is how external AI agents call tools.
trstctl's MCP server exposes four read-only tools — query_credentials,
get_blast_radius, explain_incident, compliance_status — by default. Every call is
tenant-scoped (cross-tenant calls are refused before any query), per-caller
rate-limited, and audited; answers flow through the RCA pipeline, cited and redacted. The
server itself holds a workload identity from trstctl's own
broker. Served at GET /api/v1/mcp/tools and POST /api/v1/mcp/tools/{tool} when
ai.enable_api is on.
Six more read tools answer posture and exposure questions from the served read routes, entered with the agent's own credentials, so a token sees here exactly what it may read over REST:
| Tool | Answers | subject |
|---|---|---|
posture_findings |
NHI posture: shadow, stale, static-credential, over-privileged and exposed identities | optional: a name, kind or inventory ID |
weak_key_exposure |
certificates and trust anchors whose keys are weak | optional: a subject, issuer or host |
distrust_exposure |
the certificates and host trust stores a distrust-list entry affects | the entry's name or SPKI SHA-256 |
assurance_record |
a certificate's signed lifecycle assurance record, one finding per stage | the certificate ID |
revocation_status |
a certificate's recorded revocation and the relay's CRL and OCSP observations of it | the certificate ID |
crypto_inventory |
crypto inventory assets with strength, quantum exposure and migration target | optional: an algorithm, location or library |
distrust_exposure answers "which of my certificates and hosts does this distrust entry
affect?": each affected certificate cites certificate:<id> (and the discovery finding or
connector target it was seen on), and each affected trust store cites the agent's
discovery_finding:<id>, the store's graph node (graph:ts:<host>:<kind>) and the
reporting agent. Hosts and stores no agent has scanned are not covered, and the answer
says so.
Every tool result, of every kind, has the same shape: schema
(trstctl.mcp.tool-result/v1), the narrative text, citations, generated_at and
findings. Each finding has a stable finding_id, a kind, a summary, evidence_ids
(kind:id references a caller can read back, such as certificate:<id> or
discovery_finding:<id>), the source record set, observed_at (null when the record
carries no time) and a confidence:
verified: an observation whose signature the control plane checked, such as a relay's signed revocation probe;observed: reported from outside trstctl's own record, such as a discovery scan or an agent's inventory;recorded: trstctl's record of its own action or an operator's statement;inferred: a correlation that can be wrong, such as a certificate matched to a distrusted CA by issuer name only.
The JSON Schema (draft-07) is published in the tool listing as result_schema, with a
description of each tool; trstctl-cli mcp serve gives it to MCP clients as every tool's
outputSchema and returns each result as structured content. A result says
sufficient: false when the evidence does not support an answer, when the token could
not read part of it (the text names what), or when the findings were cut at 200
(truncated: true; narrow the subject).
Write tools are a separate, explicit choice: with TRSTCTL_AI_MCP_WRITE_TOOLS=true, the
tool list also includes issue_certificate and rotate_certificate. Both act on a
lifecycle identity (identity_id), never on a CA directly. The call enters the identity
transition route with the agent's own credentials, so it meets the same lifecycle gate an
operator does: RBAC and separation of duties, attribute rules, the policy gate and dual
control. It needs certs:issue and an Idempotency-Key. issue_certificate signs the
agent's own request (csr_pem; the key stays with the agent) and rotate_certificate
renews a deployed identity.
The result's outcome says what happened:
queued: the transition is recorded and its CA call is queued. The certificate is recorded when the call completes; read it from the identity, not from the tool result.proposed: dual control applies. Nothing ran; the result names the approval request a distinct approver decides. After approval, the agent repeats the same call with the same arguments andIdempotency-Key, and the approved request runs exactly once.
A rotation cannot carry an approval, so under dual control rotate_certificate is
refused and an operator renews instead. A deny-all policy refuses both tools. Every write
is recorded as mcp.tool.write and every proposal as mcp.tool.proposed. A call that
names authority_id is refused: direct CA issuance is not available to MCP. Without the
flag, write tools are not listed and calls fail closed.
Beyond that, route-backed REST MCP tools expand the surface further, named
rest_<operationId> (e.g. rest_list_notifications maps to the notifications list
route): read routes are exposed by default when RBAC permits, while REST-backed
mutations stay behind the same write-tool flag and idempotency checks. Two kinds of route
are never MCP tools: approval decisions (an agent never approves or denies a dual-control
request) and certificate mutations outside the lifecycle gate, such as direct CA, SSH,
attested and agent-identity issuance, attester trust changes, issuance retries and
break-glass ceremonies. The MCP-vs-REST parity CI guard reads that exclusion from the
served route table and fails when any other served REST route has neither an MCP tool
nor an allowlist entry.
Use it
In the web console, open Product help at /assistant. Before it exposes a question
box, the page reads the authenticated GET /api/v1/ai/status boundary and the exact
capability operation posture. This is a readiness check, not a question: it sends no
prompt or tenant evidence to a model. The page also reads the served support entitlement
only to decide whether to show the contract-owned licensed-support handoff.
When the help backend is ready, select Ask a question to open the read-only workspace. For an exact expiry worklist, ask “Which certificates expire within 30 days?” (replace 30 with 1–365, or ask in weeks), with Certificates selected as an evidence surface. The query uses the caller's tenant, checks every selected surface's read permission, and returns only active certificates whose expiry is after the query time and before the requested cutoff. Each result includes its expiry time and inventory citation.
The same planner answers other questions exactly, and every answer begins with Interpreted as: so you can check what was answered:
| Ask | Answers |
|---|---|
| “Which RSA certificates issued by Acme expire within 60 days?” | Any mix of: expiring within N days or weeks, expired, revoked/active/superseded, issued by, using an algorithm (RSA, ECDSA, Ed25519, ML-DSA…), for a subject or SAN name, owned by an owner, without an owner |
| “How many certificates are issued by "Acme Issuing CA" and have no owner?” | An exact count, with the first matches cited |
| “Who owns api.example.com?” (a serial or fingerprint works too) | Each matching certificate with its owner; select Owners as well |
| “Which crypto assets are quantum-vulnerable and use RSA keys smaller than 2048 bits?”, “Where is RSA used?” | Crypto assets by algorithm, key size, quantum vulnerability or policy; select CBOM |
| “Which workloads exist?”, “List all issuers” | Credential-graph inventories by node kind; select Graph |
Values are single words or, for more than one word, text in double quotes; they are matched as literal, case-insensitive text and never become part of a query. An interpreted question's own clauses decide what is read; the optional subject narrows only questions that are not interpreted. When a row limit hides more matches, the answer says so and is marked insufficient; page through certificate inventory for a complete worklist. A question that needs a surface you did not select returns an error naming it, and unsupported certificate expiry wording returns a usable error. Any other question may show scoped evidence, but an unplanned question is marked insufficient rather than claiming the records answer it. The planner is a fixed grammar, not a model; questions about history or causes belong to Investigate a cause. Open Evidence and request details only when you need an exact subject or source scope. Investigate a cause uses the same tenant-scoped evidence path for root-cause analysis. Use read-only tools still loads its MCP catalog only when selected, and Runtime and privacy details reuses the status already read at entry.
If ai.enable_api is off, the status cannot be read, the role cannot run the exact
operation, or a later call returns 503, Product help fails closed before accepting more
input. It says that no question was sent and links separately to troubleshooting,
ordinary defect intake, and private vulnerability reporting. It also warns the operator
to review a redacted support bundle before sharing it and to use the named design-partner
or licensed-support channel rather than weakening authentication.
The graph is served — explore relationships and blast radius:
trstctl-cli graph nodes
trstctl-cli graph blast-radius cert:payments-tls
trstctl-cli graph query 'MATCH (w:workload)-[:OWNS]->(c)-[:DEPLOYED_TO]->(r) WHERE w.name = "payments-svc" RETURN c, r.name'
Those map to the served /api/v1/graph* routes. When ai.enable_api is on, grounded
RCA (and GET /api/v1/mcp/tools) are served too:
printf '%s\n' '{"surfaces":["certificates"],"question":"Which certificates expire within 30 days?","limit":25}' \
| trstctl-cli ai query -f -
The response is a dated, cited window. If sufficient is false because the limit
was reached, use certificate inventory pagination to get the remaining matches.
curl -sS -H "Authorization: Bearer $TRSTCTL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"question":"what is the blast radius of the payments cert?"}' \
https://trstctl.example.com/api/v1/ai/rca
Enable guarded MCP issuance only for agents that should be allowed to act:
export TRSTCTL_AI_ENABLE_API=true
export TRSTCTL_AI_MCP_WRITE_TOOLS=true
curl -sS -X POST \
-H "Authorization: Bearer $TRSTCTL_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: mcp-issue-payments-2026-06-26" \
-d '{"identity_id":"<identity id>","csr_pem":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----\n","reason":"payments rollout"}' \
https://trstctl.example.com/api/v1/mcp/tools/issue_certificate
Under dual control the answer is HTTP 202 with "outcome":"proposed" and an
approval_request_id; send the same request again, with the same Idempotency-Key, after
a distinct approver approved it.
Ask which certificates and host trust stores a distrust-list entry affects:
curl -sS -X POST \
-H "Authorization: Bearer $TRSTCTL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject":"emSign ECC Root CA - G3"}' \
https://trstctl.example.com/api/v1/mcp/tools/distrust_exposure
Pitfalls & limits
| Capability | Status today |
|---|---|
| Credential graph (F21) | Served — /api/v1/graph*, graph CLI |
| Semantic query layer (F75) | Served through /api/v1/ai/query, /api/v1/ai/rca, /api/v1/graph/query; saved prompts remain roadmap residuals |
| AI model adapter (F76) | Served optional adapter; no model configured by default, cloud/local egress opt-in; self-service model settings editor remains a roadmap residual |
| Grounded RCA / NL query (F77) | Served — POST /api/v1/ai/rca, read-only and cited |
| MCP server (F78) | Served — GET /api/v1/mcp/tools, POST /api/v1/mcp/tools/{tool}; investigation and evidence tools read-only by default, every result carries findings under the published result schema; write tools require TRSTCTL_AI_MCP_WRITE_TOOLS=true, certs:issue, Idempotency-Key, and take the identity lifecycle gate (policy, dual control) |
The graph and query layer build per request, so very large tenants pay a bounded build cost. AI features are grounded and read-only by design: no actions, no answers beyond the evidence; with no model configured, RCA returns the raw evidence listing, not a prose answer. See Current limitations.
Reference
- Graph (served):
GET /api/v1/graph,/graph/reachable/{id},/graph/blast-radius/{id},POST /api/v1/graph/query; CLIgraph. - Node kinds: workload, credential, issuer, resource, crypto-asset, attestation.
- Query surfaces: log, graph, certificates, owners, CBOM (tenant-then-RBAC, allow-listed fields/operators, no raw SQL/Cypher).
- AI: model adapter (cloud or local Ollama/vLLM) with boundary redaction; RCA returns
cited answers; MCP tools are read-only and rate-limited by default; write tools are
explicit opt-in and audited; REST MCP tools use stable
rest_<operationId>names under the MCP-vs-REST parity CI guard.
See also
Discovery & inventory (populates the graph) · Observability & risk (exposure scoring) · Incident response & JIT (blast-radius remediation) · Workload identity (the MCP server's own identity) · glossary: event sourcing, RLS
Covers: F21, F75, F76, F77, F78