Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting capabilities on the app binary #1271

Merged
merged 3 commits into from
Apr 3, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions integration_test.sh
Original file line number Diff line number Diff line change
@@ -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

4 changes: 4 additions & 0 deletions pkg/build/config.go
Original file line number Diff line number Diff line change
@@ -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"`
}
48 changes: 38 additions & 10 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
@@ -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)
}
213 changes: 213 additions & 0 deletions pkg/caps/caps.go
Original file line number Diff line number Diff line change
@@ -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)
}
64 changes: 64 additions & 0 deletions pkg/caps/caps_dd_test.go
Original file line number Diff line number Diff line change
@@ -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="},
}
100 changes: 100 additions & 0 deletions pkg/caps/caps_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
73 changes: 73 additions & 0 deletions pkg/caps/gen.sh
Original file line number Diff line number Diff line change
@@ -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
88 changes: 88 additions & 0 deletions pkg/caps/new_file_caps_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
37 changes: 35 additions & 2 deletions pkg/commands/options/build.go
Original file line number Diff line number Diff line change
@@ -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 {
4 changes: 4 additions & 0 deletions test/build-configs/.ko.yaml
Original file line number Diff line number Diff line change
@@ -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
@@ -25,3 +26,6 @@ builds:
flags:
- -toolexec
- go
- id: caps-app
dir: ./caps
main: ./cmd
19 changes: 19 additions & 0 deletions test/build-configs/caps.ko.yaml
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions test/build-configs/caps/cmd/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
17 changes: 17 additions & 0 deletions test/build-configs/caps/go.mod
Original file line number Diff line number Diff line change
@@ -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