Skip to content

Commit fdcb3ee

Browse files
authored
Merge pull request #728 from stuggi/remove-kolla
Add helpers for removing kolla
2 parents d0249ca + 5abfc1e commit fdcb3ee

13 files changed

Lines changed: 1047 additions & 26 deletions

File tree

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ repos:
1919
entry: make
2020
args: ['golangci']
2121
pass_filenames: false
22+
- id: make-verify-uids-gids
23+
name: make-verify-uids-gids
24+
language: system
25+
entry: make
26+
args: ['verify-uids-gids']
27+
pass_filenames: false
28+
files: ^modules/users/registry\.go$
2229

2330
- repo: https://github.com/pre-commit/pre-commit-hooks
2431
rev: v4.4.0

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,22 @@ ginkgo: $(GINKGO) ## Download ginkgo locally if necessary.
9898
$(GINKGO): $(LOCALBIN)
9999
test -s $(LOCALBIN)/ginkgo || GOBIN=$(LOCALBIN) go install github.com/onsi/ginkgo/v2/ginkgo
100100

101+
.PHONY: generate-uids-gids
102+
generate-uids-gids: ## Regenerate modules/users/zz_generated_uid_gid.yaml from Go constants.
103+
cd modules/users && go run ./cmd/gen-uid-gid-yaml
104+
105+
.PHONY: verify-uids-gids
106+
verify-uids-gids: ## Verify modules/users/zz_generated_uid_gid.yaml is up-to-date (fails if regeneration produces a diff).
107+
@cp modules/users/zz_generated_uid_gid.yaml modules/users/zz_generated_uid_gid.yaml.bak
108+
@$(MAKE) generate-uids-gids
109+
@diff modules/users/zz_generated_uid_gid.yaml.bak modules/users/zz_generated_uid_gid.yaml || \
110+
(echo ""; echo "ERROR: modules/users/zz_generated_uid_gid.yaml is out of date. Run 'make generate-uids-gids' and commit the result."; \
111+
mv modules/users/zz_generated_uid_gid.yaml.bak modules/users/zz_generated_uid_gid.yaml; exit 1)
112+
@rm -f modules/users/zz_generated_uid_gid.yaml.bak
113+
@echo "modules/users/zz_generated_uid_gid.yaml is up-to-date."
114+
101115
.PHONY: generate
102-
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
116+
generate: controller-gen generate-uids-gids ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
103117
for mod in $(shell find modules/ -maxdepth 1 -mindepth 1 -type d); do \
104118
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./$$mod/..." ; \
105119
done

modules/common/pod/security.go

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,15 @@ import (
2222
)
2323

