Skip to main content

Linkerd integration

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 CI/lock, 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 with client_id / server_id peer identities and a no_tls_reason per src→dst pair. An edge is mTLS-secured iff both IDs are present AND no_tls_reason is 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.

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: CI/lock'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 typeSource
https://aflock.ai/attestations/environment/v0.1host OS, kernel, env vars (sensitive ones obfuscated)
https://aflock.ai/attestations/git/v0.1commit hash, branch, dirty status
https://aflock.ai/attestations/material/v0.3Merkle root over the working tree before the capture
https://aflock.ai/attestations/command-run/v0.1literal sh -c 'linkerd check ...; linkerd viz edges ...' argv
https://aflock.ai/attestations/product/v0.3Merkle root over linkerd-check.json + linkerd-edges.json
https://aflock.ai/attestations/linkerd-check/v0.1parsed 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

AntipatternCorrect 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 CI/lock's viewThe 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 CI/lock 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 CI/lock 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 json emits 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's overall_success is the logical AND of all sub-reports. This is transparent to the operator — you just run linkerd check -o json > file and the attestor handles the quirk.
  • Viz extension required for mTLS gating. linkerd check alone doesn't surface per-edge mTLS state — that's a viz-extension feature. Install with linkerd viz install | kubectl apply -f - (or via the official Helm chart). Without viz, the linkerd-check predicate still produces, but edges_summary is absent and policies that require it fail-closed (which is the right default).
  • -A namespace scope. The validated example uses linkerd viz edges deploy -A -o json to 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 captures distinct_src_namespaces and distinct_dst_namespaces so policies can branch on scope.
  • Cluster name stamping. Set LINKERD_CLUSTER_NAME in CI so policies can branch on cluster (e.g. allow warnings on dev, deny them on prod). If unset, the field is empty.
  • Stable-channel warnings. Linkerd's linkerd check flags warnings for any non-edge channel version mismatch — running stable-2.14.10 against the current linkerd version check produces 4 warnings. Warnings are NOT release-gate failures by default; only errors block. Errors require explicit deny rules.

FAQ

Does CI/lock 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. CI/lock 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