Skip to content

Commit 1f6a357

Browse files
mejediimjasonh
authored andcommitted
Ko learns about Linux capabilities
Signed-off-by: Nick Zavaritsky <[email protected]>
1 parent 3a0416f commit 1f6a357

File tree

7 files changed

+148
-10
lines changed

7 files changed

+148
-10
lines changed

integration_test.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ for app in foo bar ; do
9696
done
9797
popd || exit 1
9898

99+
echo "9. Linux capabilities."
100+
pushd test/build-configs || exit 1
101+
# run as non-root user with net_bind_service cap granted
102+
docker_run_opts="--user 1 --cap-add=net_bind_service"
103+
RESULT="$(GO111MODULE=on GOFLAGS="" ../../ko build --local ./caps/cmd | grep "$FILTER" | xargs -I% docker run $docker_run_opts %)"
104+
if [[ "$RESULT" != "No capabilities" ]]; then
105+
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
106+
fi
107+
# build with a different config requesting net_bind_service file capability
108+
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 %)"
109+
if [[ "$RESULT_WITH_FILE_CAPS" != "Has capabilities"* ]]; then
110+
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
111+
else
112+
echo "Test PASSED"
113+
fi
114+
popd || exit 1
115+
99116
popd || exit 1
100117
popd || exit 1
101118

pkg/build/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,8 @@ type Config struct {
9292
// Gcflags StringArray `yaml:",omitempty"`
9393
// ModTimestamp string `yaml:"mod_timestamp,omitempty"`
9494
// GoBinary string `yaml:",omitempty"`
95+
96+
// extension: Linux capabilities to enable on the executable, applies
97+
// to Linux targets.
98+
LinuxCapabilities FlagArray `yaml:"linux_capabilities,omitempty"`
9599
}

