Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Home

Evidence-driven Least-Privilege Governance for Kubernetes

Learn what it needs.
Review what the evidence supports.

Runtime learning produces evidence, not deployment authority. landlock-genprof preserves attribution and uncertainty, derives a reviewable candidate-v2 Proposal, and keeps approval and application authority outside the browser.

v0.7 introduces the Observation Workbench: a workload-centric surface for durable Observations, bounded evidence, candidate-v2 derivation, provenance, and read-only governance facts. This is the technically certified development baseline, not a released v0.7.0 tag. Backend-specific evidence is scoped: NetworkPolicy denial is demonstrated only within the qualified Cilium scope; PodLock/Landlock kernel denial and capability enforcement remain unproven. Artifact application or backend readiness is not itself enforcement evidence. UNKNOWN remains first-class. Progress is the engineering record of what is demonstrated.

PLAT · nginx-demo / defaultobserved 60s
A loose, hand-guessed permission boundary compared against a tight boundary drawn around the paths, ports, and syscalls actually observed during a training run. HAND-GUESSED — everything, just in case OBSERVED & GENERATED
broad, hand-authored — never trimmed back direct evidence, confidence-annotated where applicable

LEARNED ≠ AUTHORIZED

the boundary

Runtime learning is a solved problem, and other systems do it better. security-profiles-operator records syscalls with a production eBPF recorder, generates a SeccompProfile, installs it on every node and enforces it.

What no learner provides is a decision. A recorded profile describes what a workload did; enforcing it is a statement about what it is allowed to do. Those are not the same claim.

Direct observation: landlock-genprof acquires filesystem, network, and applicable capability evidence. SPO-derived policy: in SPO mode, Security Profiles Operator owns syscall observation and produces the real SeccompProfile; landlock-genprof imports it as derived policy with provenance preserved.

Different origins converge in one reviewable SecurityProfileProposal. SPO-derived syscalls do not enter landlock-genprof TrainingHistory and receive no invented confidence.

CandidateDigest is deterministic content identity, not authority. Human approval binds authority to that exact digest; changed content cannot inherit stale approval. See the governed workflow and ADR-0008.

Observe, review, approve, apply

the governed loop

Four commands, in this order. Approval is bound to the exact candidate digest. Governed apply revalidates that authority and implemented backend readiness, refusing missing, stale, or mismatched approval. External systems enforce: applied ≠ enforced; enforced ≠ verified.

01 — trace

Watch it run

Collects direct evidence for the selected source mode and publishes a candidate. In SPO mode, syscall policy comes from the named SPO-derived SeccompProfile.

kubectl landlock-genprof trace \
  --pod nginx-demo -n default \
  --binary /usr/sbin/nginx \
  --duration 60s
02 — review

See what it saw

Prints the mixed-origin candidate, preserved provenance, applicable confidence, artifact readiness, and the CandidateDigest identifying its exact content.

kubectl landlock-genprof review \
  nginx-demo
03 — approve

Authorize that exact candidate

Approval binds to the digest review printed, not to the proposal's name — so a later run that changes the candidate does not inherit it.

kubectl landlock-genprof approve \
  nginx-demo \
  --expected-digest sha256:<from-review>
04 — apply-proposal

Apply only what was approved

Re-reads the proposal and re-checks the digest before applying anything; a missing, stale, or changed binding fails closed. The confirmation prompt is an extra operator safeguard on top, not the authority gate.

kubectl landlock-genprof \
  apply-proposal nginx-demo

See it run

real recording, not staged

A short interactive capture from a live cluster. For the complete current SPO-derived-policy, digest-bound approval, stale-authority rejection, and governed-apply scenario, follow the canonical demo.

RECORDING · nginx-demo / defaultclick to play on asciinema →
landlock-genprof live cluster recording

One candidate, four domains

what gets governed

Direct observations can carry cross-run confidence. SPO-derived syscalls are different: they enter as derived policy with provenance, never as landlock-genprof observations, and receive no fabricated TrainingHistory confidence.

Filesystem

Landlock policy

→ PodLock LandlockProfile seen on every run
Network

Egress rights

→ Kubernetes NetworkPolicy seen on every run
Syscalls

Seccomp profile

→ security-profiles-operator CR SPO-derived: provenance, no invented confidence
Capabilities

Linux capabilities

→ securityContext fragment review before prod

Complementary, not competing

positioning

landlock-genprof doesn't implement the kernel enforcement mechanisms itself — it feeds three existing, independent backends, one per domain, in the format each expects. None of them are installed by this project, and generating or applying an artifact is not the same as enforcing it: each card below says how far v0.2.0 actually goes. What each mechanism needs: enforcement prerequisites. Full positioning against PodLock/SPO/static compliance scanners: product definition.

Filesystem (Landlock)

PodLock

Kubewarden ecosystem — enforces at container startup generated · approval-bound · API applied kernel enforcement not demonstrated in v0.2.0
Syscalls (seccomp)

security-profiles-operator

materializes the profile onto every node generated · API plumbing tested tested Seccomp boundary demonstrated on a real node not a universal least-privilege claim
Network

Your CNI

any implementation of NetworkPolicy generated · approval-bound · API applied enforcement demonstrated on Cilium that result is Cilium-specific — not all CNIs

Choose your path

landlock-genprof has different prerequisites depending on what you want to do. Start with the path that matches your goal; you do not need to read the documentation in order.

Before you begin

The product observes runtime behavior and turns it into a reviewable policy candidate. Observation is not authorization, and generation is not enforcement. The normal lifecycle is:

observe → synthesize → review → approve → apply → verify

Read the governed workflow for the responsibility of each stage. Before testing enforcement, check the enforcement prerequisites: filesystem, network, and syscall policy depend on separate enforcement mechanisms.

Find the right kind of documentation

If you need…Go to…
A task-oriented walkthroughUsage guide
Exact flags and command syntaxCLI reference
Components and data flowArchitecture overview
Security assumptions and limitationsSecurity model
Proof of what works todayDemonstrated capabilities
Intended future capabilitiesProduct roadmap

Status vocabulary: “generated,” “applied,” “enforced,” and “verified” are distinct claims. The progress page is the canonical record of demonstrated behavior; the roadmap describes intent and sequencing.

Setting up a test environment

This is the canonical contributor path for trying the tool on the supported Core topology. It creates a kind cluster with Cilium and then installs the project layer. If you already have a Kubernetes cluster you want to install against instead, skip straight to INSTALL.md.

Working on the codebase itself (not just trying it)? See HOW_TO_START.md / COMMENT_COMMENCER.md instead — same cluster setup, plus git workflow, code walkthrough, and first tasks per role. This page is the lean version: cluster up, nothing else.

1. Supported host

The Core substrate is kind + Cilium. Linux amd64/arm64 uses the native container runtime. macOS amd64/arm64 uses a native-architecture Lima Linux guest; it does not provide kernel-security certification for the host. Landlock and eBPF are Linux kernel features — see README.md §6 for kernel requirements.

  • Linux amd64/arm64 and macOS amd64/arm64 are supported.
  • Windows is not a supported host for this front door; use a supported Linux environment or a Linux VM.

2. Clone and install prerequisites

git clone https://github.com/idriss-eliguene/landlock-genprof.git
cd landlock-genprof
go version   # should show go1.26 or later

If Go is missing, see HOW_TO_START.md — Step 3 for the install command (with amd64/arm64 auto-detection).

The contributor path requires Bash 4.0 or newer because the project-layer installation uses Bash 4 features. macOS’s system /bin/bash 3.2 is rejected before setup. On macOS, bootstrap.sh reuses a supported Bash when present or installs the Homebrew bash formula when Homebrew is already installed; if Homebrew is absent, it fails closed with an actionable diagnostic. The selected interpreter is passed explicitly to the child installation scripts and is rediscovered by later entrypoints without relying on PATH ordering.

3. Bootstrap the Core platform and project layer

./hack/bootstrap.sh
make env-doctor
make test-env
make test-env                 # safe convergence check

bootstrap.sh owns only the host/runtime, Lima (on macOS), kind, kubeconfig, and topology-specific readiness. It does not install project CRDs, project RBAC, Gadget, SPO, PodLock, proposals, or evidence fixtures. make test-env composes the existing project CRD, RBAC, Gadget, and plugin installation primitives. SPO and PodLock remain optional.

Every wait is bounded and emits elapsed-time progress; timeout diagnostics include nodes, pods, Cilium state, and events. hack/init-vm.sh remains only as a deprecated compatibility wrapper to bootstrap.sh --lane core.

kubectl plugin list | grep landlock-genprof   # sanity check
kubectl landlock-genprof doctor

Done — cluster and CLI are ready

Continue to INSTALL.md §3 (“Install the RBAC and CRDs”) for a separately managed cluster. From there, docs/usage.md assumes a cluster up and kubectl landlock-genprof on your PATH.

4. Cleanup

make test-env-clean is deliberately bounded. It removes only explicitly owned project-layer resources where ownership is recorded; it never destroys the kind cluster, Lima VM, shared host tools, or an unowned/shared cluster. Platform destruction is a separate operator decision and is not implicit in project cleanup.

Installing landlock-genprof

This is for installing landlock-genprof against a Kubernetes cluster you already have. Don’t have one yet? See docs/test-environment.md — spins up a disposable kind cluster and installs the CLI, in which case skip straight to §3 below (steps 1-2 here are already done for you).

For the unreleased v0.7 development baseline, use the current source checkout path below. The v0.6.1 commands are retained later as historical released-install instructions and must not be used for the v0.7 Observation Workbench.

v0.7 development baseline (current source)

From the repository checkout:

go build -o landlock-genprof ./cmd/landlock-genprof

kubectl apply -f deploy/rbac.yaml
kubectl apply -f deploy/crd-securityprofileproposal.yaml
kubectl apply -f deploy/rbac-proposal.yaml
kubectl apply -f deploy/crd-traininghistory.yaml
kubectl apply -f deploy/rbac-history.yaml
kubectl apply -f deploy/crd-observation.yaml
kubectl apply -f deploy/crd-observationcontributionreceipt.yaml
kubectl apply -f deploy/rbac-observation.yaml

The Observation Workbench uses the invoking kubeconfig identity and the configured namespace boundary. Grant that identity the read permissions for the Observation and Proposal resources it will inspect; the optional deploy/rbac-workbench.yaml role is only for ApplyAttempt/RollbackAttempt read visibility and is unbound by default. The browser has no governance mutation authority.

For the reproducible Core test environment, use:

./hack/bootstrap.sh
make env-doctor
make test-env

Inspektor Gadget is required for runtime tracing. PodLock and SPO remain optional backend integrations with their own qualification boundaries.

The v0.7 source baseline is technically complete but is not a released v0.7.0 tag. Do not substitute a future release URL until that tag exists.

Contributor bootstrap (current source checkout)

For the reproducible contributor/test environment, use the two-layer Core path from a checkout of the current source:

./hack/bootstrap.sh
make env-doctor
make test-env

The platform layer is native Linux kind + Cilium, or a native-architecture Lima Linux guest on macOS. The project layer installs the project CRDs, RBAC, Inspektor Gadget, and the local CLI plugin. SPO and PodLock remain optional; this path does not install k3s or claim kernel-security certification. Run make test-env-clean for bounded project cleanup; it does not destroy the cluster, Lima VM, or shared host tools. hack/init-vm.sh is a deprecated compatibility wrapper for hack/bootstrap.sh --lane core.

Historical v0.7.0 path:

Assumes Inspektor Gadget is already deployed on the cluster (kubectl gadget deploy) and kubectl is pointed at it — see §1 — Prerequisites below if you haven’t done that yet.

go install github.com/idriss-eliguene/landlock-genprof/cmd/landlock-genprof@v0.7.0

kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/crd-securityprofileproposal.yaml
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac-proposal.yaml
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac-patched-manifest.yaml

That’s option A in both §2 and §3 below — no clone, no Helm, works today. The rest of this page exists for the cases that need something different: a pre-built binary with no Go toolchain (§2 option B), Helm instead of raw manifests (§3 option B), or building from a local clone (§2/§3 option C). Skip straight to §4 — First run once either path above is done.

1. Prerequisites

  • Kernel version on every node — see README.md §6 for the exact table (Landlock FS ≥ 5.13, Landlock network ≥ 6.4, eBPF ≥ 5.8 recommended). Check with ./hack/check-kernel.sh if you have shell access to a node (needs a clone; or just read the version table).
  • Inspektor Gadget already deployed on the cluster (kubectl gadget deploy) — trace doesn’t work without it. This is the one hard requirement; everything else below is about getting the CLI itself in place.
  • kubectl, pointed at your cluster.
  • go 1.26+ — only for the go install method (step 2, option A). Not needed for a downloaded binary (option B) or the Helm chart (step 3, option A/B).
  • helm — only for the Helm chart (step 3, option A/B).

Enforcement is separate from all of this. Getting landlock-genprof installed and running gets you profile generation — actually enforcing what it generates (PodLock, a NetworkPolicy-capable CNI, SPO) is a different set of prerequisites entirely, not all of which this project can set up for you. See docs/enforcement-prerequisites.md before assuming the tool “isn’t working” if a generated profile doesn’t seem to do anything once applied.

2. Get the CLI

go install github.com/idriss-eliguene/landlock-genprof/cmd/landlock-genprof@v0.7.0

Puts landlock-genprof in $(go env GOPATH)/bin — confirmed working end to end (fetched straight from the module proxy, no local checkout of any kind). Swap @v0.7.0 for @latest to track the newest tag instead of pinning, or a commit hash for something unreleased.

Want it as a kubectl plugin instead of standalone? Same command, then rename:

mv "$(go env GOPATH)/bin/landlock-genprof" "$(go env GOPATH)/bin/kubectl-landlock_genprof"
kubectl plugin list   # confirms kubectl sees it

Underscore, not dash: kubectl reads a literal - in a plugin’s filename as a separator between two subcommands (kubectl-foo-barkubectl foo bar), not as a dash inside one word. _ in the filename becomes - in the invoked command, which is what gets you kubectl landlock-genprof as a single subcommand.

go install doesn’t inject build metadata by default, so landlock-genprof version prints generic dev info even though this is a real tagged release — cosmetic only, doesn’t affect behavior. Pass -ldflags yourself for a version string that matches the tag:

go install -ldflags "-X main.version=v0.7.0" github.com/idriss-eliguene/landlock-genprof/cmd/landlock-genprof@v0.6.1

Option B — download a pre-built binary

GitHub Releases, cross-compiled for linux/darwin/windows × amd64/arm64 (.goreleaser.yaml, .github/workflows/release.yml) — no Go toolchain needed at all:

# Pick your OS/arch from the release page:
# https://github.com/idriss-eliguene/landlock-genprof/releases
curl -LO https://github.com/idriss-eliguene/landlock-genprof/releases/download/<tag>/landlock-genprof_linux_amd64.tar.gz
tar -xzf landlock-genprof_linux_amd64.tar.gz
sudo install -o root -g root -m 0755 landlock-genprof /usr/local/bin/landlock-genprof

Confirmed working as of v0.7.0 — six real assets on the releases page (v0.7.0 itself predates this pipeline being wired up; if you’re pinning to that specific tag for some reason, use option A instead).

Same rename trick as option A above for the kubectl-plugin form.

Option C — build from source (clone required)

Only worth it if you’re modifying the code, want the kubectl-plugin make target, or need a build off an unreleased commit:

git clone git@github.com:idriss-eliguene/landlock-genprof.git
cd landlock-genprof
make install-plugin   # kubectl-landlock_genprof, into $(go env GOPATH)/bin, real version via -ldflags
# or, standalone:
go build -o landlock-genprof ./cmd/landlock-genprof

One kubectl-plugin quirk worth knowing regardless of how you installed it: global kubectl flags placed before the plugin name (kubectl -n foo landlock-genprof ...) are not forwarded to the plugin — kubectl only passes through arguments that come after the plugin name. Use landlock-genprof’s own -n/--namespace instead.

3. Install the RBAC and CRDs

The tracer needs its own ServiceAccount/RBAC, and every run publishes a SecurityProfileProposal object, so its CRD (plus more RBAC) is mandatory too.

Option A — raw manifests, no clone (kubectl apply -f <url>)

kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/crd-securityprofileproposal.yaml
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac-proposal.yaml
# Required whenever a run composes securityContext data (commonly true
# in practice when syscalls are observed)
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac-patched-manifest.yaml

# Only if you plan to use the matching flag:
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/crd-traininghistory.yaml   # --history
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac-history.yaml         # --history
kubectl apply -f https://raw.githubusercontent.com/idriss-eliguene/landlock-genprof/v0.7.0/deploy/rbac-restart.yaml        # --restart

Pinned to the v0.7.0 tag rather than master on purpose — reproducible, and immune to whatever’s mid-change on the default branch. Swap the tag for a newer one as releases come out.

Option B — Helm chart from GHCR (OCI), no clone

helm install landlock-genprof oci://ghcr.io/idriss-eliguene/charts/landlock-genprof --version 0.7.0

Confirmed published as of v0.7.0 — check github.com/idriss-eliguene?tab=packages if a later tag has come out since and you want that version instead (v0.7.0 itself predates this pipeline being wired up).

Option C — raw manifests from a local clone

kubectl apply -f deploy/rbac.yaml
kubectl apply -f deploy/crd-securityprofileproposal.yaml
kubectl apply -f deploy/rbac-proposal.yaml
kubectl apply -f deploy/rbac-patched-manifest.yaml
kubectl apply -f deploy/crd-traininghistory.yaml   # --history
kubectl apply -f deploy/rbac-history.yaml          # --history
kubectl apply -f deploy/rbac-restart.yaml          # --restart

Option D — Helm chart from a local clone

helm install landlock-genprof deploy/helm/landlock-genprof

Options C/D install exactly the same things as A/B — only difference is not needing network access to GitHub/GHCR at apply time, at the cost of needing a clone. See deploy/helm/landlock-genprof/README.md for the full restart.enabled/history.enabled toggle list and a CRD-upgrade caveat worth knowing before your first helm upgrade.

Legacy attempt visibility (optional)

The historical v0.7.0 installation path can optionally expose ApplyAttempt, RollbackAttempt, and the published custody epoch to the legacy proposal inspection view. This is not a standalone v0.7 Activity surface. From a checkout containing those resources, install:

kubectl apply -f deploy/crd-applyattempt.yaml
kubectl apply -f deploy/crd-rollbackattempt.yaml
kubectl apply -f deploy/rbac-workbench.yaml

The optional Workbench role grants only get/list on ApplyAttempt and RollbackAttempt and get on the exact ApplyAttempt CRD. It is unbound by default; an operator explicitly chooses whether and how to bind it. The Helm chart mirrors this role and leaves it disabled unless workbench.readerRole.create=true is selected. No browser or target mutation authority is granted.

Launch the current Workbench with kubectl landlock-genprof ui --namespace <namespace>, or enter a proposal directly with kubectl landlock-genprof ui <proposal> --namespace <namespace>. The browser is read-only; approval, application, custody activation, and rollback remain CLI operations.

4. First run

# Check host kernel and eBPF prerequisites first
kubectl landlock-genprof doctor

kubectl landlock-genprof trace \
  --pod <your-pod> -n <ns> --binary /path/to/main/binary \
  --duration 60s --out profile.yaml

Installed standalone instead of as a kubectl plugin? Drop the kubectl prefix: landlock-genprof trace .... Running from a source clone without installing anywhere? go run ./cmd/landlock-genprof trace ... works the same way.

--pod and --binary are the only required flags. See docs/usage.md for what each --*-out flag adds.

5. Next steps

landlock-genprof Demo

For the concise buyer-facing cut, start with buyer/README.md. This document describes the technical scenario and appendix that the buyer cut reuses.

The idea

LEARNED ≠ AUTHORIZED

Runtime learning is a solved problem, and other systems do it better than we do. security-profiles-operator records syscalls with a production eBPF recorder, merges across replicas, generates a SeccompProfile, installs it on every node, and enforces it.

What no learner provides is a decision. A recorded profile is a description of what a workload did. Enforcing it is a statement about what it is allowed to do — and only a human can turn the first into the second.

landlock-genprof is the authorization boundary between runtime learning and runtime enforcement. In this demo, SPO supplies the real derived SeccompProfile; landlock-genprof does not relabel SPO’s result as its own observation. It combines that derived artifact with filesystem and network evidence from its tracer, producing one reviewable candidate with one deterministic identity, binds a human’s approval to that exact identity, and refuses to enforce anything else.

