From a4c4abeba0b1b6451bf7377b816309df5834f2e6 Mon Sep 17 00:00:00 2001 From: Harsh Thakur Date: Tue, 19 Sep 2023 01:32:21 +0530 Subject: [PATCH] feat: mTLS-over-TCP buildkit (#284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Harsh Thakur Co-authored-by: Xander Grzywinski Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com> --- pkg/buildkit/buildkit.go | 7 ++++++ pkg/buildkit/buildkit_test.go | 43 +++++++++++++++++++++++++++++++++-- pkg/buildkit/drivers.go | 28 ++++++++++++++++++++--- pkg/patch/cmd.go | 17 ++++++++++---- pkg/patch/patch.go | 8 +++---- 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/pkg/buildkit/buildkit.go b/pkg/buildkit/buildkit.go index 8fbe2355..f9d0cfdf 100644 --- a/pkg/buildkit/buildkit.go +++ b/pkg/buildkit/buildkit.go @@ -40,6 +40,13 @@ type Config struct { ImageState llb.State } +type Opts struct { + Addr string + CACertPath string + CertPath string + KeyPath string +} + func dockerLoad(ctx context.Context, pipeR io.Reader) error { cmd := exec.CommandContext(ctx, "docker", "load") cmd.Stdin = pipeR diff --git a/pkg/buildkit/buildkit_test.go b/pkg/buildkit/buildkit_test.go index f926633b..bd5f91da 100644 --- a/pkg/buildkit/buildkit_test.go +++ b/pkg/buildkit/buildkit_test.go @@ -143,6 +143,39 @@ func checkMissingCapsError(t *testing.T, err error, caps ...apicaps.CapID) { } } +func TestGetServerNameFromAddr(t *testing.T) { + testCases := []struct { + name string + addr string + want string + }{ + { + name: "hostname", + addr: "tcp://hostname:1234", + want: "hostname", + }, + { + name: "IP address", + addr: "tcp://127.0.0.1:1234", + want: "127.0.0.1", + }, + { + name: "invalid URL", + addr: "hostname:1234", + want: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := getServerNameFromAddr(tc.addr) + if got != tc.want { + t.Errorf("getServerNameFromAddr(%q) = %q, want %q", tc.addr, got, tc.want) + } + }) + } +} + func TestNewClient(t *testing.T) { ctx := context.Background() @@ -151,7 +184,10 @@ func TestNewClient(t *testing.T) { t.Parallel() addr := newMockBuildkitAPI(t) ctxT, cancel := context.WithTimeout(ctx, time.Second) - client, err := NewClient(ctxT, "unix://"+addr) + bkOpts := Opts{ + Addr: "unix://" + addr, + } + client, err := NewClient(ctxT, bkOpts) cancel() assert.NoError(t, err) defer client.Close() @@ -167,7 +203,10 @@ func TestNewClient(t *testing.T) { ctxT, cancel := context.WithTimeout(ctx, time.Second) defer cancel() - client, err := NewClient(ctxT, "unix://"+addr) + bkOpts := Opts{ + Addr: "unix://" + addr, + } + client, err := NewClient(ctxT, bkOpts) assert.NoError(t, err) defer client.Close() diff --git a/pkg/buildkit/drivers.go b/pkg/buildkit/drivers.go index 10e90b75..5fa46ae6 100644 --- a/pkg/buildkit/drivers.go +++ b/pkg/buildkit/drivers.go @@ -27,17 +27,39 @@ var ( // NewClient returns a new buildkit client with the given addr. // If addr is empty it will first try to connect to docker's buildkit instance and then fallback to DefaultAddr. -func NewClient(ctx context.Context, addr string) (*client.Client, error) { - if addr == "" { +func NewClient(ctx context.Context, bkOpts Opts) (*client.Client, error) { + if bkOpts.Addr == "" { return autoClient(ctx) } - client, err := client.New(ctx, addr) + opts := getCredentialOptions(bkOpts) + client, err := client.New(ctx, bkOpts.Addr, opts...) if err != nil { return nil, err } return client, nil } +func getCredentialOptions(bkOpts Opts) []client.ClientOpt { + opts := []client.ClientOpt{} + if bkOpts.CACertPath != "" { + opts = append(opts, client.WithServerConfig(getServerNameFromAddr(bkOpts.Addr), bkOpts.CACertPath)) + } + + if bkOpts.CertPath != "" || bkOpts.KeyPath != "" { + opts = append(opts, client.WithCredentials(bkOpts.CertPath, bkOpts.KeyPath)) + } + + return opts +} + +func getServerNameFromAddr(addr string) string { + u, err := url.Parse(addr) + if err != nil { + return "" + } + return u.Hostname() +} + // ValidateClient checks to ensure the connected buildkit instance supports the features required by copa. func ValidateClient(ctx context.Context, c *client.Client) error { _, err := c.Build(ctx, client.SolveOpt{}, "", func(ctx context.Context, client gateway.Client) (*gateway.Result, error) { diff --git a/pkg/patch/cmd.go b/pkg/patch/cmd.go index e56b6ff7..1e6a59bd 100644 --- a/pkg/patch/cmd.go +++ b/pkg/patch/cmd.go @@ -25,11 +25,11 @@ type patchArgs struct { reportFile string patchedTag string workingFolder string - buildkitAddr string timeout time.Duration ignoreError bool format string output string + bkOpts buildkit.Opts } func NewPatchCmd() *cobra.Command { @@ -39,16 +39,22 @@ func NewPatchCmd() *cobra.Command { Short: "Patch container images with upgrade packages specified by a vulnerability report", Example: "copa patch -i images/python:3.7-alpine -r trivy.json -t 3.7-alpine-patched", RunE: func(cmd *cobra.Command, args []string) error { + bkopts := buildkit.Opts{ + Addr: ua.bkOpts.Addr, + CACertPath: ua.bkOpts.CACertPath, + CertPath: ua.bkOpts.CertPath, + KeyPath: ua.bkOpts.KeyPath, + } return Patch(context.Background(), ua.timeout, - ua.buildkitAddr, ua.appImage, ua.reportFile, ua.patchedTag, ua.workingFolder, ua.format, ua.output, - ua.ignoreError) + ua.ignoreError, + bkopts) }, } flags := patchCmd.Flags() @@ -56,7 +62,10 @@ func NewPatchCmd() *cobra.Command { flags.StringVarP(&ua.reportFile, "report", "r", "", "Vulnerability report file path") flags.StringVarP(&ua.patchedTag, "tag", "t", "", "Tag for the patched image") flags.StringVarP(&ua.workingFolder, "working-folder", "w", "", "Working folder, defaults to system temp folder") - flags.StringVarP(&ua.buildkitAddr, "addr", "a", "", "Address of buildkitd service, defaults to local docker daemon with fallback to "+buildkit.DefaultAddr) + flags.StringVarP(&ua.bkOpts.Addr, "addr", "a", "", "Address of buildkitd service, defaults to local docker daemon with fallback to "+buildkit.DefaultAddr) + flags.StringVarP(&ua.bkOpts.CACertPath, "cacert", "", "", "Absolute path to buildkitd CA certificate") + flags.StringVarP(&ua.bkOpts.CertPath, "cert", "", "", "Absolute path to buildkit client certificate") + flags.StringVarP(&ua.bkOpts.KeyPath, "key", "", "", "Absolute path to buildkit client key") flags.DurationVar(&ua.timeout, "timeout", 5*time.Minute, "Timeout for the operation, defaults to '5m'") flags.BoolVar(&ua.ignoreError, "ignore-errors", false, "Ignore errors and continue patching") flags.StringVarP(&ua.format, "format", "f", "openvex", "Output format, defaults to 'openvex'") diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 2ae07b68..07b7081b 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -29,13 +29,13 @@ const ( ) // Patch command applies package updates to an OCI image given a vulnerability report. -func Patch(ctx context.Context, timeout time.Duration, buildkitAddr, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool) error { +func Patch(ctx context.Context, timeout time.Duration, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ch := make(chan error) go func() { - ch <- patchWithContext(timeoutCtx, buildkitAddr, image, reportFile, patchedTag, workingFolder, format, output, ignoreError) + ch <- patchWithContext(timeoutCtx, image, reportFile, patchedTag, workingFolder, format, output, ignoreError, bkOpts) }() select { @@ -60,7 +60,7 @@ func removeIfNotDebug(workingFolder string) { } } -func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool) error { +func patchWithContext(ctx context.Context, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { imageName, err := ref.ParseNamed(image) if err != nil { return err @@ -113,7 +113,7 @@ func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patc } log.Debugf("updates to apply: %v", updates) - client, err := buildkit.NewClient(ctx, buildkitAddr) + client, err := buildkit.NewClient(ctx, bkOpts) if err != nil { return err }