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.
LEARNED ≠ AUTHORIZED
the boundaryRuntime 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 loopFour 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.
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
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
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>
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 stagedA 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.
One candidate, four domains
what gets governedDirect 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.
Landlock policy
→ PodLock LandlockProfile seen on every runEgress rights
→ Kubernetes NetworkPolicy seen on every runSeccomp profile
→ security-profiles-operator CR SPO-derived: provenance, no invented confidenceLinux capabilities
→ securityContext fragment review before prodWhere to start
pick oneSet up a test environment
A disposable kind cluster, from nothing — one script.
docs/test-environment → Already have oneInstall
Get the CLI, apply the RBAC/CRDs, against a cluster you already run.
INSTALL → ReferenceUsage & CLI reference
Every flag, one section each — plus a generated page per command.
docs/usage → Under the hoodArchitecture
Components and interactions, at a glance — deep dives nested underneath.
docs/architecture →Complementary, not competing
positioninglandlock-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.
PodLock
Kubewarden ecosystem — enforces at container startup generated · approval-bound · API applied kernel enforcement not demonstrated in v0.2.0security-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 claimYour CNI
any implementation of NetworkPolicy generated · approval-bound · API applied enforcement demonstrated on Cilium that result is Cilium-specific — not all CNIsChoose 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 walkthrough | Usage guide |
| Exact flags and command syntax | CLI reference |
| Components and data flow | Architecture overview |
| Security assumptions and limitations | Security model |
| Proof of what works today | Demonstrated capabilities |
| Intended future capabilities | Product 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) andkubectlis 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.shif 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) —tracedoesn’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 thego installmethod (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
Option A — go install (recommended, no clone)
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-bar → kubectl 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
docs/usage.md— training choices and the governed review, digest-bound approval, apply, and verify sequence.docs/enforcement-prerequisites.md— what’s needed to actually enforce a generated profile, and PodLock’s own real limitation onkind-based clusters specifically.docs/architecture.md— how the pieces fit together.docs/PROGRESS.md— what is demonstrated today.docs/PRODUCT_ROADMAP.md— intended future work.
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 derivedSeccompProfile. - SPO-derived syscalls do not enter
TrainingHistoryand do not receive a fabricated landlock-genprof confidence level. - A
SecurityProfileProposalis published to the cluster with four artifacts, and reduces to onesha256:CandidateDigest. approverefuses any digest that does not match the current candidate.- The approval is persisted in the resource’s status as
approvalState,approvedCandidateDigestandapprovalMechanismVersion. - Re-tracing updates the proposal spec and preserves the previous approval status — which is exactly how a stale approval arises.
apply-proposalrecomputes 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.diffshows 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.mdrecords this asBLOCKED. - 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-installprovides it), and the project’s CRDs applied. kubectl, and the CLI installed as a kubectl plugin (make install-plugin). SetLANDLOCK_GENPROF_BINto override with an explicit binary.- Linux for the observation stages: the tracer uses eBPF and is
Linux-only by design (
internal/tracer/trace_other.goreturns a clear error elsewhere). SeeHOW_TO_START.mdfor the dev VM. python3, used only to pretty-print status fields fromkubectl -o json.bash4+.
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:
| Stage | What happens |
|---|---|
| 1 | Baseline: the workload’s securityContext, no proposal, no history |
| 2 | Three real training runs, each driving deterministic behavior |
| 3 | TrainingHistory shows runsRecorded = 3 |
| 4 | explain renders per-rule confidence, rights, ABI, evidence |
| 5 | review prints the WORKLOAD SECURITY REVIEW and CandidateDigest A |
| 6 | approve --expected-digest <A>; status read back from the cluster |
| 7 | The workload starts writing a new path |
| 8 | A fourth training run; the proposal becomes candidate B |
| 9 | The approval status is still the one bound to A |
| 10 | Confirm the target NetworkPolicy does not exist yet |
| 11 | apply-proposal against the stale approval → refused, exit 1 |
| 12 | The NetworkPolicy still does not exist |
| 13 | diff shows which rule changed |
| 14 | review again → CandidateDigest B |
| 15 | approve --expected-digest <B> |
| 16 | apply-proposal → applies the NetworkPolicy |
| 17 | Final 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:
| Property | Evidence |
|---|---|
| Digest is deterministic over selected spec fields | internal/proposal/digest.go, proposal tests |
| Approve rejects a non-matching digest | internal/proposal/store.go, cmd/landlock-genprof/approve_test.go, authoritative E2E |
| Apply fails closed on a digest mismatch | internal/proposal/validate.go, TestRunApplyProposal_RejectsAfterSpecMutation |
| Re-trace preserves approval status | TestSave_DoesNotClobberApprovalStatus |
| Pre-apply revalidation after planning | TestRunApplyProposal_RejectsMutationAfterPlanningBeforeRevalidation and siblings |
| Approval cannot be forged through a spec write | internal/proposal/store_envtest_test.go (TestUpdateCannotModifyStatus), run in CI via make envtest |
| Confidence tiers from cross-run frequency | internal/policy/synthesize.go, internal/landlock/kernel.go |
| Governed apply path and artifact plan | cmd/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.
landlock-genprof acquires filesystem, network, and applicable capability evidence.
Security Profiles Operator observes syscalls and produces the real derived SeccompProfile.
Different origins. One candidate.
Direct evidence and the imported SPO artifact converge with provenance preserved.
CandidateDigestDeterministic content identity. Not authority.
Re-reads and revalidates the proposal, re-checks approval, checks implemented backend readiness, and refuses missing, stale, or mismatched authority.
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
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
review
Inspect the exact mixed-origin candidate, source provenance, applicable confidence, artifact readiness, and the candidate digest.
kubectl landlock-genprof review \
nginx-demo
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>
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 doctorfor 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 ofexplain,diff, andverify.
Continue
- Install and check prerequisites
- Use the complete governed lifecycle
- Check external enforcement prerequisites
- See demonstrated capabilities and limitations
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:
| Axis | Values |
|---|---|
| Execution | REQUESTED, STARTING, RUNNING, COMPLETING, COMPLETED, FAILED |
| Attribution | NOT_STARTED, IN_PROGRESS, COMPLETED, FAILED |
| Evidence | EMPTY, 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:
- Start an Observation through the certified API.
- Read authoritative Status; accepted does not fabricate
RUNNING. - Stop an Observation through the certified API where meaningful.
- Rediscover durable Observations after closing and reopening the browser.
- 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.
| Mode | Syscall observer | Seccomp artifact | History/confidence |
|---|---|---|---|
--seccomp-source=spo | SPO | Governed snapshot of SPO’s derived SeccompProfile | No syscall TrainingHistory; confidence not applicable |
--seccomp-source=internal | landlock-genprof tracer | Internally synthesized advisory profile | Syscalls 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.
| Flag | Output | Detail |
|---|---|---|
--candidate-out | Raw Landlock candidate JSON for explain, diff, and verify | Optional; omit the value to use the default filename |
--events-out | Raw captured events for offline synthesize | Optional; omit the value to use the default filename |
--network-out | Kubernetes NetworkPolicy | NetworkPolicy output |
--history | Persist cross-run evidence in TrainingHistory | Multi-run history |
--seccomp-out | Plain internally synthesized seccomp JSON | Internal seccomp output |
--capabilities-out | Capabilities add/drop fragment | Capabilities output |
--security-context-out | Composed securityContext fragment | securityContext output |
--report-out | Combined Markdown review report | Report output |
--patched-manifest-out | Workload manifest with generated security context | Patched manifest |
--seccomp-profile-out | SPO SeccompProfile resource | SPO 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
NetworkPolicyrequires a CNI that implements it.LandlockProfilerequires PodLock and a compatible Landlock environment.SeccompProfilerequires SPO; reconciliation and runtime materialization are external to landlock-genprof.- Workload
securityContextfields 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:
capget— a kernel-capability-version probe. Without it:OCI runtime create failed: ... unable to get capability version from the kernel: operation not permitted.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 --previousshowing a raw Go runtime panic (“The futex facility returned an unexpected error code”) insidelibcontainer.setupUser/finalizeNamespace.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.capset— applies thesecurityContext.capabilitiesevery 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 callfinalizeNamespacemakes afterchdir) — 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 yaml →
status.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 by | landlock-genprof | SPO |
Syscalls in TrainingHistory | yes | no — not collected at all |
| Seccomp confidence tier | yes | not applicable |
--seccomp-out (plain JSON) | available | rejected |
| Filesystem + network | ours | ours |
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.
| Gate | Refused when |
|---|---|
| API shape | not security-profiles-operator.x-k8s.io/v1, not SeccompProfile, or namespaced (SPO ≤ v0.8.4) |
| Inertness | the recording lacks disableProfileAfterRecording: true, or the profile is not spec.state: Disabled |
| Completeness | the profile carries SPO’s partial label, or the recording still has unmerged profiles outstanding |
| Lineage | recording-namespace, recording-id or container-id is absent or disagrees with the target |
| Enforcement content | any field outside defaultAction, architectures, syscalls[].names, syscalls[].action |
| Content sanity | no 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
- landlock-genprof abi - Inspects Landlock’s ABI-versioned right vocabulary
- landlock-genprof apply-proposal - Reviews and applies an approved, digest-bound SecurityProfileProposal
- landlock-genprof approve - Records an explicit approval decision on a SecurityProfileProposal
- landlock-genprof custody-epoch - Manages the ApplyAttempt custody qualification epoch
- landlock-genprof diff - Compares two synthesized candidates rule by rule
- landlock-genprof doctor - Checks this host’s (or a given kernel version’s) Landlock/eBPF prerequisites
- landlock-genprof evidence - Inspects raw captured evidence
- landlock-genprof explain - Explains why a synthesized candidate’s rules exist
- landlock-genprof export - Renders an already-synthesized candidate to a target output format
- landlock-genprof observe - Runs a bounded runtime Observation for a running Pod
- landlock-genprof policy - Inspects SecurityProfileProposal approval state
- landlock-genprof reject - Records an explicit rejection decision on a SecurityProfileProposal
- landlock-genprof review - Reviews a published SecurityProfileProposal
- landlock-genprof rollback - Explicitly rolls back an eligible ApplyAttempt
- landlock-genprof synthesize - Re-runs synthesis offline from previously captured evidence
- landlock-genprof trace - Starts a training run on a target pod and generates least-privilege security profiles
- landlock-genprof ui - Serves the local read-only Workbench HTTP boundary
- landlock-genprof verify - Checks a synthesized Landlock candidate against a target kernel’s ABI level
- landlock-genprof version - Prints the version
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
- landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod
- landlock-genprof custody-epoch activate - Proves CRD hardening and publishes a fresh custody epoch
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
- landlock-genprof custody-epoch - Manages the ApplyAttempt custody qualification epoch
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
- landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod
- landlock-genprof abi check - Reports the highest Landlock ABI level a kernel version supports
- landlock-genprof abi list - Lists Landlock rights, optionally filtered to one ABI level and below
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
- landlock-genprof abi - Inspects Landlock’s ABI-versioned right vocabulary
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
- landlock-genprof abi - Inspects Landlock’s ABI-versioned right vocabulary
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
- landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod
- landlock-genprof evidence list - Lists evidence files in a directory
- landlock-genprof evidence show - Summarizes a raw evidence file
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
- landlock-genprof evidence - Inspects raw captured evidence
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
- landlock-genprof evidence - Inspects raw captured evidence
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
- landlock-genprof - Generates least-privilege Kubernetes security profiles by observing a running pod
- landlock-genprof policy list - Lists SecurityProfileProposals and their approval state
- landlock-genprof policy status - Reports whether a SecurityProfileProposal has been approved
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
- landlock-genprof policy - Inspects SecurityProfileProposal approval state
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
- landlock-genprof policy - Inspects SecurityProfileProposal approval state
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:
| Domain | Artifact | Enforcement owner |
|---|---|---|
| Filesystem | PodLock LandlockProfile | PodLock / Landlock integration |
| Network | Kubernetes NetworkPolicy | The cluster CNI |
| Syscalls | SPO SeccompProfile plus workload reference | SPO, kubelet, and container runtime |
| Capabilities | securityContext fragment or patched workload | Kubernetes 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
- Detailed data flow — package, artifact, and RBAC boundaries.
- Runtime sequence — implementation-level trace flow and optional branches.
- Package map — Go dependency boundaries.
- Policy synthesis — direct-evidence aggregation and confidence semantics.
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:
- Tracer attack surface — exact RBAC, and
the one flag (
--restart) that genuinely widens the blast radius. - Completeness of generated profiles — what a short training run can miss, and the two gaps already fixed (startup blind spot, cross-process contamination).
- Candidate authority and governed apply — substitution, stale authority, source provenance, readiness, and partial apply.
- Runtime validation — enforcement and bypass questions.
- 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:getonpodscluster-wide (target pod resolution, namespace chosen dynamically at runtime) +listonpodsandcreateonpods/portforwardscoped to thegadgetnamespace 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 namestrace_open/trace_execspecifically — it’s daemon-reachability access only, so addingtrace_tcp/trace_bind(internal/tracer/trace_linux.go) required no new RBAC rule. Same foradvise_seccomp(runSeccompTracer, added for the seccomp exporter) andtrace_capabilities(runCapabilitiesTracer, added for the capabilities exporter): no new RBAC either. advise_seccompobserves every process on the node during the training run, not just the target container — confirmed directly in its own upstream source (program.bpf.c’ssys_enterprobe comment): container filtering can’t happen in-kernel without losing the target container’s own startup syscalls (executed byruncbefore 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 (seeinternal/tracer/trace_linux.go’srunSeccompTracer), not something introduced by this codebase — but it does mean a training run using--seccomp-outbriefly 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-outon a shared/multi-tenant node.trace_capabilitiesdoes not share this caveat — confirmed via its own source (program.bpf.cincludes<gadget/filter.h>and callsgadget_should_discard_data_current(), the same in-kernel container-filtering mechanismtrace_open/etc. use), so--capabilities-outscopes 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 needsdelete/createonpodsandpatchondeployments/statefulsets/daemonsets(seedeploy/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 basedeploy/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’sPatchedManifest/PatchedManifestForOwnerneedgetondeployments/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: sinceSecurityProfileProposalpublishing became mandatory everytracerun needs this RBAC whenever there’s asecurityContextto compose, whether or not--patched-manifest-outwas also passed to additionally write a local file. Deliberately its own manifest (deploy/rbac-patched-manifest.yaml), not folded intodeploy/rbac-restart.yamleven though it overlaps two of its threegetgrants: 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. TwoClusterRoles grantinggeton 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
Confidenceas 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 execdebug commands that only incidentally touch similar paths — seedocs/e2e-demo.mdFinding 1’s live re-verification, wherels/catviakubectl execproduced 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.
| Threat | Implemented control | Residual risk or limit |
|---|---|---|
| Candidate substituted before approval | approve --expected-digest compares the reviewed digest with current content and records authority for that digest | A reviewer can still approve the wrong digest intentionally or without adequate review |
| Candidate mutated after review or approval | Content mutation changes CandidateDigest; stale or mismatched approval is rejected | Candidate-wide digest forces full re-review even for an isolated domain change |
| Stale approval reused for a newer candidate | Approved and current digests must match under candidate-v1; mismatch fails closed | None within the current digest field set; schema evolution must update digest vectors |
| Review/apply TOCTOU | apply-proposal validates before planning, applies an immutable planned payload, and revalidates immediately before workload binding | External resources applied before a later failure are not rolled back |
| Approval recorded for the wrong content | Expected-digest comparison and retry-on-conflict prevent accidental transfer across concurrent updates | Human identity and decision quality depend on cluster authentication, RBAC, and review process |
| Seccomp source silently substituted | Source mode is explicit; there is no internal/SPO fallback; provenance is included in digested content | Switching sources deliberately requires full re-review |
| SPO provenance spoofed or cross-workload policy imported | ADR-0008 requires recording namespace, recording ID, container lineage, completion, inertness, ownership, and supported-semantics checks | Labels are structural lineage under RBAC, not cryptographic authentication; stronger UID-bound lineage needs upstream support |
| SPO source mutated after import | Import is a copied governed snapshot, not a live reference; the source object is never mutated by landlock-genprof | Re-importing changed source content creates a new candidate and requires review |
| SPO-derived data laundered as observation/confidence | SPO policy enters at the artifact layer; SPO syscalls have no route to TrainingHistory and receive no landlock-genprof confidence | Coverage is optional provenance (absent in official v1.0.0, normalized when supported); it remains neither lineage nor authority |
| Governed enforcement object mutated before binding | ADR-0007 readiness checks exact planned identity and content before binding | Only implemented backend adapters can provide readiness semantics |
| Backend reconciliation fails or times out | Readiness failure, timeout, or identity mismatch stops binding and exits as a blocking failure | Enforcement resources applied earlier can remain in the cluster |
| Partial multi-artifact application | Application is ordered; the first failure stops the sequence and binding stays last | Apply is sequential, not transactional, and does not roll back earlier resources |
| Applied mistaken for enforced | Documentation and status keep API application separate from backend realization | Generic Kubernetes API success cannot prove kernel or datapath behavior |
| Enforced mistaken for verified | Behavioral verification is a separate evidence gate | verify 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.yml—securityjob, separate frombuild-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 artifact | Enforced by | Set up by this repo? |
|---|---|---|
profile.yaml (LandlockProfile) | PodLock operator (Kubewarden) | No — see the limitation below |
{pod}-networkpolicy.yaml | Any CNI that implements NetworkPolicy | Yes — hack/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:
- The chart’s
spoImage.tagdefaulted tolatestagainst a staging registry, so the operator crashed on startup.deploy/operator.yamlpins a realregistry.k8s.ioimage at the release version. - The
spodmetrics sidecar was hardcoded togcr.io/kubebuilder/kube-rbac-proxy, a discontinued registry path, producingImagePullBackOffwith no chart value to override it. v1.0.0 contains nokube-rbac-proxyorRELATED_IMAGE_RBAC_PROXYreference 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
ProfileRecordingis imported explicitly as derived policy; merged-provenance and optional v1 coverage behavior are demonstrated by the real-node run documented inPROGRESS.md. - Behavioral syscall enforcement. Run
32561123023proved the bounded candidate experiment:getpidsucceeded under the approved profile, while naturally absentgetprioritysucceeded in the unconfined control and returnedEPERMafter 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.yamlalways works onkind— it’s a local file, no cluster object involved. kubectl apply/apply-proposalapplying it as aLandlockProfileobject does not work on this project’s referencekindsetup by default — confirmed live: fails withthe 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
kindeven with PodLock installed. Don’t stage or claim either of the above in a demo (seedemo/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
| # | Title | Status | Date |
|---|---|---|---|
| 0001 | Exit-code contract (0/1/2/3) | Accepted | 2026-08-07 |
| 0002 | Generic Behavior IR as the exporter boundary | Accepted | 2026-08-08 |
| 0007 | Governed apply ordering and enforcement readiness | Accepted | 2026-08-19 |
| 0008 | SPO derived-policy import boundary | Accepted | 2026-08-19 |
| 0009 | SPO merged SeccompProfile provenance and target separation | Accepted | 2026-08-21 |
| 0021 | Bounded Workbench Kubernetes read authority | Accepted | 2026-08-31 |
| 0022 | Workload security projection proof levels | Accepted | 2026-09-01 |
| 0023 | Local Workbench HTTP trust boundary | Accepted | 2026-09-02 |
| 0024 | Durable ApplyAttempt mutation custody | Accepted | 2026-09-03 |
| 0025 | Explicit rollback with strict custody | Accepted | 2026-09-03 |
| 0026 | Observation identity and record decomposition | Accepted | 2026-09-05 |
| 0027 | ClusterIdentity resolution model | Accepted | 2026-09-05 |
| 0028 | Workload identity, ContainerSlot, and revision split | Accepted | 2026-09-05 |
| 0029 | Observation persistence adapter boundary | Accepted | 2026-09-05 |
| 0030 | Executor claim and lease mechanism | Accepted | 2026-09-05 |
| 0031 | candidate-v1 digest boundary | Accepted | 2026-09-05 |
| 0032 | Per-source EMPTY and UNKNOWN precondition rule | Accepted | 2026-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:
| Term | Meaning |
|---|---|
| Observed | Runtime behavior was captured by an identified source |
| Derived | Evidence was transformed into a policy artifact |
| Proposed | Artifacts were assembled into a candidate |
| Reviewed | A human inspected that candidate |
| Approved | Human authority was bound to the exact candidate digest |
| Applied | An approved artifact was submitted to its API |
| Enforced | The external backend realized the policy |
| Verified | A 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.
| Gate | Certified boundary |
|---|---|
| Observation architecture | Immutable identity/spec, durable lifecycle, bounded attribution, frozen result/provenance, and first-class uncertainty |
| G5-CS | Container-scoped attribution for filesystem, exec, network connect, network bind, and capabilities |
| G6 | Normalized Observation evidence, PopulationScope v2, idempotent contribution effect, and Observation → TrainingHistory contribution |
| G7 | Candidate-v2, distinct ReviewContextDigestV2, approval custody, and governed Proposal derivation |
| G8 | Trusted-local Start, Stop, Status, and Generate Observation API with real kube-apiserver concurrency qualification |
| G9 | Workload-centric Observation Workbench, durable Observation/Proposal read models, rediscovery, and browser authority absence |
| G10 | Final 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
| Capability | Status | Demonstrated scope | Remaining limit |
|---|---|---|---|
| Evidence model and internal trace acquisition | Done | Workload filesystem, network, syscall, and capability evidence can be captured and validated | The internal tracer is an acquisition adapter, not a claim over external observations |
| Multi-run learning | Done | TrainingHistory, seenInRuns, and confidence behavior demonstrated across three runs | SPO-derived syscalls are structurally excluded from this history |
| Candidate generation | Done | Filesystem, NetworkPolicy, seccomp, and capability artifacts can form one SecurityProfileProposal | Generated does not mean authorized |
| Candidate identity | Done | Selected proposal fields produce a deterministic sha256: candidate digest | Digest vectors must evolve with the proposal schema |
| Governed approval | Done | Review, digest-bound approval, rejection, and persisted mechanism version demonstrated | Reviewer UX and structured rationale remain incomplete |
| Stale-authority rejection | Done | Changed, revoked, missing, or mismatched approval fails closed before application | New approval states require equivalent negative coverage |
| Governed apply | Done | apply-proposal reloads and revalidates the candidate, applies in governed order, and binds the workload last | Successful multi-artifact application is sequential, not transactional |
| NetworkPolicy | Verified on Cilium | Generated, approved, applied, realized, and fresh connections denied | This does not generalize to every CNI or workload |
| PodLock/Landlock artifact | Applied path only | Generation and approval-bound API application are implemented and tested | PodLock consumption and kernel denial are not demonstrated |
| SPO reconciliation | E2E-proven | A 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 32561123023 | Evidence covers the tested candidate and syscalls, not every profile or runtime combination |
| SPO-derived policy import | E2E-proven | Real 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 demonstrated | Coverage remains optional informational metadata; contributor lineage, confidence, and authority are not inferred |
| Governed Seccomp runtime boundary | E2E-proven | In run 32561123023, approved getpid succeeded and naturally absent getpriority succeeded in control but returned EPERM under the applied governed profile | This is a tested behavioral boundary, not universal least privilege or complete Seccomp verification |
| Candidate explanation and diff | Done | explain and diff render evidence and candidate changes | Broader assurance/rationale UX remains future work |
| Operator/controller reconciliation | Not started | No controller capability is claimed | Requires 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, SHAbfc3132ba4d51db5ca3b6dff84467a4a99f8436f: the documented real-node scenario completed successfully. It produced candidate Asha256:3432b896c62d6d386abcb5a59c23afa7341881edde1c9cb807abeb52cb4f4e8dand candidate Bsha256: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 measuredBUYER_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, SHA902f99228203a27aeb52da11f301760d8bc5ff60: multi-run confidence, proposal generation, digest-bound approval/apply, and Cilium NetworkPolicy behavioral verification. -
SPO Interop E2E
32230551571, SHA0e6ce7062f0cfd1b80bc42f654e371ae2a275f65: 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, SHA5fb93a45aa724e9b1a9021b96ad1da1b54911bde: two realContainerscontributors, exact union and #3355 coverage (v1,total=2), widening visibility, normalized digest/approval, stale-authority rejection, and governed runtime Seccomp behavior.getpidwas present and succeeded; naturally absentgetprioritysucceeded in control and returnedEPERMafter apply. The target referencedoperator/lg-v1-merged-target-2ed57712c490f4d5.jsonand the reviewed digest wassha256:f0d4f5116d6aca3dc0233ff15fbaea914411ffafc93602338a29be2bc432b5e3.
Current verification gates
- Install a compatible PodLock environment and demonstrate Landlock behavioral denial.
- Define capability/security-context verification evidence separately from artifact application.
- 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
CandidateDigestdescribe 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:
| Capability | Current evidence-supported state | Remaining boundary |
|---|---|---|
| Direct acquisition | Filesystem, network, internal/advisory syscall, and applicable capability evidence are implemented; the demonstrated scope is recorded in PROGRESS.md | Internal syscall acquisition is an alternative adapter, not the SPO architecture |
| SPO import | A real SPO-produced SeccompProfile can enter as a validated, provenance-bearing governed snapshot | The optional coverage annotation has no stable SPO API guarantee |
| Learning | TrainingHistory and cross-run confidence are implemented for direct evidence | SPO-derived syscalls are structurally excluded; coverage never becomes confidence |
| Candidate governance | SecurityProfileProposal, deterministic candidate-v1 identity, review, exact-digest approval, and stale/mutated-candidate rejection are implemented and demonstrated | Reviewer rationale and assurance UX remain incomplete |
| Governed apply | Approved artifacts are planned, revalidated, applied in dependency order, and the workload is bound last after supported readiness checks | Application is sequential, non-transactional, and has no rollback |
| Enforcement and verification | NetworkPolicy denial is demonstrated on Cilium; the SPO/Seccomp path has real-node merged governance and a tested behavioral denial boundary | Evidence 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
SeccompProfileenforcement 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-v1whether normalized optional metadata remains approval-relevant provenance. Today the copied provenance annotation is part of the governed artifact and therefore part ofCandidateDigest.
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/
EPERMexperiment. - 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-pagesbranch, 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 viagh 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.jsonat thegh-pagesroot 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 togh-pagesunderpr-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 tomaster(i.e., after a PR has merged): rebuildsmasterand deploys to the real/master/production path.docs-release.yml— only on avX.Y.Ztag push: builds that tag, deploys to its own/vX.Y.Z/, and updatesversions.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)
hack/build-versioned-docs.sh <ref> <tmpdir>git worktree add /tmp/landlock-gh-pages gh-pages- Copy
<tmpdir>togh-pages/<version>/ - For a new release specifically (not
master): also runhack/update-doc-versions-manifest.py gh-pages/versions.json <version> - 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.mdfor how the pieces fit together (tracer → IR → exporters → CLI → cluster objects) anddocs/roadmap.mdfor 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.mdentries 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/traceror needing a real cluster (RBAC, CRDs, livetraceruns), you need a Linux box with kernel ≥ 6.8 and akindcluster with Inspektor Gadget — seeHOW_TO_START.mdfor the full VM/cluster setup (French version:COMMENT_COMMENCER.md), ormake init-vm/make check-kernel. - No cluster available?
make docker-testruns the real Linux build/test (includinginternal/tracer) inDockerfile.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 onpkg/podlockandpkg/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 — seedocs/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-proposalandrollbackCLI 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.
- Identify the RC SHA:
git rev-list -n1 <tag>— that value is what everything below is checked against. - Get a run on that SHA. Merging the release PR produces the RC commit on
masterand 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. - 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. BothheadSha == RC SHAandconclusion == successare required. - 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). - 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 missingnodeNamestrip) — write the test that would have caught the bug you’re fixing, not just one that exercises the happy path. internal/k8stests usek8s.io/client-go/kubernetes/fake; CRD-backed packages (internal/proposal,internal/history) usek8s.io/client-go/dynamic/fake. Reuse existing fixture helpers in the matching_test.gofile 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.mdanddocs/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.
