landlock-genprof

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: Apache-2.0, MIT

README

landlock-genprof

Version française pour les étudiants : README.etudiants.md. Student onboarding guide (French): HOW_TO_START.md.

Automatic Landlock security profile generator for Kubernetes, built on observation of a running pod (a "training run") rather than manual rule authoring.

Container runs with broad, hand-guessed permissions
                    │
                    ▼
   landlock-genprof trace --pod nginx --duration 60s
                    │
                    ▼
    Observed runtime behavior — filesystem, network, syscalls
                    │
                    ▼
     Generated least-privilege profiles, confidence-annotated

  ✓ Filesystem  → PodLock LandlockProfile
  ✓ Network     → Kubernetes NetworkPolicy
  ✓ Syscalls    → seccomp (security-profiles-operator)
  ✓ Hardening   → securityContext fragment

See §8 — Example output for a real generated profile.

The name is a deliberate nod to aa-genprof / aa-logprof — the AppArmor profile generation tools. Landlock has no equivalent yet. landlock-genprof fills that gap.

Status: the observe → synthesize → export pipeline is built and confirmed end to end on a live cluster (filesystem, network, seccomp, capabilities, cross-run confidence via --history). See docs/roadmap.md for milestones and docs/product-definition-v1.md for where this is headed as a product.


Table of contents

  1. The problem
  2. Positioning — PodLock and the Kubewarden ecosystem
  3. How it works
  4. Technical stack
  5. Repository architecture
  6. Prerequisites
  7. Quick start
  8. Example output
  9. Team and task assignment
  10. Risk management
  11. Milestones
  12. Threat model
  13. Contributing
  14. License

1. The problem

Landlock is a Linux Security Module (LSM) introduced in kernel 5.13 that allows processes to confine themselves to a subset of the filesystem and network, without requiring root privileges. This is a rare and valuable property: whereas AppArmor, SELinux, or seccomp require system-wide configuration by an administrator, a process can arm Landlock itself.

Why it is hard to use in practice

Writing a Landlock policy by hand requires guessing in advance every path, directory, and port an application will ever need:

  • Too permissive → the policy protects nothing (everything is allowed to avoid breaking the app)
  • Too restrictive → the application breaks in production on a rare code path

The problem is compounded in a Kubernetes context:

  • Landlock has no native integration in containerd/runc, so there is no standard K8s support (securityContext cannot arm Landlock)
  • There is no equivalent of aa-genprof for Landlock, neither in the Security Profiles Operator nor in PodLock

landlock-genprof addresses this gap: observe first, write the policy after.


2. Positioning — PodLock and the Kubewarden ecosystem

PodLock (part of the Kubewarden ecosystem) is the closest existing project. It provides:

  • A LandlockProfile CRD to describe pod restrictions
  • A K8s operator that enforces the policy at container startup

What PodLock does not do: generate the profiles. The user must author them by hand — which is precisely the problem addressed here.

                           ┌─────────────────────────────────┐
  landlock-genprof         │  PodLock (Kubewarden)            │
  ──────────────────       │  ─────────────────────────────── │
  observes the pod  ──────►│  LandlockProfile CRD             │
  generates YAML           │  K8s operator                    │
  (human review)    ──────►│  Runtime enforcement             │
                           └─────────────────────────────────┘

landlock-genprof is complementary to PodLock, not a competitor. It generates profiles in the format expected by PodLock, upstream in the chain.


3. How it works

The full workflow runs in five steps:

Step 1 — Training run

The target pod runs normally for a defined duration (e.g. 60 s or longer, depending on application complexity). The goal is to cover the most frequent code paths.

landlock-genprof trace \
  --pod nginx-demo \
  --namespace default \
  --binary /usr/sbin/nginx \
  --duration 60s \
  --out profile.yaml
Step 2 — Syscall capture (Tracer)

During the training run, landlock-genprof captures the pod's system calls via Inspektor Gadget gadgets:

Gadget Observed syscall Output
trace_open openat, open LANDLOCK_ACCESS_FS_READ_FILE, WRITE_FILE, EXECUTE
trace_tcpconnect connect LANDLOCK_ACCESS_NET_CONNECT_TCP (kernel ≥ 6.4)
trace_bind bind LANDLOCK_ACCESS_NET_BIND_TCP (kernel ≥ 6.4)
trace_exec execve, execveat LANDLOCK_ACCESS_FS_EXECUTE
advise_seccomp every syscall issued by the container seccomp profile (--seccomp-out, see step 4)
trace_capabilities cap_capable() checks Linux capabilities fragment (--capabilities-out, see step 4)