Approve exactly what you reviewed.

What you will see

SPO recorded this workload and produced a valid SeccompProfile
    → the source profile is Disabled. Nothing enforces it.
    → import: SPO's policy enters as a CANDIDATE, not as authority
    → one candidate: filesystem + network + syscalls, one digest
    → review  → CandidateDigest A
    → approve exactly A
    → the workload changes, and SPO records it again
    → the proposal becomes candidate B
    → governed apply  → REFUSED: approved digest ≠ current digest
    → nothing was applied
    → what changed: filesystem diff + seccomp provenance
    → TrainingHistory: syscallAccesses = 0
    → review B → approve B → governed apply succeeds
    → SPO reconciles, identity verified, workload bound LAST

The learner did nothing wrong. SPO produced a legitimate, better-informed profile. It still did not acquire the authority to enforce it.

Learning is automatic. Authority is not.

Why the learner is SPO

Sourcing the drift from security-profiles-operator removes it. SPO is a legitimate upstream system with a better syscall instrument than this project’s own. Nobody attacks anything, nothing malfunctions, and the learner is right — its second profile is a better description of the workload than its first. The refusal is the product.

This is also why the demo is not an SPO demo. SPO does the observing, the generating, the reconciling and the enforcing. What it has no mechanism for is a decision, and that is the only thing this project adds.

What this proves

Each of these is exercised by a real command in scenario.sh, against a real cluster:

  • Filesystem and network behavior is observed by landlock-genprof and accumulated across runs in TrainingHistory; SPO independently records syscalls and supplies a derived SeccompProfile.
  • SPO-derived syscalls do not enter TrainingHistory and do not receive a fabricated landlock-genprof confidence level.
  • A SecurityProfileProposal is published to the cluster with four artifacts, and reduces to one sha256: CandidateDigest.
  • approve refuses any digest that does not match the current candidate.
  • The approval is persisted in the resource’s status as approvalState, approvedCandidateDigest and approvalMechanismVersion.
  • Re-tracing updates the proposal spec and preserves the previous approval status — which is exactly how a stale approval arises.
  • apply-proposal recomputes the digest over the current spec and fails closed when it does not match the approved one. The rejection happens before the first API application: the run below shows the target NetworkPolicy absent before the attempt and still absent after.
  • diff shows which rule changed, so a digest mismatch is legible as a privilege change rather than a hash comparison.
  • Re-reviewing and re-approving the new candidate makes the governed apply succeed, and the artifact is created through the Kubernetes API.

What this does NOT prove

Be precise about this. The demo deliberately stops at the Kubernetes API boundary.

  • No PodLock consumption, and no Landlock kernel enforcement. The LandlockProfile artifact is generated and can be applied, but no PodLock operator consumes it here. docs/PROGRESS.md records this as BLOCKED.
  • No SPO behavioral seccomp denial. SPO records, reconciles, and binds the governed profile, but this demo does not exercise a denied syscall.
  • No behavioral enforcement of the generated NetworkPolicy. It is applied through the API and confirmed present. Whether traffic is then blocked is the CNI’s job and is not demonstrated here. (The project has demonstrated NetworkPolicy blocking under Cilium separately, using a fixture policy in test/e2e/smoke-networkpolicy.sh — that is a different artifact and a different claim.)
  • No claim that SPO policy is raw observation. It crosses the boundary as derived policy and becomes authority only after digest-bound human approval.
  • No controller or operator reconciliation. Everything here is CLI-driven; there is no control loop.
  • No transactional apply. The rejection path applies nothing at all, which is what this demo shows. But a successful apply is sequential and continues past a failed artifact, so “nothing is ever partially applied” is not a property this product has and is not claimed.

docs/PROGRESS.md is the canonical, capability-by-capability record of what is demonstrated.

Architecture

  demo/scenario.sh          disposable orchestration
        │                   sequences commands, prints separators,
        │                   captures output, waits on readiness
        │  invokes
        ▼
  kubectl landlock-genprof  the real CLI — every security decision
        │                   (synthesis, digest, approval validation,
        │                   pre-apply revalidation) happens here
        ▼
  real Kubernetes cluster   real CRs, real API application
        │
        ▼
  real product state        TrainingHistory, SecurityProfileProposal,
                            approval status, applied artifacts

The orchestration never computes a digest, never classifies confidence, never decides whether an approval is valid and never predicts an apply outcome. Where it needs a digest, it passes back the string the product printed. The only thing it branches on is a real command’s exit status.

Requirements

  • A Kubernetes cluster with Inspektor Gadget installed and Ready (make e2e-install provides it), and the project’s CRDs applied.
  • kubectl, and the CLI installed as a kubectl plugin (make install-plugin). Set LANDLOCK_GENPROF_BIN to override with an explicit binary.
  • Linux for the observation stages: the tracer uses eBPF and is Linux-only by design (internal/tracer/trace_other.go returns a clear error elsewhere). See HOW_TO_START.md for the dev VM.
  • python3, used only to pretty-print status fields from kubectl -o json.
  • bash 4+.

Quick Start

./demo/setup.sh --with-cluster   # first time: real-node k3s + deps + SPO + recordings
./demo/setup.sh                  # subsequent: provision into an existing cluster
./demo/reset.sh                  # clear product state from a previous take
./demo/scenario.sh               # the canonical hero demo, start to finish
./demo/appendix.sh               # the technical appendix, same cluster

--with-cluster provisions a real-node k3s cluster, not kind, and pre-bakes two real SPO recordings (~127 s each). That is the slow part and it happens once, before the camera rolls.

Set DEMO_FAST=1 to collapse the presentation pauses — useful in CI, wrong for a live audience:

DEMO_FAST=1 ./demo/scenario.sh

Full Step-by-Step

scenario.sh runs seventeen stages:

StageWhat happens
1Baseline: the workload’s securityContext, no proposal, no history
2Three real training runs, each driving deterministic behavior
3TrainingHistory shows runsRecorded = 3
4explain renders per-rule confidence, rights, ABI, evidence
5review prints the WORKLOAD SECURITY REVIEW and CandidateDigest A
6approve --expected-digest <A>; status read back from the cluster
7The workload starts writing a new path
8A fourth training run; the proposal becomes candidate B
9The approval status is still the one bound to A
10Confirm the target NetworkPolicy does not exist yet
11apply-proposal against the stale approval → refused, exit 1
12The NetworkPolicy still does not exist
13diff shows which rule changed
14review again → CandidateDigest B
15approve --expected-digest <B>
16apply-proposal → applies the NetworkPolicy
17Final approval status and applied resources

The demo drives behavior through two scripts. demo/golden/run-actions.sh is the Golden fixture the authoritative E2E already owns and asserts on — it produces the 3-of-3 / 2-of-3 / 1-of-3 frequencies that make confidence tiers visible. demo/drift-action.sh is demo-only and adds the one new behavior after approval, so the E2E fixture never has to change to serve a presentation.

Expected Outputs

Real excerpts from a full run. Digests and timestamps differ per run; the shapes do not.

Approval bound to candidate A (stage 6):

landlock-genprof-e2e/nginx-demo: Approved
  Reason: reviewed with the platform team

  the approval, read back from the cluster:
  approvalState: Approved
  approvedCandidateDigest: sha256:306eac30863a2c51299d755504d5a69a9eb4cccf512a815ab9e626efef7c75cc
  approvalMechanismVersion: candidate-v1

The refusal (stage 11) — this is the product’s own wording, verbatim:

apply preflight failed: approved candidate digest mismatch: approved=sha256:306eac30863a2c51299d755504d5a69a9eb4cccf512a815ab9e626efef7c75cc computed=sha256:58614d8cf24d261197ca61e851828197ff2f064e275ebe696ca819d180a36643

  exit status: 1

Nothing applied, before and after the attempt (stages 10 and 12):

  No resources found in landlock-genprof-e2e namespace.

What actually changed (stage 13):

+ /srv/nginx/data: [write_file truncate]

  diff exit status: 1  (0 = identical, 1 = differences found)

Governed apply after re-approval (stage 16):

This will apply 1 artifact(s):
  - NetworkPolicy

Planned artifacts:
  - NetworkPolicy: networking.k8s.io/v1, Kind=NetworkPolicy landlock-genprof-e2e/nginx-demo

applied: NetworkPolicy

Done.

Recording

See recording.md for the full procedure.

./demo/record.sh signature   # full canonical scenario
./demo/record.sh hero        # short cut

Live Presentation

See live-checklist.md. The short version: pre-provision everything, keep a recording of the identical run one keystroke away, and never debug live — when the climax of your demo is a refusal, an audience cannot tell “the demo broke” from “the product refused.”

Troubleshooting

kubectl-landlock_genprof not found on PATH — run make install-plugin, or set LANDLOCK_GENPROF_BIN to a binary path.

kubeconfig context is '…', expected 'kind-landlock-genprof-e2e' — every script fails closed on this deliberately. Switch context or set DEMO_EXPECTED_CONTEXT.

Inspektor Gadget DaemonSet not found — run ./demo/setup.sh --with-cluster, or make e2e-install against the cluster.

Trace reports 0 item(s) for every domain — the tracer attached but saw nothing. Either the actions did not run (check demo/.state/actions.log), or the traced binary does not match the process doing the work. The demo traces /usr/bin/curl in the tools sidecar because curl performs every deterministic action; tracing the nginx container instead observes an idle server.

Trace fails on macOS/Windows with a platform error — expected. The tracer is Linux-only. Use the dev VM (HOW_TO_START.md).

runsRecorded is empty or not incrementing — the field is on .spec, not .status. If it is genuinely not incrementing, the --history flag or its extra RBAC is missing (docs/usage.md).

This gadget was built with ig vX and it's being run with vY — a warning, not an error: the cluster’s Gadget DaemonSet is a different version from the client. Traces still work. Reinstall the Gadget to match if the noise bothers you on camera.

review prints no Candidate digest: line — the proposal has no spec worth digesting, usually because the training run observed nothing. Fix the trace first; the scenario stops here on purpose rather than approving something empty.

approve fails with expected candidate digest mismatch — the proposal changed between review and approve. Re-run review and use the digest it prints. This is the mechanism working, not a bug.

The stale apply unexpectedly succeeds — the scenario stops and says so. It means the retrace produced a byte-identical candidate, so the approval is still valid. Ensure the drift action actually ran and wrote a genuinely new path (demo/.state/actions.log).

The apply is refused for a different reason — read the message. Common alternative: proposal not Approved if a reject ran, or authorization changed before apply if the proposal changed during an interactive confirmation. Both are the same fail-closed gate at a different point.

diff prints command reported a non-zero exit condition — that is the documented exit-code contract: 0 identical, 1 differences found, 3 error. The scenario routes it to demo/.state/diff-a-b.err so the rule lines read in order.

PodLock / SPO SeccompProfile show as skipped — intended. Those operators are not installed, so the scenario passes --skip=podlock,spo-seccompprofile. Never work around this by claiming enforcement that did not happen.

Status not visible immediately after approve — status propagation is asynchronous. Re-read the resource.

Stale resources from a previous take./demo/reset.sh, or ./demo/reset.sh --recreate-pod for a fully clean workload filesystem.

Pod apply fails with spec: Forbidden: pod updates may not change fields… — a Pod spec is immutable and an older fixture revision is running. setup.sh detects this and recreates the pod.

Digest lines wrap and become unreadable — use a terminal of at least 100 columns. record.sh refuses to record below that.

ARM64 vs AMD64 — both work; the observed syscall set can differ. Never read a specific event count aloud during a presentation.

Image pulls stall — pre-pull hashicorp/http-echo:0.2.3 and curlimages/curl:8.3.0 into the cluster before presenting.

Reset

./demo/reset.sh                  # proposal, approval, history, NetworkPolicy, local state
./demo/reset.sh --recreate-pod   # also recreate the workload pod

Reset touches only demo-owned resources in the demo namespace. It never deletes the cluster.

Reproduce from Scratch

git clone https://github.com/idriss-eliguene/landlock-genprof
cd landlock-genprof
make install-plugin
./demo/setup.sh --with-cluster
./demo/reset.sh
./demo/scenario.sh

Evidence

The properties this demo shows are backed by tests and CI, not by the demo itself:

PropertyEvidence
Digest is deterministic over selected spec fieldsinternal/proposal/digest.go, proposal tests
Approve rejects a non-matching digestinternal/proposal/store.go, cmd/landlock-genprof/approve_test.go, authoritative E2E
Apply fails closed on a digest mismatchinternal/proposal/validate.go, TestRunApplyProposal_RejectsAfterSpecMutation
Re-trace preserves approval statusTestSave_DoesNotClobberApprovalStatus
Pre-apply revalidation after planningTestRunApplyProposal_RejectsMutationAfterPlanningBeforeRevalidation and siblings
Approval cannot be forged through a spec writeinternal/proposal/store_envtest_test.go (TestUpdateCannotModifyStatus), run in CI via make envtest
Confidence tiers from cross-run frequencyinternal/policy/synthesize.go, internal/landlock/kernel.go
Governed apply path and artifact plancmd/landlock-genprof/apply_proposal.go, apply tests, authoritative Core E2E
Capability-by-capability claim boundary../docs/PROGRESS.md

The governed workflow

Runtime knowledge is useful, but it is not deployment authority. landlock-genprof brings direct evidence and derived policy into one candidate, gives that candidate deterministic content identity, and applies it only after a human authorizes that exact identity.

01 · Knowledge sources
Direct evidence

landlock-genprof acquires filesystem, network, and applicable capability evidence.

SPO-derived policy

Security Profiles Operator observes syscalls and produces the real derived SeccompProfile.

Different origins. One candidate.

02 · Candidate SecurityProfileProposal

Direct evidence and the imported SPO artifact converge with provenance preserved.

CandidateDigest

Deterministic content identity. Not authority.

03 · Authorization
Reviewed contentExact digestHuman approval

review exposes the candidate. approve binds authority to that digest only. Changed content cannot inherit an earlier approval.

04 · Apply apply-proposal

Re-reads and revalidates the proposal, re-checks approval, checks implemented backend readiness, and refuses missing, stale, or mismatched authority.

APPLIED ≠ ENFORCEDENFORCED ≠ VERIFIED

Explicit rollback

After apply, the durable ApplyAttempt records controlled Before state, attributable object identity/resourceVersion, observed state, and typed results for each mutation. An operator may explicitly invoke:

kubectl landlock-genprof rollback <apply-attempt> --namespace default

This creates a separate RollbackAttempt and performs guarded inverse mutations only for a current custody-epoch-qualified source. Strict UID/resourceVersion and controlled-state checks, dependency-aware ordering, readiness checks, and policy-reference guards apply before mutation. The operation restores recorded controlled Before state only and remains sequential and nontransactional. Partial, failed/no-effect, and OUTCOME_UNKNOWN results are durable; unknown descendants are not automatically redispatched, while known FAILED_NO_EFFECT records may be reconsidered by a later explicit continuation after fresh checks.

Rollback authority is Kubernetes RBAC plus explicit CLI confirmation, not browser action or a new proposal approval. Bare-Pod delete-then-create rollback and rollback-of-rollback are unsupported.

In SPO mode, the imported SeccompProfile is derived policy—not landlock-genprof observation. Its provenance is preserved, its syscalls do not enter landlock-genprof TrainingHistory, and landlock-genprof invents no confidence for them. The source object grants no authority; the governed candidate still requires normal digest-bound approval. See Import SPO-derived policy and ADR-0008.

Four commands, one authority chain

01

trace

Collect direct evidence for the selected source mode and publish a candidate. In SPO mode, filesystem and network evidence remain landlock-genprof-derived while syscall policy comes from the named SPO-derived SeccompProfile.

kubectl landlock-genprof trace \
  --pod nginx-demo -n default \
  --binary /usr/sbin/nginx \
  --duration 60s
02

review

Inspect the exact mixed-origin candidate, source provenance, applicable confidence, artifact readiness, and the candidate digest.

kubectl landlock-genprof review \
  nginx-demo
03

approve

Record explicit human authority for the digest printed by review. A later candidate must be reviewed and approved again.

kubectl landlock-genprof approve \
  nginx-demo \
  --expected-digest sha256:<from-review>
04

apply-proposal

Apply only a valid, current approval. Enforcement remains the responsibility of PodLock/Landlock, the CNI, and SPO with the kubelet/runtime.

kubectl landlock-genprof \
  apply-proposal nginx-demo

Before tracing, use kubectl landlock-genprof doctor for host prerequisites. After application, use backend-specific checks to distinguish API application, enforcement, and behavioral verification. The usage guide documents the complete lifecycle and the narrower semantics of explain, diff, and verify.

Continue

Observation and Evidence Semantics

v0.7 is an Observation Workbench within the broader product position of Evidence-driven Least-Privilege Governance for Kubernetes. The Workbench exposes durable workload observations, bounded evidence, uncertainty, and candidate-v2 proposals. It does not turn observation into approval, application, enforcement, or verification.

Identity

ObservationID is opaque and immutable. The Observation specification is immutable, its binding is written once, target changes are append-only, and the execution/result records become frozen at terminal completion.

ClusterIdentity is the UID of the kube-system Namespace. A ClusterLocator is display/audit information only. A WorkloadIdentity is:

ClusterIdentity + namespace + GroupKind + name + workload UID

ContainerSlot adds the container name. ContainerImageRevision adds the image digest. This makes the Observation workload-UID-bound.

Candidate-v2 intentionally has weaker persisted subject identity: Scope, Target, Container, and ImageIdentity, without workload UID. The UI must not describe a candidate-v2 Proposal as workload-UID-bound.

State axes

Execution, attribution, and evidence are independent:

AxisValues
ExecutionREQUESTED, STARTING, RUNNING, COMPLETING, COMPLETED, FAILED
AttributionNOT_STARTED, IN_PROGRESS, COMPLETED, FAILED
EvidenceEMPTY, AVAILABLE, UNKNOWN

UNKNOWN is not an execution state, and Proposal eligibility is derived rather than persisted as a lifecycle state.

Evidence

Attribution is bounded to the selected container. The sources are filesystem, exec, network connect, network bind, and capabilities. AVAILABLE means attributable evidence exists within the qualified scope. EMPTY means no attributable evidence was observed within that scope. UNKNOWN means the source-level qualification is insufficient for a complete EMPTY/AVAILABLE conclusion.

UNKNOWN preserves positive facts. EMPTY does not mean the workload never did something, and AVAILABLE does not mean complete workload behavior was observed. Exec is provenance-only for candidate-v2 TrainingHistory policy derivation.

Populations and proposals

The legacy BINARY population includes BinaryPath. The v0.7 CONTAINER population does not. There is no scope fallback.

The contribution key is ObservationID plus PopulationFingerprint, with a PREPARED-to-COMMITTED receipt. The supported guarantee is an idempotent contribution effect, not exactly-once execution.

Candidate-v2 uses Scope CONTAINER, Target, Container, and ImageIdentity. Its artifact is CONTAINER_CAPABILITIES, with Drop ALL and canonical observed CAP_* facts in Add. CandidateDigestV2 identifies candidate content; ReviewContextDigestV2 identifies review/provenance/qualification context. Provenance is not silently added to the candidate digest.

Governance boundary

Generated is not approved; approved is not applied; applied is not enforced; enforced is not behavior verified. Approval is Proposal-object-scoped and content mutation makes approval stale. LastApprovalSnapshot is last recorded approval custody, not complete history.

The browser is read-only. Approve, Reject, Revoke, Apply, and Rollback remain CLI-only where supported.

These semantics do not claim complete workload behavior, complete least privilege, global enforcement verification, fleet governance, or a full Security Operating Center.

Observation Workbench

The v0.7 Workbench is a trusted-local, loopback-only, server-rendered Observation Workbench. Its navigation is:

Overview · Observations · Proposals

There are no standalone Governance, Activity, or Assurance pages in v0.7. Read-only governance facts remain visible in Proposal context.

Install and launch

Use the installation guide for the current source/pre-release baseline. The Workbench uses the invoking kubeconfig identity and keeps its namespace pinned to the requested scope:

kubectl landlock-genprof ui --namespace <namespace>

The default URL is http://127.0.0.1:8080. Use --port <port> to select another local port. The listener is local-only and has no remote-management, session, or authentication contract.

