Repository: ko-build/ko (language: go)

Issue:
Support setting capabilities on the app binary
Linux has a notion of [capabilities](https://manpages.debian.org/unstable/manpages/capabilities.7.en.html), which is essentially a *token* allowing a certain privileged operation.

E.g. `CAP_BPF` allows loading ebpf programs for an otherwise unprivileged user.

The way to leverage capabilities with Docker is two fold:
 * at build time, use `setcap` tool to set file capabilities on a binary;
 * at run time, [request](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities) *matching* capabilities via `--cap-add` option.

We are doing ebpf in go and we'd like to take advantage of ko's fast build times and convenience. We need a way to set custom capabilities on the app binary.

It looks like currently it is not supported, as far as I can tell from docs and [implementation](https://github.com/ko-build/ko/blob/main/pkg/build/gobuild.go#L529).

Would you be open for such contribution?


Discussion / hints:
cc @imjasonh

cc @mattmoor @jonjohnsonjr

Fix patch:
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/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/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/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/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 {


Test patch:
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/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/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)
+			}
+		})
+	}
+}
diff --git a/test/build-configs/.ko.yaml b/test/build-configs/.ko.yaml
index 8bd64477eb..ae4891f3ff 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
@@ -25,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