Skip to content

Commit 8af9ecd

Browse files
committed
Add ReadOnlyRootFilesystem securityContext to build steps
Set the root filesystem to read-only for all build and buildstrategy containers as a security best practice. To support this, steps that require write access now explicitly mount `emptyDir` volumes for paths like `/tmp` `/home`. A new `AppendWriteableVolumes` function centralizes the setup for volume mounting , using idempotent helpers (`ensureVolume`, `ensureVolumeMount`) to prevent duplicate entries. The writeable home directory for the steps can be configured using `WRITABLE_HOME_DIR`. Default is value is `/writable-home` Signed-off-by: Hasan Awad <[email protected]>
1 parent 5fa5b14 commit 8af9ecd

18 files changed

+387
-45
lines changed

docs/configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The following environment variables are available:
2222
| `GIT_CONTAINER_IMAGE` | Custom container image for Git clone steps. If `GIT_CONTAINER_TEMPLATE` is also specifying an image, then the value for `GIT_CONTAINER_IMAGE` has precedence. |
2323
| `BUNDLE_CONTAINER_TEMPLATE` | JSON representation of a [Container] template that is used for steps that pulls a bundle image to obtain the packaged source code. Default is `{"image": "ghcr.io/shipwright-io/build/bundle:latest", "command": ["/ko-app/bundle"], "env": [{"name": "HOME","value": "/shared-home"},{"name": "BUNDLE_SHOW_LISTING","value": "false"}], "securityContext":{"allowPrivilegeEscalation": false, "capabilities": {"drop": ["ALL"]}, "runAsUser":1000,"runAsGroup":1000}}` [^1]. The following properties are ignored as they are set by the controller: `args`, `name`. |
2424
| `BUNDLE_CONTAINER_IMAGE` | Custom container image that pulls a bundle image to obtain the packaged source code. If `BUNDLE_IMAGE_CONTAINER_TEMPLATE` is also specifying an image, then the value for `BUNDLE_IMAGE_CONTAINER_IMAGE` has precedence. |
25-
| `IMAGE_PROCESSING_CONTAINER_TEMPLATE` | JSON representation of a [Container](https://pkg.go.dev/k8s.io/api/core/v1#Container) template that is used for steps that processes the image. Default is `{"image": "ghcr.io/shipwright-io/build/image-processing:latest", "command": ["/ko-app/image-processing"], "env": [{"name": "HOME","value": "/shared-home"}], "securityContext": {"allowPrivilegeEscalation": false, "capabilities": {"add": ["DAC_OVERRIDE"], "drop": ["ALL"]}, "runAsUser": 0, "runAsgGroup": 0}}`. The following properties are ignored as they are set by the controller: `args`, `name`. |
25+
| `IMAGE_PROCESSING_CONTAINER_TEMPLATE` | JSON representation of a [Container](https://pkg.go.dev/k8s.io/api/core/v1#Container) template that is used for steps that processes the image. Default is `{"image": "ghcr.io/shipwright-io/build/image-processing:latest", "command": ["/ko-app/image-processing"], "env": [{"name": "HOME","value": "/shared-home"}, {"name": "TRIVY_CACHE_DIR", "value": "/trivy-cache-data/trivy-cache"}], "securityContext": {"allowPrivilegeEscalation": false, "capabilities": {"add": ["DAC_OVERRIDE"], "drop": ["ALL"]}, "runAsUser": 0, "runAsGroup": 0}}`. The following properties are ignored as they are set by the controller: `args`, `name`. |
2626
| `IMAGE_PROCESSING_CONTAINER_IMAGE` | Custom container image that is used for steps that processes the image. If `IMAGE_PROCESSING_CONTAINER_TEMPLATE` is also specifying an image, then the value for `IMAGE_PROCESSING_CONTAINER_IMAGE` has precedence. |
2727
| `WAITER_CONTAINER_TEMPLATE` | JSON representation of a [Container] template that waits for local source code to be uploaded to it. Default is `{"image":"ghcr.io/shipwright-io/build/waiter:latest", "command": ["/ko-app/waiter"], "args": ["start"], "env": [{"name": "HOME","value": "/shared-home"}], "securityContext":{"allowPrivilegeEscalation": false, "capabilities": {"drop": ["ALL"]}, "runAsUser":1000,"runAsGroup":1000}}`. The following properties are ignored as they are set by the controller: `args`, `name`. |
2828
| `WAITER_CONTAINER_IMAGE` | Custom container image that waits for local source code to be uploaded to it. If `WAITER_IMAGE_CONTAINER_TEMPLATE` is also specifying an image, then the value for `WAITER_IMAGE_CONTAINER_IMAGE` has precedence. |
@@ -37,6 +37,7 @@ The following environment variables are available:
3737
| `KUBE_API_BURST` | Burst to use for the Kubernetes API client. See [Config.Burst]. A value of 0 or lower will use the default from client-go, which currently is 10. Default is 0. |
3838
| `KUBE_API_QPS` | QPS to use for the Kubernetes API client. See [Config.QPS]. A value of 0 or lower will use the default from client-go, which currently is 5. Default is 0. |
3939
| `VULNERABILITY_COUNT_LIMIT` | holds vulnerability count limit if vulnerability scan is enabled for the output image. If it is defined as 10, then it will output only 10 vulnerabilities sorted by severity in the buildrun status.Output. Default is 50. |
40+
| `WRITABLE_HOME_DIR` | Specifies the mount path for writable home directories in build containers. Each container gets its own isolated emptyDir volume mounted at this path, enabling write operations when `readOnlyRootFilesystem: true` is used. Default is `/writable-home`. |
4041

4142
[^1]: The `runAsUser` and `runAsGroup` are dynamically overwritten depending on the build strategy that is used. See [Security Contexts](buildstrategies.md#security-contexts) for more information.
4243

pkg/config/config.go

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ const (
7979

8080
// environment variable to hold vulnerability count limit
8181
VulnerabilityCountLimitEnvVar = "VULNERABILITY_COUNT_LIMIT"
82+
83+
// Trivy related environment variables
84+
trivyCacheDirEnvVar = "TRIVY_CACHE_DIR"
85+
// Default paths for Trivy
86+
defaultTrivyCacheDir = "/trivy-cache-data/trivy-cache"
87+
88+
// Default writable home directory path inside containers
89+
// Note: Each container gets its own isolated emptyDir volume mounted at this path
90+
// The actual volume names are unique per container (e.g., shp-writable-home-step-name)
91+
defaultWritableHomeDir = "/writable-home"
92+
writableHomeDirEnvVar = "WRITABLE_HOME_DIR"
8293
)
8394

8495
var (
@@ -107,6 +118,14 @@ type Config struct {
107118
KubeAPIOptions KubeAPIOptions
108119
GitRewriteRule bool
109120
VulnerabilityCountLimit int
121+
ContainersWritableDir WritableDirsConfig
122+
}
123+
124+
type WritableDirsConfig struct {
125+
TrivyCacheDir string
126+
// WritableHomeDir is the path where each container's writable home directory is mounted
127+
// Each container gets its own isolated emptyDir volume at this mount path
128+
WritableHomeDir string
110129
}
111130

112131
// PrometheusConfig contains the specific configuration for the
@@ -163,22 +182,28 @@ func NewDefaultConfig() *Config {
163182
TerminationLogPath: terminationLogPathDefault,
164183
GitRewriteRule: false,
165184
VulnerabilityCountLimit: 50,
166-
185+
ContainersWritableDir: WritableDirsConfig{
186+
TrivyCacheDir: defaultTrivyCacheDir,
187+
WritableHomeDir: defaultWritableHomeDir,
188+
},
167189
GitContainerTemplate: Step{
168190
Image: gitDefaultImage,
169191
Command: []string{
170192
"/ko-app/git",
171193
},
172194
Env: []corev1.EnvVar{
173-
// This directory is created in the base image as writable for everybody
174195
{
175196
Name: "HOME",
176-
Value: "/shared-home",
197+
Value: defaultWritableHomeDir,
177198
},
178199
{
179200
Name: "GIT_SHOW_LISTING",
180201
Value: "false",
181202
},
203+
{
204+
Name: "TMPDIR",
205+
Value: "/tmp",
206+
},
182207
},
183208
SecurityContext: &corev1.SecurityContext{
184209
AllowPrivilegeEscalation: ptr.To(false),
@@ -187,8 +212,9 @@ func NewDefaultConfig() *Config {
187212
"ALL",
188213
},
189214
},
190-
RunAsUser: nonRoot,
191-
RunAsGroup: nonRoot,
215+
RunAsUser: nonRoot,
216+
RunAsGroup: nonRoot,
217+
ReadOnlyRootFilesystem: ptr.To(true),
192218
},
193219
},
194220

@@ -197,11 +223,10 @@ func NewDefaultConfig() *Config {
197223
Command: []string{
198224
"/ko-app/bundle",
199225
},
200-
// This directory is created in the base image as writable for everybody
201226
Env: []corev1.EnvVar{
202227
{
203228
Name: "HOME",
204-
Value: "/shared-home",
229+
Value: defaultWritableHomeDir,
205230
},
206231
{
207232
Name: "BUNDLE_SHOW_LISTING",
@@ -215,8 +240,9 @@ func NewDefaultConfig() *Config {
215240
"ALL",
216241
},
217242
},
218-
RunAsUser: nonRoot,
219-
RunAsGroup: nonRoot,
243+
RunAsUser: nonRoot,
244+
RunAsGroup: nonRoot,
245+
ReadOnlyRootFilesystem: ptr.To(true),
220246
},
221247
},
222248

@@ -225,11 +251,10 @@ func NewDefaultConfig() *Config {
225251
Command: []string{
226252
"/ko-app/image-processing",
227253
},
228-
// This directory is created in the base image as writable for everybody
229254
Env: []corev1.EnvVar{
230255
{
231256
Name: "HOME",
232-
Value: "/shared-home",
257+
Value: defaultWritableHomeDir,
233258
},
234259
},
235260
// The image processing step runs after the build strategy steps where an arbitrary
@@ -241,6 +266,7 @@ func NewDefaultConfig() *Config {
241266
AllowPrivilegeEscalation: ptr.To(false),
242267
RunAsUser: root,
243268
RunAsGroup: root,
269+
ReadOnlyRootFilesystem: ptr.To(true),
244270
Capabilities: &corev1.Capabilities{
245271
Add: []corev1.Capability{
246272
"DAC_OVERRIDE",
@@ -260,11 +286,10 @@ func NewDefaultConfig() *Config {
260286
Args: []string{
261287
"start",
262288
},
263-
// This directory is created in the base image as writable for everybody
264289
Env: []corev1.EnvVar{
265290
{
266291
Name: "HOME",
267-
Value: "/shared-home",
292+
Value: defaultWritableHomeDir,
268293
},
269294
},
270295
SecurityContext: &corev1.SecurityContext{
@@ -274,8 +299,9 @@ func NewDefaultConfig() *Config {
274299
"ALL",
275300
},
276301
},
277-
RunAsUser: nonRoot,
278-
RunAsGroup: nonRoot,
302+
RunAsUser: nonRoot,
303+
RunAsGroup: nonRoot,
304+
ReadOnlyRootFilesystem: ptr.To(true),
279305
},
280306
},
281307

@@ -455,6 +481,11 @@ func (c *Config) SetConfigFromEnv() error {
455481
c.TerminationLogPath = terminationLogPath
456482
}
457483

484+
// Update writable directory paths if environment variables are set
485+
if err := updateWritableDirOption(&c.ContainersWritableDir.WritableHomeDir, writableHomeDirEnvVar); err != nil {
486+
return err
487+
}
488+
458489
return nil
459490
}
460491

@@ -509,3 +540,11 @@ func updateIntOption(i *int, envVarName string) error {
509540

510541
return nil
511542
}
543+
544+
// updateWritableDirOption updates the writable directory paths if the environment variable is set
545+
func updateWritableDirOption(path *string, envVarName string) error {
546+
if value := os.Getenv(envVarName); value != "" {
547+
*path = value
548+
}
549+
return nil
550+
}

pkg/config/config_test.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,12 @@ var _ = Describe("Config", func() {
132132
"/ko-app/git",
133133
},
134134
Env: []corev1.EnvVar{
135-
{Name: "HOME", Value: "/shared-home"},
135+
{Name: "HOME", Value: "/writable-home"},
136136
{Name: "GIT_SHOW_LISTING", Value: "false"},
137+
{
138+
Name: "TMPDIR",
139+
Value: "/tmp",
140+
},
137141
},
138142
SecurityContext: &corev1.SecurityContext{
139143
AllowPrivilegeEscalation: ptr.To(false),
@@ -142,8 +146,9 @@ var _ = Describe("Config", func() {
142146
"ALL",
143147
},
144148
},
145-
RunAsUser: nonRoot,
146-
RunAsGroup: nonRoot,
149+
RunAsUser: nonRoot,
150+
RunAsGroup: nonRoot,
151+
ReadOnlyRootFilesystem: ptr.To(true),
147152
},
148153
}))
149154
})
@@ -235,16 +240,17 @@ var _ = Describe("Config", func() {
235240
Image: "myregistry/custom/image",
236241
Command: []string{"/ko-app/waiter"},
237242
Args: []string{"start"},
238-
Env: []corev1.EnvVar{{Name: "HOME", Value: "/shared-home"}},
243+
Env: []corev1.EnvVar{{Name: "HOME", Value: "/writable-home"}},
239244
SecurityContext: &corev1.SecurityContext{
240245
AllowPrivilegeEscalation: ptr.To(false),
241246
Capabilities: &corev1.Capabilities{
242247
Drop: []corev1.Capability{
243248
"ALL",
244249
},
245250
},
246-
RunAsUser: nonRoot,
247-
RunAsGroup: nonRoot,
251+
RunAsUser: nonRoot,
252+
RunAsGroup: nonRoot,
253+
ReadOnlyRootFilesystem: ptr.To(true),
248254
},
249255
}))
250256
})