Workload and Observation flow

The Workbench begins with workload/container selection and displays the selected identity: namespace, GroupKind, workload name and UID, container, image identity, and ClusterIdentity where available. Display locators are not authority-bearing identity.

The user can:

  1. Start an Observation through the certified API.
  2. Read authoritative Status; accepted does not fabricate RUNNING.
  3. Stop an Observation through the certified API where meaningful.
  4. Rediscover durable Observations after closing and reopening the browser.
  5. Inspect Execution, Attribution, Evidence, and Proposal eligibility as separate axes.

Evidence sources are shown independently: filesystem, exec, network connect, network bind, and capabilities. AVAILABLE, EMPTY, and UNKNOWN remain distinct. UNKNOWN preserves positive facts; EMPTY does not mean that the workload never performed a behavior; AVAILABLE does not mean complete workload behavior.

Proposal flow

Generate Proposal calls the certified API and reloads the persisted Proposal through the read model. Candidate-v2 displays:

  • Subject Scope CONTAINER, Target, Container, and ImageIdentity;
  • artifact CONTAINER_CAPABILITIES;
  • Drop ALL;
  • canonical observed CAP_* facts in Add;
  • provenance ObservationIDs, qualification, and derivation status;
  • CandidateDigest and ReviewContextDigest as separate bindings.

Candidate-v2 does not contain workload UID. The Observation is workload-UID bound; the Proposal subject is intentionally weaker and must not be presented as UID-bound.

The browser can display approval state, approved digests, authority state, and LastApprovalSnapshot when the read model provides them. The last snapshot is last recorded approval custody, not a complete approval history.

Authority boundary

The browser cannot Approve, Reject, Revoke, Apply, or Rollback. It cannot activate custody or execute CLI commands. Where useful, the page may display copyable CLI-only guidance. Approval is not application; application is not enforcement; enforcement is not behavioral verification.

The Workbench does not claim complete workload behavior, complete least privilege, global enforcement verification, fleet governance, or a full Security Operating Center.

Trust and bounded reads

Reads are namespace-scoped and bounded through the Workbench read capability. The page uses durable Kubernetes state rather than browser-local authority. Host/origin and Fetch Metadata protections remain active on mutation routes, and the trusted-local listener must not be exposed through an ingress or used as a shared multi-user service.

Usage — the governed lifecycle

The normal product path is not a collection of exported files. It is a governed transition from knowledge to exact human authority:

diagnose → select sources → acquire → propose → review → approve → apply → verify

This page assumes the CLI, RBAC, and CRDs are installed. See installation or test environment first.

1. Diagnose

kubectl landlock-genprof doctor

doctor checks the host kernel’s Landlock support and local eBPF bpffs prerequisite. It does not validate cluster RBAC, CRDs, CNI behavior, or external operators; those remain installation and enforcement prerequisite checks.

2. Select acquisition and policy sources

Seccomp source selection is explicit and is never inferred from cluster state.

ModeSyscall observerSeccomp artifactHistory/confidence
--seccomp-source=spoSPOGoverned snapshot of SPO’s derived SeccompProfileNo syscall TrainingHistory; confidence not applicable
--seccomp-source=internallandlock-genprof tracerInternally synthesized advisory profileSyscalls may participate in landlock-genprof history/confidence

SPO mode is the primary integration when real SPO-derived policy is available. landlock-genprof still observes filesystem and network behavior in this mode. The SPO artifact enters as derived policy with provenance, participates in the candidate digest, and receives no authority until the exact digest is approved. See SPO-derived policy import.

3. Acquire and publish a candidate

SPO-derived seccomp path

After SPO has completed the named ProfileRecording and produced the named cluster-scoped profile:

kubectl landlock-genprof trace \
  --pod nginx-demo --namespace default \
  --binary /usr/sbin/nginx --duration 60s \
  --seccomp-source=spo \
  --spo-recording nginx-demo \
  --spo-profile nginx-demo-nginx

The source profile is validated for API shape, completion, lineage, inertness, and supported semantics. Import creates a governed snapshot; it does not mutate or retain a live reference to the source.

Internal/advisory seccomp path

kubectl landlock-genprof trace \
  --pod nginx-demo --namespace default \
  --binary /usr/sbin/nginx --duration 60s \
  --seccomp-source=internal --history

internal is the current default and remains supported. Its syscall capture has a wider node-level observation caveat documented in the threat model. Use representative workload traffic; a short or incomplete run can omit legitimate behavior.

Both paths publish a SecurityProfileProposal. Proposal publication is mandatory, not an opt-in export. The proposal is the review boundary; its CandidateDigest gives exact content identity but does not itself grant authority.

Use --restart when startup-only behavior must be captured. It is disruptive and requires separate RBAC; see target restart.

4. Review

kubectl landlock-genprof review nginx-demo --namespace default

Review the candidate contents, artifact availability, source provenance, confidence where applicable, and the printed candidate digest. For SPO-derived syscalls, the source is SPO, the epistemic class is derived policy, coverage is unknown unless SPO supplied it, and confidence is not applicable.

5. Explain or compare when necessary

explain and diff currently operate on raw Landlock candidate JSON files, not on the complete SecurityProfileProposal:

kubectl landlock-genprof explain \
  --candidate-file nginx-demo-candidate.json

kubectl landlock-genprof diff \
  nginx-demo-candidate-old.json nginx-demo-candidate-new.json

Use them to inspect filesystem rights, ABI requirements, evidence counts, or rule changes. They do not replace review of mixed-origin proposal artifacts or approval of the proposal digest.

6. Approve the exact digest

kubectl landlock-genprof approve nginx-demo \
  --namespace default \
  --expected-digest sha256:<digest-from-review> \
  --reason "reviewed with the platform security team"

Approval binds human authority to that exact candidate. If any digested content or provenance changes, the candidate digest changes and the previous approval becomes stale. Re-review and explicitly approve the new digest; authority never transfers by proposal name.

7. Governed apply

kubectl landlock-genprof apply-proposal nginx-demo --namespace default

The command fails closed before application when approval is absent, malformed, revoked, stale, or mismatched. It applies available enforcement artifacts in governed order. The patched workload manifest is deliberately excluded unless --restart is passed:

kubectl landlock-genprof apply-proposal nginx-demo \
  --namespace default --restart

With workload binding enabled, supported backend readiness and identity are checked before the workload is bound, and approval is revalidated immediately before binding. Application is sequential rather than transactional: a failure stops the remaining sequence, but previously applied resources are not rolled back.

8. ApplyAttempt custody and explicit rollback

Every governed apply creates a durable namespaced ApplyAttempt before the first target mutation. Each mutation records its canonical target, live UID and resourceVersion, controlled Before state, intended state, observed state, and typed result where the outcome is known. The attempt remains IN_PROGRESS while active and can finish as APPLIED, PARTIALLY_APPLIED, FAILED, or OUTCOME_UNKNOWN. A current custody epoch qualifies newly created attempts for rollback; missing or stale qualification is refused.

ApplyAttempt is custody, not approval. Approval remains bound to the exact proposal candidate digest, and application remains distinct from enforcement and behavioral verification. Apply is sequential and nontransactional; prior successful mutations are not automatically undone.

An operator may explicitly request rollback of an eligible ApplyAttempt:

kubectl landlock-genprof rollback <apply-attempt> --namespace default

Rollback creates a separate durable RollbackAttempt. Before each inverse it requires the source-attributable UID and resourceVersion and recorded controlled state to match the live object. It restores only recorded controlled Before state. Processing is dependency-aware: references are not restored to unready policies and a policy is not deleted while a relevant workload still references it.

Rollback uses Kubernetes RBAC plus explicit CLI confirmation, not a new proposal approval. It is sequential and nontransactional. Partial results remain durable; a definite FAILED_NO_EFFECT inverse may be reconsidered by a later explicit continuation after fresh guards, while an OUTCOME_UNKNOWN descendant is not automatically redispatched. Bare-Pod delete-then-create records and rollback-of-rollback are unsupported. Rollback does not claim atomicity, automatic recovery, compensation, exactly-once execution, generic Kubernetes, kernel, CNI, or external-backend restoration.

9. Verify

The current verify command checks a Landlock candidate against a target kernel’s ABI. It does not prove backend reconciliation or behavioral denial:

kubectl landlock-genprof verify \
  --candidate-file nginx-demo-candidate.json \
  --kernel 6.8

Backend-specific verification remains separate: confirm CNI realization and fresh network behavior, SPO reconciliation and workload binding, or PodLock/Landlock behavior as appropriate. Current demonstrated limits are recorded in PROGRESS.md: NetworkPolicy denial is Cilium-specific; SPO syscall denial and PodLock/Landlock kernel denial remain unproven.

Advanced artifact outputs

Standalone outputs are useful for inspection, compatibility, and offline workflows. They are not equivalent to proposal review, digest-bound approval, or governed apply.

FlagOutputDetail
--candidate-outRaw Landlock candidate JSON for explain, diff, and verifyOptional; omit the value to use the default filename
--events-outRaw captured events for offline synthesizeOptional; omit the value to use the default filename
--network-outKubernetes NetworkPolicyNetworkPolicy output
--historyPersist cross-run evidence in TrainingHistoryMulti-run history
--seccomp-outPlain internally synthesized seccomp JSONInternal seccomp output
--capabilities-outCapabilities add/drop fragmentCapabilities output
--security-context-outComposed securityContext fragmentsecurityContext output
--report-outCombined Markdown review reportReport output
--patched-manifest-outWorkload manifest with generated security contextPatched manifest
--seccomp-profile-outSPO SeccompProfile resourceSPO resource output

Applying one of these files directly bypasses the proposal authority check. It must not be described as equivalent to approve plus apply-proposal.

Operational prerequisites

  • NetworkPolicy requires a CNI that implements it.
  • LandlockProfile requires PodLock and a compatible Landlock environment.
  • SeccompProfile requires SPO; reconciliation and runtime materialization are external to landlock-genprof.
  • Workload securityContext fields are immutable on a running bare Pod, which is why governed binding is an explicit, disruptive operation.

See enforcement prerequisites for installation and demonstrated-environment detail, and the published CLI reference for exact flags and exit behavior.

Step 5 — Optional NetworkPolicy generation

PodLock’s own CRD has no field for network rights, so connect/bind observations get their own output format instead: pass --network-out to also generate a Kubernetes NetworkPolicy from the same training run (skipped if no network activity was observed). --out/--network-out both default to a filename derived from the traced pod (<pod>-profile.yaml, <pod>-networkpolicy.yaml) when passed with no value — pass an explicit filename (--network-out my-policy.yaml) to override:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: nginx-demo
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: nginx        # copied from the traced pod's own labels
  policyTypes:
    - Egress
  egress:
    - ports:
        - protocol: TCP
          port: 443      # confidence: high

Only the observed port is encoded — no from/to peer restriction, since the tracer knows a port was contacted, not a peer pod/service identity.

Step 6 — Optional target restart (--restart)

Resources a process opens once at startup (a pid file, a log fd) and keeps writing to are invisible to a trace attached to an already-running container — trace_open only observes openat(), not later write()s on an already-open fd. Pass --restart to have the CLI restart the target right before observing it, attaching the tracer first in every case so the restart’s startup activity is actually captured — delete +recreate for a bare pod, or the same rollout-restart mechanism kubectl rollout restart uses for a Deployment/StatefulSet/DaemonSet-owned one.

The tracer is pre-targeted differently depending on whether the owner keeps a stable pod name: a bare pod or StatefulSet keeps its name across the restart, so the tracer is pre-attached by that name directly. A Deployment/DaemonSet’s replacement gets an unpredictable new name, so the tracer is instead pre-attached by the workload’s own label selector — which also means the generated profile is identified by the workload’s name (e.g. nginx-ds), not one ephemeral pod, and the PodLock guidance patches the pod template (kubectl patch deployment/ daemonset) instead of labeling a single pod that a future rollout would replace anyway.

Opt-in: it’s disruptive to the running workload, and needs additional RBAC beyond the base manifest — apply ../../deploy/rbac-restart.yaml first.

Step 7 — Optional multi-run history (--history)

Confidence is bounded to how many separate training runs belonging to one explicit observation population observed an access (“seen on every run” vs “seen once out of 5 runs”), but a single trace run has no way to know that — it can only measure how many times something was seen within that one run. Pass --history to persist a TrainingHistory custom resource (internal/history, no controller — the CLI reads/writes it directly) that accumulates populations keyed by the resolved target, container, immutable image identity, and binary path, so Confidence can be computed from a bounded within-population ratio. An unknown image identity is never merged into a qualified population. One recorded run yields LOW under the current tier model and provides no cross-run recurrence evidence; HIGH requires at least two runs. Existing records without populations remain legacy/unknown and are preserved but do not emit compatibility-qualified confidence. Requires the CRD and additional RBAC, applied once: ../../deploy/crd-traininghistory.yaml, ../../deploy/rbac-history.yaml. Query the result directly: kubectl get traininghistory <container>-<binary-basename> -o yaml. profile.yaml/networkpolicy.yaml/capabilities.yaml themselves show it too — every path/port/capability gets a trailing # confidence: ... comment (see Step 4), and with --history that comment reflects the real cross-run ratio instead of the single-run estimate used without it. seccomp.json (Step 8) can’t carry a comment — its confidence is printed to stdout instead.

Internal/advisory seccomp JSON (--seccomp-out)

This output belongs to --seccomp-source=internal. In SPO mode, SPO owns syscall observation and derives the SeccompProfile; --seccomp-out is rejected rather than presenting SPO-derived policy as landlock-genprof observation.