2424
// RestrictiveSecurityContext returns a hardened container SecurityContext
25-
// suitable for unprivileged workloads. It sets RunAsNonRoot, drops all
26-
// capabilities, disables privilege escalation, and applies the RuntimeDefault
27-
// seccomp profile. The provided uid is used for both RunAsUser and RunAsGroup.
25+
// suitable for unprivileged workloads. It sets RunAsUser to uid, RunAsGroup
26+
// to gid, RunAsNonRoot, drops all capabilities, disables privilege escalation,
27+
// and applies the RuntimeDefault seccomp profile.
2828
// Optional addCapabilities are added back after dropping ALL.
29-
func RestrictiveSecurityContext(uid int64, addCapabilities ...corev1.Capability) *corev1.SecurityContext {
30-
return RestrictiveSecurityContextWithGID(uid, uid, addCapabilities...)
31-
}
32-
33-
// RestrictiveSecurityContextWithGID is like RestrictiveSecurityContext but
34-
// allows specifying a different GID.
35-
func RestrictiveSecurityContextWithGID(uid, gid int64, addCapabilities ...corev1.Capability) *corev1.SecurityContext {
29+
//
30+
// Does not set ReadOnlyRootFilesystem -- a future RestrictiveReadOnlySecurityContext
31+
// is the intended way for an individual service to opt into that later, without
32+
// changing this function's behavior for every existing caller.
33+
func RestrictiveSecurityContext(uid, gid int64, addCapabilities ...corev1.Capability) *corev1.SecurityContext {
3634
caps := &corev1.Capabilities{
3735
Drop: []corev1.Capability{"ALL"},
3836
}
@@ -43,11 +41,57 @@ func RestrictiveSecurityContextWithGID(uid, gid int64, addCapabilities ...corev1
4341
RunAsUser: ptr.To(uid),
4442
RunAsGroup: ptr.To(gid),
4543
RunAsNonRoot: ptr.To(true),
46-
ReadOnlyRootFilesystem: ptr.To(true),
4744
AllowPrivilegeEscalation: ptr.To(false),
4845
Capabilities: caps,
4946
SeccompProfile: &corev1.SeccompProfile{
5047
Type: corev1.SeccompProfileTypeRuntimeDefault,
5148
},
5249
}
5350
}
51+
52+
// RestrictivePodSecurityContext returns a hardened PodSecurityContext for
53+
// unprivileged workloads. It sets RunAsUser to uid, RunAsGroup and FSGroup
54+
// to gid, RunAsNonRoot, and applies the RuntimeDefault seccomp profile.
55+
// FSGroup ensures that volumes mounted from Secrets/ConfigMaps are
56+
// group-readable by the service process without needing chown.
57+
//
58+
// Optional supplementalGroups grant additional GIDs to the pod — use this
59+
// when the workload needs to read files not covered by FSGroup, e.g.
60+
// RPM-shipped configs baked into the container image with restrictive
61+
// group ownership rather than mounted from a Secret/ConfigMap.
62+
func RestrictivePodSecurityContext(uid, gid int64, supplementalGroups ...int64) *corev1.PodSecurityContext {
63+
return &corev1.PodSecurityContext{
64+
RunAsUser: ptr.To(uid),
65+
RunAsGroup: ptr.To(gid),
66+
RunAsNonRoot: ptr.To(true),
67+
FSGroup: ptr.To(gid),
68+
SupplementalGroups: supplementalGroups,
69+
SeccompProfile: &corev1.SeccompProfile{
70+
Type: corev1.SeccompProfileTypeRuntimeDefault,
71+
},
72+
}
73+
}
74+
75+
// RestrictiveSecurityContextWithGID is an alias for RestrictiveSecurityContext.
76+
// Deprecated: use RestrictiveSecurityContext directly.
77+
func RestrictiveSecurityContextWithGID(uid, gid int64, addCapabilities ...corev1.Capability) *corev1.SecurityContext {
78+
return RestrictiveSecurityContext(uid, gid, addCapabilities...)
79+
}
80+
81+
// PrivilegedSecurityContext returns a SecurityContext for a workload that
82+
// needs full Privileged access to the host (e.g. LVM/iSCSI/multipath device
83+
// management via nsenter'd host binaries) and therefore cannot use
84+
// RestrictiveSecurityContext — Privileged is incompatible with
85+
// ReadOnlyRootFilesystem and capability dropping. RunAsUser is set to uid,
86+
// RunAsGroup to gid. RunAsNonRoot is only set when uid is non-zero, since
87+
// Kubernetes rejects a pod at admission if RunAsNonRoot is true while
88+
// RunAsUser is 0 — some privileged host tooling genuinely needs to run as
89+
// root.
90+
func PrivilegedSecurityContext(uid, gid int64) *corev1.SecurityContext {
91+
return &corev1.SecurityContext{
92+
RunAsUser: ptr.To(uid),
93+
RunAsGroup: ptr.To(gid),
94+
RunAsNonRoot: ptr.To(uid != 0),
95+
Privileged: ptr.To(true),
96+
}
97+
}

modules/common/pod/security_test.go

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ import (
2424

2525
func TestRestrictiveSecurityContext(t *testing.T) {
2626
var uid int64 = 42457
27-
sc := RestrictiveSecurityContext(uid)
27+
var gid int64 = 42458
28+
sc := RestrictiveSecurityContext(uid, gid)
2829

2930
if sc.RunAsUser == nil || *sc.RunAsUser != uid {
3031
t.Errorf("expected RunAsUser %d, got %v", uid, sc.RunAsUser)
3132
}
32-
if sc.RunAsGroup == nil || *sc.RunAsGroup != uid {
33-
t.Errorf("expected RunAsGroup %d, got %v", uid, sc.RunAsGroup)
33+
if sc.RunAsGroup == nil || *sc.RunAsGroup != gid {
34+
t.Errorf("expected RunAsGroup %d, got %v", gid, sc.RunAsGroup)
3435
}
3536
if sc.RunAsNonRoot == nil || !*sc.RunAsNonRoot {
3637
t.Error("expected RunAsNonRoot true")
@@ -44,15 +45,52 @@ func TestRestrictiveSecurityContext(t *testing.T) {
4445
if sc.SeccompProfile == nil || sc.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault {
4546
t.Errorf("expected SeccompProfile RuntimeDefault, got %v", sc.SeccompProfile)
4647
}
47-
if sc.ReadOnlyRootFilesystem == nil || !*sc.ReadOnlyRootFilesystem {
48-
t.Error("expected ReadOnlyRootFilesystem true")
48+
if sc.ReadOnlyRootFilesystem != nil {
49+
t.Errorf("expected ReadOnlyRootFilesystem unset, got %v", sc.ReadOnlyRootFilesystem)
50+
}
51+
}
52+
53+
func TestRestrictivePodSecurityContext(t *testing.T) {
54+
var uid int64 = 42425
55+
var gid int64 = 42426
56+
psc := RestrictivePodSecurityContext(uid, gid)
57+
58+
if psc.RunAsUser == nil || *psc.RunAsUser != uid {
59+
t.Errorf("expected RunAsUser %d, got %v", uid, psc.RunAsUser)
60+
}
61+
if psc.RunAsGroup == nil || *psc.RunAsGroup != gid {
62+
t.Errorf("expected RunAsGroup %d, got %v", gid, psc.RunAsGroup)
63+
}
64+
if psc.RunAsNonRoot == nil || !*psc.RunAsNonRoot {
65+
t.Error("expected RunAsNonRoot true")
66+
}
67+
if psc.FSGroup == nil || *psc.FSGroup != gid {
68+
t.Errorf("expected FSGroup %d, got %v", gid, psc.FSGroup)
69+
}
70+
if psc.SeccompProfile == nil || psc.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault {
71+
t.Errorf("expected SeccompProfile RuntimeDefault, got %v", psc.SeccompProfile)
72+
}
73+
}
74+
75+
func TestRestrictivePodSecurityContextNoSupplementalGroups(t *testing.T) {
76+
psc := RestrictivePodSecurityContext(42425, 42425)
77+
if psc.SupplementalGroups != nil {
78+
t.Errorf("expected no SupplementalGroups, got %v", psc.SupplementalGroups)
4979
}
5080
}
5181

52-
func TestRestrictiveSecurityContextWithGID(t *testing.T) {
53-
var uid int64 = 42415
54-
var gid int64 = 42416
55-
sc := RestrictiveSecurityContextWithGID(uid, gid)
82+
func TestRestrictivePodSecurityContextWithSupplementalGroups(t *testing.T) {
83+
psc := RestrictivePodSecurityContext(42425, 42425, 48)
84+
85+
if len(psc.SupplementalGroups) != 1 || psc.SupplementalGroups[0] != 48 {
86+
t.Errorf("expected SupplementalGroups [48], got %v", psc.SupplementalGroups)
87+
}
88+
}
89+
90+
func TestPrivilegedSecurityContext(t *testing.T) {
91+
var uid int64 = 42407
92+
var gid int64 = 42408
93+
sc := PrivilegedSecurityContext(uid, gid)
5694

5795
if sc.RunAsUser == nil || *sc.RunAsUser != uid {
5896
t.Errorf("expected RunAsUser %d, got %v", uid, sc.RunAsUser)
@@ -63,20 +101,37 @@ func TestRestrictiveSecurityContextWithGID(t *testing.T) {
63101
if sc.RunAsNonRoot == nil || !*sc.RunAsNonRoot {
64102
t.Error("expected RunAsNonRoot true")
65103
}
66-
if sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation {
67-
t.Error("expected AllowPrivilegeEscalation false")
104+
if sc.Privileged == nil || !*sc.Privileged {
105+
t.Error("expected Privileged true")
106+
}
107+
if sc.Capabilities != nil {
108+
t.Errorf("expected no Capabilities set, got %v", sc.Capabilities)
109+
}
110+
if sc.ReadOnlyRootFilesystem != nil {
111+
t.Errorf("expected ReadOnlyRootFilesystem unset, got %v", sc.ReadOnlyRootFilesystem)
112+
}
113+
}
114+
115+
func TestPrivilegedSecurityContextRootUser(t *testing.T) {
116+
sc := PrivilegedSecurityContext(0, 42408)
117+
118+
if sc.RunAsUser == nil || *sc.RunAsUser != 0 {
119+
t.Errorf("expected RunAsUser 0, got %v", sc.RunAsUser)
120+
}
121+
if sc.RunAsNonRoot == nil || *sc.RunAsNonRoot {
122+
t.Error("expected RunAsNonRoot false when uid is 0")
68123
}
69124
}
70125

71126
func TestRestrictiveSecurityContextNoAddCaps(t *testing.T) {
72-
sc := RestrictiveSecurityContext(42457)
127+
sc := RestrictiveSecurityContext(42457, 42457)
73128
if sc.Capabilities.Add != nil {
74129
t.Errorf("expected no Add capabilities, got %v", sc.Capabilities.Add)
75130
}
76131
}
77132

78133
func TestRestrictiveSecurityContextWithAddCaps(t *testing.T) {
79-
sc := RestrictiveSecurityContext(42457, "NET_BIND_SERVICE", "CHOWN")
134+
sc := RestrictiveSecurityContext(42457, 42457, "NET_BIND_SERVICE", "CHOWN")
80135

81136
if len(sc.Capabilities.Drop) != 1 || sc.Capabilities.Drop[0] != "ALL" {
82137
t.Errorf("expected Drop [ALL], got %v", sc.Capabilities.Drop)

modules/common/util/templates/common/config/ssl.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
AddType application/x-pkcs7-crl .crl
99

1010
SSLPassPhraseDialog builtin
11-
SSLSessionCache "shmcb:/var/cache/mod_ssl/scache(512000)"
11+
SSLSessionCache "shmcb:/run/httpd/ssl_scache(512000)"
1212
SSLSessionCacheTimeout 300
1313
Mutex default
1414
SSLCryptoDevice builtin

modules/common/volume/volume.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
Copyright 2026 Red Hat
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 volume
18+
19+
import (
20+
"strconv"
21+
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/resource"
24+
)
25+
26+
const (
27+
// RunHttpdVolumeName is the standard volume name for the httpd PID file directory.
28+
RunHttpdVolumeName = "run-httpd"
29+
// RunHttpdMountPath is the canonical mount path for the httpd PID directory.
30+
RunHttpdMountPath = "/run/httpd"
31+
// VarLogHttpdVolumeName is the standard volume name for the httpd log directory.
32+
VarLogHttpdVolumeName = "var-log-httpd"
33+
// VarLogHttpdMountPath is the mount path for the httpd log directory.
34+
VarLogHttpdMountPath = "/var/log/httpd"
35+
// TmpVolumeName is the standard volume name for /tmp.
36+
TmpVolumeName = "tmp"
37+
// TmpMountPath is the mount path for /tmp.
38+
TmpMountPath = "/tmp"
39+
// HomeDirCacheSubdir is the ".cache" subdirectory under a service's
40+
// home directory. RHEL's python3-setuptools downstream patch caches
41+
// iter_entry_points() scans under $HOME/.cache/python-entrypoints/
42+
// on every process start. Needs a writable emptyDir mount when
43+
// ReadOnlyRootFilesystem is enabled.
44+
HomeDirCacheSubdir = ".cache"
45+
)
46+
47+
// WritableDirVolume returns an emptyDir Volume. Used for any path that needs
48+
// to be writable by a non-root service user: /run/httpd (PID file),
49+
// /var/log/httpd, /var/log/<service>, /tmp, service home-dir subdirs, etc.
50+
// Pass an optional sizeLimit to cap the volume's ephemeral storage.
51+
func WritableDirVolume(name string, sizeLimit ...*resource.Quantity) corev1.Volume {
52+
var limit *resource.Quantity
53+
if len(sizeLimit) > 0 {
54+
limit = sizeLimit[0]
55+
}
56+
return corev1.Volume{
57+
Name: name,
58+
VolumeSource: corev1.VolumeSource{
59+
EmptyDir: &corev1.EmptyDirVolumeSource{SizeLimit: limit},
60+
},
61+
}
62+
}
63+
64+
// WritableDirVolumeMount returns a VolumeMount for a writable emptyDir.
65+
func WritableDirVolumeMount(name, mountPath string) corev1.VolumeMount {
66+
return corev1.VolumeMount{
67+
Name: name,
68+
MountPath: mountPath,
69+
}
70+
}
71+
72+
// WritableDirSubPathMounts returns SubPath VolumeMounts onto the emptyDir
73+
// named volumeName for the given subdirectories of baseDir. Mounted via
74+
// SubPath rather than shadowing baseDir itself, since the image may bake
75+
// real content there (e.g. shell dotfiles under a service home directory).
76+
func WritableDirSubPathMounts(volumeName, baseDir string, subdirs ...string) []corev1.VolumeMount {
77+
mounts := make([]corev1.VolumeMount, 0, len(subdirs))
78+
for _, subdir := range subdirs {
79+
mounts = append(mounts, corev1.VolumeMount{
80+
Name: volumeName,
81+
MountPath: baseDir + "/" + subdir,
82+
SubPath: subdir,
83+
})
84+
}
85+
return mounts
86+
}
87+
88+
// ConfigSecretVolumes returns Volumes and VolumeMounts for a list of Secret
89+
// names, each mounted read-only at /var/lib/config-data/secret-{idx} with
90+
// DefaultMode 0440.
91+
func ConfigSecretVolumes(secretNames []string) ([]corev1.Volume, []corev1.VolumeMount) {
92+
var configSecretMode int32 = 0440
93+
volumes := make([]corev1.Volume, 0, len(secretNames))
94+
mounts := make([]corev1.VolumeMount, 0, len(secretNames))
95+
96+
for idx, secretName := range secretNames {
97+
volumes = append(volumes, corev1.Volume{
98+
Name: secretName,
99+
VolumeSource: corev1.VolumeSource{
100+
Secret: &corev1.SecretVolumeSource{
101+
SecretName: secretName,
102+
DefaultMode: &configSecretMode,
103+
},
104+
},
105+
})
106+
mounts = append(mounts, corev1.VolumeMount{
107+
Name: secretName,
108+
MountPath: "/var/lib/config-data/secret-" + strconv.Itoa(idx),
109+
ReadOnly: true,
110+
})
111+
}
112+
113+
return volumes, mounts
114+
}

0 commit comments

Comments
 (0)