advise_seccomp is Inspektor Gadget's own seccomp-profile advisor, reused as-is rather than reimplemented — it already records a container's syscalls and formats them into the target seccomp JSON shape. One difference from the other four: it observes every process on the node during the run, not just the target container (its own probe can't filter earlier without losing the target's own startup syscalls) — filtering to the target container happens at its own output stage. trace_capabilities doesn't share this quirk: it filters in-kernel by container the normal way, just like trace_open/etc.

Each captured event produces an Event object:

type Event struct {
    Timestamp time.Time
    Syscall   string // "openat", "connect", "bind", "execve", or a bare syscall/capability name
    Path      string // file path, if applicable
    Port      int    // network port, if applicable
    Mode      string // "read", "write", "read_write", "exec", "egress", "ingress", "syscall", "capability"
}
Step 3 — Policy synthesis

Events are aggregated by directory (to avoid per-file overfitting) and a confidence level is calculated for each rule based on how consistently it was observed across multiple runs:

Level Meaning
high Observed consistently on every run — reliable rule
medium Observed on multiple runs, but with inconsistencies
low Observed only once — must be reviewed before deployment
Step 4 — YAML generation

The profile is exported in PodLock's LandlockProfile CRD format:

apiVersion: podlock.kubewarden.io/v1alpha1
kind: LandlockProfile
metadata:
  name: nginx-demo
  namespace: default
spec:
  profilesByContainer:
    nginx:
      "/usr/sbin/nginx":
        readExec:
          - /lib
          - /lib64
        readOnly:
          - /usr/share/nginx        # confidence: high
        readWrite:
          - /tmp                    # confidence: high
          - /var/cache/nginx/proxy  # confidence: low — review before prod
Step 4bis — 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 4ter — 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 4quater — Optional multi-run history (--history)

Confidence is meant to reflect how many separate training runs 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 across every --history run for the same container/binary, so Confidence can finally be computed from the real ratio. 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 4quinquies) can't carry a comment — its confidence is printed to stdout instead.

Step 4quinquies — Optional seccomp profile generation (--seccomp-out)

Pass --seccomp-out to also generate a seccomp 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", "epoll_wait", "openat", "read", "write"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

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 one 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 run before deploying it.

Step 4sexies — 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 docs/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.

Step 4septies — Optional composed securityContext (--security-context-out)

Pass --security-context-out to also generate a composed securityContext fragment combining the same capabilities data from Step 4sexies with a reference to the seccomp profile — generated whenever syscalls were observed, independent of whether --seccomp-out/ --seccomp-profile-out (Step 4quinquies/4undecies) were also passed this run:

capabilities:
  add:
    - NET_BIND_SERVICE   # confidence: high
  drop:
    - ALL
