diff --git a/client/client.go b/client/client.go index 37c499638d54..62bbdbd45f9a 100644 --- a/client/client.go +++ b/client/client.go @@ -1,12 +1,16 @@ package client import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" "time" controlapi "github.com/moby/buildkit/api/services/control" "github.com/moby/buildkit/util/appdefaults" "github.com/pkg/errors" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" ) type Client struct { @@ -18,15 +22,26 @@ type ClientOpt interface{} // New returns a new buildkit client. Address can be empty for the system-default address. func New(address string, opts ...ClientOpt) (*Client, error) { gopts := []grpc.DialOption{ - grpc.WithInsecure(), grpc.WithTimeout(30 * time.Second), grpc.WithDialer(dialer), grpc.FailOnNonTempDialError(true), } + needWithInsecure := true for _, o := range opts { if _, ok := o.(*withBlockOpt); ok { gopts = append(gopts, grpc.WithBlock(), grpc.FailOnNonTempDialError(true)) } + if credInfo, ok := o.(*withCredentials); ok { + opt, err := loadCredentials(credInfo) + if err != nil { + return nil, err + } + gopts = append(gopts, opt) + needWithInsecure = false + } + } + if needWithInsecure { + gopts = append(gopts, grpc.WithInsecure()) } if address == "" { address = appdefaults.Address @@ -54,3 +69,49 @@ type withBlockOpt struct{} func WithBlock() ClientOpt { return &withBlockOpt{} } + +type withCredentials struct { + ServerName string + CACert string + Cert string + Key string +} + +// WithCredentials configures the TLS parameters of the client. +// Arguments: +// * serverName: specifies the name of the target server +// * ca: specifies the filepath of the CA certificate to use for verification +// * cert: specifies the filepath of the client certificate +// * key: specifies the filepath of the client key +func WithCredentials(serverName, ca, cert, key string) ClientOpt { + return &withCredentials{serverName, ca, cert, key} +} + +func loadCredentials(opts *withCredentials) (grpc.DialOption, error) { + ca, err := ioutil.ReadFile(opts.CACert) + if err != nil { + return nil, errors.Wrap(err, "could not read ca certificate") + } + + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(ca); !ok { + return nil, errors.New("failed to append ca certs") + } + + cfg := &tls.Config{ + ServerName: opts.ServerName, + RootCAs: certPool, + } + + // we will produce an error if the user forgot about either cert or key if at least one is specified + if opts.Cert != "" || opts.Key != "" { + cert, err := tls.LoadX509KeyPair(opts.Cert, opts.Key) + if err != nil { + return nil, errors.Wrap(err, "could not read certificate/key") + } + cfg.Certificates = []tls.Certificate{cert} + cfg.BuildNameToCertificate() + } + + return grpc.WithTransportCredentials(credentials.NewTLS(cfg)), nil +} diff --git a/cmd/buildctl/main.go b/cmd/buildctl/main.go index 1ea03800707e..98e72fd31299 100644 --- a/cmd/buildctl/main.go +++ b/cmd/buildctl/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "net/url" "os" "github.com/moby/buildkit/client" @@ -28,9 +29,29 @@ func main() { }, cli.StringFlag{ Name: "addr", - Usage: "listening address", + Usage: "buildkitd address", Value: defaultAddress, }, + cli.StringFlag{ + Name: "tlsservername", + Usage: "buildkitd server name for certificate validation", + Value: "", + }, + cli.StringFlag{ + Name: "tlscacert", + Usage: "CA certificate for validation", + Value: "", + }, + cli.StringFlag{ + Name: "tlscert", + Usage: "client certificate", + Value: "", + }, + cli.StringFlag{ + Name: "tlskey", + Usage: "client key", + Value: "", + }, } app.Commands = []cli.Command{ @@ -62,5 +83,21 @@ func main() { } func resolveClient(c *cli.Context) (*client.Client, error) { - return client.New(c.GlobalString("addr"), client.WithBlock()) + serverName := c.GlobalString("tlsservername") + if serverName == "" { + // guess servername as hostname of target address + uri, err := url.Parse(c.GlobalString("addr")) + if err != nil { + return nil, err + } + serverName = uri.Hostname() + } + caCert := c.GlobalString("tlscacert") + cert := c.GlobalString("tlscert") + key := c.GlobalString("tlskey") + opts := []client.ClientOpt{client.WithBlock()} + if caCert != "" || cert != "" || key != "" { + opts = append(opts, client.WithCredentials(serverName, caCert, cert, key)) + } + return client.New(c.GlobalString("addr"), opts...) } diff --git a/cmd/buildkitd/main.go b/cmd/buildkitd/main.go index 1ae07594b993..c02d9aad1a34 100644 --- a/cmd/buildkitd/main.go +++ b/cmd/buildkitd/main.go @@ -1,7 +1,10 @@ package main import ( + "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "net" "os" "path/filepath" @@ -27,6 +30,7 @@ import ( "golang.org/x/net/context" "golang.org/x/sync/errgroup" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" ) type workerInitializerOpt struct { @@ -79,6 +83,18 @@ func main() { Usage: "debugging address (eg. 0.0.0.0:6060)", Value: "", }, + cli.StringFlag{ + Name: "tlscert", + Usage: "certificate file to use", + }, + cli.StringFlag{ + Name: "tlskey", + Usage: "key file to use", + }, + cli.StringFlag{ + Name: "tlscacert", + Usage: "ca certificate to verify clients", + }, } app.Flags = append(app.Flags, appFlags...) @@ -91,8 +107,15 @@ func main() { return err } } - - server := grpc.NewServer(unaryInterceptor(ctx)) + opts := []grpc.ServerOption{unaryInterceptor(ctx)} + creds, err := serverCredentials(c) + if err != nil { + return err + } + if creds != nil { + opts = append(opts, creds) + } + server := grpc.NewServer(opts...) // relative path does not work with nightlyone/lockfile root, err := filepath.Abs(c.GlobalString("root")) @@ -214,6 +237,44 @@ func unaryInterceptor(globalCtx context.Context) grpc.ServerOption { }) } +func serverCredentials(c *cli.Context) (grpc.ServerOption, error) { + certFile := c.GlobalString("tlscert") + keyFile := c.GlobalString("tlskey") + caFile := c.GlobalString("tlscacert") + if certFile == "" && keyFile == "" { + return nil, nil + } + err := errors.New("you must specify key and cert file if one is specified") + if certFile == "" { + return nil, err + } + if keyFile == "" { + return nil, err + } + certificate, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, errors.Wrap(err, "could not load server key pair") + } + tlsConf := &tls.Config{ + Certificates: []tls.Certificate{certificate}, + } + if caFile != "" { + certPool := x509.NewCertPool() + ca, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, errors.Wrap(err, "could not read ca certificate") + } + // Append the client certificates from the CA + if ok := certPool.AppendCertsFromPEM(ca); !ok { + return nil, errors.New("failed to append ca cert") + } + tlsConf.ClientAuth = tls.RequireAndVerifyClientCert + tlsConf.ClientCAs = certPool + } + creds := grpc.Creds(credentials.NewTLS(tlsConf)) + return creds, nil +} + func newController(c *cli.Context, root string) (*control.Controller, error) { sessionManager, err := session.NewManager() if err != nil {