pkg/reconciler/buildrun/resources/image_processing.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ func SetupImageProcessing(taskRun *pipelineapi.TaskRun, cfg *config.Config, crea
174174
SecurityContext: cfg.ImageProcessingContainerTemplate.SecurityContext,
175175
WorkingDir: cfg.ImageProcessingContainerTemplate.WorkingDir,
176176
}
177-
178177
if volumeAdded {
179178
imageProcessingStep.VolumeMounts = append(imageProcessingStep.VolumeMounts, core.VolumeMount{
180179
Name: prefixedOutputDirectory,
@@ -201,6 +200,23 @@ func SetupImageProcessing(taskRun *pipelineapi.TaskRun, cfg *config.Config, crea
201200
)
202201
}
203202

203+
sources.AppendWriteableVolumes(taskRun.Spec.TaskSpec, &imageProcessingStep, cfg.ContainersWritableDir.WritableHomeDir)
204+
205+
taskRun.Spec.TaskSpec.Volumes = append(taskRun.Spec.TaskSpec.Volumes, core.Volume{
206+
Name: "trivy-cache-data",
207+
VolumeSource: core.VolumeSource{
208+
EmptyDir: &core.EmptyDirVolumeSource{},
209+
},
210+
})
211+
imageProcessingStep.VolumeMounts = append(imageProcessingStep.VolumeMounts, core.VolumeMount{
212+
Name: "trivy-cache-data",
213+
MountPath: cfg.ContainersWritableDir.TrivyCacheDir,
214+
})
215+
216+
imageProcessingStep.Env = append(imageProcessingStep.Env, core.EnvVar{
217+
Name: "TRIVY_CACHE_DIR",
218+
Value: cfg.ContainersWritableDir.TrivyCacheDir,
219+
})
204220
// append the mutate step
205221
taskRun.Spec.TaskSpec.Steps = append(taskRun.Spec.TaskSpec.Steps, imageProcessingStep)
206222
}

pkg/reconciler/buildrun/resources/sources/bundle.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func AppendBundleStep(cfg *config.Config, taskSpec *pipelineapi.TaskSpec, oci *b
6767
bundleStep.Args = append(bundleStep.Args, "--prune")
6868
}
6969

70+
AppendWriteableVolumes(taskSpec, &bundleStep, cfg.ContainersWritableDir.WritableHomeDir)
7071
taskSpec.Steps = append(taskSpec.Steps, bundleStep)
7172
}
7273