Pass --seccomp-out to generate a plain advisory profile from the same training run (skipped if no syscalls were observed), via Inspektor Gadget’s own advise_seccomp gadget (see Step 2’s gadget table):

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["accept4", "capget", "capset", "chdir", "epoll_wait", "futex", "openat", "read", "write"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

capget, capset, chdir, and futex are always folded in alongside whatever was actually traced — none of the four is something the traced binary itself calls, but the container runtime (runc) needs all of them during container init, before it even execs into the binary. Confirmed live (2026-07-30) one at a time, each the next crash after fixing the last, all inside runc’s own finalizeNamespace (libcontainer/init_linux.go), called in this exact order:

  1. capget — a kernel-capability-version probe. Without it: OCI runtime create failed: ... unable to get capability version from the kernel: operation not permitted.
  2. futex — runc’s own init is itself written in Go, and the Go runtime’s scheduler/GC depend on futex(2) for their whole lifetime, not just one step. Without it, the container gets created but crashes immediately (cannot start a stopped process), kubectl logs --previous showing a raw Go runtime panic (“The futex facility returned an unexpected error code”) inside libcontainer.setupUser/finalizeNamespace.
  3. chdir — sets the container’s configured working directory. Without it: OCI runtime create failed: ... chdir to cwd ("/") set in config.json failed: operation not permitted.
  4. capset — applies the securityContext.capabilities every patched manifest this project generates sets explicitly (internal/exporter/capabilities). Predicted rather than independently confirmed by its own distinct crash (it’s the very next call finalizeNamespace makes after chdir) — if a live test still crash-loops with a different error after this, that prediction was wrong.

A trace of the traced binary’s own behavior can never observe any of these, since all four happen in a separate process phase before exec.

Deliberately plain JSON, not YAML with a # confidence: ... comment like the other two outputs: this file is loaded directly by the kubelet/ container runtime (referenced via a pod’s securityContext.seccompProfile.localhostProfile, never kubectl applyd), so it has to stay valid, comment-free JSON. Instead, the CLI prints the syscalls not yet confirmed across multiple --history runs to stdout after writing the file — on a single run without --history, that means every syscall, since advise_seccomp reports one deduplicated set per run rather than a per-occurrence count, so Confidence can only ever be low until --history accumulates more runs.

This standalone file has no proposal authority on its own. It is worth taking seriously before enforcing: a missing syscall doesn’t just narrow access like an overly-strict NetworkPolicy would — it breaks the container outright. Prefer --history over a single internal run, then use the governed proposal workflow rather than treating confidence as authorization.

Step 9 — Optional Linux capabilities fragment (--capabilities-out)

Pass --capabilities-out to also generate a Linux capabilities fragment from observed capability checks (skipped if none were observed), via Inspektor Gadget’s trace_capabilities gadget (see Step 2’s gadget table):

add:
  - NET_BIND_SERVICE   # confidence: high
drop:
  - ALL

Unlike the other three outputs, this isn’t a complete, standalone artifact: Linux capabilities only ever live inside a container’s own securityContext.capabilities field, there’s no equivalent of a NetworkPolicy or seccomp profile to generate on their own. This file is a bare fragment for you to paste directly under that key — drop: [ALL] always, add listing every capability observed (Kubernetes’ own short-name convention, CAP_ prefix stripped). Since this is meant for manual pasting, not something the kubelet loads directly, it keeps the same # confidence: ... comment style as profile.yaml/ networkpolicy.yaml.

Combine with --restart on an already-running container (see e2e-demo.md Finding 5): privilege-related capability checks (dropping root via setuid/setgid, binding a privileged port, chowning files during init) cluster heavily at container startup. Tracing a container that’s already been running for a while will often come back with nothing observed at all — not wrong, just nothing left to see — the same startup blind spot --restart already exists to close for filesystem access (Finding 2), applying here too.

Composed securityContext output (--security-context-out)

Pass --security-context-out to also generate a composed securityContext fragment combining capabilities data with a reference to the governed seccomp profile — generated whenever seccomp policy is available from internal observation or SPO-derived import, independent of whether standalone seccomp outputs were also requested this run:

capabilities:
  add:
    - NET_BIND_SERVICE   # confidence: high
  drop:
    - ALL
seccompProfile:
  type: Localhost
  localhostProfile: operator/lg-v1-nginx-demo-<hash>.json

This is not a merge of the seccomp and capabilities exporters — seccomp.json/capabilities.yaml are still generated exactly as before, independently. A seccomp profile has to ship as its own file for the kubelet to load (localhostProfile only ever takes a path reference, never inline content), so merging the files themselves wouldn’t actually reduce anything — it’d just add indirection. This flag adds a third, composed view on top, for the common case of wanting both in one place to paste under a container’s securityContext: key. localhostProfile always follows security-profiles-operator (SPO)’s operator/<name>.json convention, where <name> is this project’s deterministic governed name. There is no namespace segment: SeccompProfile is cluster-scoped from SPO v0.9.0 on, and the namespaced form belongs to an API this project no longer targets. See the SeccompProfile resource page for the object at that path, and ADR-0008 for how the name is derived.

Deliberately does not infer privileged, allowPrivilegeEscalation, runAsNonRoot, readOnlyRootFilesystem, or runAsUser — nothing in this project observes any of them today, and guessing “safe defaults” regardless of what was actually seen would contradict the project’s own positioning: observe, don’t guess.

Step 11 — Optional unified review report (--report-out)

Pass --report-out to also generate one Markdown report combining all four observed domains — filesystem, network, syscalls, capabilities — for a single review pass, instead of up to five separate files:

# Security Profile Review — nginx-demo

- **Generated:** 2026-07-24T10:00:00Z
- **Namespace/Container:** default/nginx
- **Binary:** /usr/sbin/nginx
- **Training duration:** 1m0s
- **--history used:** no — Confidence below is internal/policy's single-run proxy, not a real cross-run ratio

## Filesystem
| Path | Permissions | Confidence |
|---|---|---|
| `/etc/nginx` | read | high |

## Capabilities
No capability checks observed. Capability checks cluster heavily at
container startup — if this container was already running before this
trace started, there may be nothing left to observe — see
`e2e-demo.md` Finding 5 and re-run with `--restart`.

## Review checklist
- [ ] Re-run with `--history` a few times before trusting any `low`/`medium` entry above.
- [ ] Re-run with `--restart` — capabilities and/or syscalls came back empty...

Unlike every other --*-out flag, this one is never skipped when passed, even if a domain observed nothing at all — an empty domain is itself useful review content (usually the startup blind spot from Step 6/Finding 5, worth surfacing directly rather than leaving the reader to rediscover it). It also works standalone, independent of the other --*-out flags: internal/policy.Synthesize already populates all four IR domains every run regardless of which flags were passed (all six gadgets always run), so the report shows the real data directly — and additionally links to any of the other files that were also generated this same run.

Proposal publishing (mandatory)

Every trace run publishes its generated multi-domain profile as a SecurityProfileProposal custom resource — stored as a cluster object instead of only local files, reviewable via kubectl/GitOps. This isn’t an opt-in flag: it’s the primary reviewable artifact this tool produces, so a run fails outright if it can’t publish (missing CRD or RBAC below) rather than silently degrading to local files only. See ../../examples/nginx-generated-proposal.yaml for a complete example.

kubectl get securityprofileproposal nginx-demo -o yaml

# Product-facing review summary (kubectl-plugin form — swap for
# `go run ./cmd/landlock-genprof review` from a source checkout, see
# ../../INSTALL.md)
kubectl landlock-genprof review nginx-demo

Each field is the exact rendered content of the corresponding local file — spec.podLock is the full, real profile.yaml (apiVersion/kind/metadata/spec included), spec.networkPolicy the full networkpolicy.yaml, spec.patchedManifest the full <identity>-patched.yaml — the live owner’s (or bare pod’s) complete manifest with the generated securityContext already merged in, not the bare fragment --security-context-out produces, spec.spoSeccompProfile the full <pod>-seccompprofile.yaml — a security-profiles-operator SeccompProfile custom resource, the sole seccomp-related field (its own spec.syscalls already carries the same data a raw spec.seccomp field would, so there’s no separate copy to keep in sync).

These fields are proposal content, not independently authorized artifacts. Do not extract them and run kubectl apply -f - for a governed rollout. The authoritative workflow is:

kubectl landlock-genprof review nginx-demo --namespace default
# Use the exact Candidate digest printed by review:
kubectl landlock-genprof approve nginx-demo --namespace default \
  --expected-digest sha256:<candidate-digest-from-review>
kubectl landlock-genprof apply-proposal nginx-demo --namespace default

make export-proposal PROPOSAL=<name> remains available for NON-AUTHORITATIVE INSPECTION/DEBUG ONLY. Its output is a mutable snapshot of proposal.spec; it does not retain approval authority and does not substitute for approve plus apply-proposal.

spec.patchedManifest’s securityContext.seccompProfile.localhostProfile always references the governed profile’s own operator/<governed-name>.json path whenever spec.spoSeccompProfile is non-empty — never the name of any externally-generated profile, so the approved artifact and the reference to it are bound by one digest (docs/adr/0008) — see the SeccompProfile resource page for why a plain filename isn’t enough and what applying spec.spoSeccompProfile actually does.

TrainingHistory is the direct-evidence stage and SecurityProfileProposal is the proposal and approval-status stage — both are plain CRUD, with no controller. Continuous reconciliation remains future roadmap work. The object’s name is the target pod (overwritten on every re-run, not accumulated — a proposal is the latest recommendation, same as the local files). Requires the CRD and additional RBAC, applied once: ../../deploy/crd-securityprofileproposal.yaml, ../../deploy/rbac-proposal.yaml.

Approval status (status.approvalState)

Every proposal carries a lifecycle separate from the generated content above — Draft (set once, when trace first publishes it) → Reviewed (set automatically the first time kubectl landlock-genprof review runs against it) → Approved/Rejected (only ever set by an explicit human decision, never inferred):

kubectl landlock-genprof approve nginx-demo \
  --expected-digest sha256:<candidate-digest-from-review> \
  --reason "reviewed with the platform team"
kubectl landlock-genprof reject nginx-demo --reason "syscalls list looks too broad"

Approval is authoritative for governed application: approve persists approvalState=Approved, the reviewed approvedCandidateDigest, and approvalMechanismVersion=candidate-v1. apply-proposal validates that binding and fails closed when approval is missing, malformed, stale, or replaced. Its confirmation prompt is an additional operator confirmation, not a substitute for digest-bound approval.

Stored via the CRD’s status subresource specifically so a re-run of trace against the same pod (which overwrites .spec in full, see above) can never silently wipe an approval decision — .status is a different write path entirely. This means approve/reject/review’s MarkReviewed all need securityprofileproposals/status write access in addition to the base resource, on top of whatever RBAC the invoking user’s own kubectl identity already has — same “runs under your own RBAC, not the tracer’s ServiceAccount” pattern apply-proposal uses (see patched-manifest.md). deploy/rbac-proposal.yaml only grants this to the tracer’s own ServiceAccount (needed to stamp the initial Draft state on Create) — grant your own identity the matching permission separately if review/approve/reject report a permissions error setting status.

Step 13 — Optional ready-to-apply patched manifest (--patched-manifest-out)

--security-context-out’s fragment (Step 10) still needs manual pasting into a real spec. Pass --patched-manifest-out instead to get a complete, ready-to-apply manifest with the generated securityContext already merged in:

kubectl apply -f nginx-ds-patched.yaml

Important nuance: most container-spec fields, including securityContext, are immutable on an already-running Pod — you can’t kubectl apply a modified one directly onto a live Pod. So for a pod owned by a Deployment/StatefulSet/DaemonSet, this fetches and patches the owner’s manifest, not the pod’s own — applying it triggers a rollout, the real supported way to change this (same reasoning --restart already applies for which identity to target). Only for a bare pod is the pod’s own manifest the right target, and even then, applying it means delete+recreate.

Merges, never replaces: only capabilities/seccompProfile are ever set on the target container’s securityContext — every other field the live manifest already has (runAsUser, runAsNonRoot, readOnlyRootFilesystem, …) is preserved exactly as-is. This tool only ever contributes what it actually generated. Requires additional RBAC (read-only — this never writes to the cluster, only fetches to build a local file): ../../deploy/rbac-patched-manifest.yaml.

The same content is embedded in spec.patchedManifest of the SecurityProfileProposal (Step 12) on every run regardless of whether --patched-manifest-out was passed — that flag only controls whether it’s also written as a local file.

SeccompProfile resource output (--seccomp-profile-out)

securityContext.seccompProfile.localhostProfile can never carry a seccomp profile’s content inline — only a path Kubernetes resolves by asking the kubelet to look on that node’s own local filesystem, never from any API object directly. That means neither the plain seccomp.json (see internal seccomp output) nor a hand-rolled ConfigMap actually closes the loop: something still has to copy the file onto every node.

security-profiles-operator (SPO) is the real, upstream Kubernetes-native answer: its own controller/ DaemonSet watches SeccompProfile objects and materializes them onto every node’s seccomp directory automatically. Pass --seccomp-profile-out to generate one:

kubectl apply -f nginx-demo-seccompprofile.yaml
apiVersion: security-profiles-operator.x-k8s.io/v1
kind: SeccompProfile
metadata:
  # Deterministic and cluster-unique: SeccompProfile is cluster-scoped from
  # SPO v0.9.0 on, so there is no namespace here and the name encodes
  # (namespace, pod, container). See docs/adr/0008.
  name: lg-v1-nginx-demo-<hash>
  annotations:
    landlockgenprof.io/managed-by: landlock-genprof
    landlockgenprof.io/seccomp-source: internal
spec:
  defaultAction: SCMP_ACT_ERRNO
  architectures: [SCMP_ARCH_X86_64]
  syscalls:
    - names: [accept4, capget, capset, chdir, epoll_wait, futex, openat, read, write]
      action: SCMP_ACT_ALLOW

(capget/capset/chdir/futex are explained in the internal seccomp page — always included, none is something the traced binary itself calls.)

spec.defaultAction/architectures/syscalls[].names/.action mirror pkg/seccomp.Profile’s own fields exactly (confirmed against SPO’s own Go source) — this is the same data as seccomp.json, just wrapped as a directly appliable Kubernetes object instead of a file a human has to copy by hand.

Requires SPO actually installed in the cluster — applying this manifest alone does nothing without SPO’s controller running to reconcile it. Once it does, SPO writes the profile to /var/lib/kubelet/seccomp/operator/<name>.json on every node and exposes that same path as status.localhostProfile — the operator/<name>.json value --security-context-out/ --patched-manifest-out/the SecurityProfileProposal all already reference, computed ahead of time. During governed workload binding, apply-proposal --restart waits for SPO reconciliation and rechecks the realized identity as required by ADR-0007 — confirmed live against a real reconciliation (kubectl get seccompprofile <name> -o yamlstatus.localhostProfile); the namespace segment used to be missing here, which broke every target pod once its patched manifest was actually applied (containerd refuses to start a container whose referenced localhostProfile doesn’t resolve to a real file — SPO never writes to the un-namespaced path this tool used to assume). See ../enforcement-prerequisites.md for installing SPO itself.

Applying this standalone file directly is not governed authorization. The normal path includes it in SecurityProfileProposal, binds it into CandidateDigest, requires explicit approval of that digest, and applies it through apply-proposal.

Importing SPO-derived seccomp policy (--seccomp-source=spo)

By default this project observes syscalls itself and synthesizes the seccomp profile from what it saw. security-profiles-operator (SPO) does the same job with a better instrument — a production eBPF recorder or an audit-log enricher, merged across replicas — so trace can instead import SPO’s generated profile and govern it.

What SPO does not record is filesystem access as Landlock authority, or network activity at all. Those stay ours in both modes. SPO replaces one domain of observation, never all of it.

The normative contract is ADR-0008.

Two modes, always explicit

--seccomp-source=internal (default)--seccomp-source=spo
Syscalls observed bylandlock-genprofSPO
Syscalls in TrainingHistoryyesno — not collected at all
Seccomp confidence tieryesnot applicable
--seccomp-out (plain JSON)availablerejected
Filesystem + networkoursours

The source is never auto-detected. If it were inferred from whether SPO happens to be installed, the same command would govern different authority on different clusters, and explain’s syscall section would mean something different depending on invisible cluster state.

There is also no fallback in either direction. If you select spo and the material is missing, the command fails — it does not quietly fall back to internally-synthesized syscalls, because that would change what is being governed without saying so.

Recording with SPO

The recording must produce a complete profile that is inert until a human approves it:

apiVersion: security-profiles-operator.x-k8s.io/v1
kind: ProfileRecording
metadata:
  name: nginx-rec
  namespace: prod
spec:
  kind: SeccompProfile
  recorder: Bpf
  # Required by the import. It makes SPO leave the generated profile in
  # spec.state: Disabled, so the recorded authority is never enforced on any
  # node before it has been reviewed and approved.
  disableProfileAfterRecording: true
  podSelector:
    matchLabels:
      app: nginx

Note ProfileRecording is namespaced and must live in the workload’s namespace, while the SeccompProfile it generates is cluster-scoped. That asymmetry is why lineage is carried by labels rather than by comparing namespaces.

Importing

Both names are given explicitly. Nothing is discovered by searching the cluster for a profile that looks plausible — that is precisely how one workload’s authority ends up governing another.

kubectl landlock-genprof trace \
  --pod nginx-demo --namespace prod --container tools \
  --binary /usr/sbin/nginx --duration 60s \
  --seccomp-source=spo \
  --spo-recording nginx-rec \
  --spo-profile nginx-rec-tools

What the import checks

Every gate below fails closed, with a message naming what failed.

GateRefused when
API shapenot security-profiles-operator.x-k8s.io/v1, not SeccompProfile, or namespaced (SPO ≤ v0.8.4)
Inertnessthe recording lacks disableProfileAfterRecording: true, or the profile is not spec.state: Disabled
Completenessthe profile carries SPO’s partial label, or the recording still has unmerged profiles outstanding
Lineagerecording-namespace, recording-id or container-id is absent or disagrees with the target
Enforcement contentany field outside defaultAction, architectures, syscalls[].names, syscalls[].action
Content sanityno defaultAction, or a syscall rule with no action

Why unsupported fields are refused rather than dropped

Dropping changes what is enforced relative to what was reviewed — in both directions. baseProfileName narrows: SPO unions the named base profile’s syscalls in, so a profile with a base plus three syscalls may really permit hundreds. syscalls[].args widens: a rule permitted only for specific argument values becomes unconditional.

The allow-list is closed, so a field SPO adds in a future release is refused too, rather than silently lost.

What you get

The import is a snapshot. Content is copied into a new landlock-genprof-owned SeccompProfile; the SPO source object is read once and never modified, adopted, renamed, or referenced afterwards. Mutating or deleting the source later cannot change an approved candidate.

The workload is bound to the governed copy — operator/lg-v1-<pod>-<hash>.json — never to the SPO source profile.

Provenance rides inside the governed artifact as annotations, so it is covered by CandidateDigest. review shows it:

Seccomp:
  Source: security-profiles-operator
  Origin: derived policy (not observed by landlock-genprof)
  Source profile: nginx-rec-tools
  Recording: prod/nginx-rec
  Container: tools
  Coverage: unknown
  Confidence: not applicable (derived policy carries no occurrence data)

Confidence is not applicable, and that is stated rather than left blank. SPO’s generated profile carries syscall names only — no timestamps, no occurrence counts. Any tier would be invented, and a blank where filesystem rules show high would read as low.

Coverage is optional. Official SPO v1.0.0 output does not set spo.x-k8s.io/syscall-coverage, so absent coverage is recorded as the explicit token unknown — never 0, never full, never a confidence tier — and does not block the import. The tested #3355-compatible merged path accepts schema v1, normalizes its partial-profile presence counts, and keeps the result informational and provenance-bound rather than confidence or authority.

Merged profiles

SPO’s recording merger (mergeStrategy: Containers) sets only recording-id and recording-namespace on the merged profile — it does not carry container-id through. landlock-genprof therefore imports it only via the explicit merged-provenance contract: provenance is recording-level, contributor lineage is unavailable, and the application target is selected independently. A merged union may be broader than any contributor; review displays that widening risk before approval. This contract was demonstrated with real SPO output in run 32561123023.

Single-replica recordings (mergeStrategy: None, the default) carry all three labels and import normally.

This is a fail-closed refusal, not a security gap: the alternative would be importing a profile whose container cannot be confirmed. Supporting merged profiles requires either an upstream change or a further decision about what lineage evidence is sufficient without container-id.

Switching sources invalidates approval

Provenance is digested, so changing the source changes CandidateDigest and any prior approval goes stale — apply then fails closed. That is intended:

  • internal → SPO, or SPO → internal, with byte-identical syscalls: the digest still changes, because which system’s authority is enforced is part of what the reviewer signed off on;
  • re-importing from a different recording: the digest changes, because a new recording is genuinely new evidence.

CLI reference

landlock-genprof

Generates least-privilege Kubernetes security profiles by observing a running pod

Synopsis

Observes a running Kubernetes pod and generates least-privilege security profiles from what it actually saw — a PodLock LandlockProfile always, plus NetworkPolicy/seccomp/Linux capabilities/securityContext outputs behind their own flags.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

Options

  -h, --help   help for landlock-genprof

SEE ALSO

trace

landlock-genprof trace

Starts a training run on a target pod and generates least-privilege security profiles

Synopsis

Starts a training run on a target pod and generates least-privilege security profiles.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof trace [flags]

Examples

  # Minimal run — filesystem-only profile (PodLock LandlockProfile), the one mandatory artifact
  kubectl landlock-genprof trace --pod nginx-demo --namespace default \
    --binary /usr/sbin/nginx --duration 60s

  # Also generate every optional artifact this run's training observed
  kubectl landlock-genprof trace --pod nginx-demo --namespace default \
    --binary /usr/sbin/nginx --duration 60s \
    --network-out --seccomp-out --capabilities-out --security-context-out \
    --report-out --patched-manifest-out --seccomp-profile-out

  # Restart the target first, so the tracer also catches startup-only
  # activity (bind(), config/log file opens) — see docs/usage/target-restart.md
  kubectl landlock-genprof trace --pod nginx-demo --namespace default \
    --binary /usr/sbin/nginx --duration 60s --restart --seccomp-profile-out

  # Accumulate this run into cross-run Confidence instead of a single-run estimate
  kubectl landlock-genprof trace --pod nginx-demo --namespace default \
    --binary /usr/sbin/nginx --duration 60s --history

Options

      --binary string                               Path of the main binary observed, e.g. /usr/sbin/nginx (required) — filters events to this process, see docs/usage.md for why
      --candidate-out verify[="-"]                  Output file for the raw, uncollapsed Landlock candidate (default <pod>-candidate.json); this is what verify reads — see internal/exporter/landlockjson. Carries rights (e.g. TRUNCATE) the LandlockProfile YAML above can't represent at all
      --capabilities-out string[="-"]               Output file for a capabilities add/drop fragment (default <pod>-capabilities.yaml); not a standalone resource, see docs/usage.md
  -c, --container string                            Target container (deduced if the pod has only one)
  -d, --duration duration                           Training run duration (default 1m0s)
      --events-out synthesize --events-file[="-"]   Output file for the raw captured events (default <pod>-events.json); this is what synthesize --events-file reads to re-run synthesis offline, without re-tracing — see internal/evidence
  -h, --help                                        help for trace
      --history                                     Accumulate this run into a TrainingHistory resource for cross-run confidence. Requires additional RBAC — see docs/usage.md
  -n, --namespace string                            Kubernetes namespace (default "default")
      --network-out string[="-"]                    Output file for a generated NetworkPolicy (default <pod>-networkpolicy.yaml); skipped if no network activity was observed
  -o, --out string                                  Output file for the generated LandlockProfile (default: <pod>-profile.yaml)
      --patched-manifest-out string[="-"]           Output file for a ready-to-apply manifest with securityContext merged in (default <identity>-patched.yaml); requires deploy/rbac-patched-manifest.yaml, see docs/usage.md
  -p, --pod string                                  Target pod name (required)
      --report-out string[="-"]                     Output file for a combined Markdown review report (default <pod>-report.md); always written when passed, works standalone — see docs/usage.md
      --restart                                     Restart the pod right before tracing, to catch startup-time activity. Disruptive — requires deploy/rbac-restart.yaml, see docs/usage.md
      --seccomp-out string[="-"]                    Output file for a generated seccomp profile (default <pod>-seccomp.json); skipped if no syscalls observed. Disruptive if misapplied — see docs/usage.md
      --seccomp-profile-out string[="-"]            Output file for an SPO SeccompProfile resource wrapping the seccomp profile (default <pod>-seccompprofile.yaml); requires security-profiles-operator, see docs/usage.md
      --seccomp-source string                       Where seccomp authority comes from: "internal" (this tool observes syscalls and synthesizes the profile) or "spo" (import a security-profiles-operator SeccompProfile as derived policy). Never auto-detected — see docs/adr/0008 (default "internal")
      --security-context-out string[="-"]           Output file for a composed securityContext fragment (default <pod>-securitycontext.yaml), combining capabilities + seccomp profile — see docs/usage.md
      --spo-import-mode string                      SPO provenance contract: strong-lineage or merged-provenance. Never selected by fallback (default "strong-lineage")
      --spo-profile string                          Name of the SPO-generated SeccompProfile to import (cluster-scoped, so no namespace). Required with --seccomp-source=spo
      --spo-recording string                        Name of the SPO ProfileRecording that produced the source profile, in the target namespace. Required with --seccomp-source=spo; the source is named, never guessed
      --spo-recording-namespace string              Source ProfileRecording namespace; required for merged-provenance because source and target are independent

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

synthesize

landlock-genprof synthesize

Re-runs synthesis offline from previously captured evidence

Synopsis

Re-runs synthesis from a training run’s persisted evidence (see trace --events-out), without re-tracing — produces the same PodLock profile and candidate JSON trace writes inline. Deliberately minimal today: unlike trace, this doesn’t write NetworkPolicy/seccomp/capabilities/securityContext/report, doesn’t record history, and doesn’t publish a SecurityProfileProposal — those all need a live cluster connection this command doesn’t have.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof synthesize --events-file <path> --pod <name> --container <name> --binary <path> [flags]

Examples

  kubectl landlock-genprof synthesize --events-file nginx-demo-events.json \
    --pod nginx-demo --namespace default --container nginx --binary /usr/sbin/nginx --candidate-out

Options

      --binary string                Binary path to label the generated profile with (required)
      --candidate-out verify[="-"]   Output file for the raw candidate JSON (default <pod>-candidate.json); this is what verify reads
  -c, --container string             Container name to label the generated profile with (required)
      --events-file string           Path to an evidence JSON file (see trace --events-out) (required)
  -h, --help                         help for synthesize
  -n, --namespace string             Namespace to label the generated profile with (default "default")
  -o, --out string                   Output file for the generated LandlockProfile (default: <pod>-profile.yaml)
  -p, --pod string                   Pod name to label the generated profile with (required)

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

review

landlock-genprof review

Reviews a published SecurityProfileProposal

Synopsis

Reviews a published SecurityProfileProposal.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof review <proposal> [flags]

Examples

  # Same proposal name as the pod trace was run against
  kubectl landlock-genprof review nginx-demo

  kubectl landlock-genprof review nginx-demo --namespace prod

Options

  -h, --help               help for review
  -n, --namespace string   Kubernetes namespace (default "default")

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

approve

landlock-genprof approve

Records an explicit approval decision on a SecurityProfileProposal

Synopsis

Records an explicit approval decision on a SecurityProfileProposal, binding approval to the reviewed candidate digest. Governed apply-proposal requires a valid Approved state with that digest and the supported candidate-v1 mechanism; its confirmation prompt is additional operator confirmation.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof approve <proposal> [flags]

Examples

  kubectl landlock-genprof approve nginx-demo

  kubectl landlock-genprof approve nginx-demo --reason "reviewed with the platform team, looks right" --expected-digest sha256:...

Options

      --expected-digest string   (approve only) Expected candidate digest to bind approval to (format: sha256:<hex>)
  -h, --help                     help for approve
  -n, --namespace string         Kubernetes namespace (default "default")
      --reason string            Optional free-text note explaining this decision

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

reject

landlock-genprof reject

Records an explicit rejection decision on a SecurityProfileProposal

Synopsis

Records an explicit rejection decision on a SecurityProfileProposal. A rejected proposal cannot pass the fail-closed approval validation required by governed apply-proposal; re-run trace and review, then approve the new candidate digest when it is ready.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof reject <proposal> [flags]

Examples

  kubectl landlock-genprof reject nginx-demo --reason "syscalls list looks too broad, retrace with more traffic"

Options

  -h, --help               help for reject
  -n, --namespace string   Kubernetes namespace (default "default")
      --reason string      Optional free-text note explaining this decision

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

apply-proposal

landlock-genprof apply-proposal

Reviews and applies an approved, digest-bound SecurityProfileProposal

Synopsis

Reviews and applies a published SecurityProfileProposal’s artifacts. Requires approvalState=Approved with a valid candidate digest and candidate-v1 mechanism; fails closed before planning or applying when that binding is missing, malformed, stale, or changed. A confirmation prompt is additional operator confirmation.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof apply-proposal <proposal> [flags]

Examples

  # Applies PodLock/NetworkPolicy/SPO SeccompProfile if available — Patched
  # Manifest is left out unless --restart is also passed, see below
  kubectl landlock-genprof apply-proposal nginx-demo --namespace default

  # Also apply the Patched Manifest artifact, restarting the target pod
  kubectl landlock-genprof apply-proposal nginx-demo --restart

  # Skip PodLock (e.g. its operator isn't installed on this cluster)
  kubectl landlock-genprof apply-proposal nginx-demo --skip=podlock

  # Non-interactive, for CI/scripted use — still prints what it applied
  kubectl landlock-genprof apply-proposal nginx-demo --yes

Options

  -h, --help                         help for apply-proposal
  -n, --namespace string             Kubernetes namespace (default "default")
      --readiness-timeout duration   How long to wait for an external controller to make an enforcement artifact usable before binding the workload to it — see docs/adr/0007-governed-apply-ordering-and-enforcement-readiness.md. Applies only when the Patched Manifest is being applied and references a generated profile; on timeout the workload binding is not applied. (default 2m0s)
      --restart                      Also apply the Patched Manifest artifact, if available. Opt-in, not on by default: unlike the other three artifacts, applying it deletes and recreates the target pod outright (see internal/k8s.applyPod) — every other artifact is either inert until its operator reconciles it or a live-updatable resource. Confirmed live: repeatedly force-restarting a pod whose enforcement side wasn't actually ready yet (SPO/PodLock) is how nginx-demo ended up in a 73-minute, 15-restart CrashLoopBackOff with no single moment where restarting it was an actual decision — --skip=patched-manifest used to be the only way to avoid that, but it's easy to not know to reach for an opt-out flag you've never needed before; an opt-in one can't be missed by accident the same way.
      --skip strings                 Artifact(s) to leave out of this apply, comma-separated or repeated — one of: podlock, networkpolicy, patched-manifest, spo-seccompprofile. Patched Manifest is already left out by default (see --restart); --skip=patched-manifest is accepted but redundant with it.
  -y, --yes                          Skip the confirmation prompt (for CI/non-interactive use); still prints what it applied

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

rollback

landlock-genprof rollback

Explicitly rolls back an eligible ApplyAttempt

Synopsis

Creates a durable RollbackAttempt and reverses only eligible, strict-resource-version-bound mutations. Rollback is explicit, sequential, nontransactional, and CLI-only.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof rollback <apply-attempt> [flags]

Options

  -h, --help               help for rollback
  -n, --namespace string   Kubernetes namespace (default "default")
  -y, --yes                Skip the explicit rollback confirmation

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

custody-epoch

landlock-genprof custody-epoch

Manages the ApplyAttempt custody qualification epoch

Options

  -h, --help   help for custody-epoch

SEE ALSO

custody-epoch activate

landlock-genprof custody-epoch activate

Proves CRD hardening and publishes a fresh custody epoch

landlock-genprof custody-epoch activate [flags]

Options

  -h, --help   help for activate

SEE ALSO

ui

landlock-genprof ui

Serves the local read-only Workbench HTTP boundary

Synopsis

Serves the local, read-only Workbench: the given SecurityProfileProposal at “/”, plus live workload/security-projection reads under “/api”. Every read goes through the bounded G0.5 read capability; there is no approval, rejection, apply, or other mutation control.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof ui [proposal] [flags]

Options

  -h, --help               help for ui
  -n, --namespace string   Kubernetes namespace (default "default")
      --port int           Loopback HTTP port (default 8080)

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

doctor

landlock-genprof doctor

Checks this host’s (or a given kernel version’s) Landlock/eBPF prerequisites

Synopsis

Checks that a kernel supports Landlock (filesystem and network) and, for the local host, that eBPF’s bpffs is mounted — the same checks hack/check-kernel.sh has always run, now built into the CLI itself instead of a separate shell script a user has to already know exists. –kernel lets you check a kernel you’re not currently running on (a fleet’s node-pool version, for instance) without needing to run this on that host directly.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof doctor [flags]

Examples

  kubectl landlock-genprof doctor
  kubectl landlock-genprof doctor --kernel 5.15.0

Options

  -h, --help            help for doctor
      --kernel string   Check this kernel version instead of the current host's (e.g. 5.15.0)

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

verify

landlock-genprof verify

Checks a synthesized Landlock candidate against a target kernel’s ABI level

Synopsis

Checks every rule in a synthesized Landlock candidate (see internal/exporter/landlockjson) against a target kernel’s Landlock ABI level — reports which rules need a right the target kernel doesn’t support, and at which ABI level that right actually exists. –kernel defaults to the local host’s, matching doctor/abi check. –output sarif renders findings as a SARIF 2.1.0 log instead of text, for CI dashboards (GitHub Code Scanning and similar) that already know how to annotate it — the exit-code contract (0/2/3) is unchanged either way.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof verify --candidate-file <path> [flags]

Examples

  kubectl landlock-genprof verify --candidate-file nginx-demo-candidate.json
  kubectl landlock-genprof verify --candidate-file nginx-demo-candidate.json --kernel 5.19
  kubectl landlock-genprof verify --candidate-file nginx-demo-candidate.json --output sarif > verify.sarif

Options

      --candidate-file string   Path to a candidate JSON file (see internal/exporter/landlockjson)
  -h, --help                    help for verify
      --kernel string           Kernel version to verify against (e.g. 6.2); defaults to the local host's
      --output string           Output format: text or sarif (default "text")

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

explain

landlock-genprof explain

Explains why a synthesized candidate’s rules exist

Synopsis

Explains why each rule in a synthesized candidate (see internal/exporter/landlockjson) exists: which rights it carries, the ABI level and minimum kernel version each right actually needs, how confident the synthesis is, and how many observations support it. –path restricts this to a single rule.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof explain --candidate-file <path> [flags]

Examples

  kubectl landlock-genprof explain --candidate-file nginx-demo-candidate.json
  kubectl landlock-genprof explain --candidate-file nginx-demo-candidate.json --path /etc/nginx

Options

      --candidate-file string   Path to a candidate JSON file (see internal/exporter/landlockjson) (required)
  -h, --help                    help for explain
      --path string             Explain only the rule for this path (default: every rule)

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

export

landlock-genprof export

Renders an already-synthesized candidate to a target output format

Synopsis

Renders an already-synthesized candidate (see internal/exporter/landlockjson) to a target output format, without re-running synthesis. Never mutates a cluster — pure rendering; see a future apply for actually applying an approved artifact. Prints to stdout unless –out is given.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof export --candidate-file <path> --format podlock --pod <name> --container <name> --binary <path> [flags]

Examples

  kubectl landlock-genprof export --candidate-file nginx-demo-candidate.json \
    --format podlock --pod nginx-demo --namespace default --container nginx --binary /usr/sbin/nginx

  kubectl landlock-genprof export --candidate-file nginx-demo-candidate.json \
    --format podlock --pod nginx-demo --container nginx --binary /usr/sbin/nginx --out profile.yaml

Options

      --binary string           Binary path to label the output with (required)
      --candidate-file string   Path to a candidate JSON file (see internal/exporter/landlockjson) (required)
  -c, --container string        Container name to label the output with (required)
      --format string           Output format (supported today: podlock) (default "podlock")
  -h, --help                    help for export
  -n, --namespace string        Namespace to label the output with (default "default")
  -o, --out string              Output file (default: stdout)
  -p, --pod string              Pod name to label the output with (required)

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

diff

landlock-genprof diff

Compares two synthesized candidates rule by rule

Synopsis

Compares two synthesized candidates (see internal/exporter/landlockjson), reporting rules added, removed, or whose rights changed — the check for a dependency bump silently widening or narrowing what a workload needs between two training runs. –output junit renders one testcase per rule path (failed = changed) instead of text, for CI dashboards that already render JUnit results — the exit-code contract (0/1/3) is unchanged either way.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof diff <old-candidate-file> <new-candidate-file> [flags]

Examples

  kubectl landlock-genprof diff nginx-demo-candidate-old.json nginx-demo-candidate-new.json
  kubectl landlock-genprof diff old.json new.json --output junit > diff.xml

Options

  -h, --help            help for diff
      --output string   Output format: text or junit (default "text")

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

ABI compatibility

landlock-genprof abi

Inspects Landlock’s ABI-versioned right vocabulary

Synopsis

Inspects Landlock’s ABI-versioned right vocabulary — which rights exist at which ABI level, and which kernel version documents each level. See abi list/abi check.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

Options

  -h, --help   help for abi

SEE ALSO

abi list

landlock-genprof abi list

Lists Landlock rights, optionally filtered to one ABI level and below

Synopsis

Lists every Landlock right this project’s ABI table knows about, its introducing ABI level, and that level’s documented minimum kernel version. –abi restricts the list to rights available at or below a given level (e.g. –abi 3 for what a 6.2 kernel supports).

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof abi list [flags]

Examples

  kubectl landlock-genprof abi list
  kubectl landlock-genprof abi list --abi 3

Options

      --abi int   Restrict to rights available at or below this ABI level (0 = all known levels)
  -h, --help      help for list

SEE ALSO

abi check

landlock-genprof abi check

Reports the highest Landlock ABI level a kernel version supports

Synopsis

Reports the highest Landlock ABI level a kernel version supports, per this project’s documented kernel-version table — an approximation, not the authoritative detection method (a live kernel’s real ABI level is only truly known via landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION), which correctly reports backported support this table can’t predict). Useful for planning against a kernel you’re not currently running on — a fleet’s node-pool version, for instance.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof abi check [flags]

