Skip to content

Commit 95cf602

Browse files
author
Evsyukov Denis
committed
feat: add new container security rules for NoNewPrivileges and SeccompProfile
- Introduced NoNewPrivilegesRule to ensure containers do not allow privilege escalation. - Added SeccompProfileRule to validate seccomp profiles for containers, ensuring proper security configurations. - Updated linters settings to include new exclusion rules for NoNewPrivileges and SeccompProfile. - Enhanced documentation in README.md to reflect new checks and their implications.
1 parent 4e4506c commit 95cf602

File tree

7 files changed

+713
-8
lines changed

7 files changed

+713
-8
lines changed

pkg/config/linters_settings.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ type ContainerExcludeRules struct {
5656
HostNetworkPorts ContainerRuleExcludeList `mapstructure:"host-network-ports"`
5757
Ports ContainerRuleExcludeList `mapstructure:"ports"`
5858
ReadOnlyRootFilesystem ContainerRuleExcludeList `mapstructure:"read-only-root-filesystem"`
59+
NoNewPrivileges ContainerRuleExcludeList `mapstructure:"no-new-privileges"`
60+
SeccompProfile ContainerRuleExcludeList `mapstructure:"seccomp-profile"`
5961
ImageDigest ContainerRuleExcludeList `mapstructure:"image-digest"`
6062
Resources ContainerRuleExcludeList `mapstructure:"resources"`
6163
SecurityContext ContainerRuleExcludeList `mapstructure:"security-context"`

pkg/linters/container/README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
## Description
22

33
Checks containers inside the template spec. This linter protects against the next cases:
4-
- containers with the duplicated names
5-
- containers with the duplicated env variables
6-
- misconfigured images repository and digest
7-
- imagePullPolicy is "Always" (should be unspecified or "IfNotPresent")
8-
- ephemeral storage is not defined in .resources
9-
- SecurityContext is not defined
10-
- container uses port <= 1024
4+
5+
- containers with the duplicated names
6+
- containers with the duplicated env variables
7+
- misconfigured images repository and digest
8+
- imagePullPolicy is "Always" (should be unspecified or "IfNotPresent")
9+
- ephemeral storage is not defined in .resources
10+
- SecurityContext is not defined
11+
- ReadOnlyRootFilesystem is not set to true (prevents write access to container root filesystem)
12+
- AllowPrivilegeEscalation is not set to false (prevents privilege escalation attacks)
13+
- Seccomp profile is not properly configured (ensures default seccomp filtering is enabled)
14+
- container uses port <= 1024
1115
- Checks for probes defined in containers.
1216

1317
## Settings example
@@ -26,6 +30,16 @@ linters-settings:
2630
name: deckhouse
2731
container: init-downloaded-modules
2832
# exclude if object kind, object name and containers name are equal
33+
no-new-privileges:
34+
- kind: Deployment
35+
name: privileged-deployment
36+
container: init-container
37+
# exclude if object kind, object name and containers name are equal
38+
seccomp-profile:
39+
- kind: DaemonSet
40+
name: system-daemon
41+
container: system-container
42+
# exclude if object kind, object name and containers name are equal
2943
resources:
3044
- kind: Deployment
3145
name: standby-holder-name
@@ -57,4 +71,4 @@ linters-settings:
5771
name: okmeter
5872
container: okagent
5973
impact: error
60-
```
74+
```

pkg/linters/container/rules.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ func (l *Container) applyContainerRules(object storage.StoreObject, errorList *e
5959
rules.NewNameDuplicatesRule().ContainerNameDuplicates,
6060
rules.NewCheckReadOnlyRootFilesystemRule(l.cfg.ExcludeRules.ReadOnlyRootFilesystem.Get()).
6161
ObjectReadOnlyRootFilesystem,
62+
rules.NewNoNewPrivilegesRule(l.cfg.ExcludeRules.NoNewPrivileges.Get()).
63+
ContainerNoNewPrivileges,
64+
rules.NewSeccompProfileRule(l.cfg.ExcludeRules.SeccompProfile.Get()).
65+
ContainerSeccompProfile,
6266
rules.NewHostNetworkPortsRule(l.cfg.ExcludeRules.HostNetworkPorts.Get()).ObjectHostNetworkPorts,
6367

6468
// old with module names skipping
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package rules
18+
19+
import (
20+
corev1 "k8s.io/api/core/v1"
21+
22+
"github.com/deckhouse/dmt/internal/storage"
23+
"github.com/deckhouse/dmt/pkg"
24+
"github.com/deckhouse/dmt/pkg/errors"
25+
)
26+
27+
const (
28+
NoNewPrivilegesRuleName = "no-new-privileges"
29+
)
30+
31+
func NewNoNewPrivilegesRule(excludeRules []pkg.ContainerRuleExclude) *NoNewPrivilegesRule {
32+
return &NoNewPrivilegesRule{
33+
RuleMeta: pkg.RuleMeta{
34+
Name: NoNewPrivilegesRuleName,
35+
},
36+
ContainerRule: pkg.ContainerRule{
37+
ExcludeRules: excludeRules,
38+
},
39+
}
40+
}
41+
42+
type NoNewPrivilegesRule struct {
43+
pkg.RuleMeta
44+
pkg.ContainerRule
45+
}
46+
47+
// ContainerNoNewPrivileges checks that containers have allowPrivilegeEscalation set to false
48+
// This prevents privilege escalation attacks by ensuring containers cannot gain additional privileges
49+
// Reference: CIS Kubernetes Benchmark 5.2.5
50+
func (r *NoNewPrivilegesRule) ContainerNoNewPrivileges(object storage.StoreObject, containers []corev1.Container, errorList *errors.LintRuleErrorsList) {
51+
errorList = errorList.WithRule(r.GetName()).WithFilePath(object.ShortPath())
52+
53+
switch object.Unstructured.GetKind() {
54+
case "Deployment", "DaemonSet", "StatefulSet", "Pod", "Job", "CronJob":
55+
default:
56+
return
57+
}
58+
59+
for i := range containers {
60+
c := &containers[i]
61+
62+
if !r.Enabled(object, c) {
63+
// TODO: add metrics
64+
continue
65+
}
66+
67+
if c.SecurityContext == nil {
68+
errorList.WithObjectID(object.Identity() + " ; container = " + c.Name).
69+
Error("Container's SecurityContext is missing - cannot verify allowPrivilegeEscalation setting")
70+
continue
71+
}
72+
73+
if c.SecurityContext.AllowPrivilegeEscalation == nil {
74+
errorList.WithObjectID(object.Identity() + " ; container = " + c.Name).
75+
Error("Container's SecurityContext missing parameter AllowPrivilegeEscalation - should be set to false to prevent privilege escalation")
76+
continue
77+
}
78+
79+
if *c.SecurityContext.AllowPrivilegeEscalation {
80+
errorList.WithObjectID(object.Identity() + " ; container = " + c.Name).
81+
Error("Container's SecurityContext has `AllowPrivilegeEscalation: true`, but it must be `false` to prevent privilege escalation attacks")
82+
}
83+
}
84+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package rules
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
26+
"github.com/deckhouse/dmt/internal/storage"
27+
"github.com/deckhouse/dmt/pkg"
28+
"github.com/deckhouse/dmt/pkg/errors"
29+
)
30+
31+
func TestNoNewPrivilegesRule_ContainerNoNewPrivileges(t *testing.T) {
32+
tests := []struct {
33+
name string
34+
kind string
35+
containers []corev1.Container
36+
expectedErrors []string
37+
}{
38+
{
39+
name: "unsupported kind should be ignored",
40+
kind: "Service",
41+
containers: []corev1.Container{{
42+
Name: "test",
43+
}},
44+
expectedErrors: []string{},
45+
},
46+
{
47+
name: "missing security context should error",
48+
kind: "Deployment",
49+
containers: []corev1.Container{{
50+
Name: "test-container",
51+
}},
52+
expectedErrors: []string{
53+
"Container's SecurityContext is missing - cannot verify allowPrivilegeEscalation setting",
54+
},
55+
},
56+
{
57+
name: "missing allowPrivilegeEscalation should error",
58+
kind: "Deployment",
59+
containers: []corev1.Container{{
60+
Name: "test-container",
61+
SecurityContext: &corev1.SecurityContext{
62+
RunAsNonRoot: boolPtr(true),
63+
},
64+
}},
65+
expectedErrors: []string{
66+
"Container's SecurityContext missing parameter AllowPrivilegeEscalation - should be set to false to prevent privilege escalation",
67+
},
68+
},
69+
{
70+
name: "allowPrivilegeEscalation true should error",
71+
kind: "Deployment",
72+
containers: []corev1.Container{{
73+
Name: "test-container",
74+
SecurityContext: &corev1.SecurityContext{
75+
AllowPrivilegeEscalation: boolPtr(true),
76+
},
77+
}},
78+
expectedErrors: []string{
79+
"Container's SecurityContext has `AllowPrivilegeEscalation: true`, but it must be `false` to prevent privilege escalation attacks",
80+
},
81+
},
82+
{
83+
name: "allowPrivilegeEscalation false should pass",
84+
kind: "Deployment",
85+
containers: []corev1.Container{{
86+
Name: "test-container",
87+
SecurityContext: &corev1.SecurityContext{
88+
AllowPrivilegeEscalation: boolPtr(false),
89+
},
90+
}},
91+
expectedErrors: []string{},
92+
},
93+
{
94+
name: "multiple containers with mixed settings",
95+
kind: "Pod",
96+
containers: []corev1.Container{
97+
{
98+
Name: "good-container",
99+
SecurityContext: &corev1.SecurityContext{
100+
AllowPrivilegeEscalation: boolPtr(false),
101+
},
102+
},
103+
{
104+
Name: "bad-container",
105+
SecurityContext: &corev1.SecurityContext{
106+
AllowPrivilegeEscalation: boolPtr(true),
107+
},
108+
},
109+
{
110+
Name: "missing-context",
111+
},
112+
},
113+
expectedErrors: []string{
114+
"Container's SecurityContext has `AllowPrivilegeEscalation: true`, but it must be `false` to prevent privilege escalation attacks",
115+
"Container's SecurityContext is missing - cannot verify allowPrivilegeEscalation setting",
116+
},
117+
},
118+
}
119+
120+
for _, tt := range tests {
121+
t.Run(tt.name, func(t *testing.T) {
122+
rule := NewNoNewPrivilegesRule([]pkg.ContainerRuleExclude{})
123+
errorList := errors.NewLintRuleErrorsList()
124+
125+
obj := storage.StoreObject{
126+
Path: "test.yaml",
127+
Unstructured: unstructured.Unstructured{
128+
Object: map[string]any{
129+
"kind": tt.kind,
130+
"metadata": map[string]any{"name": "test-obj"},
131+
},
132+
},
133+
}
134+
135+
rule.ContainerNoNewPrivileges(obj, tt.containers, errorList)
136+
errs := errorList.GetErrors()
137+
138+
if len(tt.expectedErrors) == 0 {
139+
assert.Empty(t, errs, "Expected no errors")
140+
} else {
141+
assert.Len(t, errs, len(tt.expectedErrors), "Expected %d errors", len(tt.expectedErrors))
142+
for i, expectedError := range tt.expectedErrors {
143+
assert.Contains(t, errs[i].Text, expectedError, "Error %d should contain expected text", i)
144+
}
145+
}
146+
})
147+
}
148+
}
149+
150+
func TestNoNewPrivilegesRule_WithExclusions(t *testing.T) {
151+
excludeRules := []pkg.ContainerRuleExclude{
152+
{
153+
Kind: "Deployment",
154+
Name: "excluded-deployment",
155+
Container: "excluded-container",
156+
},
157+
}
158+
159+
rule := NewNoNewPrivilegesRule(excludeRules)
160+
errorList := errors.NewLintRuleErrorsList()
161+
162+
obj := storage.StoreObject{
163+
Path: "test.yaml",
164+
Unstructured: unstructured.Unstructured{
165+
Object: map[string]any{
166+
"kind": "Deployment",
167+
"metadata": map[string]any{"name": "excluded-deployment"},
168+
},
169+
},
170+
}
171+
172+
containers := []corev1.Container{{
173+
Name: "excluded-container",
174+
SecurityContext: &corev1.SecurityContext{
175+
AllowPrivilegeEscalation: boolPtr(true), // This would normally fail
176+
},
177+
}}
178+
179+
rule.ContainerNoNewPrivileges(obj, containers, errorList)
180+
errs := errorList.GetErrors()
181+
182+
assert.Empty(t, errs, "Excluded container should not generate errors")
183+
}
184+
185+
// Helper function to create bool pointers
186+
func boolPtr(b bool) *bool {
187+
return &b
188+
}

0 commit comments

Comments
 (0)