From 96bf1524c875f2b70f13d6852785cd881b8f8d3a Mon Sep 17 00:00:00 2001 From: Nick Zavaritsky Date: Tue, 2 Apr 2024 02:25:38 +0000 Subject: [PATCH 1/3] Handle shorthand syntax in build config Build.Config contains LdFlags and Flags, both arrays of strings. For user convenience it should be possible to specify a single string instead. FlagArray (Flags) and StringArray (LdFlags) implement YAMLUnmarshaller interface to handle custom parsing logic. Since build.Config is loaded via viper/mapstructure and not the YAML parser, YAMLUnmarshaller interface was ignored. Wire things up. Signed-off-by: Nick Zavaritsky --- go.mod | 2 +- pkg/commands/options/build.go | 37 +++++++++++++++++++++++++++++++++-- test/build-configs/.ko.yaml | 1 + 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 52eb438548..0ae7cdf94e 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-training/helloworld v0.0.0-20200225145412-ba5f4379d78b github.com/google/go-cmp v0.6.0 github.com/google/go-containerregistry v0.19.1 + github.com/mitchellh/mapstructure v1.5.0 github.com/opencontainers/image-spec v1.1.0 github.com/sigstore/cosign/v2 v2.2.3 github.com/spf13/cobra v1.8.0 @@ -93,7 +94,6 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go index a16c1033ad..a906d3282e 100644 --- a/pkg/commands/options/build.go +++ b/pkg/commands/options/build.go @@ -19,8 +19,10 @@ import ( "fmt" "os" "path/filepath" + "reflect" "github.com/google/go-containerregistry/pkg/name" + "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/tools/go/packages" @@ -158,8 +160,12 @@ func (bo *BuildOptions) LoadConfig() error { if len(bo.BuildConfigs) == 0 { var builds []build.Config - if err := v.UnmarshalKey("builds", &builds); err != nil { - return fmt.Errorf("configuration section 'builds' cannot be parsed") + useYAMLTagsAndUnmarshallers := func(c *mapstructure.DecoderConfig) { + c.TagName = "yaml" // defaults to `mapstructure:""` + c.DecodeHook = yamlUnmarshallerHookFunc + } + if err := v.UnmarshalKey("builds", &builds, useYAMLTagsAndUnmarshallers); err != nil { + return fmt.Errorf("configuration section 'builds' cannot be parsed: %w", err) } buildConfigs, err := createBuildConfigMap(bo.WorkingDirectory, builds) if err != nil { @@ -171,6 +177,33 @@ func (bo *BuildOptions) LoadConfig() error { return nil } +func yamlUnmarshallerHookFunc(_ reflect.Type, to reflect.Type, data any) (any, error) { + type yamlUnmarshaller interface { + UnmarshalYAML(func(any) error) error + } + result := reflect.New(to).Interface() + unmarshaller, ok := result.(yamlUnmarshaller) + if !ok { + return data, nil + } + if err := unmarshaller.UnmarshalYAML(func(target any) error { + dest := reflect.Indirect(reflect.ValueOf(target)) + src := reflect.ValueOf(data) + if dest.CanSet() && src.Type().AssignableTo(dest.Type()) { + dest.Set(src) + return nil + } + return fmt.Errorf("want %v, got %v", dest.Type(), src.Type()) + }); err != nil { + // We do not implement []string <- []any above, therefore YAML + // unmarshaller could fail given perfectly valid input. Return + // data AS IS, allowing mapstructure's logic to perform the + // conversion. + return data, nil + } + return result, nil +} + func createBuildConfigMap(workingDirectory string, configs []build.Config) (map[string]build.Config, error) { buildConfigsByImportPath := make(map[string]build.Config) for i, config := range configs { diff --git a/test/build-configs/.ko.yaml b/test/build-configs/.ko.yaml index 8bd64477eb..1962dc041c 100644 --- a/test/build-configs/.ko.yaml +++ b/test/build-configs/.ko.yaml @@ -16,6 +16,7 @@ builds: - id: foo-app dir: ./foo main: ./cmd + flags: -v -v # build.Config parser must handle shorthand syntax - id: bar-app dir: ./bar main: ./cmd From 1c7678af2722b6df2a60343cd87eab6d69cd23c2 Mon Sep 17 00:00:00 2001 From: Nick Zavaritsky Date: Tue, 26 Mar 2024 12:35:07 +0000 Subject: [PATCH 2/3] Add package for handling Linux capabilities Signed-off-by: Nick Zavaritsky --- pkg/caps/caps.go | 213 +++++++++++++++++++++++++++++++++ pkg/caps/caps_dd_test.go | 64 ++++++++++ pkg/caps/caps_test.go | 100 ++++++++++++++++ pkg/caps/gen.sh | 73 +++++++++++ pkg/caps/new_file_caps_test.go | 88 ++++++++++++++ 5 files changed, 538 insertions(+) create mode 100644 pkg/caps/caps.go create mode 100644 pkg/caps/caps_dd_test.go create mode 100644 pkg/caps/caps_test.go create mode 100755 pkg/caps/gen.sh create mode 100644 pkg/caps/new_file_caps_test.go diff --git a/pkg/caps/caps.go b/pkg/caps/caps.go new file mode 100644 index 0000000000..5da04f2c9b --- /dev/null +++ b/pkg/caps/caps.go @@ -0,0 +1,213 @@ +// Copyright 2024 ko Build Authors All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package caps implements a subset of Linux capabilities handling +// relevant in the context of authoring container images. +package caps + +import ( + "bytes" + "encoding/binary" + "fmt" + "strconv" + "strings" +) + +// Mask captures a set of Linux capabilities +type Mask uint64 + +// Parse text representation of a single Linux capability. +// +// It accepts all variations recognized by Docker's --cap-add, such as +// 'chown', 'cap_chown', and 'CHOWN'. Additionally, we allow numeric +// values, e.g. '42' to support future capabilities that are not yet +// known to us. +func Parse(s string) (Mask, error) { + if index, err := strconv.ParseUint(s, 10, 6); err == nil { + return 1 << index, nil + } + name := strings.ToUpper(s) + if name == "ALL" { + return allKnownCaps(), nil + } + name = strings.TrimPrefix(name, "CAP_") + if index, ok := nameToIndex[name]; ok { + return 1 << index, nil + } + return 0, fmt.Errorf("unknown capability: %#v", s) +} + +func allKnownCaps() Mask { + var mask Mask + for _, index := range nameToIndex { + mask |= 1 << index + } + return mask +} + +var nameToIndex = map[string]int{ + "CHOWN": 0, + "DAC_OVERRIDE": 1, + "DAC_READ_SEARCH": 2, + "FOWNER": 3, + "FSETID": 4, + "KILL": 5, + "SETGID": 6, + "SETUID": 7, + "SETPCAP": 8, + "LINUX_IMMUTABLE": 9, + "NET_BIND_SERVICE": 10, + "NET_BROADCAST": 11, + "NET_ADMIN": 12, + "NET_RAW": 13, + "IPC_LOCK": 14, + "IPC_OWNER": 15, + "SYS_MODULE": 16, + "SYS_RAWIO": 17, + "SYS_CHROOT": 18, + "SYS_PTRACE": 19, + "SYS_PACCT": 20, + "SYS_ADMIN": 21, + "SYS_BOOT": 22, + "SYS_NICE": 23, + "SYS_RESOURCE": 24, + "SYS_TIME": 25, + "SYS_TTY_CONFIG": 26, + "MKNOD": 27, + "LEASE": 28, + "AUDIT_WRITE": 29, + "AUDIT_CONTROL": 30, + "SETFCAP": 31, + + "MAC_OVERRIDE": 32, + "MAC_ADMIN": 33, + "SYSLOG": 34, + "WAKE_ALARM": 35, + "BLOCK_SUSPEND": 36, + "AUDIT_READ": 37, + "PERFMON": 38, + "BPF": 39, + "CHECKPOINT_RESTORE": 40, +} + +// Flags alter certain aspects of capabilities handling +type Flags uint32 + +const ( + // FlagEffective causes all of the new permitted capabilities to be + // also raised in the effective set diring execve(2) + FlagEffective Flags = 1 +) + +// XattrBytes encodes capabilities in the format of +// security.capability extended filesystem attribute. This is how Linux +// tracks file capabilities internally. +func XattrBytes(permitted, inheritable Mask, flags Flags) ([]byte, error) { + // Underlying data layout as defined by Linux kernel (vfs_ns_cap_data) + type vfsNsCapData struct { + MagicEtc uint32 + Data [2]struct { + Permitted uint32 + Inheritable uint32 + } + } + + const vfsCapRevision2 = 0x02000000 + + data := vfsNsCapData{MagicEtc: vfsCapRevision2 | uint32(flags)} + data.Data[0].Permitted = uint32(permitted) + data.Data[0].Inheritable = uint32(inheritable) + data.Data[1].Permitted = uint32(permitted >> 32) + data.Data[1].Inheritable = uint32(inheritable >> 32) + + buf := &bytes.Buffer{} + if err := binary.Write(buf, binary.LittleEndian, data); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// FileCaps encodes Linux file capabilities +type FileCaps struct { + permitted, inheritable Mask + flags Flags +} + +// NewFileCaps produces file capabilities object from a list of string +// terms. A term is either a single capability name (added as permitted) +// or a cap_from_text(3) clause. +func NewFileCaps(terms ...string) (*FileCaps, error) { + var permitted, inheritable, effective Mask + for _, term := range terms { + var caps, actionList string + if index := strings.IndexAny(term, "+-="); index != -1 { + caps, actionList = term[:index], term[index:] + } else { + mask, err := Parse(term) + if err != nil { + return nil, err + } + permitted |= mask + continue + } + // Handling cap_from_text(3) syntax, e.g. cap1,cap2=pie + if caps == "" && actionList[0] == '=' { + caps = "all" + } + var mask, mask2 Mask + for _, capname := range strings.Split(caps, ",") { + m, err := Parse(capname) + if err != nil { + return nil, fmt.Errorf("%#v: %w", term, err) + } + mask |= m + } + for _, c := range actionList { + switch c { + case '+': + mask2 = ^Mask(0) + case '-': + mask2 = ^mask + case '=': + mask2 = ^Mask(0) + permitted &= ^mask + inheritable &= ^mask + effective &= ^mask + case 'p': + permitted = (permitted | mask) & mask2 + case 'i': + inheritable = (inheritable | mask) & mask2 + case 'e': + effective = (effective | mask) & mask2 + default: + return nil, fmt.Errorf("%#v: unknown flag '%c'", term, c) + } + } + } + if permitted != 0 || inheritable != 0 { + var flags Flags + if effective != 0 { + flags = FlagEffective + } + return &FileCaps{permitted: permitted, inheritable: inheritable, flags: flags}, nil + } + return nil, nil +} + +// ToXattrBytes encodes capabilities in the format of +// security.capability extended filesystem attribute. +func (fc *FileCaps) ToXattrBytes() ([]byte, error) { + return XattrBytes(fc.permitted, fc.inheritable, fc.flags) +} diff --git a/pkg/caps/caps_dd_test.go b/pkg/caps/caps_dd_test.go new file mode 100644 index 0000000000..cc71de9282 --- /dev/null +++ b/pkg/caps/caps_dd_test.go @@ -0,0 +1,64 @@ +// Generated file, do not edit. + +// Copyright 2024 ko Build Authors All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caps + +var ddTests = []ddTest{ + {permitted: "chown", inheritable: "", effective: false, res: "AAAAAgEAAAAAAAAAAAAAAAAAAAA="}, + {permitted: "chown", inheritable: "", effective: true, res: "AQAAAgEAAAAAAAAAAAAAAAAAAAA="}, + {permitted: "", inheritable: "chown", effective: false, res: "AAAAAgAAAAABAAAAAAAAAAAAAAA="}, + {permitted: "chown", inheritable: "chown", effective: true, res: "AQAAAgEAAAABAAAAAAAAAAAAAAA="}, + {permitted: "dac_override", inheritable: "dac_override", effective: true, res: "AQAAAgIAAAACAAAAAAAAAAAAAAA="}, + {permitted: "dac_read_search", inheritable: "dac_read_search", effective: true, res: "AQAAAgQAAAAEAAAAAAAAAAAAAAA="}, + {permitted: "fowner", inheritable: "fowner", effective: true, res: "AQAAAggAAAAIAAAAAAAAAAAAAAA="}, + {permitted: "fsetid", inheritable: "fsetid", effective: true, res: "AQAAAhAAAAAQAAAAAAAAAAAAAAA="}, + {permitted: "kill", inheritable: "kill", effective: true, res: "AQAAAiAAAAAgAAAAAAAAAAAAAAA="}, + {permitted: "setgid", inheritable: "setgid", effective: true, res: "AQAAAkAAAABAAAAAAAAAAAAAAAA="}, + {permitted: "setuid", inheritable: "setuid", effective: true, res: "AQAAAoAAAACAAAAAAAAAAAAAAAA="}, + {permitted: "setpcap", inheritable: "setpcap", effective: true, res: "AQAAAgABAAAAAQAAAAAAAAAAAAA="}, + {permitted: "linux_immutable", inheritable: "linux_immutable", effective: true, res: "AQAAAgACAAAAAgAAAAAAAAAAAAA="}, + {permitted: "net_bind_service", inheritable: "net_bind_service", effective: true, res: "AQAAAgAEAAAABAAAAAAAAAAAAAA="}, + {permitted: "net_broadcast", inheritable: "net_broadcast", effective: true, res: "AQAAAgAIAAAACAAAAAAAAAAAAAA="}, + {permitted: "net_admin", inheritable: "net_admin", effective: true, res: "AQAAAgAQAAAAEAAAAAAAAAAAAAA="}, + {permitted: "net_raw", inheritable: "net_raw", effective: true, res: "AQAAAgAgAAAAIAAAAAAAAAAAAAA="}, + {permitted: "ipc_lock", inheritable: "ipc_lock", effective: true, res: "AQAAAgBAAAAAQAAAAAAAAAAAAAA="}, + {permitted: "ipc_owner", inheritable: "ipc_owner", effective: true, res: "AQAAAgCAAAAAgAAAAAAAAAAAAAA="}, + {permitted: "sys_module", inheritable: "sys_module", effective: true, res: "AQAAAgAAAQAAAAEAAAAAAAAAAAA="}, + {permitted: "sys_rawio", inheritable: "sys_rawio", effective: true, res: "AQAAAgAAAgAAAAIAAAAAAAAAAAA="}, + {permitted: "sys_chroot", inheritable: "sys_chroot", effective: true, res: "AQAAAgAABAAAAAQAAAAAAAAAAAA="}, + {permitted: "sys_ptrace", inheritable: "sys_ptrace", effective: true, res: "AQAAAgAACAAAAAgAAAAAAAAAAAA="}, + {permitted: "sys_pacct", inheritable: "sys_pacct", effective: true, res: "AQAAAgAAEAAAABAAAAAAAAAAAAA="}, + {permitted: "sys_admin", inheritable: "sys_admin", effective: true, res: "AQAAAgAAIAAAACAAAAAAAAAAAAA="}, + {permitted: "sys_boot", inheritable: "sys_boot", effective: true, res: "AQAAAgAAQAAAAEAAAAAAAAAAAAA="}, + {permitted: "sys_nice", inheritable: "sys_nice", effective: true, res: "AQAAAgAAgAAAAIAAAAAAAAAAAAA="}, + {permitted: "sys_resource", inheritable: "sys_resource", effective: true, res: "AQAAAgAAAAEAAAABAAAAAAAAAAA="}, + {permitted: "sys_time", inheritable: "sys_time", effective: true, res: "AQAAAgAAAAIAAAACAAAAAAAAAAA="}, + {permitted: "sys_tty_config", inheritable: "sys_tty_config", effective: true, res: "AQAAAgAAAAQAAAAEAAAAAAAAAAA="}, + {permitted: "mknod", inheritable: "mknod", effective: true, res: "AQAAAgAAAAgAAAAIAAAAAAAAAAA="}, + {permitted: "lease", inheritable: "lease", effective: true, res: "AQAAAgAAABAAAAAQAAAAAAAAAAA="}, + {permitted: "audit_write", inheritable: "audit_write", effective: true, res: "AQAAAgAAACAAAAAgAAAAAAAAAAA="}, + {permitted: "audit_control", inheritable: "audit_control", effective: true, res: "AQAAAgAAAEAAAABAAAAAAAAAAAA="}, + {permitted: "setfcap", inheritable: "setfcap", effective: true, res: "AQAAAgAAAIAAAACAAAAAAAAAAAA="}, + {permitted: "mac_override", inheritable: "mac_override", effective: true, res: "AQAAAgAAAAAAAAAAAQAAAAEAAAA="}, + {permitted: "mac_admin", inheritable: "mac_admin", effective: true, res: "AQAAAgAAAAAAAAAAAgAAAAIAAAA="}, + {permitted: "syslog", inheritable: "syslog", effective: true, res: "AQAAAgAAAAAAAAAABAAAAAQAAAA="}, + {permitted: "wake_alarm", inheritable: "wake_alarm", effective: true, res: "AQAAAgAAAAAAAAAACAAAAAgAAAA="}, + {permitted: "block_suspend", inheritable: "block_suspend", effective: true, res: "AQAAAgAAAAAAAAAAEAAAABAAAAA="}, + {permitted: "audit_read", inheritable: "audit_read", effective: true, res: "AQAAAgAAAAAAAAAAIAAAACAAAAA="}, + {permitted: "perfmon", inheritable: "perfmon", effective: true, res: "AQAAAgAAAAAAAAAAQAAAAEAAAAA="}, + {permitted: "bpf", inheritable: "bpf", effective: true, res: "AQAAAgAAAAAAAAAAgAAAAIAAAAA="}, + {permitted: "checkpoint_restore", inheritable: "checkpoint_restore", effective: true, res: "AQAAAgAAAAAAAAAAAAEAAAABAAA="}, +} diff --git a/pkg/caps/caps_test.go b/pkg/caps/caps_test.go new file mode 100644 index 0000000000..3877bb0d46 --- /dev/null +++ b/pkg/caps/caps_test.go @@ -0,0 +1,100 @@ +// Copyright 2024 ko Build Authors All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caps + +import ( + "encoding/base64" + "fmt" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + arg string + res Mask + mustFail bool + }{ + {arg: "chown", res: 1}, + {arg: "cap_chown", res: 1}, + {arg: "cAp_cHoWn", res: 1}, + {arg: "unknown", mustFail: true}, + {arg: "63", res: 1 << 63}, + {arg: "64", mustFail: true}, + {arg: "all", res: allKnownCaps()}, + } + for _, tc := range tests { + t.Run(tc.arg, func(t *testing.T) { + mask, err := Parse(tc.arg) + if err == nil && tc.mustFail { + t.Fatal("invalid input accepted") + } + if err != nil && !tc.mustFail { + t.Fatal(err) + } + if mask != tc.res { + t.Fatalf("unexpected result: %x", mask) + } + }) + } +} + +//go:generate ./gen.sh + +type ddTest struct { + permitted, inheritable string + effective bool + res string +} + +func TestDd(t *testing.T) { + for _, test := range ddTests { + label := fmt.Sprintf("%s,%s,%v", test.permitted, test.inheritable, test.effective) + t.Run(label, func(t *testing.T) { + var permitted, inheritable Mask + var flags Flags + + if test.permitted != "" { + mask, err := Parse(test.permitted) + if err != nil { + t.Fatal(err) + } + permitted = mask + } + + if test.inheritable != "" { + mask, err := Parse(test.inheritable) + if err != nil { + t.Fatal(err) + } + inheritable = mask + } + + if test.effective { + flags = FlagEffective + } + + res, err := XattrBytes(permitted, inheritable, flags) + if err != nil { + t.Fatal(err) + } + + resBase64 := make([]byte, base64.StdEncoding.EncodedLen(len(res))) + base64.StdEncoding.Encode(resBase64, res) + if string(resBase64) != test.res { + t.Fatalf("expected %s, result %s", test.res, resBase64) + } + }) + } +} diff --git a/pkg/caps/gen.sh b/pkg/caps/gen.sh new file mode 100755 index 0000000000..bbbd0cb35c --- /dev/null +++ b/pkg/caps/gen.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# Copyright 2024 ko Build Authors All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script assigns different capabilities to files and captures +# resulting xattr blobs for testing (generates caps_dd_test.go). +# +# It has to be run on a reasonably recent Linux to ensure that the full +# set of capabilities is supported. Setting capabilities requires +# privileges; the script assumes paswordless sudo is available. + +set -o errexit +set -o nounset +set -o pipefail +shopt -s inherit_errexit + +# capblob CAP_STRING +# Obtain base64-encoded value of the underlying xattr that implemens +# specified capabilities, setcap syntax. +# Example: capblob cap_chown=eip +capblob() { + f=$(mktemp) + sudo -n setcap $1 $f + getfattr -n security.capability --absolute-names --only-values $f | base64 + rm $f +} + +( + license=$(sed -e '/^$/,$d' caps.go) + + echo "// Generated file, do not edit." + echo "" + echo "$license" + echo "" + echo "package caps" + echo "var ddTests = []ddTest{" + + res=$(capblob cap_chown=p) + echo "{permitted: \"chown\", inheritable: \"\", effective: false, res: \"$res\"}," + + res=$(capblob cap_chown=ep) + echo "{permitted: \"chown\", inheritable: \"\", effective: true, res: \"$res\"}," + + res=$(capblob cap_chown=i) + echo "{permitted: \"\", inheritable: \"chown\", effective: false, res: \"$res\"}," + + CAPS="chown dac_override dac_read_search fowner fsetid kill setgid setuid + setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner + sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice + sys_resource sys_time sys_tty_config mknod lease audit_write audit_control setfcap + mac_override mac_admin syslog wake_alarm block_suspend audit_read perfmon bpf + checkpoint_restore" + for cap in $CAPS; do + res=$(capblob cap_$cap=eip) + echo "{permitted: \"$cap\", inheritable: \"$cap\", effective: true, res: \"$res\"}," + done + + echo "}" +) > caps_dd_test.go + +gofmt -w -s ./caps_dd_test.go diff --git a/pkg/caps/new_file_caps_test.go b/pkg/caps/new_file_caps_test.go new file mode 100644 index 0000000000..e84472f334 --- /dev/null +++ b/pkg/caps/new_file_caps_test.go @@ -0,0 +1,88 @@ +// Copyright 2024 ko Build Authors All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caps + +import ( + "reflect" + "strings" + "testing" +) + +func TestNewFileCaps(t *testing.T) { + tests := []struct { + args []string + res *FileCaps + mustFail bool + }{ + {}, + { + args: []string{"chown", "dac_override", "dac_read_search"}, + res: &FileCaps{permitted: 7}, + }, + { + args: []string{"chown,dac_override,dac_read_search=p"}, + res: &FileCaps{permitted: 7}, + }, + { + args: []string{"chown,dac_override,dac_read_search=i"}, + res: &FileCaps{inheritable: 7}, + }, + { + args: []string{"chown,dac_override,dac_read_search=e"}, + }, + { + args: []string{"chown,dac_override,dac_read_search=pe"}, + res: &FileCaps{permitted: 7, flags: FlagEffective}, + }, + { + args: []string{"=pe"}, + res: &FileCaps{permitted: allKnownCaps(), flags: FlagEffective}, + }, + { + args: []string{"chown=ie", "chown=p"}, + res: &FileCaps{permitted: 1}, + }, + { + args: []string{"chown=ie", "chown="}, + }, + { + args: []string{"chown=ie", "chown+p"}, + res: &FileCaps{permitted: 1, inheritable: 1, flags: FlagEffective}, + }, + { + args: []string{"chown=pie", "dac_override,chown-p"}, + res: &FileCaps{inheritable: 1, flags: FlagEffective}, + }, + {args: []string{"chown,=pie"}, mustFail: true}, + {args: []string{"-pie"}, mustFail: true}, + {args: []string{"+pie"}, mustFail: true}, + {args: []string{"="}}, + } + for _, tc := range tests { + label := strings.Join(tc.args, ":") + t.Run(label, func(t *testing.T) { + res, err := NewFileCaps(tc.args...) + if tc.mustFail && err == nil { + t.Fatal("didn't fail") + } + if !tc.mustFail && err != nil { + t.Fatalf("unexpectedly failed: %v", err) + } + if !reflect.DeepEqual(res, tc.res) { + t.Fatalf("got %v expected %v", res, tc.res) + } + }) + } +} From 8e10beeda994fcf71ee799f0d832af419ddd9247 Mon Sep 17 00:00:00 2001 From: Nick Zavaritsky Date: Mon, 1 Apr 2024 13:36:17 +0000 Subject: [PATCH 3/3] Ko learns about Linux capabilities Signed-off-by: Nick Zavaritsky --- integration_test.sh | 17 ++++++++++ pkg/build/config.go | 4 +++ pkg/build/gobuild.go | 48 +++++++++++++++++++++------ test/build-configs/.ko.yaml | 3 ++ test/build-configs/caps.ko.yaml | 19 +++++++++++ test/build-configs/caps/cmd/main.go | 50 +++++++++++++++++++++++++++++ test/build-configs/caps/go.mod | 17 ++++++++++ 7 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 test/build-configs/caps.ko.yaml create mode 100644 test/build-configs/caps/cmd/main.go create mode 100644 test/build-configs/caps/go.mod diff --git a/integration_test.sh b/integration_test.sh index ddc3a65f63..a09733e51b 100755 --- a/integration_test.sh +++ b/integration_test.sh @@ -96,6 +96,23 @@ for app in foo bar ; do done popd || exit 1 +echo "9. Linux capabilities." +pushd test/build-configs || exit 1 +# run as non-root user with net_bind_service cap granted +docker_run_opts="--user 1 --cap-add=net_bind_service" +RESULT="$(GO111MODULE=on GOFLAGS="" ../../ko build --local ./caps/cmd | grep "$FILTER" | xargs -I% docker run $docker_run_opts %)" +if [[ "$RESULT" != "No capabilities" ]]; then + echo "Test FAILED. Saw '$RESULT' but expected 'No capabilities'. Docker 'cap-add' must have no effect unless matching capabilities are granted to the file." && exit 1 +fi +# build with a different config requesting net_bind_service file capability +RESULT_WITH_FILE_CAPS="$(KO_CONFIG_PATH=caps.ko.yaml GO111MODULE=on GOFLAGS="" ../../ko build --local ./caps/cmd | grep "$FILTER" | xargs -I% docker run $docker_run_opts %)" +if [[ "$RESULT_WITH_FILE_CAPS" != "Has capabilities"* ]]; then + echo "Test FAILED. Saw '$RESULT_WITH_FILE_CAPS' but expected 'Has capabilities'. Docker 'cap-add' must work when matching capabilities are granted to the file." && exit 1 +else + echo "Test PASSED" +fi +popd || exit 1 + popd || exit 1 popd || exit 1 diff --git a/pkg/build/config.go b/pkg/build/config.go index 84e243c34e..7218d9fb4e 100644 --- a/pkg/build/config.go +++ b/pkg/build/config.go @@ -92,4 +92,8 @@ type Config struct { // Gcflags StringArray `yaml:",omitempty"` // ModTimestamp string `yaml:"mod_timestamp,omitempty"` // GoBinary string `yaml:",omitempty"` + + // extension: Linux capabilities to enable on the executable, applies + // to Linux targets. + LinuxCapabilities FlagArray `yaml:"linux_capabilities,omitempty"` } diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index bae088b408..172e9e3163 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -39,6 +39,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/ko/internal/sbom" + "github.com/google/ko/pkg/caps" specsv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sigstore/cosign/v2/pkg/oci" ocimutate "github.com/sigstore/cosign/v2/pkg/oci/mutate" @@ -486,7 +487,7 @@ func appFilename(importpath string) string { // owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU") const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==" -func tarBinary(name, binary string, platform *v1.Platform) (*bytes.Buffer, error) { +func tarBinary(name, binary string, platform *v1.Platform, opts *layerOptions) (*bytes.Buffer, error) { buf := bytes.NewBuffer(nil) tw := tar.NewWriter(buf) defer tw.Close() @@ -533,13 +534,21 @@ func tarBinary(name, binary string, platform *v1.Platform) (*bytes.Buffer, error // Use a fixed Mode, so that this isn't sensitive to the directory and umask // under which it was created. Additionally, windows can only set 0222, // 0444, or 0666, none of which are executable. - Mode: 0555, + Mode: 0555, + PAXRecords: map[string]string{}, } - if platform.OS == "windows" { + switch platform.OS { + case "windows": // This magic value is for some reason needed for Windows to be // able to execute the binary. - header.PAXRecords = map[string]string{ - "MSWINDOWS.rawsd": userOwnerAndGroupSID, + header.PAXRecords["MSWINDOWS.rawsd"] = userOwnerAndGroupSID + case "linux": + if opts.linuxCapabilities != nil { + xattr, err := opts.linuxCapabilities.ToXattrBytes() + if err != nil { + return nil, fmt.Errorf("caps.FileCaps.ToXattrBytes: %w", err) + } + header.PAXRecords["SCHILY.xattr.security.capability"] = string(xattr) } } // write the header to the tarball archive @@ -826,7 +835,8 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl return nil, fmt.Errorf("base image platform %q does not match desired platforms %v", platform, g.platformMatcher.platforms) } // Do the build into a temporary file. - file, err := g.build(ctx, ref.Path(), g.dir, *platform, g.configForImportPath(ref.Path())) + config := g.configForImportPath(ref.Path()) + file, err := g.build(ctx, ref.Path(), g.dir, *platform, config) if err != nil { return nil, fmt.Errorf("build: %w", err) } @@ -862,11 +872,24 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl appFileName := appFilename(ref.Path()) appPath := path.Join(appDir, appFileName) + var lo layerOptions + lo.linuxCapabilities, err = caps.NewFileCaps(config.LinuxCapabilities...) + if err != nil { + return nil, fmt.Errorf("linux_capabilities: %w", err) + } + miss := func() (v1.Layer, error) { - return buildLayer(appPath, file, platform, layerMediaType) + return buildLayer(appPath, file, platform, layerMediaType, &lo) } - binaryLayer, err := g.cache.get(ctx, file, miss) + var binaryLayer v1.Layer + switch { + case lo.linuxCapabilities != nil: + log.Printf("Some options prevent us from using layer cache") + binaryLayer, err = miss() + default: + binaryLayer, err = g.cache.get(ctx, file, miss) + } if err != nil { return nil, fmt.Errorf("cache.get(%q): %w", file, err) } @@ -946,9 +969,14 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl return si, nil } -func buildLayer(appPath, file string, platform *v1.Platform, layerMediaType types.MediaType) (v1.Layer, error) { +// layerOptions captures additional options to apply when authoring layer +type layerOptions struct { + linuxCapabilities *caps.FileCaps +} + +func buildLayer(appPath, file string, platform *v1.Platform, layerMediaType types.MediaType, opts *layerOptions) (v1.Layer, error) { // Construct a tarball with the binary and produce a layer. - binaryLayerBuf, err := tarBinary(appPath, file, platform) + binaryLayerBuf, err := tarBinary(appPath, file, platform, opts) if err != nil { return nil, fmt.Errorf("tarring binary: %w", err) } diff --git a/test/build-configs/.ko.yaml b/test/build-configs/.ko.yaml index 1962dc041c..ae4891f3ff 100644 --- a/test/build-configs/.ko.yaml +++ b/test/build-configs/.ko.yaml @@ -26,3 +26,6 @@ builds: flags: - -toolexec - go +- id: caps-app + dir: ./caps + main: ./cmd diff --git a/test/build-configs/caps.ko.yaml b/test/build-configs/caps.ko.yaml new file mode 100644 index 0000000000..71655863c7 --- /dev/null +++ b/test/build-configs/caps.ko.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 ko Build Authors All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +builds: +- id: caps-app-with-caps + dir: ./caps + main: ./cmd + linux_capabilities: net_bind_service chown diff --git a/test/build-configs/caps/cmd/main.go b/test/build-configs/caps/cmd/main.go new file mode 100644 index 0000000000..4ba80fb8e7 --- /dev/null +++ b/test/build-configs/caps/cmd/main.go @@ -0,0 +1,50 @@ +// Copyright 2024 ko Build Authors All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" +) + +func permittedCaps() (uint64, error) { + data, err := ioutil.ReadFile("/proc/self/status") + if err != nil { + return 0, err + } + const prefix = "CapPrm:" + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, prefix) { + return strconv.ParseUint(strings.TrimSpace(line[len(prefix):]), 16, 64) + } + } + return 0, fmt.Errorf("didn't find %#v in /proc/self/status", prefix) +} + +func main() { + caps, err := permittedCaps() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + if caps == 0 { + fmt.Println("No capabilities") + } else { + fmt.Printf("Has capabilities (%x)\n", caps) + } +} diff --git a/test/build-configs/caps/go.mod b/test/build-configs/caps/go.mod new file mode 100644 index 0000000000..3fe119ddf9 --- /dev/null +++ b/test/build-configs/caps/go.mod @@ -0,0 +1,17 @@ +// Copyright 2024 ko Build Authors All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module example.com/caps + +go 1.16