Examples

  kubectl landlock-genprof abi check --kernel 6.2
  kubectl landlock-genprof abi check # checks the local host

Options

  -h, --help            help for check
      --kernel string   Kernel version to check (e.g. 6.2); defaults to the local host's

SEE ALSO

Evidence

landlock-genprof evidence

Inspects raw captured evidence

Synopsis

Inspects raw captured evidence (see internal/evidence). See evidence show/evidence list.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

Options

  -h, --help   help for evidence

SEE ALSO

evidence show

landlock-genprof evidence show

Summarizes a raw evidence file

Synopsis

Summarizes a raw evidence file (see trace --events-out): how many events of each kind, distinct filesystem paths and network ports touched, and the observation time span. Answers “what did the tracer actually see,” distinct from explain, which answers “what rules did synthesis produce from it.”

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof evidence show <events-file> [flags]

Examples

  kubectl landlock-genprof evidence show nginx-demo-events.json

Options

  -h, --help   help for show

SEE ALSO

evidence list

landlock-genprof evidence list

Lists evidence files in a directory

Synopsis

Scans a directory (default: current directory) for files that parse as evidence (see trace --events-out), one summary line each: event count and observation window. Not a registry — there isn’t one — just a scan; files that don’t parse as evidence (e.g. a candidate.json sitting next to them) are silently skipped.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof evidence list [directory] [flags]

Examples

  kubectl landlock-genprof evidence list
  kubectl landlock-genprof evidence list ./captures

Options

  -h, --help   help for list

SEE ALSO

Policy

landlock-genprof policy

Inspects SecurityProfileProposal approval state

Synopsis

Inspects SecurityProfileProposal approval state. See policy list/policy status.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

Options

  -h, --help   help for policy

SEE ALSO

policy list

landlock-genprof policy list

Lists SecurityProfileProposals and their approval state

Synopsis

Lists every SecurityProfileProposal in a namespace and its current approval state.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof policy list [flags]

Examples

  kubectl landlock-genprof policy list
  kubectl landlock-genprof policy list --namespace prod

Options

  -h, --help               help for list
  -n, --namespace string   Kubernetes namespace (default "default")

SEE ALSO

policy status

landlock-genprof policy status

Reports whether a SecurityProfileProposal has been approved

Synopsis

Reports a SecurityProfileProposal’s current approval state — exits 0 only if Approved, 2 (blocking) otherwise. Meant as a CI gate: “has a human signed off on this before it gets applied,” distinct from every correctness/ABI check the rest of this CLI does.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof policy status <name> [flags]

Examples

  kubectl landlock-genprof policy status nginx-demo

Options

  -h, --help               help for status
  -n, --namespace string   Kubernetes namespace (default "default")

SEE ALSO

version

landlock-genprof version

Prints the version

Synopsis

Prints the version.

Installed as a kubectl plugin (the common case): run this as kubectl landlock-genprof <command>. Running this binary directly instead (standalone, not via kubectl) works the same way, without that prefix.

landlock-genprof version [flags]

Examples

  kubectl landlock-genprof version

Options

  -h, --help   help for version

SEE ALSO

  • landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod

Architecture

landlock-genprof is an evidence-driven governance and verification layer for Kubernetes workload security policies. The v0.7 product surface is the Observation Workbench; the existing CLI remains the downstream governance and application path.

Central invariant: learned policy is not authorized policy. Observed, derived, proposed, reviewed, approved, applied, enforced, and verified are distinct states.

Demonstrated behavior is tracked in PROGRESS.md. Normative apply ordering and SPO import boundaries are defined by ADR-0007 and ADR-0008.

v0.7 Observation architecture

flowchart TD
    WORKLOAD["Workload / container"]
    OBS["Durable Observation\nidentity + lifecycle"]
    EVID["Bounded attributed evidence\nfilesystem · exec · network · capabilities\nUNKNOWN remains first-class"]
    HISTORY["CONTAINER TrainingHistory\nObservation contribution"]
    CANDIDATE["Candidate-v2 Proposal\nCONTAINER_CAPABILITIES"]
    READ["Observation Workbench read model\nOverview · Observations · Proposals"]
    APPROVAL["CLI governance\napproval / custody"]
    APPLY["CLI application\nsequential, nontransactional"]
    ENFORCE["External backend enforcement"]
    VERIFY["Backend-specific behavioral verification"]

    WORKLOAD --> OBS --> EVID --> HISTORY --> CANDIDATE --> READ
    CANDIDATE --> APPROVAL --> APPLY --> ENFORCE --> VERIFY

The Observation identity contains the exact workload UID, ContainerSlot, and ImageIdentity. Candidate-v2 intentionally has asymmetric identity: its Proposal subject is Scope CONTAINER plus Target, Container, and ImageIdentity, and does not contain workload UID. The Workbench must preserve that distinction rather than claim a stronger Proposal binding.

The Workbench exposes durable state and read-only governance facts. It does not approve, reject, revoke, apply, or rollback. Those authorities remain in the supported CLI/governance path.

Downstream CLI, governance, and enforcement architecture

flowchart TD
    WORKLOAD["Target workload"]

    subgraph SOURCES["Knowledge sources"]
        DIRECT["landlock-genprof direct evidence<br/>filesystem / network / capabilities"]
        SPO["Security Profiles Operator<br/>syscall observation"]
        DERIVED["SeccompProfile<br/>derived policy with provenance"]
    end

    PROPOSAL["SecurityProfileProposal<br/>provenance preserved"]
    REVIEW["Review exact candidate"]
    DIGEST["CandidateDigest<br/>deterministic identity, not authority"]
    APPROVAL["Human approval<br/>exact digest only; changes require review"]
    CUSTODY["ApplyAttempt custody<br/>Before state and outcome"]
    APPLY["Governed mutation<br/>applied is not enforced"]
    ROLLBACK["Explicit CLI rollback"]
    ROLLBACKCUSTODY["RollbackAttempt custody"]
    INVERSE["Guarded inverse mutation"]

    subgraph BACKENDS["External enforcement ownership"]
        FILESYSTEM["PodLock / Landlock<br/>filesystem"]
        NETWORK["NetworkPolicy / CNI<br/>network"]
        HARDENING["securityContext / Kubernetes runtime<br/>capabilities and hardening"]
        SECCOMP["SPO SeccompProfile / container runtime<br/>seccomp"]
    end

    VERIFY["Behavioral verification<br/>enforced is not verified"]

    WORKLOAD --> DIRECT
    WORKLOAD --> SPO
    SPO --> DERIVED
    DIRECT --> PROPOSAL
    DERIVED --> PROPOSAL
    PROPOSAL --> REVIEW
    REVIEW --> DIGEST
    DIGEST --> APPROVAL
    APPROVAL --> CUSTODY
    CUSTODY --> APPLY
    APPLY --> FILESYSTEM
    APPLY --> NETWORK
    APPLY --> HARDENING
    APPLY --> SECCOMP
    FILESYSTEM --> VERIFY
    NETWORK --> VERIFY
    HARDENING --> VERIFY
    SECCOMP --> VERIFY
    CUSTODY --> ROLLBACK
    ROLLBACK --> ROLLBACKCUSTODY
    ROLLBACKCUSTODY --> INVERSE
    INVERSE --> FILESYSTEM
    INVERSE --> NETWORK
    INVERSE --> HARDENING
    INVERSE --> SECCOMP

1. Acquisition sources

In the primary SPO mode, landlock-genprof traces filesystem and network behavior while SPO observes syscalls with its recorder and produces the real derived SeccompProfile. The sources have different epistemic types: tracer events are observations; the SPO profile is already derived policy.

The internal path remains supported as an explicit alternative: --seccomp-source=internal lets the landlock-genprof tracer observe syscalls and synthesize an advisory seccomp artifact. It is not silently selected when SPO is unavailable. Source selection is explicit and visible in provenance.

2. Evidence versus derived policy

Direct filesystem and network observations may enter TrainingHistory, where cross-run occurrence supports confidence. An SPO-derived profile enters at the artifact layer. Its syscalls never enter landlock-genprof TrainingHistory, and landlock-genprof does not invent confidence or occurrence data for them. Coverage is optional provenance: official SPO v1.0.0 output may be absent, while the tested #3355-compatible merged path normalizes schema v1 without turning coverage into confidence, lineage, or authority.

Import copies validated enforcement semantics and provenance into a governed snapshot. It is not a live reference: later mutation or deletion of the SPO source object does not change an existing candidate. landlock-genprof does not mutate the source object, and that object grants no deployment authority.

3. Proposal assembly and identity

trace publishes the available filesystem, network, seccomp, capability, and workload-binding artifacts in one SecurityProfileProposal. Mixed origin is preserved: a candidate can combine landlock-genprof observations with SPO-derived seccomp policy without claiming they came from the same source.