pkg/build/gobuild.go

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/google/go-containerregistry/pkg/v1/tarball"
4040
"github.com/google/go-containerregistry/pkg/v1/types"
4141
"github.com/google/ko/internal/sbom"
42+
"github.com/google/ko/pkg/caps"
4243
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
4344
"github.com/sigstore/cosign/v2/pkg/oci"
4445
ocimutate "github.com/sigstore/cosign/v2/pkg/oci/mutate"
@@ -486,7 +487,7 @@ func appFilename(importpath string) string {
486487
// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU")
487488
const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA=="
488489

489-
func tarBinary(name, binary string, platform *v1.Platform) (*bytes.Buffer, error) {
490+
func tarBinary(name, binary string, platform *v1.Platform, opts *layerOptions) (*bytes.Buffer, error) {
490491
buf := bytes.NewBuffer(nil)
491492
tw := tar.NewWriter(buf)
492493
defer tw.Close()
@@ -533,13 +534,21 @@ func tarBinary(name, binary string, platform *v1.Platform) (*bytes.Buffer, error
533534
// Use a fixed Mode, so that this isn't sensitive to the directory and umask
534535
// under which it was created. Additionally, windows can only set 0222,
535536
// 0444, or 0666, none of which are executable.
536-
Mode: 0555,
537+
Mode: 0555,
538+
PAXRecords: map[string]string{},
537539
}
538-
if platform.OS == "windows" {
540+
switch platform.OS {
541+
case "windows":
539542
// This magic value is for some reason needed for Windows to be
540543
// able to execute the binary.
541-
header.PAXRecords = map[string]string{
542-
"MSWINDOWS.rawsd": userOwnerAndGroupSID,
544+
header.PAXRecords["MSWINDOWS.rawsd"] = userOwnerAndGroupSID
545+
case "linux":
546+
if opts.linuxCapabilities != nil {
547+
xattr, err := opts.linuxCapabilities.ToXattrBytes()
548+
if err != nil {
549+
return nil, fmt.Errorf("caps.FileCaps.ToXattrBytes: %w", err)
550+
}
551+
header.PAXRecords["SCHILY.xattr.security.capability"] = string(xattr)
543552
}
544553
}
545554
// write the header to the tarball archive
@@ -826,7 +835,8 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
826835
return nil, fmt.Errorf("base image platform %q does not match desired platforms %v", platform, g.platformMatcher.platforms)
827836
}
828837
// Do the build into a temporary file.
829-
file, err := g.build(ctx, ref.Path(), g.dir, *platform, g.configForImportPath(ref.Path()))
838+
config := g.configForImportPath(ref.Path())
839+
file, err := g.build(ctx, ref.Path(), g.dir, *platform, config)
830840
if err != nil {
831841
return nil, fmt.Errorf("build: %w", err)
832842
}
@@ -862,11 +872,24 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
862872
appFileName := appFilename(ref.Path())
863873
appPath := path.Join(appDir, appFileName)
864874

875+
var lo layerOptions
876+
lo.linuxCapabilities, err = caps.NewFileCaps(config.LinuxCapabilities...)
877+
if err != nil {
878+
return nil, fmt.Errorf("linux_capabilities: %w", err)
879+
}
880+
865881
miss := func() (v1.Layer, error) {
866-
return buildLayer(appPath, file, platform, layerMediaType)
882+
return buildLayer(appPath, file, platform, layerMediaType, &lo)
867883
}
868884

869-
binaryLayer, err := g.cache.get(ctx, file, miss)
885+
var binaryLayer v1.Layer
886+
switch {
887+
case lo.linuxCapabilities != nil:
888+
log.Printf("Some options prevent us from using layer cache")
889+
binaryLayer, err = miss()
890+
default:
891+
binaryLayer, err = g.cache.get(ctx, file, miss)
892+
}
870893
if err != nil {
871894
return nil, fmt.Errorf("cache.get(%q): %w", file, err)
872895
}
@@ -946,9 +969,14 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
946969
return si, nil
947970
}
948971

949-
func buildLayer(appPath, file string, platform *v1.Platform, layerMediaType types.MediaType) (v1.Layer, error) {
972+
// layerOptions captures additional options to apply when authoring layer
973+
type layerOptions struct {
974+
linuxCapabilities *caps.FileCaps
975+
}
976+
977+
func buildLayer(appPath, file string, platform *v1.Platform, layerMediaType types.MediaType, opts *layerOptions) (v1.Layer, error) {
950978
// Construct a tarball with the binary and produce a layer.
951-
binaryLayerBuf, err := tarBinary(appPath, file, platform)
979+
binaryLayerBuf, err := tarBinary(appPath, file, platform, opts)
952980
if err != nil {
953981
return nil, fmt.Errorf("tarring binary: %w", err)
954982
}

test/build-configs/.ko.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ builds:
2626
flags:
2727
- -toolexec
2828
- go
29+
- id: caps-app
30+
dir: ./caps
31+
main: ./cmd

test/build-configs/caps.ko.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2024 ko Build Authors All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
builds:
16+
- id: caps-app-with-caps
17+
dir: ./caps
18+
main: ./cmd
19+
linux_capabilities: net_bind_service chown
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2024 ko Build Authors All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"fmt"
19+
"io/ioutil"
20+
"os"
21+
"strconv"
22+
"strings"
23+
)
24+
25+
func permittedCaps() (uint64, error) {
26+
data, err := ioutil.ReadFile("/proc/self/status")
27+
if err != nil {
28+
return 0, err
29+
}
30+
const prefix = "CapPrm:"
31+
for _, line := range strings.Split(string(data), "\n") {
32+
if strings.HasPrefix(line, prefix) {
33+
return strconv.ParseUint(strings.TrimSpace(line[len(prefix):]), 16, 64)
34+
}
35+
}
36+
return 0, fmt.Errorf("didn't find %#v in /proc/self/status", prefix)
37+
}
38+
39+
func main() {
40+
caps, err := permittedCaps()
41+
if err != nil {
42+
fmt.Println(err)
43+
os.Exit(1)
44+
}
45+
if caps == 0 {
46+
fmt.Println("No capabilities")
47+
} else {
48+
fmt.Printf("Has capabilities (%x)\n", caps)
49+
}
50+
}

test/build-configs/caps/go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2024 ko Build Authors All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
module example.com/caps
16+
17+
go 1.16

0 commit comments

Comments
 (0)