Skip to main content

Falco integration

Not in the default binary

The falco attestor exists in rookery but is not compiled into the default cilock binarycilock attestors list won't show it. To use it, build a custom binary that includes the plugin with rookery-builder. The flows below assume such a build.

Falco is the de-facto open-source runtime-security engine for Kubernetes — it loads a kernel eBPF probe (or the legacy module driver) and fires structured events whenever a rule matches a syscall, container behavior, or Kubernetes audit event. Under CI/lock, the rookery native falco attestor ingests Falco's line-delimited JSON event output and produces a signed in-toto attestation linked to the host environment, the git commit, the literal capture argv, and per-rule + per-priority aggregations.

Unlike SARIF-shaped tools, Falco's output is a stream of events with a stable JSON schema (time, rule, priority, output, output_fields, K8s context). The native attestor parses every event, aggregates them per rule and per priority, and embeds both the raw events and the summary in the same envelope. A release-gate Rego policy can deny on falco.summary.priorities.error > 0, or on a specific rule firing more than zero times, without having to walk every event.

Validated invocation

# Pre-reqs: Falco installed in your cluster (falcosecurity/falco Helm chart),
# kubeconfig pointed at it, ed25519 key at key.pem.

FALCO_CLUSTER_NAME=<your-cluster> cilock run --step falco-capture \
--signer-file-key-path key.pem \
--outfile attestation.json \
--attestations falco,environment,git \
--enable-archivista=false \
-- sh -c 'kubectl logs daemonset/falco -n <falco-ns> --tail=500 \
| grep "\"rule\"" > falco-events.jsonl'