CandidateDigest deterministically binds the proposal fields selected by the candidate-v1 contract, including the governed SPO artifact, its provenance, and the patched manifest that refers to its governed profile name. The digest is content identity, not approval.

4. Review and authorization

review exposes the candidate and digest. approve --expected-digest … records explicit human authority for that exact content. A changed candidate produces a different digest and cannot inherit the earlier approval. Missing, malformed, revoked, or stale authority fails closed.

5. Governed apply

apply-proposal is the authoritative application path. It validates approval before planning, uses an immutable planned payload, applies enforcement artifacts before the optional workload-binding manifest, waits for supported backend readiness, rechecks live identity, and revalidates authority immediately before binding. Readiness is an execution precondition, never a source of authority.

Multi-artifact apply is sequential, not transactional. A failure stops subsequent application and prevents workload binding, but resources applied before the failure are not rolled back.

6. External enforcement and verification

landlock-genprof is not a kernel enforcement mechanism:

DomainArtifactEnforcement owner
FilesystemPodLock LandlockProfilePodLock / Landlock integration
NetworkKubernetes NetworkPolicyThe cluster CNI
SyscallsSPO SeccompProfile plus workload referenceSPO, kubelet, and container runtime
CapabilitiessecurityContext fragment or patched workloadKubernetes and the container runtime

Applied does not mean enforced, and enforced does not mean verified. Backend reconciliation establishes readiness for supported paths; behavioral verification separately establishes what restriction the workload actually experiences. See enforcement prerequisites and demonstrated capabilities.

7. ApplyAttempt custody and explicit rollback

apply-proposal records durable ApplyAttempt custody before each governed mutation. The record preserves canonical target identity, controlled Before state, intended and observed state, resource identity, resourceVersion, and typed outcome. A current custody epoch qualifies newly created attempts for explicit rollback; it is a qualification marker, not authority.

An explicit CLI rollback creates a separate RollbackAttempt. It validates strict UID/resourceVersion and controlled-state equality before each inverse, uses dependency/readiness and policy-reference guards, and restores only the recorded controlled Before state. Both apply and rollback are sequential and nontransactional; partial and unknown outcomes remain durable.

8. Observation Workbench presentation adapter

The v0.7 Observation Workbench is a read-only projection over canonical workload resolution, durable namespace-scoped Observations, Proposal state, evidence, uncertainty, and read-only governance facts. It performs reads through the pinned WorkbenchReadCapability and does not expose a Kubernetes mutation client or browser mutation route:

Kubernetes API / kubeconfig
          │ bounded namespace-scoped reads
          ▼
Canonical workload/container target
          ├── durable Observation lifecycle and evidence
          ├── candidate-v2 Proposal and provenance
          └── read-only governance facts
                          │
                          ▼
                    workbenchView
                          │
                          ▼
                    html/template
                          │
                          ▼
                   127.0.0.1 HTTP
                          │
                          ▼
                       Browser

The Workbench owns none of the candidate digest, approval, provenance, coverage, custody, or Kubernetes mutation semantics. Its HTTP application capability is read-only, namespace-pinned, and bounded by request, concurrency, response, and timeout controls. Browser interaction cannot approve, reject, revoke, apply, rollback, or activate custody. The v0.7 navigation is Overview, Observations, and Proposals; there are no standalone Governance, Activity, or Assurance experiences. The page is not a controller, generic dashboard, approval interface, or source of new policy meaning.

Engineering references

These references describe implementation detail; this page owns the current product architecture.

Future architecture

Controller reconciliation, continuous drift handling, detection, and governed response are roadmap capabilities, not current architecture. They must preserve source attribution, candidate identity, explicit human authority, and the separation between application, enforcement, and verification.

Threat model and validation methodology

In short: the tracer itself needs zero elevated privileges — it’s an ordinary API client, same category as kubectl. The real elevated access belongs to Inspektor Gadget’s own DaemonSet, a separate component this project depends on but doesn’t control. What follows:

  1. Tracer attack surface — exact RBAC, and the one flag (--restart) that genuinely widens the blast radius.
  2. Completeness of generated profiles — what a short training run can miss, and the two gaps already fixed (startup blind spot, cross-process contamination).
  3. Candidate authority and governed apply — substitution, stale authority, source provenance, readiness, and partial apply.
  4. Runtime validation — enforcement and bypass questions.
  5. CI hardening — SAST/SCA status.

Workbench listener (v0.6)

The experimental Workbench adds a short-lived local HTTP listener to the CLI. It is not a remotely exposed service and does not change the CLI’s Kubernetes authority model:

HTTP request → pinned ReadSession / WorkbenchReadCapability
        → canonical domain functions and bounded projections
        → html/template → 127.0.0.1:<port> → browser

The relevant trust boundaries are:

  • TB1 — Kubernetes API to local CLI: the process uses the user’s existing kubeconfig and pinned namespace ReadSession to perform bounded reads for workloads, projections, proposals, and optionally attempt custody.
  • TB2 — domain model to projection: the Workbench projects canonical proposal, digest, approval, provenance, and coverage semantics. It does not own or reinterpret them.
  • TB3 — listener to browser: the browser receives escaped, read-only HTML over an explicit loopback bind. The browser does not inherit Kubernetes credentials or authority.

The handler serves GET-only pages and API reads through the bounded read capability. It has no write-capable Kubernetes client. There are no write routes, persistence, sessions, or browser approval/application operations. Current mitigations include the explicit 127.0.0.1 bind, Host and browser origin validation, Fetch Metadata checks, request deadlines and concurrency limits, response/body bounds, GET-only routing, html/template escaping, and explicit read-state wording. Browser interaction cannot trigger a Kubernetes mutation in this architecture. Copyable CLI commands are advisory text only.

The listener remains unsuitable for remote or shared exposure despite these browser-origin controls. The Workbench does not claim browser mutation, generic Kubernetes restoration, kernel enforcement verification, or a global security score. Attempt history is rendered newest-first with a 100-record display cap after a namespace-scoped List; the cap is not server-side pagination.

1. Tracer attack surface

landlock-genprof itself needs zero elevated Linux capabilities — confirmed directly in internal/tracer/trace_linux.go: no cilium/ebpf, no raw bpf()/netlink syscalls, golang.org/x/sys/unix is used only for an O_DIRECTORY flag constant. It’s an ordinary Kubernetes API client, same category as kubectl/helm — it never runs on a node and is never privileged/hostNetwork/hostPID. All syscall observation happens via Inspektor Gadget’s own gRPC runtime (grpcruntime.WithConnectUsingK8SProxy), tunneled through a pods/portforward subresource — the same K8s API mechanism kubectl port-forward uses to reach a pod that’s already running.

The actual elevated privileges (CAP_BPF, CAP_SYS_ADMIN depending on kernel version) belong entirely to Inspektor Gadget’s own DaemonSet — a separate component this project depends on but doesn’t deploy or control (kubectl gadget deploy, out of scope here; see docs/enforcement-prerequisites.md’s sibling docs for what this project does and doesn’t set up). That DaemonSet, always running on every node once deployed, is the real attack surface worth scrutinizing — not landlock-genprof’s own process, which never runs permanently and never touches a node directly. This project’s own contribution to the attack surface is much narrower: the RBAC below, granting API-level (not kernel-level) access to reach that already-running daemon.

  • What’s the tracer’s service account’s minimal RBAC? See deploy/rbac.yaml: get on pods cluster-wide (target pod resolution, namespace chosen dynamically at runtime) + list on pods and create on pods/portforward scoped to the gadget namespace only (reaching Inspektor Gadget’s daemon via the K8s proxy). Every rule traces back to a specific API call in the code — see the manifest’s own comments. Confirmed generic, not per-gadget: nothing in the manifest names trace_open/trace_exec specifically — it’s daemon-reachability access only, so adding trace_tcp/ trace_bind (internal/tracer/trace_linux.go) required no new RBAC rule. Same for advise_seccomp (runSeccompTracer, added for the seccomp exporter) and trace_capabilities (runCapabilitiesTracer, added for the capabilities exporter): no new RBAC either.
  • advise_seccomp observes every process on the node during the training run, not just the target container — confirmed directly in its own upstream source (program.bpf.c’s sys_enter probe comment): container filtering can’t happen in-kernel without losing the target container’s own startup syscalls (executed by runc before the container’s filter is installed), so this specific gadget deliberately records node-wide and only filters down to the target container afterwards, at its own formatting stage. This is an upstream design choice in a gadget this project reuses as-is (see internal/tracer/trace_linux.go’s runSeccompTracer), not something introduced by this codebase — but it does mean a training run using --seccomp-out briefly observes syscall activity from every other workload on the same node, a wider blast radius than the other four gadgets (which scope in-kernel via the standard mount-namespace filter). Worth knowing before running --seccomp-out on a shared/multi-tenant node. trace_capabilities does not share this caveat — confirmed via its own source (program.bpf.c includes <gadget/filter.h> and calls gadget_should_discard_data_current(), the same in-kernel container-filtering mechanism trace_open/etc. use), so --capabilities-out scopes to the target container the normal way.
  • What’s the blast radius if the tracer itself is compromised? --restart (internal/k8s/restart.go) genuinely widens this — unlike everything above, it’s not read-only. It needs delete/ create on pods and patch on deployments/statefulsets/ daemonsets (see deploy/rbac-restart.yaml), meaning a compromised tracer ServiceAccount with this manifest applied could kill and recreate the pods it’s pointed at, or force a rollout restart on their owning Deployment/StatefulSet/DaemonSet — not just read one pod. Deliberately kept in a separate, opt-in manifest, not folded into the base deploy/rbac.yaml: deploying the base manifest alone keeps today’s read-only posture unchanged; --restart (and the extra blast radius that comes with it) is a choice an operator makes explicitly by also applying the second manifest.
  • internal/k8s/patch.go’s PatchedManifest/PatchedManifestForOwner need get on deployments/statefulsets/daemonsets, but stay read-only — meaningfully smaller blast radius than --restart’s manifest: they only ever fetch objects to build a manifest, never patch or delete anything in the cluster. No longer tied to an opt-in flag: since SecurityProfileProposal publishing became mandatory every trace run needs this RBAC whenever there’s a securityContext to compose, whether or not --patched-manifest-out was also passed to additionally write a local file. Deliberately its own manifest (deploy/rbac-patched-manifest.yaml), not folded into deploy/rbac-restart.yaml even though it overlaps two of its three get grants: it doesn’t require opting into --restart’s disruptive delete/patch capabilities too, and this project’s own RBAC principle is one self-sufficient manifest per capability, even for one that’s now baseline-required rather than optional. Two ClusterRoles granting get on the same resource (if both manifests are applied) is harmless, standard RBAC composition.

2. Completeness of generated profiles (false-negative risk)

A short training run doesn’t cover every possible code path (errors, edge cases, rarely triggered behavior). A profile generated by observation can therefore be:

  • too restrictive if it’s missing legitimate rules (the app breaks in prod)
  • silently incomplete if nobody knows what wasn’t observed

Startup blind spot — fixed, opt-in, via trace --restart. docs/e2e-demo.md Finding 2 documented that resources opened once at process startup (a pid file, a log fd) are invisible to a trace attached to an already-running container. internal/k8s.Restart closes this by restarting the target (delete+recreate for a bare pod, a rollout-restart annotation patch for a Deployment-owned one) right before the observation window starts — opt-in via --restart because it’s disruptive to the running workload and needs the additional RBAC noted in §1.

Recommended validation protocol:

  • Exercise normal, startup, error, and infrequent paths across multiple runs; no fixed duration or run count proves completeness.
  • Treat Confidence as cross-run occurrence evidence, not correctness, completeness, authorization, enforcement, or verification.
  • Exercise the target with real traffic (e.g. an actual HTTP request to nginx), not kubectl exec debug commands that only incidentally touch similar paths — see docs/e2e-demo.md Finding 1’s live re-verification, where ls/cat via kubectl exec produced a fully empty profile once correctly excluded from nginx’s own attribution, because nginx itself never did anything observable during that window.

Contamination risk — fixed at the tracer level for all four gadgets. docs/e2e-demo.md Finding 1 documented that the tracer’s Inspektor Gadget filter scopes events by namespace/podname/containername only, never by process — any process sharing the container’s namespaces during the training window got attributed to the traced binary, which produced false readExec: /bin, /usr/bin rules from a kubectl exec debugging session. The same gap affected trace_tcp/trace_bind: a connect/bind made by anything sharing the pod’s network namespace during training — a debugging session, a sidecar, an attacker — would be attributed to the traced workload and broaden its generated NetworkPolicy the same way. internal/tracer/trace_linux.go now scopes every one of the four run*Tracer functions to the traced binary’s comm (commFromBinaryPath), closing this for both PodLock and NetworkPolicy output. Residual risk, deliberately accepted: a legitimate child process spawned under a different comm (e.g. a CGI script) is filtered out too — a false negative traded for closing this false positive, see commFromBinaryPath’s own comment.

3. Candidate authority and governed apply

The authority model assumes an authenticated human reviewer is permitted to approve policy for the target namespace and protects that decision with content identity. Kubernetes authentication, RBAC, and admission policy still decide who may write proposal status or enforcement resources; a compromised authorized reviewer can intentionally approve dangerous content.

ThreatImplemented controlResidual risk or limit
Candidate substituted before approvalapprove --expected-digest compares the reviewed digest with current content and records authority for that digestA reviewer can still approve the wrong digest intentionally or without adequate review
Candidate mutated after review or approvalContent mutation changes CandidateDigest; stale or mismatched approval is rejectedCandidate-wide digest forces full re-review even for an isolated domain change
Stale approval reused for a newer candidateApproved and current digests must match under candidate-v1; mismatch fails closedNone within the current digest field set; schema evolution must update digest vectors
Review/apply TOCTOUapply-proposal validates before planning, applies an immutable planned payload, and revalidates immediately before workload bindingExternal resources applied before a later failure are not rolled back
Approval recorded for the wrong contentExpected-digest comparison and retry-on-conflict prevent accidental transfer across concurrent updatesHuman identity and decision quality depend on cluster authentication, RBAC, and review process
Seccomp source silently substitutedSource mode is explicit; there is no internal/SPO fallback; provenance is included in digested contentSwitching sources deliberately requires full re-review
SPO provenance spoofed or cross-workload policy importedADR-0008 requires recording namespace, recording ID, container lineage, completion, inertness, ownership, and supported-semantics checksLabels are structural lineage under RBAC, not cryptographic authentication; stronger UID-bound lineage needs upstream support
SPO source mutated after importImport is a copied governed snapshot, not a live reference; the source object is never mutated by landlock-genprofRe-importing changed source content creates a new candidate and requires review
SPO-derived data laundered as observation/confidenceSPO policy enters at the artifact layer; SPO syscalls have no route to TrainingHistory and receive no landlock-genprof confidenceCoverage is optional provenance (absent in official v1.0.0, normalized when supported); it remains neither lineage nor authority
Governed enforcement object mutated before bindingADR-0007 readiness checks exact planned identity and content before bindingOnly implemented backend adapters can provide readiness semantics
Backend reconciliation fails or times outReadiness failure, timeout, or identity mismatch stops binding and exits as a blocking failureEnforcement resources applied earlier can remain in the cluster
Partial multi-artifact applicationApplication is ordered; the first failure stops the sequence and binding stays lastApply is sequential, not transactional, and does not roll back earlier resources
Applied mistaken for enforcedDocumentation and status keep API application separate from backend realizationGeneric Kubernetes API success cannot prove kernel or datapath behavior
Enforced mistaken for verifiedBehavioral verification is a separate evidence gateverify currently checks Landlock ABI compatibility; it is not general runtime verification

These controls implement fail-closed authority and binding behavior: ambiguity, missing approval, stale content, unsupported SPO semantics, lineage failure, readiness timeout, or governed identity mismatch prevents the affected governed transition. They do not make the whole apply transaction atomic, authenticate human intent beyond Kubernetes controls, or prove external enforcement.

4. Runtime validation

Once a profile is deployed (via PodLock), try to bypass it:

  • Can a pod escape its Landlock policy?
  • Can the traced process detect that it’s being observed and change its behavior during the training run (evasion)?
  • Can the human review workflow be bypassed in practice?

5. CI hardening

  • Integrate a SAST/SCA scan (gosec, Trivy) on the project’s Go code, in .github/workflows/ci.ymlsecurity job, separate from build-and-test (not yet a required status check: first results need triaging before making it blocking).

Enforcement prerequisites

landlock-genprof generates profiles; it never enforces them itself (see architecture.md §1 — “this tool’s job ends” at kubectl apply). Three different external controllers are what actually enforce what gets generated. This doc exists because none of that was documented anywhere before — someone could get the CLI itself fully working and still hit a wall trying to see real enforcement, with no pointer to why.

Generated artifactEnforced bySet up by this repo?
profile.yaml (LandlockProfile)PodLock operator (Kubewarden)No — see the limitation below
{pod}-networkpolicy.yamlAny CNI that implements NetworkPolicyYeshack/init-vm.sh installs Cilium
{pod}-seccompprofile.yaml (SeccompProfile CR)security-profiles-operator (SPO)No — opt-in, see below

CNI (NetworkPolicy enforcement)

Already handled: hack/init-vm.sh creates the kind cluster with the default CNI (kindnet) disabled and installs Cilium instead — kindnet does not implement NetworkPolicy at all, so a generated networkpolicy.yaml would apply successfully and enforce nothing, silently. Nothing extra to do if you ran the current version of that script.

security-profiles-operator (SPO) — opt-in, and not installed by default

Only needed if you want a generated {pod}-seccompprofile.yaml to actually be materialized as a localhostProfile on the node. Not opt-in for kubectl apply/apply-proposal themselves, though — confirmed live: with nothing SPO-related installed (this project’s reference kind cluster’s default state), applying the SeccompProfile artifact fails outright with the server could not find the requested resource — no CRD registered, not a graceful “created but inert” fallback. That only holds if SPO’s CRD specifically has been installed without its controller, an unusual, deliberate scenario — not what “SPO not set up” means for anyone following this doc from scratch.

This project targets SPO v1.0.0, which serves SeccompProfile at security-profiles-operator.x-k8s.io/v1 and, since v0.9.0, cluster-scoped. That API shape lives in internal/spobackend, which is the single authority on it — the exporter, the generic apply path and ADR-0007’s readiness gate all ask that package rather than restating any of it.

test/e2e/install-spo.sh performs exactly the steps below and is what the SPO Interop E2E workflow runs.

Requires cert-manager first — SPO’s CRDs carry cert-manager.io/inject-ca-from annotations, so its webhook never becomes ready without it:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml
kubectl -n cert-manager wait --for=condition=Available deployment --all --timeout=300s

