Skip to content

Commit 0d21d84

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 Trivy cache directory for vulnerability scanning can be configured using `TRIVY_CACHE_DIR`. Signed-off-by: Hasan Awad <[email protected]>
1 parent 5fa5b14 commit 0d21d84

18 files changed

+408
-45
lines changed

docs/configuration.md

Lines changed: 1 addition & 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. |

pkg/config/config.go

Lines changed: 58 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,15 @@ 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.TrivyCacheDir, trivyCacheDirEnvVar); err != nil {
486+
return err
487+
}
488+
489+
if err := updateWritableDirOption(&c.ContainersWritableDir.WritableHomeDir, writableHomeDirEnvVar); err != nil {
490+
return err
491+
}
492+
458493
return nil
459494
}
460495

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

510545
return nil
511546
}
547+
548+
// updateWritableDirOption updates the writable directory paths if the environment variable is set
549+
func updateWritableDirOption(path *string, envVarName string) error {
550+
if value := os.Getenv(envVarName); value != "" {
551+
*path = value
552+
}
553+
return nil
554+
}

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)