Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ container parameters:
- Block I/O class
- RDT class
- Linux seccomp policy
- Linux namespaces

### Container Updates

Expand Down Expand Up @@ -349,11 +350,14 @@ for rejecting adjustment of the seccomp policy profile based on the type of poli
profile set for the container. These types include the runtime default seccomp
policy profile, a custom policy profile, and unconfined security profiles.

3. Verify global mandatory plugins: Verify that all configured mandatory
3. Reject Linux Namespace adjustment: Reject any adjustment which tries to
alter Linux namespaces of a container.

4. Verify global mandatory plugins: Verify that all configured mandatory
plugins are present and have processed a container. Otherwise reject the
creation of the container.

4. Verify annotated mandatory plugins: Verify that an annotated set of
5. Verify annotated mandatory plugins: Verify that an annotated set of
container-specific mandatory plugins are present and have processed a
container. Otherwise reject the creation of the container.

Expand All @@ -362,11 +366,11 @@ allows one to deploy mandatory plugins as containers themselves.

#### Default Validation Scope

Currently only OCI hook injection and Linux seccomp policy can be restricted
using the default validator. However, this probably will change in the future.
Especially when NRI is extended with control over more container parameters.
If newly added controls will have security implications, expect corresponding
configurable restrictions in the default validator.
Currently only OCI hook injection, Linux seccomp policy and Linux namespace
adjustment can be restricted using the default validator. However, this probably
will change in the future. Especially when NRI is extended with control over more
container parameters. If newly added controls will have security implications,
expect corresponding configurable restrictions in the default validator.

## Runtime Adaptation

Expand Down
105 changes: 105 additions & 0 deletions pkg/adaptation/adaptation_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,12 @@ var _ = Describe("Plugin container creation adjustments", func() {
}
a.AddDevice(dev)

case "namespace":
ns := &api.LinuxNamespace{
Type: "cgroup",
}
a.AddOrReplaceNamespace(ns)

case "rlimit":
a.AddRlimit("nofile", 456, 123)

Expand Down Expand Up @@ -716,6 +722,17 @@ var _ = Describe("Plugin container creation adjustments", func() {
},
},
),
Entry("adjust namespace", "namespace",
&api.ContainerAdjustment{
Linux: &api.LinuxContainerAdjustment{
Namespaces: []*api.LinuxNamespace{
{
Type: "cgroup",
},
},
},
},
),
Entry("adjust rlimits", "rlimit",
&api.ContainerAdjustment{
Rlimits: []*api.POSIXRlimit{{Type: "nofile", Soft: 123, Hard: 456}},
Expand Down Expand Up @@ -1800,6 +1817,94 @@ var _ = Describe("Plugin container creation adjustments", func() {
})
})

When("the default validator is enabled and namespace adjustment is disabled", func() {
BeforeEach(func() {
s.Prepare(
&mockRuntime{
options: []nri.Option{
nri.WithDefaultValidator(
&validator.DefaultValidatorConfig{
Enable: true,
RejectNamespaceAdjustment: true,
},
),
},
},
&mockPlugin{idx: "00", name: "foo"},
&mockPlugin{idx: "10", name: "validator1"},
&mockPlugin{idx: "20", name: "validator2"},
)
})

It("should reject namespace adjustment", func() {
var (
create = func(_ *mockPlugin, _ *api.PodSandbox, ctr *api.Container) (*api.ContainerAdjustment, []*api.ContainerUpdate, error) {
a := &api.ContainerAdjustment{}
if ctr.GetName() == "ctr1" {
a.AddOrReplaceNamespace(
&api.LinuxNamespace{
Type: "cgroup",
Path: "/",
},
)
}
return a, nil, nil
}

validate = func(_ *mockPlugin, _ *api.ValidateContainerAdjustmentRequest) error {
return nil
}

runtime = s.runtime
plugins = s.plugins
ctx = context.Background()

pod = &api.PodSandbox{
Id: "pod0",
Name: "pod0",
Uid: "uid0",
Namespace: "default",
}
ctr0 = &api.Container{
Id: "ctr0",
PodSandboxId: "pod0",
Name: "ctr0",
State: api.ContainerState_CONTAINER_CREATED,
}
ctr1 = &api.Container{
Id: "ctr1",
PodSandboxId: "pod0",
Name: "ctr1",
State: api.ContainerState_CONTAINER_CREATED,
}
)

plugins[0].createContainer = create
plugins[1].validateAdjustment = validate
plugins[2].validateAdjustment = validate

s.Startup()
podReq := &api.RunPodSandboxRequest{Pod: pod}
Expect(runtime.RunPodSandbox(ctx, podReq)).To(Succeed())

ctrReq := &api.CreateContainerRequest{
Pod: pod,
Container: ctr0,
}
reply, err := runtime.CreateContainer(ctx, ctrReq)
Expect(reply).ToNot(BeNil())
Expect(err).To(BeNil())

ctrReq = &api.CreateContainerRequest{
Pod: pod,
Container: ctr1,
}
reply, err = runtime.CreateContainer(ctx, ctrReq)
Expect(err).ToNot(BeNil())
Expect(reply).To(BeNil())
})
})