Then SPO itself. The v1.0.0 manifest is self-contained: it creates the security-profiles-operator namespace with the privileged pod-security.kubernetes.io/* labels already set, and pins its image to registry.k8s.io/security-profiles-operator/security-profiles-operator:v1.0.0. No namespace labelling step and no image overrides are needed.

kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/security-profiles-operator/v1.0.0/deploy/operator.yaml
kubectl -n security-profiles-operator rollout status deployment/security-profiles-operator --timeout=300s

The spod DaemonSet is not in that manifest — the operator creates it from a SecurityProfilesOperatorDaemon resource after it starts, so wait for it separately once it appears:

kubectl -n security-profiles-operator rollout status daemonset/spod --timeout=300s

Verify the API this project actually targets, rather than assuming:

kubectl get crd seccompprofiles.security-profiles-operator.x-k8s.io \
  -o jsonpath='{.spec.scope}{" "}{.spec.versions[*].name}'
# expect: Cluster v1

Historical: the v0.8.4 workarounds are obsolete

Earlier revisions of this document pinned SPO v0.8.4 — the last release serving a namespaced SeccompProfile — and carried two workarounds for bugs in that release’s Helm chart. Both were verified against the v1.0.0 manifest and no longer apply:

  1. The chart’s spoImage.tag defaulted to latest against a staging registry, so the operator crashed on startup. deploy/operator.yaml pins a real registry.k8s.io image at the release version.
  2. The spod metrics sidecar was hardcoded to gcr.io/kubebuilder/kube-rbac-proxy, a discontinued registry path, producing ImagePullBackOff with no chart value to override it. v1.0.0 contains no kube-rbac-proxy or RELATED_IMAGE_RBAC_PROXY reference at all.

A third historical note, clock_gettime missing from v0.7.1’s own self-applied profile, was fixed upstream in v0.8.3 and is likewise not a concern here.

Do not reintroduce any of these against v1.0.0.

What SPO interoperability currently demonstrates

Demonstrated (SPO Interop E2E, test/e2e/spo-interop.sh): landlock-genprof generates a native cluster-scoped SeccompProfile v1, governs it through review and digest-bound approval, applies it via the governed apply path, waits for real SPO reconciliation (status.localhostProfile), verifies the live enforcement content still equals the approved content, and binds the workload only after the profile is ready.

Current demonstrated boundary:

  • SPO observation feeding landlock-genprof. A real ProfileRecording is imported explicitly as derived policy; merged-provenance and optional v1 coverage behavior are demonstrated by the real-node run documented in PROGRESS.md.
  • Behavioral syscall enforcement. Run 32561123023 proved the bounded candidate experiment: getpid succeeded under the approved profile, while naturally absent getpriority succeeded in the unconfined control and returned EPERM after governed application. This does not claim universal least privilege or complete Seccomp verification.

PodLock — not supported on this project’s reference environment

PodLock’s own documentation explicitly advises against this project’s entire kind-based setup:

“It is not recommended to run PodLock with clusters spawned by kind, the nodes should be running inside a VM or physical machine with Landlock support.” — PodLock quickstart

kind nodes are Docker containers, not separate VMs — exactly the setup PodLock’s docs warn about. (minikube is explicitly ruled out too, for the same underlying reason: its VM doesn’t support Landlock.) This is specific to PodLock’s own operator — it is not a limitation of landlock-genprof or of Landlock itself: the tracer’s own use of Landlock-adjacent kernel features works fine on this project’s kind setup (confirmed repeatedly, see docs/roadmap.md/docs/e2e-demo.md), because those syscalls hit the VM’s real host kernel directly. It’s specifically PodLock’s controller that has trouble with nodes that are themselves containers.

What this means in practice:

  • Generating profile.yaml always works on kind — it’s a local file, no cluster object involved.
  • kubectl apply/apply-proposal applying it as a LandlockProfile object does not work on this project’s reference kind setup by default — confirmed live: fails with the server could not find the requested resource, since nothing here installs PodLock’s CRD at all. It would only succeed if you’d separately installed PodLock’s CRD (even without its controller/webhook) — not something this repo or its docs currently walk through, on purpose, given the limitation below.
  • Actually seeing PodLock’s operator arm Landlock on the target pod at runtime does not work reliably on kind even with PodLock installed. Don’t stage or claim either of the above in a demo (see demo/script.md) unless you’ve set up a real VM-per-node environment and verified it live.

If you actually need to verify live PodLock enforcement: follow PodLock’s own quickstart with Lima instead of kind — out of scope for this repo’s own setup scripts, since it would mean maintaining a second, incompatible reference environment alongside the kind-based one everything else here assumes.

Architecture Decision Records

An ADR records one architectural decision this project has actually made — what was decided, why, and what it costs — so a future contributor doesn’t have to reconstruct the reasoning from commit messages or code comments alone.

Why this project uses ADRs

Several real, consequential decisions currently live only in scattered places — a comment in a package’s own source, a paragraph inside a larger design doc, an RBAC manifest’s own header. An ADR gives one such decision a single, indexed, discoverable home, without duplicating the reasoning that already exists elsewhere: an ADR points to the code or doc where a decision is explained in detail, it doesn’t restate it.

ADR vs. RFC

  • ADR — decided. Records a decision this project has actually made, usually already implemented. Written after the fact, not as a proposal.
  • RFC — proposed. A design under discussion, not yet decided — may end up accepted, rejected, or deferred. This project doesn’t have an RFC directory yet. When one exists, an RFC that gets accepted becomes an ADR at that point, not before — the two are never the same document at different stages, they’re different documents with a hand-off between them.

Don’t write an ADR for something still being debated — that’s an RFC’s job once this project has a place for those. Don’t leave a real, shipped decision undocumented either — that’s the gap this index exists to close.

Status and immutability

Once an ADR is Accepted, it’s a historical record — its content doesn’t change to reflect new information or a change of mind. If a later decision changes course, it gets its own new ADR, and the earlier one is marked Superseded by ADR-NNNN — never edited to pretend the original reasoning was different than it actually was.

Statuses used here: Proposed, Accepted, Superseded, Deprecated.

Index

#TitleStatusDate
0001Exit-code contract (0/1/2/3)Accepted2026-08-07
0002Generic Behavior IR as the exporter boundaryAccepted2026-08-08
0007Governed apply ordering and enforcement readinessAccepted2026-08-19
0008SPO derived-policy import boundaryAccepted2026-08-19
0009SPO merged SeccompProfile provenance and target separationAccepted2026-08-21
0021Bounded Workbench Kubernetes read authorityAccepted2026-08-31
0022Workload security projection proof levelsAccepted2026-09-01
0023Local Workbench HTTP trust boundaryAccepted2026-09-02
0024Durable ApplyAttempt mutation custodyAccepted2026-09-03
0025Explicit rollback with strict custodyAccepted2026-09-03
0026Observation identity and record decompositionAccepted2026-09-05
0027ClusterIdentity resolution modelAccepted2026-09-05
0028Workload identity, ContainerSlot, and revision splitAccepted2026-09-05
0029Observation persistence adapter boundaryAccepted2026-09-05
0030Executor claim and lease mechanismAccepted2026-09-05
0031candidate-v1 digest boundaryAccepted2026-09-05
0032Per-source EMPTY and UNKNOWN precondition ruleAccepted2026-09-05

Adding a new ADR

Copy 0000-template.md, number it sequentially (0001, 0002, …), fill it in, add a row to the index above.

Architecture governance guide

See docs/architecture/governance.md for the project’s architecture governance, review methods, identity-sensitive change policy, and ADR/RFC lifecycle guidance.

Demonstrated capabilities

This page is the canonical record of what the project has demonstrated. It is deliberately separate from the product roadmap, which records intended future capabilities.

How to read status claims

The lifecycle terms are cumulative and must not be collapsed:

TermMeaning
ObservedRuntime behavior was captured by an identified source
DerivedEvidence was transformed into a policy artifact
ProposedArtifacts were assembled into a candidate
ReviewedA human inspected that candidate
ApprovedHuman authority was bound to the exact candidate digest
AppliedAn approved artifact was submitted to its API
EnforcedThe external backend realized the policy
VerifiedA behavioral check proved the expected restriction

Code existence, artifact generation, and API persistence are not evidence of behavioral enforcement.

Current v0.7 technical baseline

Authoritative source SHA: dfe80b3e2626cb1ba12c5ca7ad978f7fca0e85ec Authoritative tree: ba9edb71fd329e0e7ae6cc39d284f65e2f1b7b64

The v0.7 technical baseline is COMPLETE. It is implemented and certified but not yet released or pre-release authorized.

GateCertified boundary
Observation architectureImmutable identity/spec, durable lifecycle, bounded attribution, frozen result/provenance, and first-class uncertainty
G5-CSContainer-scoped attribution for filesystem, exec, network connect, network bind, and capabilities
G6Normalized Observation evidence, PopulationScope v2, idempotent contribution effect, and Observation → TrainingHistory contribution
G7Candidate-v2, distinct ReviewContextDigestV2, approval custody, and governed Proposal derivation
G8Trusted-local Start, Stop, Status, and Generate Observation API with real kube-apiserver concurrency qualification
G9Workload-centric Observation Workbench, durable Observation/Proposal read models, rediscovery, and browser authority absence
G10Final integration path from workload-bound Observation through bounded evidence and CONTAINER contribution to candidate-v2 Proposal; G10-DEFECT-01 schema closure included

The maximum claim remains bounded: v0.7 provides an evidence-driven workload governance path while preserving uncertainty, provenance, custody, and the separation of browser visibility from governance authority. It does not claim complete workload behavior, complete least privilege, global enforcement verification, exactly-once execution, transactional apply, or Proposal workload-UID binding.

Accepted engineering-only debts are recorded in the contributor/testing documentation: the legacy E7 nil result in TestObservationContributionEnvtestE1ToE7, and the non-authoritative fake-client markerless-provenance signature in TestReceiptConcurrencySameKeyConvergesOnOneEffect. Real kube-apiserver same-key convergence is certified.

Current capability ledger

CapabilityStatusDemonstrated scopeRemaining limit
Evidence model and internal trace acquisitionDoneWorkload filesystem, network, syscall, and capability evidence can be captured and validatedThe internal tracer is an acquisition adapter, not a claim over external observations
Multi-run learningDoneTrainingHistory, seenInRuns, and confidence behavior demonstrated across three runsSPO-derived syscalls are structurally excluded from this history
Candidate generationDoneFilesystem, NetworkPolicy, seccomp, and capability artifacts can form one SecurityProfileProposalGenerated does not mean authorized
Candidate identityDoneSelected proposal fields produce a deterministic sha256: candidate digestDigest vectors must evolve with the proposal schema
Governed approvalDoneReview, digest-bound approval, rejection, and persisted mechanism version demonstratedReviewer UX and structured rationale remain incomplete
Stale-authority rejectionDoneChanged, revoked, missing, or mismatched approval fails closed before applicationNew approval states require equivalent negative coverage
Governed applyDoneapply-proposal reloads and revalidates the candidate, applies in governed order, and binds the workload lastSuccessful multi-artifact application is sequential, not transactional
NetworkPolicyVerified on CiliumGenerated, approved, applied, realized, and fresh connections deniedThis does not generalize to every CNI or workload
PodLock/Landlock artifactApplied path onlyGeneration and approval-bound API application are implemented and testedPodLock consumption and kernel denial are not demonstrated
SPO reconciliationE2E-provenA governed cluster-scoped SPO SeccompProfile was applied, reconciled, identity-checked, bound to a running workload, and exercised at the syscall boundary in real-node run 32561123023Evidence covers the tested candidate and syscalls, not every profile or runtime combination
SPO-derived policy importE2E-provenReal ProfileRecording with mergeStrategy: Containers produced two partial profiles and an exact merged union; explicit merged-provenance import, v1 coverage normalization, widening review, approval, apply, and stale-authority rejection were demonstratedCoverage remains optional informational metadata; contributor lineage, confidence, and authority are not inferred
Governed Seccomp runtime boundaryE2E-provenIn run 32561123023, approved getpid succeeded and naturally absent getpriority succeeded in control but returned EPERM under the applied governed profileThis is a tested behavioral boundary, not universal least privilege or complete Seccomp verification
Candidate explanation and diffDoneexplain and diff render evidence and candidate changesBroader assurance/rationale UX remains future work
Operator/controller reconciliationNot startedNo controller capability is claimedRequires a separate product contract and E2E evidence

SPO source boundary

SPO owns syscall observation and produces the real derived SeccompProfile in SPO mode. landlock-genprof does not claim those syscalls as its own observations, does not insert them into TrainingHistory, and does not invent confidence for them.

landlock-genprof observes filesystem and network behavior in that mode. It imports the SPO result as derived policy, creates a governed snapshot with provenance, includes it in the candidate digest, and requires human approval of that exact candidate. The SPO source object is not mutated and supplies no deployment authority.

The normative contract is ADR-0008. Apply ordering and fail-closed readiness are defined by ADR-0007.

Authoritative evidence

  • Buyer-facing demo run 33332753584, SHA bfc3132ba4d51db5ca3b6dff84467a4a99f8436f: the documented real-node scenario completed successfully. It produced candidate A sha256:3432b896c62d6d386abcb5a59c23afa7341881edde1c9cb807abeb52cb4f4e8d and candidate B sha256:dba2222af3d77530effca0d2f01b954bead53501a876e4085f6dde9ce89348f8; the stale approval for A was refused before application with the canonical digest-mismatch error, the real SPO source was shown as derived policy, and candidate B reached the governed apply path. The instrumented buyer cut measured BUYER_DEMO_ELAPSED_SECONDS=94.468, excluding preparation and cleanup. This demonstrates the scenario and timing in that tested environment; it does not prove universal compatibility, global minimality, or behavioral verification for every backend.

  • Core E2E 32037183484, SHA 902f99228203a27aeb52da11f301760d8bc5ff60: multi-run confidence, proposal generation, digest-bound approval/apply, and Cilium NetworkPolicy behavioral verification.

  • SPO Interop E2E 32230551571, SHA 0e6ce7062f0cfd1b80bc42f654e371ae2a275f65: SPO v1 profile application, reconciliation, governed identity, and workload binding. This is not syscall-denial evidence.

  • SPO D-MIN E2E 32264419754: real SPO recording, derived-policy import, governance, approval, apply, and a running bound workload. This is not a claim that SPO output is raw landlock-genprof evidence.

  • SPO merged-provenance E2E 32561123023, SHA 5fb93a45aa724e9b1a9021b96ad1da1b54911bde: two real Containers contributors, exact union and #3355 coverage (v1, total=2), widening visibility, normalized digest/approval, stale-authority rejection, and governed runtime Seccomp behavior. getpid was present and succeeded; naturally absent getpriority succeeded in control and returned EPERM after apply. The target referenced operator/lg-v1-merged-target-2ed57712c490f4d5.json and the reviewed digest was sha256:f0d4f5116d6aca3dc0233ff15fbaea914411ffafc93602338a29be2bc432b5e3.

Current verification gates

  1. Install a compatible PodLock environment and demonstrate Landlock behavioral denial.
  2. Define capability/security-context verification evidence separately from artifact application.
  3. Complete the reviewer rationale and assurance experience without weakening digest-bound authority.

Historical v0.5.0 Cluster Workbench release status

G0, G0.5, G1, G1.5, and G1.6 are closed. G2, G3, G4, and G5 are CERTIFIED_AND_MERGED. G5 merge commit: cde96c3178129146970b9a03071d2b7c4ef6bbc9; master tree: 62ccc3a88e3e49aeaa06745148f220c0573ab337. G6 is IN_PROGRESS for pilot readiness and operational closure. v0.5.0 remains NOT_YET_RELEASE_CERTIFIED.

The full G5 evidence and claim boundary are recorded in G5-CERTIFICATION.md and the pilot package. This status does not claim Landlock kernel denial, same-Pod PodLock plus application-derived Seccomp compatibility, generic NetworkPolicy behavior, or transactional apply.

G6 package status: PILOT_SUPPORT_MATRIX = COMPLETE, PILOT_RUNBOOK = COMPLETE, ABORT_ROLLBACK_PROCEDURE = COMPLETE, EVIDENCE_HANDLING = COMPLETE, and PILOT_READINESS = NOT_YET_CERTIFIED pending final gate review. A fresh customer pilot execution is not claimed by this package.

Deferred custody work remains bounded: v0.5.1 for durable ApplyAttempt and pre-state mutation custody; v0.5.2 for explicit rollback <attempt-id>; v0.6.0 for the Full Visual Workbench; and v0.7.0+ for browser mutation via an explicit mutation executor. No durable apply journal or rollback command is part of v0.5.0.

Release certification procedures belong in CONTRIBUTING.md. Historical evidence and planning documents remain in the repository but are not current status authority.

Product roadmap

This is the authoritative product roadmap. It records intended capability sequencing, not demonstrated release status. Demonstrated capabilities remains the authority for what works today and the evidence supporting each claim.

The roadmap is capability-based, not tied to release numbers or dates. A phase advances only when its trust boundary is supported by the stated acceptance evidence.

Post-v0.6.1 architecture sequence

v0.6.1 is the RELEASED / FROZEN BASELINE. v0.7 is the Observation Foundation: a workload observation becomes a durable, attributable, epistemically qualified object that can safely feed the existing proposal and governance pipeline. The authoritative decisions are ADR-0026 through ADR-0032.

The later direction is v0.8 Operate / Productize, v0.9 Continuous Assurance, and v1.0 Stable Least-Privilege Governance Contract. These are roadmap direction, not implementation or release claims. Enforcement & Verification, Governance, Drift & Continuous Learning, Detection / Continuous Assurance, Controlled Response, Productization, and Stable Product Contract remain capability areas.

Product direction

landlock-genprof is a governance layer for runtime-derived Kubernetes security policy. It preserves origin, provenance, candidate identity, human authority, and backend ownership while converging policy from sources with different trust semantics.

direct evidence -----------\
                           SecurityProfileProposal -> CandidateDigest -> human approval
SPO-derived policy --------/                                      -> governed apply
                                                                       -> external enforcement
                                                                       -> verification

The separations are permanent product constraints:

  • observation is not policy; derived policy is not direct evidence;
  • coverage is not confidence, and neither is authorization;
  • provenance and CandidateDigest describe a candidate but do not authorize it;
  • learned is not authorized;
  • applied is not enforced; enforced is not verified.

In SPO mode, Security Profiles Operator owns syscall observation and its recording lifecycle and produces the SeccompProfile. landlock-genprof imports a snapshot of that object as derived policy with provenance; it does not place SPO syscalls in TrainingHistory, invent confidence, or build a parallel SPO-equivalent observer. The investigation in SPO issue #3354 led to the merged upstream coverage contribution #3355. Subsequent maintainer feedback confirmed that this separation makes sense from the SPO side: SPO remains focused on its profile domains while cross-domain convergence happens at the governance layer. This is architectural feedback, not project endorsement or a stability guarantee.

Current state

The completed observation, multi-run learning, and governance milestones are product baseline, not future roadmap work:

CapabilityCurrent evidence-supported stateRemaining boundary
Direct acquisitionFilesystem, network, internal/advisory syscall, and applicable capability evidence are implemented; the demonstrated scope is recorded in PROGRESS.mdInternal syscall acquisition is an alternative adapter, not the SPO architecture
SPO importA real SPO-produced SeccompProfile can enter as a validated, provenance-bearing governed snapshotThe optional coverage annotation has no stable SPO API guarantee
LearningTrainingHistory and cross-run confidence are implemented for direct evidenceSPO-derived syscalls are structurally excluded; coverage never becomes confidence
Candidate governanceSecurityProfileProposal, deterministic candidate-v1 identity, review, exact-digest approval, and stale/mutated-candidate rejection are implemented and demonstratedReviewer rationale and assurance UX remain incomplete
Governed applyApproved artifacts are planned, revalidated, applied in dependency order, and the workload is bound last after supported readiness checksApplication is sequential, non-transactional, and has no rollback
Enforcement and verificationNetworkPolicy denial is demonstrated on Cilium; the SPO/Seccomp path has real-node merged governance and a tested behavioral denial boundaryEvidence is candidate/syscall-specific; PodLock/Landlock kernel denial and capability verification remain unproven, and results are not portable across every backend

The exact demonstrated scope and run identifiers live in PROGRESS.md. Roadmap intent never upgrades that ledger.

v0.5.0 Cluster Workbench gate status

G0, G0.5, G1, G1.5, G1.6, G2, G3, and G4 are closed, with G2–G4 merged. G5 is CERTIFIED_AND_MERGED: its product evidence is bound to 5881f9807395d1eee8e89426f32110867f41e8fb (tree 457dbfc7be3f2096ef2fa1f5b5164222d44f3051) under the narrowed claim in the G5 certification record. G6 is IN_PROGRESS for pilot-package and release closure; v0.5.0 remains NOT_YET_RELEASE_CERTIFIED until the G6 decision is complete. The product evidence SHA remains distinct from the documentation/certification-record SHA.

The bounded future sequence is explicit: v0.5.1 adds durable ApplyAttempt and pre-state mutation custody; v0.5.2 may add explicit rollback <attempt-id>; v0.6.0 may address the Full Visual Workbench; and v0.7.0+ may address browser mutation through an explicit mutation executor. These are not v0.5.0 capabilities. The future ApplyAttempt invariant is:

Persist(IntentToMutate(Mi) + Before(Mi)) happens-before Mutate(Mi)

No mutation is permitted if pre-state persistence fails; future records must distinguish Before, IntendedAfter, and ObservedAfter, including OUTCOME_UNKNOWN.

Next engineering work

Phase A — External policy and provenance boundary

Objective: Keep core SeccompProfile import independent from optional, unstable SPO metadata while preserving truthful provenance.

Trust boundary: SPO-derived policy must not be reclassified as landlock-genprof observation, and optional annotations must not become an accidental hard API dependency.

Deliverables:

  • Document the exact SPO API fields, annotations, and formats consumed by the importer.
  • Introduce a version-aware compatibility contract for optional syscall-coverage metadata, including absent, malformed, and unsupported forms.
  • Keep valid core SeccompProfile enforcement content importable when optional coverage metadata is absent.
  • Preserve the existing snapshot, lineage, source-selection, TrainingHistory, confidence, and no-live-reference boundaries.
  • Decide through an ADR before changing candidate-v1 whether normalized optional metadata remains approval-relevant provenance. Today the copied provenance annotation is part of the governed artifact and therefore part of CandidateDigest.

Acceptance evidence: focused compatibility tests cover known, absent, malformed, and unsupported metadata; an SPO interop run proves core import without the annotation; review output labels coverage without converting it to confidence; consumed metadata is documented against tested SPO versions.

Non-goals: treating SPO annotations as stable, parsing SPO policy as raw evidence, silently falling back to internal syscall collection, or supporting SELinux/AppArmor through this import contract.

Phase B — Authorization integrity

Objective: Extend the existing exact-digest authority proof across every candidate input and compatibility path.

Trust boundary: Authority for one reviewed candidate must never migrate to changed direct evidence, changed derived policy, or differently interpreted provenance.

Deliverables:

  • Preserve deterministic candidate identity and explicit approval under a versioned mechanism.
  • Add reproducible adversarial scenarios for direct-evidence change after approval and SPO-policy re-import after approval.
  • Maintain fail-closed rejection for missing, malformed, revoked, stale, or mismatched authority and for candidate mutation during readiness waits.
  • Define migration gates before any candidate schema or digest mechanism change.

Acceptance evidence: experiments produce C1/D1, mutate direct evidence or the SPO snapshot to C2/D2, and prove authority for D1 cannot authorize C2; digest vectors remain deterministic; apply-time and pre-binding mutation tests continue to fail closed.

Non-goals: per-domain approval, automatic approval, treating digest equality as human authority, or inferring reviewer intent.

Phase C — Application versus enforcement

Objective: Make successful governed apply report only what each backend transition actually establishes.

Trust boundary: Kubernetes API acceptance and backend readiness are not equivalent to active kernel or datapath enforcement.

Deliverables:

  • Define backend-specific states for submitted, API accepted, reconciled/ready, and behaviorally enforced where observable.
  • Complete readiness semantics for supported backend paths without transferring authority from a backend to the proposal.
  • Make sequential partial failure and absence of rollback explicit in status and operator guidance.
  • Preserve external ownership: PodLock/Landlock for filesystem, the CNI for network, Kubernetes/container runtime for capabilities, and SPO/container runtime for seccomp.

Acceptance evidence: an approved multi-artifact scenario records each transition, injects a backend failure, proves later artifacts and workload binding stop, and reports earlier applied resources without calling the operation transactional or enforced.

Non-goals: claiming landlock-genprof is the enforcement engine, generic readiness inferred from API creation, or silent rollback claims.

Phase D — Backend-specific verification

Objective: Define and demonstrate what verified means independently for each supported enforcement backend.

Trust boundary: Reconciled API state must not be presented as behavioral or kernel-level evidence.

Deliverables:

  • Preserve the demonstrated SPO/Seccomp boundary: approved-policy membership, target binding, and a control-vs-governed allow/EPERM experiment.
  • Demonstrate allowed and denied filesystem behavior in a compatible PodLock/Landlock environment.
  • Retain the Cilium NetworkPolicy experiment and add an explicit portability boundary for other CNIs.
  • Define capability/security-context verification evidence separately from artifact application.
  • Keep Landlock ABI compatibility checks distinct from workload behavioral verification.

Acceptance evidence: the SPO/Seccomp boundary is recorded by real-node run 32561123023; each remaining backend experiment must record the approved digest, applied resource identity, readiness evidence where applicable, and both allowed and denied workload behavior at its enforcement layer. Results are reported per backend, never generalized from one implementation.

Non-goals: a universal verification claim, equating verify ABI diagnostics with runtime proof, or upgrading reconciliation to behavioral enforcement.

Phase E — Assurance and adoption

Objective: Make the governance contract reproducible and supportable outside creator-operated environments.

Trust boundary: A successful demonstration in one controlled cluster is not a stable, portable product contract.

Deliverables:

  • Reproducible adversarial E2E scenarios and compatibility matrices for Kubernetes, kernels, runtimes, CNIs, PodLock, and tested SPO versions.
  • Fuzzing and security tests for import, canonicalization, approval, and apply boundaries.
  • Signed artifacts, SBOMs, upgrade/migration tests, diagnostics, and installation hardening.
  • Structured reviewer rationale and assurance UX without weakening exact-digest approval.
  • External workload pilots with documented failure modes and evidence retention.

Acceptance evidence: clean-environment runs reproduce the trust-boundary experiments; supported combinations have explicit results and limitations; upgrades preserve or deliberately invalidate authority according to a tested migration contract; external users complete install, review, approve, apply, and backend-specific verification without creator intervention.

Non-goals: claiming universal cluster support, replacing external security review with CI, or treating adoption activity as implementation evidence.

Longer-term direction

Controller reconciliation, drift handling, detection, and controlled response remain possible later capabilities, not the next default milestones:

  • A controller may reconcile approved intent but must never approve, silently replace, or broaden a candidate.
  • Drift may explain change or create a new proposal; changed content must make prior authority stale.
  • Detection may use confidence-aware evidence but must preserve provenance and uncertainty.
  • Response actions must have their own explicit, auditable authority and must not inherit authority from detection.

These capabilities begin only after the relevant provenance, authorization, application, verification, and assurance gates above are explicit. Release numbers remain undecided; repository history shows patch releases in the current line but no contract mapping the next maturity phase to a semantic version.

Engineering and evidence trajectory

The same work supports product maturity and the technical thesis “Learned Is Not Authorized: Trust Boundaries for Runtime-Derived Kubernetes Security Policy.” The thesis must emerge from reproducible mutation, compatibility, partial-failure, and backend-verification experiments—not from product positioning. Additional engineering resources are most useful where they expand compatibility infrastructure, adversarial assurance, backend-specific verification, reviewer UX, documentation, and external pilots.

Normative boundaries remain in ADR-0007 and ADR-0008. Changes to those decisions require a new ADR; the roadmap cannot silently supersede them.

Versioned docs site

The mdBook site (idriss-eliguene.github.io/landlock-genprof/) is published per-version, with a selector to jump between them — not just whatever master currently looks like.

How it works

  • gh-pages branch, not the Actions-artifact Pages deployment method — each version is built once and persists as static files, cheap to serve on every subsequent request. Repo Pages settings: “Deploy from a branch” → gh-pages (build_type: legacy, checked via gh api repos/.../pages).
  • One subdirectory per version: /v0.1.1/, /v0.1.2/, /master/ (the unreleased, currently-in-development docs). The site root / is a redirect to the latest tagged version, not /master/.
  • versions.json at the gh-pages root lists every published version; version-selector.js/.css (also at the root, so every version’s pages can reference the same copy) read it and inject a dropdown into mdBook’s own menu bar (#menu-bar .right-buttons).

Building a version

hack/build-versioned-docs.sh <git-ref> <output-dir>

Builds <git-ref>’s book/ (via a git worktree, so it doesn’t disturb your actual checkout) using this checkout’s mdbook/mdbook-mermaid/Go tooling — not whatever was pinned in that ref’s own CI config. Older tags don’t need a working toolchain of their own kept alive forever, and the version selector doesn’t need to have existed in that tag’s source at all: it gets injected into the built HTML afterward, unconditionally.

Skips silently (not an error) if the ref has no book/ directory at all — true for v0.1.0, before the mdBook site existed.

How publishing works now (automated, staging-first)

Three workflows, each touching only its own path — a change never reaches a production path without a human having reviewed a live preview of it first:

  • docs-preview.yml — every PR touching doc-relevant paths gets its own build, deployed to gh-pages under pr-preview/pr-<number>/, with a comment on the PR linking to it. Cleaned up automatically when the PR closes.
  • docs.yml — only on an actual push to master (i.e., after a PR has merged): rebuilds master and deploys to the real /master/ production path.
  • docs-release.yml — only on a vX.Y.Z tag push: builds that tag, deploys to its own /vX.Y.Z/, and updates versions.json + the root redirect to point at it as latest.

None of the three ever writes to a path another one owns. This exists specifically because the first rollout of this whole mechanism skipped straight to editing production directly (see the gotchas below) — twice.

Adding a version manually (rare — normally the workflows above do this)

  1. hack/build-versioned-docs.sh <ref> <tmpdir>
  2. git worktree add /tmp/landlock-gh-pages gh-pages
  3. Copy <tmpdir> to gh-pages/<version>/
  4. For a new release specifically (not master): also run hack/update-doc-versions-manifest.py gh-pages/versions.json <version>
  5. Commit and push directly to gh-pages — this branch holds generated output, not reviewed source, so it doesn’t go through a PR. The two scripts above are source and do.

Gotchas that actually broke production (twice, before the staging

workflow above existed)

Both hit during the very first rollout, both fixed by pushing straight to gh-pages at the time — which is exactly the shortcut the workflows above now exist to stop happening again.

1. Every absolute path needs the /landlock-genprof/ prefix. This is served from idriss-eliguene.github.io/landlock-genprof/ — a project page, not a user/org root page (idriss-eliguene.github.io/). version-selector.js, the injected <link>/<script> tags, and the root index.html redirect all assumed domain-root hosting at first — a bare /v0.1.2/ resolves to the wrong path entirely and 404s. Verified with curl at the time, which doesn’t follow <meta http-equiv="refresh"> or execute JS, so the bug was invisible to that check and only showed up in an actual browser.

2. gh-pages needs a .nojekyll file at its root, not just inside each version subdirectory. mdBook generates one per build (inside book/dist/), which lands inside v0.1.2/, master/, etc. — but GitHub Pages only skips Jekyll processing for the whole site if that file sits at the branch root. Without it, GitHub Pages ran everything through Jekyll, which is why index.html files that clearly existed in the branch (confirmed via the contents API) still 404’d on the live site. Also discovered mid-incident: the old Actions-artifact-based docs.yml was still running and “succeeding” on every push even after the repo’s Pages source was switched to branch-based (legacy) — dead weight at best, a second confusing deployment path at worst. Removed entirely as part of adding the staging workflow above.

Verify any future change to these paths (or to Pages settings) by actually navigating the live site, not just curling individual files for a 200 — that’s exactly what missed both of these.

Contributing to landlock-genprof

Thanks for considering a contribution. This project generates least-privilege Kubernetes security profiles (Landlock, seccomp, NetworkPolicy, capabilities) from observed workload behavior, then requires human review before anything is ever applied — see README.md for the full pitch, and docs/product-definition-v1.md, docs/product-design-v1.md, and docs/product-roadmap-v1.md for where the product is headed. See GOVERNANCE.md for how decisions get made, MAINTAINERS.md for who makes them, and CODE_OF_CONDUCT.md for the expected conduct.

Before you start

  • Skim docs/architecture.md for how the pieces fit together (tracer → IR → exporters → CLI → cluster objects) and docs/roadmap.md for what’s already built and why, in the order it was built.
  • For anything non-trivial (a new exporter, a new flag, a behavior change), open an issue or a draft PR describing the approach before writing a lot of code — this project has a strong “confirm the nuance before building” habit (see how many docs/roadmap.md entries start with “confirmed via real source/live testing”); it’s cheaper to align early.
  • Small, focused PRs over large ones. One exporter, one bug fix, one flag — not a grab-bag.

Development setup

  • Go, per go.mod. go build ./... works on macOS/Windows too — internal/tracer.Trace() compiles to a stub there (the Inspektor Gadget Go SDK is Linux-only), so cross-platform contributors can still build and work on everything except the tracer itself.
  • For anything touching internal/tracer or needing a real cluster (RBAC, CRDs, live trace runs), you need a Linux box with kernel ≥ 6.8 and a kind cluster with Inspektor Gadget — see HOW_TO_START.md for the full VM/cluster setup (French version: COMMENT_COMMENCER.md), or make init-vm/make check-kernel.
  • No cluster available? make docker-test runs the real Linux build/test (including internal/tracer) in Dockerfile.dev, without needing a VM or cluster — the closest local equivalent to CI for the parts that don’t need a live cluster.

Before opening a PR

go build ./...
GOOS=linux go build ./...   # internal/tracer only compiles for real on Linux
gofmt -l .                  # must print nothing
go vet ./...
go test ./...

All of these are exactly what .github/workflows/ci.yml’s build-and-test job runs — matching it locally before pushing saves a round trip. The security job (gosec, Trivy) runs too; both build-and-test and security are required checks on master. Run gosec ./... locally (go install github.com/securego/gosec/v2/cmd/gosec@latest) before pushing anything that touches conversions, file paths, or subprocess calls — it’s fast and catches this class of bug before CI does.

For changes crossing the Observation, contribution, or Workbench read-model boundaries, also run the repository-supported real API-server qualification:

make envtest

Real kube-apiserver results are authoritative for resourceVersion, status-subresource, Create/AlreadyExists, and concurrent Update behavior. Fake-client concurrency results are not authoritative for those properties. The accepted engineering-only debts are the legacy E7 nil result in TestObservationContributionEnvtestE1ToE7 and the diagnostic markerless- provenance signature in TestReceiptConcurrencySameKeyConvergesOnOneEffect; real kube-apiserver same-key convergence is certified. Do not weaken production invariants to satisfy that fake-client signature.

The v0.7 integration boundary is:

workload → durable Observation → bounded evidence → CONTAINER contribution
         → candidate-v2 Proposal → read-only Workbench projection

Approval, application, enforcement, and behavioral verification remain separate authority boundaries.

Code conventions

  • No comments explaining what the code does — names should carry that. Comments exist for the why: a non-obvious constraint, a real bug a test caught, a decision made after checking real upstream source instead of guessing. Skim any file under internal/ for the tone — comments here routinely cite the exact source/version confirmed, or the specific test that caught a bug, rather than asserting from theory.
  • Confirm, don’t guess, against real schemas. When generating a manifest for another project’s CRD (PodLock, security-profiles-operator, …), verify field names/behavior against that project’s actual source or docs — several bugs in this codebase’s history came from an initial guess that turned out wrong (see docs/roadmap.md’s entries on pkg/podlock and pkg/spo).
  • Only report what was actually observed. Exporters never infer “safe defaults” (e.g. runAsNonRoot, privileged) for something that wasn’t seen during a training run — see docs/policy-synthesis.md.
  • Never auto-apply anything. The CLI may write YAML, publish a review object, or execute the explicit governed application path; none of these add implicit application authority. The explicit apply-proposal and rollback CLI paths are governed, sequential, and nontransactional. The browser remains read-only. Any new feature that touches the cluster should stay read-only unless there’s a very strong, explicit reason otherwise (see how --restart’s write access is deliberately isolated into its own opt-in RBAC manifest, docs/threat-model.md §1).

Commit messages

This repo uses Conventional-Commits-style subjects: type(scope): imperative summary, e.g. fix(k8s): strip nodeName from patched bare-pod manifests, feat(exporter): add the seccomp backend, docs: record live confirmation of the restart fix. Explain why in the body when it’s not obvious from the subject — git log is itself part of this project’s documentation trail.

Releases

Automated via release-please (.github/workflows/release-please.yml), which reads the Conventional Commits above to keep a standing “Release vX.Y.Z” PR up to date with the next version number and changelog. Merging that PR creates the real tag, which triggers .github/workflows/release.yml (cross-platform binaries via goreleaser, the Helm chart pushed to GHCR as an OCI artifact — see INSTALL.md).

Only counts commits that land via a merged PR — the workflow trigger is pull_request: types: [closed] filtered to merged == true, not a plain push to master. A commit pushed directly to master (bypassing review) won’t appear in the next release PR until an actual PR gets merged. Deliberate: a release should only ever account for reviewed work, not whatever happened to reach master however it got there.

The decision is driven by the PR title, not the commits inside it. Squash-merge is the only merge method allowed on this repo, with the squash commit’s subject forced to the PR title (squash_merge_commit_title: PR_TITLE) — so master only ever gains one commit per PR, and that commit’s message is exactly the PR title. Give the PR title itself a Conventional Commits subject (fix(doc): ..., feat(exporter): ...) — individual in-PR commit messages can be looser (the local .githooks/commit-msg hook still checks each one, but that’s about commit hygiene during review, not what release-please reads after merge). .github/workflows/pr-title-lint.yml enforces this on the PR title itself before merge, same type list as the local hook.

The release PR also bumps the “current version” mentions in INSTALL.md, README.md, demo/script.md, and book/src/index.md — configured via extra-files in release-please-config.json. Those files carry <!-- x-release-please-start-version --> / <!-- x-release-please-end --> (or single-line <!-- x-release-please-version -->) HTML-comment markers; anything between a start/end pair gets its old-version string swapped for the new one automatically. If you add another doc mentioning the current tag, either wrap it in the same markers or add it to extra-files — don’t hand-edit the version there, it’ll just get overwritten by the next release PR. This exists specifically so the tag never again points at a commit whose own docs haven’t caught up yet.

Release certification

Passing CI is not release authorization. Before a release is authorized, Core E2E, SPO Interop E2E and SPO D-MIN E2E must each have passed on the exact RC SHA — the commit the tag points at, not an ancestor and not “the branch was green last week”. The rule and its rationale are in docs/PROGRESS.md; this is how to satisfy it.

SPO Interop E2E is deliberately not a required per-PR check, so this step is where it is enforced.

  1. Identify the RC SHA: git rev-list -n1 <tag> — that value is what everything below is checked against.
  2. Get a run on that SHA. Merging the release PR produces the RC commit on master and a tag at the same commit, and both trigger the workflow, so normally the run already exists. Otherwise dispatch it: gh workflow run spo-e2e.yml --ref <tag>. Dispatch takes a branch or tag name — a bare commit SHA is not a valid ref, which is exactly why step 3 is not optional.
  3. Verify the run actually ran on the RC SHA rather than on whatever the ref resolved to at the time: gh run view <run-id> --json headSha,conclusion. Both headSha == RC SHA and conclusion == success are required.
  4. Repeat 1–3 for Core E2E and for SPO D-MIN E2E (spo-dmin-e2e.yml, which runs on a real-node k3s cluster rather than kind).
  5. Only then authorize the release (gh workflow run release.yml -f tag=<tag>, which is the manual path release-please’s tags require — see the anti-recursion note in .github/workflows/release.yml).

If SPO Interop E2E cannot pass on the RC SHA, the release may still proceed only by dropping the SPO interoperability claim from that release’s notes. Shipping the claim without the evidence is not an option.

Testing expectations

  • New behavior needs a test. This codebase has repeatedly caught real bugs this way (a status: {} leak, a stale RBAC assumption, a missing nodeName strip) — write the test that would have caught the bug you’re fixing, not just one that exercises the happy path.
  • internal/k8s tests use k8s.io/client-go/kubernetes/fake; CRD-backed packages (internal/proposal, internal/history) use k8s.io/client-go/dynamic/fake. Reuse existing fixture helpers in the matching _test.go file before writing new ones.
  • No live cluster in CI — anything that needs one is a manual, documented VM verification step (see the “confirmed live” entries throughout docs/roadmap.md and docs/e2e-demo.md), not an automated test.

Licensing

Dual-licensed, contributor’s and recipient’s choice: Apache-2.0 or MIT — see COPYRIGHT. By contributing, you agree your changes are licensed under the same terms.

Sign off your commits (DCO)

Every commit must carry a Signed-off-by trailer certifying you wrote it (or otherwise have the right to submit it under this project’s license) — the Developer Certificate of Origin, the same mechanism the Linux kernel and most CNCF projects use. It’s about provenance, not a transfer of your copyright: you keep it.

git commit -s -m "fix(k8s): strip nodeName from patched bare-pod manifests"

-s appends the trailer automatically, using your configured user.name/ user.email:

Signed-off-by: Jane Doe <jane@example.com>

Missing sign-off on an existing commit: git commit --amend -s (last commit) or git rebase --signoff <base> (a range).

Where to start

Look for issues labeled good first issue. If nothing’s labeled yet, open an issue describing what you’d like to work on — a small, well-scoped exporter gap or a missing test is always a safe place to start; see docs/roadmap.md’s milestones for what’s built and what isn’t yet.