pkg/reconciler/buildrun/resources/sources/git.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func AppendGitStep(
114114
secretMountPath,
115115
)
116116
}
117-
117+
AppendWriteableVolumes(taskSpec, &gitStep, cfg.ContainersWritableDir.WritableHomeDir)
118118
// append the git step
119119
taskSpec.Steps = append(taskSpec.Steps, gitStep)
120120
}

pkg/reconciler/buildrun/resources/sources/git_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ var _ = Describe("Git", func() {
7979
})
8080

8181
It("adds a volume for the secret", func() {
82-
Expect(len(taskSpec.Volumes)).To(Equal(1))
82+
Expect(len(taskSpec.Volumes)).To(Equal(3))
8383
Expect(taskSpec.Volumes[0].Name).To(Equal("shp-a-secret"))
8484
Expect(taskSpec.Volumes[0].VolumeSource.Secret).NotTo(BeNil())
8585
Expect(taskSpec.Volumes[0].VolumeSource.Secret.SecretName).To(Equal("a.secret"))
@@ -100,7 +100,7 @@ var _ = Describe("Git", func() {
100100
"--result-file-source-timestamp", "$(results.shp-source-default-source-timestamp.path)",
101101
"--secret-path", "/workspace/shp-source-secret",
102102
}))
103-
Expect(len(taskSpec.Steps[0].VolumeMounts)).To(Equal(1))
103+
Expect(len(taskSpec.Steps[0].VolumeMounts)).To(Equal(3))
104104
Expect(taskSpec.Steps[0].VolumeMounts[0].Name).To(Equal("shp-a-secret"))
105105
Expect(taskSpec.Steps[0].VolumeMounts[0].MountPath).To(Equal("/workspace/shp-source-secret"))
106106
Expect(taskSpec.Steps[0].VolumeMounts[0].ReadOnly).To(BeTrue())
@@ -188,7 +188,7 @@ var _ = Describe("Git", func() {
188188
Revision: ptr.To(revision),
189189
CloneSecret: ptr.To("another.secret"),
190190
}, "default")
191-
191+
192192
Expect(len(taskSpec.Steps)).To(Equal(1))
193193
Expect(taskSpec.Steps[0].Args).To(ContainElements(
194194
"--url", "https://github.com/shipwright-io/another-repo",
@@ -200,4 +200,4 @@ var _ = Describe("Git", func() {
200200
Expect(taskSpec.Steps[0].VolumeMounts).To(ContainElement(HaveField("Name", "shp-another-secret")))
201201
})
202202
})
203-
})
203+
})

pkg/reconciler/buildrun/resources/sources/local_copy.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func AppendLocalCopyStep(cfg *config.Config, taskSpec *pipelineapi.TaskSpec, tim
3232
WorkingDir: cfg.WaiterContainerTemplate.WorkingDir,
3333
}
3434

35+
AppendWriteableVolumes(taskSpec, &step, cfg.ContainersWritableDir.WritableHomeDir)
3536
if timeout != nil {
3637
step.Args = append(step.Args, fmt.Sprintf("--timeout=%s", timeout.Duration.String()))
3738
}

0 commit comments

Comments
 (0)