When("the default validator is enabled with some required plugins", func() {
const AnnotationDomain = plugin.AnnotationDomain
BeforeEach(func() {
Expand Down
41 changes: 41 additions & 0 deletions pkg/adaptation/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package adaptation

import (
"fmt"
"maps"
"slices"
"strings"

Expand Down Expand Up @@ -78,6 +79,9 @@ func collectCreateContainerResult(request *CreateContainerRequest) *result {
if request.Container.Linux.Resources.Unified == nil {
request.Container.Linux.Resources.Unified = map[string]string{}
}
if request.Container.Linux.Namespaces == nil {
request.Container.Linux.Namespaces = []*LinuxNamespace{}
}

return &result{
request: resultRequest{
Expand All @@ -99,6 +103,7 @@ func collectCreateContainerResult(request *CreateContainerRequest) *result {
HugepageLimits: []*HugepageLimit{},
Unified: map[string]string{},
},
Namespaces: []*LinuxNamespace{},
},
},
},
Expand Down Expand Up @@ -227,6 +232,9 @@ func (r *result) adjust(rpl *ContainerAdjustment, plugin string) error {
if err := r.adjustSeccompPolicy(rpl.Linux.SeccompPolicy, plugin); err != nil {
return err
}
if err := r.adjustNamespaces(rpl.Linux.Namespaces, plugin); err != nil {
return err
}
}
if err := r.adjustRlimits(rpl.Rlimits, plugin); err != nil {
return err
Expand Down Expand Up @@ -410,6 +418,39 @@ func (r *result) adjustDevices(devices []*LinuxDevice, plugin string) error {
return nil
}

func (r *result) adjustNamespaces(namespaces []*LinuxNamespace, plugin string) error {
if len(namespaces) == 0 {
return nil
}

create, id := r.request.create, r.request.create.Container.Id

creatensmap := map[string]*LinuxNamespace{}
for _, n := range create.Container.Linux.Namespaces {
creatensmap[n.Type] = n
}

for _, n := range namespaces {
if n == nil {
continue
}
key, marked := n.IsMarkedForRemoval()
if err := r.owners.ClaimNamespace(id, key, plugin); err != nil {
return err
}
if marked {
delete(creatensmap, key)
} else {
creatensmap[key] = n
}
r.reply.adjust.Linux.Namespaces = append(r.reply.adjust.Linux.Namespaces, n)
}

create.Container.Linux.Namespaces = slices.Collect(maps.Values(creatensmap))

return nil
}

func (r *result) adjustCDIDevices(devices []*CDIDevice, plugin string) error {
if len(devices) == 0 {
return nil
Expand Down
21 changes: 21 additions & 0 deletions pkg/api/adjustment.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@ func (a *ContainerAdjustment) AddCDIDevice(d *CDIDevice) {
a.CDIDevices = append(a.CDIDevices, d) // TODO: should we dup d here ?
}

// AddOrReplaceNamespace records the addition or replacement of the given namespace to a container.
func (a *ContainerAdjustment) AddOrReplaceNamespace(n *LinuxNamespace) {
a.initLinuxNamespaces()
a.Linux.Namespaces = append(a.Linux.Namespaces, n) // TODO: should we dup n here ?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by this TODO? oci only allows one namespace per type and the func we are calling AddOrReplaceLinuxNamespace ensures that..

Copy link
Member

@klihub klihub Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikebrow It's related to the similar comments about the corresponding helpers for mounts and [CDI] devices (as this is done consistently with those).

Basically we append here the user-supplied n *LinuxNamespace as such to the set of adjustments, as opposed to appending a copy of it. That (being a pointer) allows the user, in principle, to modify it once it has been recorded as an adjustment.

Since the plugin can only shoot itself in the foot with that, I think no too big harm is done. But I think it's good to have the comments in place, so that if we ever decide to change one, we update all the others consistently.

}

// RemoveNamespace records the removal of the given namespace from a container.
func (a *ContainerAdjustment) RemoveNamespace(n *LinuxNamespace) {
a.initLinuxNamespaces()
a.Linux.Namespaces = append(a.Linux.Namespaces, &LinuxNamespace{
Type: MarkForRemoval(n.Type),
})
}

// SetLinuxMemoryLimit records setting the memory limit for a container.
func (a *ContainerAdjustment) SetLinuxMemoryLimit(value int64) {
a.initLinuxResourcesMemory()
Expand Down Expand Up @@ -323,6 +337,13 @@ func (a *ContainerAdjustment) initLinux() {
}
}

func (a *ContainerAdjustment) initLinuxNamespaces() {
a.initLinux()
if a.Linux.Namespaces == nil {
a.Linux.Namespaces = []*LinuxNamespace{}
}
}

func (a *ContainerAdjustment) initLinuxResources() {
a.initLinux()
if a.Linux.Resources == nil {
Expand Down
Loading
Loading