seccompProfile:
  type: Localhost
  localhostProfile: operator/nginx-demo.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 own operator/<pod>.json naming convention — see Step 4undecies for why, and for the flag that actually generates the object at that path.

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 4octies — 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
`docs/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 4ter/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.

Step 4nonies — 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.

kubectl get securityprofileproposal nginx-demo -o yaml

# Product-facing review summary
go run ./cmd/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 (Step 4decies below) — 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 (Step 4undecies below) — 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). Copy any of them directly out of kubectl get -o yaml and use as-is (kubectl apply -f - for all four).

spec.patchedManifest's securityContext.seccompProfile.localhostProfile always references SPO's own operator/<pod>.json naming convention whenever spec.spoSeccompProfile is non-empty — see Step 4undecies for why a plain filename isn't enough and what applying spec.spoSeccompProfile actually does.

This is the first slice of a larger evidence/proposal/approved-policy model: TrainingHistory (--history, Step 4quater) is the evidence stage, SecurityProfileProposal is the proposal stage — both are plain CRUD, no controller. An eventual approved-policy stage (WorkloadSecurityProfile) and an enforcement operator to keep it from drifting are not part of this — that's real controller-runtime work, deliberately out of scope for now. 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.

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

--security-context-out's fragment (Step 4septies) 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 4nonies) on every run regardless of whether --patched-manifest-out was passed — that flag only controls whether it's also written as a local file.

Step 4undecies — Optional SeccompProfile custom resource (--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 (Step 4quinquies) 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:
  name: nginx-demo
  namespace: default
spec:
  defaultAction: SCMP_ACT_ERRNO
  architectures: [SCMP_ARCH_X86_64]
  syscalls:
    - names: [accept4, epoll_wait, openat, read, write]
      action: SCMP_ACT_ALLOW

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/<pod>.json value --security-context-out/ --patched-manifest-out/the SecurityProfileProposal all already reference (Step 4septies), computed ahead of time since this tool never waits for SPO's own reconciliation to run.

Step 5 — Mandatory human review

landlock-genprof never deploys a profile automatically. The generated YAML is a starting point for human review, not a final result. The Confidence field per rule makes explicit what is reliable and what requires attention. See docs/threat-model.md for the recommended validation methodology.

Applying a LandlockProfile alone has no effect. PodLock's admission webhook matches a running pod to a LandlockProfile object via a label on the podpodlock.kubewarden.io/profile: <profile-name> — not by anything embedded in the CRD itself. landlock-genprof trace prints the exact kubectl label command to run after kubectl apply-ing the generated profile.


4. Technical stack

Component Choice Rationale
Language Go 1.26 Native K8s ecosystem (client-go, controller-runtime); Inspektor Gadget Go SDK
Tracer Inspektor Gadget Pre-written, CNCF-maintained eBPF gadgets — avoids writing eBPF from scratch (high risk for beginners)
Output format LandlockProfile CRD (PodLock) Existing format, Kubewarden ecosystem — complementary, not competing
Dev cluster kind Shares host kernel — required for Landlock and eBPF to work
CI GitHub Actions (ubuntu-24.04) Kernel 6.8 — covers both FS and network Landlock
License Apache-2.0 OR MIT Dual license, recipient's choice (convention from landlock-lsm/island) — compatible with PodLock and the CNCF ecosystem

Key Go dependencies (all pinned to exact versions in go.mod, never @latest):

github.com/inspektor-gadget/inspektor-gadget  # tracer SDK (Linux-only, see internal/tracer)
sigs.k8s.io/yaml                               # YAML serialization
k8s.io/client-go                               # pod resolution
github.com/spf13/cobra                         # CLI

5. Repository architecture

Flow diagrams (components, training-run sequence, package dependencies): see docs/architecture.md.

landlock-genprof/
│
├── cmd/landlock-genprof/      CLI entry point
│   └── main.go                Sub-command dispatch (trace, version)
│
├── internal/
│   ├── tracer/                Syscall event capture
│   │   └── tracer.go          Event, Options types — Inspektor Gadget integration
│   ├── policy/                Event aggregation → Behavior IR
│   │   └── synthesize.go      Synthesize() — aggregation algorithm (technology-neutral)
│   ├── profile/                Behavior IR — independent of any output format
│   │   └── profile.go         BehaviorProfile, FilesystemProfile, FileAccess, Confidence
│   ├── exporter/
│   │   ├── podlock/           IR → PodLock conversion (only package depending on both)
│   │   │   └── export.go      ToProfile(), ToYAML()
│   │   ├── networkpolicy/     IR → Kubernetes NetworkPolicy conversion
│   │   │   └── export.go      ToPolicy(), ToYAML()
│   │   ├── seccomp/           IR → seccomp profile conversion
│   │   │   └── export.go      ToProfile(), ToJSON()
│   │   ├── capabilities/      IR → Linux capabilities fragment conversion
│   │   │   └── export.go      ToProfile(), ToYAML()
│   │   ├── securitycontext/   Composes capabilities + a seccomp reference
│   │   │   └── export.go      ToSecurityContext(), ToYAML()
│   │   └── report/            Unified Markdown review report
│   │       └── export.go      ToMarkdown()
│   ├── history/                TrainingHistory CRD (multi-run Confidence)
│   │   └── record.go          Record, Merge(), ApplyConfidence()
│   ├── proposal/                SecurityProfileProposal CRD (publishable snapshot)
│   │   └── store.go            Spec, Save(), Get()
│   └── k8s/                   Pod target orchestration
│       ├── target.go          Namespace/pod/container resolution via client-go
│       ├── restart.go         --restart: DetectOwner(), Restart(), PodSelectorFor()
│       └── patch.go           --patched-manifest-out: PatchedManifest()
│
├── pkg/
│   ├── podlock/                Go types for the LandlockProfile CRD (PodLock)
│   │   └── types.go           LandlockProfile, Profile, Metadata
│   └── seccomp/                Go types for the seccomp profile JSON format
│       └── types.go           Profile, SyscallRule
│
├── examples/
│   └── nginx-generated-profile.yaml   Illustrative example of a generated profile
│
├── docs/
│   ├── roadmap.md             Milestones, task assignments, fallback plan
│   └── threat-model.md        Attack surface, validation methodology
│
├── hack/
│   └── check-kernel.sh        Kernel prerequisite check (Landlock + eBPF)
│
├── .github/workflows/
│   └── ci.yml                 Build, test, vet (ubuntu-24.04 / kernel 6.8)
│
├── Makefile                   build/test/vet/docker-* targets (see `make help`)
├── Dockerfile.dev             Build/test in a Linux container without the VM
├── go.mod
├── LICENSE-APACHE             Full Apache-2.0 text
├── LICENSE-MIT                Full MIT text
├── COPYRIGHT                  Explains the "either, your choice" dual license
├── README.md                  This document
└── README.etudiants.md        French version for students

6. Prerequisites

Linux kernel

landlock-genprof's only real requirement is the kernel version — not a specific distro. Nothing under hack/ calls a distro-specific package manager (apt/dnf/yum, ...); check-kernel.sh/init-vm.sh only use uname, curl, tar, and generic Linux tooling. Any distro shipping a kernel meeting the versions below should work.

Feature Minimum kernel version Notes
Landlock FS ≥ 5.13 File/directory confinement
Landlock network ≥ 6.4 TCP confinement (connect/bind)
eBPF (Inspektor Gadget) ≥ 5.8 recommended BPF ring buffer

Actually tested (this is a "known to work" list, not a restriction — see above):

Distro Kernel Status
Ubuntu 24.04 6.8 ✅ validated
Ubuntu 26.04 7.0 ✅ validated

Check host prerequisites:

./hack/check-kernel.sh
Tools
go 1.26+        # Build and tests
kind            # Local K8s cluster (shares host kernel)
kubectl         # Cluster interaction
Setting up kind and the dev cluster
# Install kind (pinned version, not @latest)
go install sigs.k8s.io/kind@v0.32.0

# Create cluster
kind create cluster --name landlock-dev

# Verify
kubectl cluster-info --context kind-landlock-dev

./hack/init-vm.sh (or make init-vm) automates this plus kubectl, Inspektor Gadget, and a test pod in one idempotent script — see HOW_TO_START.md §2 for a detailed walkthrough of what it does.


7. Quick start

# Clone the repo
git clone git@github.com:idriss-eliguene/landlock-genprof.git
cd landlock-genprof

# Check kernel prerequisites
./hack/check-kernel.sh

# Build
go build ./...

# Tests (unit — no cluster required)
go test ./...

# Apply required CRDs/RBAC before the first trace run
kubectl apply -f deploy/rbac.yaml
kubectl apply -f deploy/crd-securityprofileproposal.yaml
kubectl apply -f deploy/rbac-proposal.yaml
# Required whenever this run composes securityContext data
# (commonly true in practice when syscalls are observed)
kubectl apply -f deploy/rbac-patched-manifest.yaml

# CLI (Trace() captures openat via Inspektor Gadget — Linux + a real
# cluster with Inspektor Gadget deployed required, see HOW_TO_START.md)
go run ./cmd/landlock-genprof trace --pod nginx --namespace default --binary /usr/sbin/nginx --duration 60s --out profile.yaml

Or, instead of applying the individual deploy/*.yaml files above, install them all as one Helm release — see deploy/helm/landlock-genprof/README.md for what it installs, the --restart/--history opt-in toggles, and a CRD-upgrade caveat worth knowing before your first helm upgrade:

helm install landlock-genprof deploy/helm/landlock-genprof
Installing as a kubectl plugin

landlock-genprof works standalone (as above), but also installs as a kubectl plugin: a plugin is nothing more than an executable named kubectl-<name> somewhere on $PATHkubectl <name> finds and runs it. The tool already resolves the kubeconfig the same way kubectl itself does (internal/k8s.RestConfig()), so no code changes were needed for this, only a differently-named build:

make install-plugin   # builds kubectl-landlock-genprof and installs it into $(go env GOPATH)/bin
kubectl plugin list   # confirms kubectl sees it
kubectl landlock-genprof trace --pod nginx --namespace default --binary /usr/sbin/nginx --duration 60s

One kubectl-plugin quirk worth knowing: global kubectl flags placed before the plugin name (kubectl -n foo landlock-genprof ...) are not forwarded to the plugin — kubectl only passes through the arguments that come after the plugin name. Use landlock-genprof's own -n/--namespace flag instead: kubectl landlock-genprof trace -n foo ....


8. Example output

Profile generated for an nginx pod after a 60 s training run. See examples/nginx-generated-profile.yaml.

apiVersion: podlock.kubewarden.io/v1alpha1
kind: LandlockProfile
metadata:
  name: nginx-demo
  namespace: default
spec:
  profilesByContainer:
    nginx:
      "/usr/sbin/nginx":
        readExec:
          - /lib
          - /lib64
        readOnly:
          - /usr/share/nginx        # confidence: high — seen on every run
        readWrite:
          - /tmp                    # confidence: high — seen on every run
          - /var/cache/nginx/proxy  # confidence: low — seen 1 out of 5 runs

The confidence annotation makes explicit what is reliable and what needs verification before production deployment.


9. Team and task assignment

Three-student project. Each role is independent to allow parallel progress from day one.

Student Component Technical focus
Student A internal/tracer/ Inspektor Gadget SDK integration, syscall → Landlock right mapping, event formats
Student B cmd/, internal/k8s/, internal/policy/ CLI (cobra), K8s orchestration via client-go, synthesis algorithm and directory-level aggregation
Student C docs/threat-model.md, adversarial tests, CI Profile validation methodology, tracer attack surface, pentesting (evasion, RBAC), CI hardening (gosec, Trivy)
How to work in parallel from week 1

Students B and C do not need a working tracer to make progress. Mock trace data (a static []Event slice hard-coded in tests) allows developing the synthesis algorithm and the threat model independently. Real integration with Student A's tracer happens at M1.


10. Risk management

Primary risk: eBPF is hard for beginners

eBPF is notoriously complex (kernel verifier, CO-RE, bpftool). Two mitigations were established at design time:

Mitigation 1 — Do not write eBPF from scratch

We consume existing Inspektor Gadget gadgets via their Go SDK (trace_open, trace_tcpconnect, etc.). These gadgets are authored, tested, and maintained by the CNCF community. Student A does not write eBPF programs — they call a Go API that returns Event objects.

Mitigation 2 — Hard checkpoint at week 3-4

If the tracer does not produce real events (at minimum openat) by week 3-4, immediately switch to the fallback plan: capture events using strace -f and parse its output. Less elegant than eBPF, but:

  • Sufficient for a one-shot training run (no production performance requirement)
  • Students B and C are not blocked
  • The rest of the pipeline (synthesis, YAML generation, CLI) is unchanged
Plan A (nominal)          Plan B (fallback week 3-4)
─────────────────         ──────────────────────────
Inspektor Gadget    →     strace -f + parsing
  Go SDK                  (same Event{} interface)
  eBPF kernel             no eBPF kernel requirement
Secondary risk: completeness of generated profiles

A short training run does not cover all code paths (error handling, edge cases, rarely-triggered behaviour). An incomplete profile may break the application in production on an unobserved path. Mitigation: the Confidence field per rule makes this risk visible in the YAML rather than giving a false impression of completeness. See docs/threat-model.md.


11. Milestones

Milestone Content Owner
M0 — Setup Repo, CI, go.mod with real dependencies, hack/check-kernel.sh, kind cluster All
⚠️ Checkpoint week 3-4 Tracer produces real events on at least openat. Otherwise: switch to strace fallback Student A
M1 Working tracer (openat + connect), end-to-end trace CLI on an nginx pod A + B
M2 Policy synthesis (directory-level aggregation, confidence levels), PodLock YAML export B + C
M3 Full K8s integration (pod resolution via client-go, minimal tracer RBAC) B + C
M4 End-to-end demo on kind — generated profile for nginx, comparison with a hand-written profile All
M5 (stretch) Post-deployment drift detection: Landlock denial logs → policy adjustment suggestions All

12. Threat model

The tracer itself introduces an attack surface: it requires elevated capabilities (CAP_BPF, CAP_SYS_ADMIN depending on kernel version) to observe a pod's syscalls. Open questions to document in docs/threat-model.md:

  • Which exact capabilities are required, and can they be reduced?
  • Should the tracer run permanently or only during training runs (preferred)?
  • What is the minimal RBAC for the tracer's service account (dedicated namespace, no cluster-wide rights beyond what is strictly needed)?
  • Can an observed pod detect it is being traced and modify its behaviour to generate an artificial profile (evasion)?
  • Can the human review workflow be bypassed in practice?

13. Contributing

External contributions are welcome. See CONTRIBUTING.md for the development setup, code conventions, and what to check before opening a PR. For where the product is headed, see docs/product-definition-v1.md, docs/product-design-v1.md, and docs/product-roadmap-v1.md.


14. License

Dual-licensed, recipient's choice: Apache-2.0 or MIT — see COPYRIGHT. Convention borrowed from landlock-lsm/island, the official Landlock sandboxing tool. Compatible with PodLock and the CNCF ecosystem.

Directories

Path Synopsis
cmd
landlock-genprof command
Command landlock-genprof observes a running Kubernetes pod and generates a minimal Landlock profile in the PodLock format.
Command landlock-genprof observes a running Kubernetes pod and generates a minimal Landlock profile in the PodLock format.
internal
analysis
Package analysis turns observed behavior (internal/profile) into product-facing, explainable security recommendations.
Package analysis turns observed behavior (internal/profile) into product-facing, explainable security recommendations.
exporter/capabilities
Package capabilities converts a Behavior IR (internal/profile) into a Linux capabilities fragment and serializes it to YAML.
Package capabilities converts a Behavior IR (internal/profile) into a Linux capabilities fragment and serializes it to YAML.
exporter/networkpolicy
Package networkpolicy converts a Behavior IR (internal/profile) into a Kubernetes NetworkPolicy and serializes it to YAML.
Package networkpolicy converts a Behavior IR (internal/profile) into a Kubernetes NetworkPolicy and serializes it to YAML.
exporter/podlock
Package podlock converts a Behavior IR (internal/profile) into the PodLock format (pkg/podlock) and serializes it to YAML.
Package podlock converts a Behavior IR (internal/profile) into the PodLock format (pkg/podlock) and serializes it to YAML.
exporter/report
Package report renders a Behavior IR (internal/profile) into a single Markdown review artifact combining all four observed domains (filesystem, network, syscalls, capabilities) for one human review pass, instead of the four-to-five separate files the other exporters each produce on their own.
Package report renders a Behavior IR (internal/profile) into a single Markdown review artifact combining all four observed domains (filesystem, network, syscalls, capabilities) for one human review pass, instead of the four-to-five separate files the other exporters each produce on their own.
exporter/seccomp
Package seccomp converts a Behavior IR (internal/profile) into a seccomp profile (pkg/seccomp) and serializes it to JSON.
Package seccomp converts a Behavior IR (internal/profile) into a seccomp profile (pkg/seccomp) and serializes it to JSON.
exporter/securitycontext
Package securitycontext composes a Behavior IR (internal/profile) and a reference to a separately-generated seccomp profile into a Kubernetes corev1.SecurityContext fragment, and serializes it to YAML.
Package securitycontext composes a Behavior IR (internal/profile) and a reference to a separately-generated seccomp profile into a Kubernetes corev1.SecurityContext fragment, and serializes it to YAML.
exporter/spo
Package spo converts a Behavior IR (internal/profile) into a security-profiles-operator (SPO) SeccompProfile custom resource (pkg/spo) and serializes it to YAML.
Package spo converts a Behavior IR (internal/profile) into a security-profiles-operator (SPO) SeccompProfile custom resource (pkg/spo) and serializes it to YAML.
history
Package history persists a training target's observed accesses across multiple `trace --history` runs, in a TrainingHistory custom resource (see internal/history/store.go), so Confidence can finally be computed the way internal/profile.Confidence's own doc comment already describes it: "seen across how many distinct training runs" — not the single-run seenCount proxy internal/policy.confidenceFor computes for lack of any persisted state (see docs/policy-synthesis.md's "Confidence: a deliberately provisional heuristic").
Package history persists a training target's observed accesses across multiple `trace --history` runs, in a TrainingHistory custom resource (see internal/history/store.go), so Confidence can finally be computed the way internal/profile.Confidence's own doc comment already describes it: "seen across how many distinct training runs" — not the single-run seenCount proxy internal/policy.confidenceFor computes for lack of any persisted state (see docs/policy-synthesis.md's "Confidence: a deliberately provisional heuristic").
k8s
Package k8s locates and prepares the target pod for a training run (namespace/pod/container resolution, checking the RBAC permissions the tracer needs).
Package k8s locates and prepares the target pod for a training run (namespace/pod/container resolution, checking the RBAC permissions the tracer needs).
policy
Package policy aggregates tracing events (internal/tracer) into a Behavior IR (internal/profile) — one FileAccess per directory, not per file, to avoid overfitting on overly specific paths.
Package policy aggregates tracing events (internal/tracer) into a Behavior IR (internal/profile) — one FileAccess per directory, not per file, to avoid overfitting on overly specific paths.
profile
Package profile defines the Behavior IR (intermediate representation): the internal, technology-neutral description of what a workload was observed doing, independent of any specific output format (PodLock, Kubernetes NetworkPolicy, Cilium, ...).
Package profile defines the Behavior IR (intermediate representation): the internal, technology-neutral description of what a workload was observed doing, independent of any specific output format (PodLock, Kubernetes NetworkPolicy, Cilium, ...).
proposal
Package proposal persists a training run's generated multi-domain profile as a SecurityProfileProposal custom resource (see internal/proposal/store.go), so it can be reviewed via kubectl/GitOps instead of only as local files.
Package proposal persists a training run's generated multi-domain profile as a SecurityProfileProposal custom resource (see internal/proposal/store.go), so it can be reviewed via kubectl/GitOps instead of only as local files.
tracer
Package tracer captures a pod's syscall events during a training run, building on the existing Inspektor Gadget gadgets (trace_open, trace_tcpconnect, trace_bind, trace_exec) rather than writing eBPF programs from scratch.
Package tracer captures a pod's syscall events during a training run, building on the existing Inspektor Gadget gadgets (trace_open, trace_tcpconnect, trace_bind, trace_exec) rather than writing eBPF programs from scratch.
pkg
podlock
Package podlock defines the Go types matching the LandlockProfile CRD schema of the PodLock project (github.com/flavio/podlock, Kubewarden ecosystem), so that landlock-genprof generates profiles that are directly usable without further transformation.
Package podlock defines the Go types matching the LandlockProfile CRD schema of the PodLock project (github.com/flavio/podlock, Kubewarden ecosystem), so that landlock-genprof generates profiles that are directly usable without further transformation.
seccomp
Package seccomp defines the Go types matching the standard OCI runtime- spec / Kubernetes "localhost" seccomp profile JSON format (see https://kubernetes.io/docs/tutorials/security/seccomp/), so that landlock-genprof generates a profile directly usable without further transformation — same reasoning as pkg/podlock, and confirmed against the exact shape Inspektor Gadget's own advise_seccomp gadget produces (gadgets/advise_seccomp/README.mdx, vendored SDK v0.54.1): this schema is small and stable enough that a hand-rolled type is safer than pulling in a dependency for it.
Package seccomp defines the Go types matching the standard OCI runtime- spec / Kubernetes "localhost" seccomp profile JSON format (see https://kubernetes.io/docs/tutorials/security/seccomp/), so that landlock-genprof generates a profile directly usable without further transformation — same reasoning as pkg/podlock, and confirmed against the exact shape Inspektor Gadget's own advise_seccomp gadget produces (gadgets/advise_seccomp/README.mdx, vendored SDK v0.54.1): this schema is small and stable enough that a hand-rolled type is safer than pulling in a dependency for it.
spo
Package spo defines the Go types matching the SeccompProfile CRD schema of security-profiles-operator (SPO, github.com/kubernetes-sigs/security-profiles-operator), so landlock-genprof can generate a SeccompProfile directly usable without further transformation — same reasoning pkg/podlock already documents for PodLock's own CRD.
Package spo defines the Go types matching the SeccompProfile CRD schema of security-profiles-operator (SPO, github.com/kubernetes-sigs/security-profiles-operator), so landlock-genprof can generate a SeccompProfile directly usable without further transformation — same reasoning pkg/podlock already documents for PodLock's own CRD.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL