From 17d15ff37e3c76059f359c3d7d75dd0da14af7f0 Mon Sep 17 00:00:00 2001 From: Massimiliano Giovagnoli Date: Thu, 12 Dec 2024 21:09:47 +0100 Subject: [PATCH] feat: add support for adding linux capabilities This commit adds the configuration to add Linux capabilies to the container for bubblewrap and docker runners, enabling a declarative way to do least privilege at both build and test time. Furthermore, the minimum set of process capabilities in Bubblewrap container is now the same of Docker default capabilities. Signed-off-by: Massimiliano Giovagnoli --- go.mod | 3 +++ go.sum | 4 ++++ pkg/build/build.go | 6 +++++ pkg/build/test.go | 6 +++++ pkg/config/config.go | 12 +++++++++- pkg/config/schema.json | 21 ++++++++++++------ pkg/container/bubblewrap_runner.go | 21 +++++++++++++++++- pkg/container/config.go | 2 ++ pkg/container/docker/docker_runner.go | 8 +++++++ .../generated/x86_64/shbang-test-1-r1.apk | Bin 4522 -> 5907 bytes 10 files changed, 74 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 16db26bbc..cda8ac1f3 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/klauspost/compress v1.17.11 github.com/klauspost/pgzip v1.2.6 github.com/kubescape/go-git-url v0.0.30 + github.com/moby/moby v27.4.0+incompatible github.com/opencontainers/image-spec v1.1.0 github.com/package-url/packageurl-go v0.1.3 github.com/pkg/errors v0.9.1 @@ -93,6 +94,8 @@ require ( github.com/cloudflare/circl v1.5.0 // indirect github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect + github.com/containerd/containerd v1.7.24 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect diff --git a/go.sum b/go.sum index 08013ae5a..517294eb8 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,8 @@ github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8E github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= +github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA= +github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.16.2 h1:DMcqm1rd1ak2hFghkyHlquacSo+zRe+cysRR3CmSpGk= @@ -349,6 +351,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby v27.4.0+incompatible h1:jGXXZCMAmFZS9pKsQqUt9yAPHOC450PM9lbQYPSQnuc= +github.com/moby/moby v27.4.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= diff --git a/pkg/build/build.go b/pkg/build/build.go index b0812e34e..233242237 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -1175,6 +1175,12 @@ func (b *Build) buildWorkspaceConfig(ctx context.Context) *container.Config { cfg.Memory = b.Configuration.Package.Resources.Memory cfg.Disk = b.Configuration.Package.Resources.Disk } + if b.Configuration.Capabilities.Add != nil { + cfg.Capabilities.CapAdd = b.Configuration.Capabilities.Add + } + if b.Configuration.Capabilities.Drop != nil { + cfg.Capabilities.CapDrop = b.Configuration.Capabilities.Drop + } for k, v := range b.Configuration.Environment.Environment { cfg.Environment[k] = v diff --git a/pkg/build/test.go b/pkg/build/test.go index af06e686c..5cff297ee 100644 --- a/pkg/build/test.go +++ b/pkg/build/test.go @@ -574,6 +574,12 @@ func (t *Test) buildWorkspaceConfig(ctx context.Context, imgRef, pkgName string, Environment: map[string]string{}, RunAs: imgcfg.Accounts.RunAs, } + if t.Configuration.Capabilities.Add != nil { + cfg.Capabilities.CapAdd = t.Configuration.Capabilities.Add + } + if t.Configuration.Capabilities.Drop != nil { + cfg.Capabilities.CapDrop = t.Configuration.Capabilities.Drop + } for k, v := range imgcfg.Environment { cfg.Environment[k] = v diff --git a/pkg/config/config.go b/pkg/config/config.go index 711c39899..11b62ecba 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -595,12 +595,22 @@ type Input struct { Required bool `json:"required,omitempty"` } -// The root melange configuration +type Capabilities struct { + // Linux process capabilities to add to the pipeline container. + Add []string `json:"add,omitempty" yaml:"add,omitempty"` + // Linux process capabilities to drop from the pipeline container. + Drop []string `json:"drop,omitempty" yaml:"drop,omitempty"` +} + +// Configuration is the root melange configuration. type Configuration struct { // Package metadata Package Package `json:"package" yaml:"package"` // The specification for the packages build environment Environment apko_types.ImageConfiguration `json:"environment" yaml:"environment"` + // Optional: Linux capabilities configuration to apply to the melange runner. + Capabilities Capabilities `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` + // Required: The list of pipelines that produce the package. Pipeline []Pipeline `json:"pipeline,omitempty" yaml:"pipeline,omitempty"` // Optional: The list of subpackages that this package also produces. diff --git a/pkg/config/schema.json b/pkg/config/schema.json index 61a9feb40..0bc952988 100644 --- a/pkg/config/schema.json +++ b/pkg/config/schema.json @@ -430,7 +430,7 @@ "properties": { "description": { "type": "string", - "description": "Optional: The human readable description of the input" + "description": "Optional: The human-readable description of the input" }, "default": { "type": "string", @@ -499,7 +499,7 @@ }, "description": { "type": "string", - "description": "A human readable description of the package" + "description": "A human-readable description of the package" }, "url": { "type": "string", @@ -608,6 +608,10 @@ }, "Pipeline": { "properties": { + "if": { + "type": "string", + "description": "Optional: A condition to evaluate before running the pipeline" + }, "name": { "type": "string", "description": "Optional: A user defined name for the pipeline" @@ -632,7 +636,7 @@ "$ref": "#/$defs/Pipeline" }, "type": "array", - "description": "Optional: The list of pipelines to run.\n\nEach pipeline runs in it's own context that is not shared between other\npipelines. To share context between pipelines, nest a pipeline within an\nexisting pipeline. This can be useful when you wish to share common\nconfiguration, such as an alternative `working-directory`." + "description": "Optional: The list of pipelines to run.\n\nEach pipeline runs in its own context that is not shared between other\npipelines. To share context between pipelines, nest a pipeline within an\nexisting pipeline. This can be useful when you wish to share common\nconfiguration, such as an alternative `working-directory`." }, "inputs": { "additionalProperties": { @@ -649,10 +653,6 @@ "type": "string", "description": "Optional: Labels to apply to the pipeline" }, - "if": { - "type": "string", - "description": "Optional: A condition to evaluate before running the pipeline" - }, "assertions": { "$ref": "#/$defs/PipelineAssertions", "description": "Optional: Assertions to evaluate whether the pipeline was successful" @@ -733,6 +733,9 @@ "cpu": { "type": "string" }, + "cpumodel": { + "type": "string" + }, "memory": { "type": "string" }, @@ -898,6 +901,10 @@ "type": "boolean", "description": "Indicates that this package should be manually updated, usually taking\ncare over special version numbers" }, + "require-sequential": { + "type": "boolean", + "description": "Indicates that automated pull requests should be merged in order rather than superseding and closing previous unmerged PRs" + }, "shared": { "type": "boolean", "description": "Indicate that an update to this package requires an epoch bump of\ndownstream dependencies, e.g. golang, java" diff --git a/pkg/container/bubblewrap_runner.go b/pkg/container/bubblewrap_runner.go index 576e23a11..ac90fbb1a 100644 --- a/pkg/container/bubblewrap_runner.go +++ b/pkg/container/bubblewrap_runner.go @@ -25,11 +25,13 @@ import ( "path/filepath" "strings" + "chainguard.dev/melange/internal/logwriter" + apko_build "chainguard.dev/apko/pkg/build" apko_types "chainguard.dev/apko/pkg/build/types" - "chainguard.dev/melange/internal/logwriter" "github.com/chainguard-dev/clog" v1 "github.com/google/go-containerregistry/pkg/v1" + moby "github.com/moby/moby/oci/caps" "go.opentelemetry.io/otel" ) @@ -126,6 +128,23 @@ func (bw *bubblewrap) cmd(ctx context.Context, cfg *Config, debug bool, envOverr baseargs = append(baseargs, "--gid", buildUserID) } + // Add Docker runner-parity kernel capabilities to the container. + for _, c := range moby.DefaultCapabilities() { + baseargs = append(baseargs, "--cap-add", c) + } + // Add additional process kernel capabilities to the container as configured. + if cfg.Capabilities.CapAdd != nil { + for _, c := range cfg.Capabilities.CapAdd { + baseargs = append(baseargs, "--cap-add", c) + } + } + // Drop process kernel capabilities from the container as configured. + if cfg.Capabilities.CapDrop != nil { + for _, c := range cfg.Capabilities.CapDrop { + baseargs = append(baseargs, "--cap-drop", c) + } + } + if !debug { // This flag breaks job control, which we only care about for --interactive debugging. // So we usually include it, but if we're about to debug, don't set it. diff --git a/pkg/container/config.go b/pkg/container/config.go index 3e8cebc9a..0256e7e2f 100644 --- a/pkg/container/config.go +++ b/pkg/container/config.go @@ -40,6 +40,8 @@ type BindMount struct { type Capabilities struct { Networking bool + CapAdd []string // List of kernel capabilities to add to the container. + CapDrop []string // List of kernel capabilities to drop from the container. } type Config struct { diff --git a/pkg/container/docker/docker_runner.go b/pkg/container/docker/docker_runner.go index fda74ab4c..742dbd988 100644 --- a/pkg/container/docker/docker_runner.go +++ b/pkg/container/docker/docker_runner.go @@ -99,6 +99,14 @@ func (dk *docker) StartPod(ctx context.Context, cfg *mcontainer.Config) error { hostConfig := &container.HostConfig{ Mounts: mounts, } + // Add process kernel capabilities to the container if configured. + if len(cfg.Capabilities.CapAdd) > 0 { + hostConfig.CapAdd = cfg.Capabilities.CapAdd + } + // Drop process kernel capabilities from the container if configured. + if len(cfg.Capabilities.CapDrop) > 0 { + hostConfig.CapDrop = cfg.Capabilities.CapDrop + } platform := &image_spec.Platform{ Architecture: cfg.Arch.String(), diff --git a/pkg/sca/testdata/generated/x86_64/shbang-test-1-r1.apk b/pkg/sca/testdata/generated/x86_64/shbang-test-1-r1.apk index 76f402ed1ca19b7f146e9e714d6669957f71333d..573c02b824027a51dd00ed1b43fc6060308fecf7 100644 GIT binary patch literal 5907 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 XUlDGafj}StAOca0QT+kUfI$BR(PbXu literal 4522 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