linkerd-check attestor
Linkerd is the CNCF-graduated Kubernetes service mesh — a sidecar-based mTLS, retries, and traffic-routing layer that's auto-injected into meshed workloads. Under cilock, the rookery native linkerd-check attestor ingests two of Linkerd's JSON outputs and produces a single signed in-toto attestation linked to the host environment, the git commit, and the literal capture argv:
linkerd check -o json— control-plane + extension health (kubernetes-api, linkerd-existence, linkerd-identity, linkerd-control-plane-proxy, linkerd-viz, etc.) with per-check pass/warn/error results.linkerd viz edges deploy -A -o json— the meshed service graph withclient_id/server_idpeer identities and ano_tls_reasonper src→dst pair. An edge is mTLS-secured iff both IDs are present ANDno_tls_reasonis empty.
The headline use case: a release-gate Rego that denies any deploy where any meshed edge is non-mTLS. The full positive + negative case is exercised end-to-end against the dropbox-clone-dev EKS cluster with the emojivoto demo in tool-linkerd-check.
| Name | linkerd-check |
|---|---|
| Predicate type | https://aflock.ai/attestations/linkerd-check/v0.1 |
| Lifecycle | postproduct |
| Default binary? | No |
| Category | compliance-scan (primary) |
| Recommended trace | off — no syscall tracing needed |
| Auto-attaches when |
|
The facts in this box are generated from the cilock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.
Validated invocation
# Pre-reqs: linkerd CLI + kubeconfig pointed at a cluster with Linkerd
# control plane installed. Viz extension required for the edges capture.
LINKERD_CLUSTER_NAME=<your-cluster> cilock run --step linkerd-mesh-check \
--signer-file-key-path key.pem \
--outfile attestation.json \
--attestations linkerd-check,environment,git \
--enable-archivista=false \
-- sh -c 'linkerd check -o json > linkerd-check.json; \
linkerd viz edges deploy -A -o json > linkerd-edges.json'
This is the exact command exercised in tool-linkerd-check. The single sh -c wrapper writes both JSON files into the working directory in one shot: cilock's product attestor hashes both, the linkerd-check attestor parses both. The command-run/v0.1 predicate records the full sh -c argv.
LINKERD_CLUSTER_NAME is read by the attestor and stamped into the predicate's cluster_name field so cluster-aware Rego can branch on it. Optional; if unset the field is empty but the attestation still signs.
What gets captured
| Predicate type | Source |
|---|---|
https://aflock.ai/attestations/environment/v0.1 | host OS, kernel, env vars (sensitive ones obfuscated) |
https://aflock.ai/attestations/git/v0.1 | commit hash, branch, dirty status |
https://aflock.ai/attestations/material/v0.3 | Merkle root over the working tree before the capture |
https://aflock.ai/attestations/command-run/v0.1 | literal sh -c 'linkerd check ...; linkerd viz edges ...' argv |
https://aflock.ai/attestations/product/v0.3 | Merkle root over linkerd-check.json + linkerd-edges.json |
https://aflock.ai/attestations/linkerd-check/v0.1 | parsed reports with per-category pass/warn/error counts, edges summary with secured/insecure counts, cluster name |
The linkerd-check/v0.1 predicate body has this shape:
{
"cluster_name": "dropbox-clone-dev",
"check_summary": {
"pass": 50, "warn": 4, "error": 0,
"overall_success": true, "distinct_categories": 10,
"categories": [
{ "category": "kubernetes-api", "pass": 2, "warn": 0, "error": 0 },
...
]
},
"check_report": { /* full parsed check JSON */ },
"edges_summary": {
"total_edges": 15, "secured": 15, "insecure": 0,
"distinct_src_namespaces": ["emojivoto", "linkerd-viz"],
"distinct_dst_namespaces": ["emojivoto", "linkerd", "linkerd-viz"]
},
"edge_report": [ /* full parsed edges array */ ]
}
Why this shape
| Antipattern | Correct shape (this example) |
|---|---|
cilock run ... -- bash -c "linkerd check ... > check.json && cp check.json out.json" | cilock run ... -- sh -c 'linkerd check -o json > linkerd-check.json; linkerd viz edges deploy -A -o json > linkerd-edges.json' |
command-run.cmd records ["bash","-c","... && cp ..."] | command-run.cmd records the literal sh -c argv invoking the two linkerd subcommands |
| The product is a copy of a file written outside cilock's view | The product is the JSON file as linkerd wrote it during the wrapped step |
Three properties matter: (1) command-run/v0.1.cmd records the real argv (the sh -c invoking the two linkerd subcommands), not a chained shell with a separate cp. (2) The ptrace spy traces the shell + linkerd child processes because cilock is sh's direct parent. (3) product/v0.3 captures linkerd-check.json and linkerd-edges.json as written via the single redirects inside the wrapped step, and the linkerd-check attestor parses those exact files.
The single sh -c wrapper is the same pattern as hadolint, govulncheck, and falco — tools that need stdout-to-file conversion to be hashed by the product attestor. The shell redirect is a one-shot conversion, not the cp antipattern.
The mTLS-required policy (the value cilock adds)
Without policy, you have a signed envelope but no enforcement. The canonical service-mesh release gate is "any insecure edge → block deploy." A minimal Rego:
package linkerd_mtls
deny[msg] {
input.edges_summary.insecure > 0
msg := sprintf(
"linkerd viz edges reports %d insecure (non-mTLS) edge(s) of %d total — refusing to deploy through unmeshed traffic",
[input.edges_summary.insecure, input.edges_summary.total_edges]
)
}
deny[msg] {
input.check_summary.error > 0
msg := sprintf("linkerd reports %d error-level check(s)", [input.check_summary.error])
}
deny[msg] {
not input.edges_summary
msg := "edges report not captured — mTLS contract cannot be enforced"
}
Full Rego with five deny rules + the positive/negative end-to-end runner is in tool-linkerd-check/policy/.
Validate it locally
List the predicate types in the captured envelope:
jq -r '.payload' attestation.json | base64 -d | jq '.predicate.attestations | map(.type)'
Expected output:
[
"https://aflock.ai/attestations/environment/v0.1",
"https://aflock.ai/attestations/git/v0.1",
"https://aflock.ai/attestations/material/v0.3",
"https://aflock.ai/attestations/command-run/v0.1",
"https://aflock.ai/attestations/product/v0.3",
"https://aflock.ai/attestations/linkerd-check/v0.1"
]
Confirm command-run.cmd carries the literal sh -c argv:
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/command-run/v0.1") | .attestation.cmd'
# ["sh","-c","linkerd check -o json > linkerd-check.json; linkerd viz edges deploy -A -o json > linkerd-edges.json"]
Pull the mesh summary from the signed envelope:
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/linkerd-check/v0.1") | .attestation | {
cluster: .cluster_name,
check: .check_summary | {pass, warn, error, overall_success},
edges: .edges_summary | {total_edges, secured, insecure}
}'
# {
# "cluster": "dropbox-clone-dev",
# "check": { "pass": 50, "warn": 4, "error": 0, "overall_success": true },
# "edges": { "total_edges": 15, "secured": 15, "insecure": 0 }
# }
Notes
- Extension JSON concatenation.
linkerd check -o jsonemits the core report and one report per installed extension (viz, jaeger, multicluster) with no delimiter between objects (linkerd/linkerd2#5837). The attestor uses a streaming JSON decoder to read every object and merges their categories; the merged report'soverall_successis the logical AND of all sub-reports. This is transparent to the operator — you just runlinkerd check -o json > fileand the attestor handles the quirk. - Viz extension required for mTLS gating.
linkerd checkalone doesn't surface per-edge mTLS state — that's a viz-extension feature. Install withlinkerd viz install | kubectl apply -f -(or via the official Helm chart). Without viz, the linkerd-check predicate still produces, butedges_summaryis absent and policies that require it fail-closed (which is the right default). -Anamespace scope. The validated example useslinkerd viz edges deploy -A -o jsonto scope across all namespaces. For a tighter release gate, scan only the namespaces your release touches —linkerd viz edges deploy -n <ns1> -n <ns2>. Either way the predicate capturesdistinct_src_namespacesanddistinct_dst_namespacesso policies can branch on scope.- Cluster name stamping. Set
LINKERD_CLUSTER_NAMEin CI so policies can branch on cluster (e.g. allow warnings ondev, deny them onprod). If unset, the field is empty. - Stable-channel warnings. Linkerd's
linkerd checkflags warnings for any non-edge channel version mismatch — running stable-2.14.10 against the currentlinkerd versioncheck produces 4 warnings. Warnings are NOT release-gate failures by default; only errors block. Errors require explicit deny rules.
FAQ
Does cilock support Linkerd?
Yes. Wrap sh -c 'linkerd check -o json > check.json; linkerd viz edges deploy -A -o json > edges.json' with cilock run --attestations linkerd-check,environment,git. The native linkerd-check attestor parses both files into a https://aflock.ai/attestations/linkerd-check/v0.1 predicate with per-category check rollup and per-edge mTLS booleans, alongside the standard collection (environment, git, material, command-run, product).
How do I gate a release on every edge being mTLS-secured?
Write a Rego that denies on input.edges_summary.insecure > 0. The Rego runs against the linkerd-check/v0.1 predicate's attestation field, so it sees the same edges_summary block. A 3-line policy with one deny rule is enough for the headline case; the tool-linkerd-check/policy/decoded-rego-linkerd-mtls.txt shows the production-ready 5-rule version.
Why parse linkerd check JSON into a custom predicate instead of using k8smanifest?
linkerd check validates dynamic cluster state (control plane pods are healthy, identity certs are valid, viz proxies are running the right version) — none of that lives in a static Kubernetes manifest. k8smanifest would snapshot the policy CRDs (Server, ServerAuthorization, HTTPRoute) but couldn't capture whether the linkerd-identity Deployment is actually ready or whether the trust anchor is about to expire. The two attestors are complementary: capture CRDs with k8smanifest for the desired state, capture linkerd check for the actual state.
Does this require the viz extension?
For the mTLS gate, yes — linkerd viz edges is what provides per-edge mTLS booleans. The plain linkerd check capture works without viz but only surfaces control-plane health, not data-plane integrity. Install viz with linkerd viz install | kubectl apply -f -.
How does this differ from running Linkerd standalone?
Standalone linkerd check and linkerd viz edges give you JSON outputs with no provenance — nothing binds them to a release, a cluster (modulo the kubeconfig that ran the command), or a policy. cilock adds five predicates around the same JSON: git/v0.1 (the commit that defined the policy), environment/v0.1 (the host running the capture), material/v0.3 (the working tree), command-run/v0.1 (the exact sh -c argv + exit code), and product/v0.3 (the JSON files' content hashes). The linkerd JSON is unchanged — same bytes, same downstream pipeline — but the surrounding evidence is now signed and policy-checkable.
See also
linkerd-checkattestor — the underlying ingestion path- Validated example: tool-linkerd-check — real EKS-cluster capture + raw envelopes + mTLS-required Rego (positive + negative)
- Linkerd project — upstream
linkerd checkCLI referencelinkerd vizCLI reference- Tools index