This is the recipe exercised in tool-falco-events — validated against the dropbox-clone-dev EKS cluster with a deterministic "Read sensitive file untrusted" rule firing (a test pod cat's /etc/shadow).

The sh -c wrapper is necessary because kubectl logs writes to stdout — the shell redirect routes that stream to falco-events.jsonl so the product/v0.3 Merkle tree can hash it. The command-run/v0.1 predicate records the full sh -c argv; this is not the cp antipattern (you're not making a copy of a file written outside CI/lock's view, you're routing a streaming-only tool's stdout into a file). The grep filters out Falco's startup banner so only event lines are captured.

FALCO_CLUSTER_NAME is the only env var the falco attestor reads — it stamps the captured envelope's falco.cluster field so policies can branch on which cluster the events came from. If unset, the field is empty but the attestation still signs.

Availability

The falco attestor is available today via rookery-builder --preset all (guide). It will land in the canonical default cilock binary once rookery#147 merges.

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 'kubectl logs … > falco-events.jsonl' argv + exit code
https://aflock.ai/attestations/product/v0.3Merkle root over falco-events.jsonl as a real product file
https://aflock.ai/attestations/falco/v0.1parsed events + per-rule aggregation + priority counts + cluster name

The falco/v0.1 predicate body has this shape:

{
"events": [ /* every Falco event verbatim: time, rule, priority, output, output_fields, K8s context */ ],
"summary": {
"total_events": 2,
"priorities": { "warning": 2 },
"rule_hits": [
{ "rule": "Read sensitive file untrusted", "count": 2, "highest_priority": "Warning" }
]
},
"cluster": "dropbox-clone-dev",
"source_file": { "path": "falco-events.jsonl", "sha256": "..." }
}

Why this shape

AntipatternCorrect shape (this example)
cilock run ... -- bash -c "kubectl logs ... > events.jsonl && cp events.jsonl falco-product.jsonl"cilock run ... -- sh -c 'kubectl logs ... > falco-events.jsonl'
command-run.cmd records the bash -c "... && cp ..." chaincommand-run.cmd records the single sh -c with the kubectl + redirect; no cp
The product is a copy of a file written outside CI/lock's viewThe product is falco-events.jsonl as the wrapped shell wrote it during the step

Three properties matter under the falco attestor: (1) command-run/v0.1.cmd records the real sh -c argv including the kubectl invocation — not a chained shell with a separate cp. (2) The ptrace spy traces the shell + kubectl child processes because CI/lock is sh's direct parent. (3) product/v0.3 captures falco-events.jsonl as written via the single redirect inside the wrapped step, then the falco attestor parses the same file to produce falco/v0.1.

The single sh -c wrapper is the same pattern as hadolint and govulncheck — tools (or in Falco's case, the kubectl logs consumer) that write structured output to stdout. The shell-redirect is the one-shot conversion from stdout to a file the product attestor can hash. The command-run predicate records the full argv; there's no copy of a file written outside CI/lock's view, so this is not the cp antipattern.

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/falco/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","kubectl logs daemonset/falco -n <ns> --tail=500 | grep \"\\\"rule\\\"\" > falco-events.jsonl"]

Pull the Falco summary from the signed envelope:

jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/falco/v0.1") | .attestation | {total: .summary.total_events, priorities: .summary.priorities, rules: .summary.rule_hits, cluster}'
# {
# "total": 2,
# "priorities": { "warning": 2 },
# "rules": [ { "rule": "Read sensitive file untrusted", "count": 2, "highest_priority": "Warning" } ],
# "cluster": "dropbox-clone-dev"
# }

Notes

  • Falco install. The validated example uses the falcosecurity/falco Helm chart with driver.kind=modern-bpf. JSON output is enabled via --set json_output=true --set falco.json_output=true --set falco.json_include_output_property=true. The full install + capture recipe is in tool-falco-events/reproduce.sh.
  • Why --tail=500. A long-running Falco daemonset's log buffer can be huge. --tail=500 keeps the capture deterministic for a single release-gate step; for forensic captures, run without --tail or stream via kubectl logs -f against a separate sidecar.
  • FALCO_CLUSTER_NAME env var. The attestor reads this single env var to stamp the envelope's falco.cluster field. Set it in CI so policies can branch on cluster (dropbox-clone-dev vs prod vs staging). If unset, the field is empty but the attestation still signs.
  • Streaming vs windowed capture. This page documents a windowed capture (kubectl logs --tail=500). For continuous streaming, run Falco's JSON output to a file via the json_output_file chart option, then cilock run -- cat /var/log/falco.jsonl > events.jsonl for the capture step. Either way the attestor parses the same line-delimited JSON.
  • Real-infra validation. The captured envelope in tool-falco-events/raw/attestation.json is from a real dropbox-clone-dev EKS cluster, not a synthetic fixture. The "Read sensitive file untrusted" rule firing is from a deliberate trigger pod (kubectl run --image=alpine -- cat /etc/shadow); the capture has no sensitive data because Falco redacts output_fields containing real file contents.

FAQ

Does CI/lock support Falco?

Yes. Wrap sh -c 'kubectl logs daemonset/falco -n <ns> --tail=N | grep "\"rule\"" > falco-events.jsonl' with cilock run --attestations falco,environment,git. The native falco attestor parses every event into a https://aflock.ai/attestations/falco/v0.1 predicate with per-rule and per-priority summaries, alongside the standard collection (environment, git, material, command-run, product).

Does this require the canonical cilock binary?

The falco attestor is on presets/all today — build via rookery-builder --preset all and the resulting binary has it. It will land in the canonical default cilock binary once rookery#147 merges. The attestor itself is stable; only the canonical-main registration is pending.

How do I gate a release on Falco priority counts?

Author a Rego policy on the falco/v0.1 predicate's summary.priorities block. Example: deny if priorities.error > 0 or if any of priorities.alert + priorities.critical + priorities.emergency is nonzero. A per-rule gate (e.g. deny on Read sensitive file untrusted firing more than zero times) uses summary.rule_hits[]. Examples in the policy/ directory once the example's policy bundle lands.

Why parse Falco JSON into a custom predicate instead of SARIF?

Falco events have a richer schema than SARIF's locations-and-rules model — every event has K8s context (pod, namespace, container, image), syscall metadata, and free-form output_fields. A SARIF flattening would lose those; the falco/v0.1 predicate preserves them verbatim and adds the per-rule + per-priority summaries policies care about most.

How does this differ from running Falco standalone?

Standalone Falco emits a JSON event stream with no provenance — nothing binds it to a release, a cluster, a capture window, or a policy. CI/lock adds five predicates around the same events: git/v0.1 (the commit), environment/v0.1 (the host running the capture step), material/v0.3 (the working tree), command-run/v0.1 (the exact sh -c argv + exit code), and product/v0.3 (the events file's content hash). The Falco events themselves are unchanged — same JSON, same downstream pipeline — but the surrounding evidence is now signed and policy-checkable.

See also