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
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions integration_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions pkg/build/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
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)
}
Loading
Loading