Kubernetes Security Profiles Merger

A standalone Go library for merging security profiles
(seccomp,
AppArmor,
Landlock) used by Kubernetes CRI runtimes and the
Security Profiles Operator.
Overview
This library provides two core operations on security profiles:
- Intersect: Produces an effective profile that permits an operation only if
all input profiles permit it. Used by CRI runtimes (CRI-O, containerd) to
merge OCI-pulled profiles with node baselines per
KEP-6061.
- Union: Produces a profile that permits an operation if any input profile
permits it. Used by the
Security Profiles Operator
to merge recorded profiles.
Installation
go get github.com/saschagrunert/security-profiles-merger
Packages
seccomp
Seccomp profile merge operating on specs.LinuxSeccomp from the
OCI runtime-spec.
import "github.com/saschagrunert/security-profiles-merger/seccomp"
Functions:
seccomp.Intersect(profiles ...*specs.LinuxSeccomp) (*specs.LinuxSeccomp, error) -
Merge profiles via intersection. The resulting profile permits a syscall only
if all input profiles permit it. For each syscall present in multiple profiles,
the more restrictive action is chosen.
seccomp.Union(profiles ...*specs.LinuxSeccomp) (*specs.LinuxSeccomp, error) -
Merge profiles via union. The resulting profile permits a syscall if any input
profile permits it. For each overlapping syscall, the less restrictive action
is chosen.
seccomp.MoreRestrictive(a, b LinuxSeccompAction) LinuxSeccompAction -
Returns the more restrictive of two seccomp actions.
seccomp.LessRestrictive(a, b LinuxSeccompAction) LinuxSeccompAction -
Returns the less restrictive of two seccomp actions.
Merge semantics:
- Default actions are merged using the same restrictiveness comparison as
syscalls.
- Architectures: intersection keeps only architectures present in all profiles;
union combines all. An empty architecture list is treated as "unspecified" and
defers to the other profile. Per the OCI runtime-spec, empty means "native
architecture only", but the native architecture is unknown at merge time.
Callers that need precise architecture intersection should populate the native
architecture explicitly before merging.
- Flags: intersection keeps only flags present in all profiles; union combines
all. An empty flag list means "no flags".
- Argument filters: during intersection, non-identical argument filters result
in a conservative denial (
SCMP_ACT_KILL_PROCESS). During union, argument
filters from both sides are combined. When only one side has argument filters,
intersection keeps them and union drops them.
DefaultErrnoRet is taken from whichever profile's default action is selected.
When both profiles share the same action, the earlier (leftmost) profile's
DefaultErrnoRet wins. The same applies to per-syscall ErrnoRet.
ListenerPath and ListenerMetadata are taken from the first profile.
Action restrictiveness ordering (most to least restrictive):
KILL_PROCESS > KILL_THREAD > TRAP > ERRNO > TRACE > NOTIFY > LOG > ALLOW
Unknown actions are treated as maximally restrictive.
apparmor
AppArmor profile merge using structured profile types defined in this package.
import "github.com/saschagrunert/security-profiles-merger/apparmor"
Functions:
apparmor.Intersect(profiles ...*Profile) (*Profile, error) -
Merge profiles via intersection. Capabilities, executables, libraries, and
filesystem paths are intersected; boolean network permissions use AND semantics.
apparmor.Union(profiles ...*Profile) (*Profile, error) -
Merge profiles via union. All rule types are combined; boolean network
permissions use OR semantics.
Types:
Profile - Top-level profile containing all rule sections.
CapabilityRules - Allowed Linux capabilities.
ExecutableRules - Allowed executables and libraries.
FilesystemRules - Read-only, write-only, and read-write path rules.
NetworkRules - Raw socket access and protocol permissions.
AllowedProtocols - TCP/UDP protocol permissions.
Nil vs empty semantics: A nil field means "unspecified" and defers to the
other profile during merge. A non-nil field with empty contents means "explicitly
no permissions". For example, intersecting {caps: [NET_ADMIN]} with
{caps: nil} yields [NET_ADMIN], while intersecting with {caps: []} yields
[].
Filesystem merge: Paths are expanded into read/write permission pairs, merged
per path (AND for intersection, OR for union), and collapsed back into
read-only, write-only, and read-write lists. A read-write path intersected with
a read-only path becomes read-only (only the shared permission survives). A
read-only path in one profile and write-only in the other is dropped on
intersection (no shared permissions) but becomes read-write on union. When two
non-nil filesystem rule sets produce no overlapping paths after intersection, the
result is a non-nil empty FilesystemRules (preserving the nil-vs-empty
distinction).
landlock
Landlock profile merge for Linux unprivileged sandboxing rulesets.
import "github.com/saschagrunert/security-profiles-merger/landlock"
Functions:
landlock.Intersect(profiles ...*Profile) (*Profile, error) -
Merge profiles via intersection. HandledAccessFS and HandledAccessNet are
unioned (more handled rights = more restrictive). Path and network rules
are intersected per key.
landlock.Union(profiles ...*Profile) (*Profile, error) -
Merge profiles via union. HandledAccessFS and HandledAccessNet are
intersected (fewer handled rights = less restrictive). Path and network
rules are unioned per key.
Types:
Profile - Top-level Landlock ruleset containing handled access sets and rules.
FSAccessRight - Filesystem access right (execute, read_file, write_file, etc.).
NetAccessRight - Network access right (bind_tcp, connect_tcp).
PathRule - Per-path filesystem access rights.
NetRule - Per-port network access rights.
Handled access semantics: Landlock has inverted merge semantics for
handled-access sets compared to rules. Unhandled access rights are implicitly
allowed, so intersection unions the handled sets (handling more rights makes
the ruleset more restrictive), and union intersects them (handling fewer rights
makes it less restrictive).
Path and network rules: During intersection, rules for entries present in
both profiles have their access rights intersected. Entries only in one profile
are dropped if the access right is handled by the other profile, or kept if
unhandled. During union, access rights are combined for matching entries, and
all non-matching entries are kept.
Usage
CRI runtime: merge OCI-pulled profile with node baseline (intersection)
effective, err := seccomp.Intersect(nodeBaseline, ociPulledProfile)
if err != nil {
return err
}
// effective permits only syscalls allowed by both profiles
SPO: combine recorded profiles (union)
combined, err := seccomp.Union(recording1, recording2, recording3)
if err != nil {
return err
}
// combined permits all syscalls seen in any recording
AppArmor profile merge
aaEffective, err := apparmor.Intersect(baseProfile, ociProfile)
aaCombined, err := apparmor.Union(recorded1, recorded2)
Landlock profile merge
llEffective, err := landlock.Intersect(baseRuleset, ociRuleset)
llCombined, err := landlock.Union(recorded1, recorded2)
Community, discussion, contribution, and support
Learn how to engage with the Kubernetes community on the
community page.
You can reach the maintainers of this project at the
SIG Node mailing list.
Code of Conduct
Participation in the Kubernetes community is governed by the
Kubernetes Code of Conduct.