From cceab7f9699530994500e58e2346b394346a384a Mon Sep 17 00:00:00 2001 From: Andrew Melnick Date: Sat, 16 Nov 2024 13:15:25 -0700 Subject: [PATCH] Add flags to "system service" and "system connnection add" for TLS material for TCP remotes Signed-off-by: Andrew Melnick --- cmd/podman/root.go | 24 +++++++ cmd/podman/system/connection/add.go | 56 +++++++++++++--- cmd/podman/system/service.go | 35 +++++++--- .../podman-system-connection-add.1.md | 16 +++++ .../markdown/podman-system-service.1.md | 18 ++++- pkg/api/server/server.go | 38 +++++++++-- pkg/bindings/connection.go | 66 +++++++++++++++---- pkg/domain/entities/engine.go | 3 + pkg/domain/entities/types/system.go | 11 ++-- pkg/domain/infra/runtime_abi.go | 4 +- pkg/domain/infra/runtime_tunnel.go | 8 +-- pkg/util/tls.go | 29 ++++++++ 12 files changed, 264 insertions(+), 44 deletions(-) create mode 100644 pkg/util/tls.go diff --git a/cmd/podman/root.go b/cmd/podman/root.go index e48b497d77..f6932aa6d2 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -162,6 +162,9 @@ func readRemoteCliFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) } podmanConfig.URI = con.URI podmanConfig.Identity = con.Identity + podmanConfig.TLSCertFile = con.TLSCertFile + podmanConfig.TLSKeyFile = con.TLSKeyFile + podmanConfig.TLSCAFile = con.TLSCAFile podmanConfig.MachineMode = con.IsMachine case url.Changed: podmanConfig.URI = url.Value.String() @@ -174,6 +177,9 @@ func readRemoteCliFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) } podmanConfig.URI = con.URI podmanConfig.Identity = con.Identity + podmanConfig.TLSCertFile = con.TLSCertFile + podmanConfig.TLSKeyFile = con.TLSKeyFile + podmanConfig.TLSCAFile = con.TLSCAFile podmanConfig.MachineMode = con.IsMachine } case host.Changed: @@ -208,6 +214,9 @@ func setupRemoteConnection(podmanConfig *entities.PodmanConfig) string { } podmanConfig.URI = con.URI podmanConfig.Identity = con.Identity + podmanConfig.TLSCertFile = con.TLSCertFile + podmanConfig.TLSKeyFile = con.TLSKeyFile + podmanConfig.TLSCAFile = con.TLSCAFile podmanConfig.MachineMode = con.IsMachine return con.Name case hostEnv != "": @@ -220,6 +229,9 @@ func setupRemoteConnection(podmanConfig *entities.PodmanConfig) string { if err == nil { podmanConfig.URI = con.URI podmanConfig.Identity = con.Identity + podmanConfig.TLSCertFile = con.TLSCertFile + podmanConfig.TLSKeyFile = con.TLSKeyFile + podmanConfig.TLSCAFile = con.TLSCAFile podmanConfig.MachineMode = con.IsMachine return con.Name } @@ -505,6 +517,18 @@ func rootFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) { lFlags.StringVar(&podmanConfig.Identity, identityFlagName, podmanConfig.Identity, "path to SSH identity file, (CONTAINER_SSHKEY)") _ = cmd.RegisterFlagCompletionFunc(identityFlagName, completion.AutocompleteDefault) + tlsCertFileFlagName := "tls-cert" + lFlags.StringVar(&podmanConfig.TLSCertFile, tlsCertFileFlagName, "", "path to TLS client certificate PEM file for remote, (CONTAINER_TLS_CERT)") + _ = cmd.RegisterFlagCompletionFunc(tlsCertFileFlagName, completion.AutocompleteDefault) + + tlsKeyFileFlagName := "tls-key" + lFlags.StringVar(&podmanConfig.TLSKeyFile, tlsKeyFileFlagName, "", "path to TLS client certificate private key PEM file for remote, (CONTAINER_TLS_KEY)") + _ = cmd.RegisterFlagCompletionFunc(tlsKeyFileFlagName, completion.AutocompleteDefault) + + tlsCAFileFlagName := "tls-ca" + lFlags.StringVar(&podmanConfig.TLSCAFile, tlsCAFileFlagName, "", "path to TLS certificate Authority PEM file for remote, (CONTAINER_TLS_CA)") + _ = cmd.RegisterFlagCompletionFunc(tlsCAFileFlagName, completion.AutocompleteDefault) + // Flags that control or influence any kind of output. outFlagName := "out" lFlags.StringVar(&useStdout, outFlagName, "", "Send output (stdout) from podman to a file") diff --git a/cmd/podman/system/connection/add.go b/cmd/podman/system/connection/add.go index ba0723ac9f..478ebfa0ce 100644 --- a/cmd/podman/system/connection/add.go +++ b/cmd/podman/system/connection/add.go @@ -27,7 +27,7 @@ var ( "destination" is one of the form: [user@]hostname (will default to ssh) ssh://[user@]hostname[:port][/path] (will obtain socket path from service, if not given.) - tcp://hostname:port (not secured) + tcp://hostname:port (not secured without TLS enabled) unix://path (absolute path required) `, RunE: add, @@ -36,6 +36,7 @@ var ( podman system connection add --identity ~/.ssh/dev_rsa testing ssh://root@server.fubar.com:2222 podman system connection add --identity ~/.ssh/dev_rsa --port 22 production root@server.fubar.com podman system connection add debug tcp://localhost:8080 + podman system connection add production-tls --tls-ca=ca.crt --tls-cert=tls.crt --tls-key=tls.key tcp://localhost:8080 `, } @@ -51,11 +52,14 @@ var ( dockerPath string cOpts = struct { - Identity string - Port int - UDSPath string - Default bool - Farm string + Identity string + Port int + UDSPath string + Default bool + Farm string + TLSCertFile string + TLSKeyFile string + TLSCAFile string }{} ) @@ -74,6 +78,18 @@ func init() { flags.StringVar(&cOpts.Identity, identityFlagName, "", "path to SSH identity file") _ = addCmd.RegisterFlagCompletionFunc(identityFlagName, completion.AutocompleteDefault) + tlsCertFileFlagName := "tls-cert" + flags.StringVar(&cOpts.TLSCertFile, tlsCertFileFlagName, "", "path to TLS client certificate PEM file") + _ = addCmd.RegisterFlagCompletionFunc(tlsCertFileFlagName, completion.AutocompleteDefault) + + tlsKeyFileFlagName := "tls-key" + flags.StringVar(&cOpts.TLSKeyFile, tlsKeyFileFlagName, "", "path to TLS client certificate private key PEM file") + _ = addCmd.RegisterFlagCompletionFunc(tlsKeyFileFlagName, completion.AutocompleteDefault) + + tlsCAFileFlagName := "tls-ca" + flags.StringVar(&cOpts.TLSCAFile, tlsCAFileFlagName, "", "path to TLS certificate Authority PEM file") + _ = addCmd.RegisterFlagCompletionFunc(tlsCAFileFlagName, completion.AutocompleteDefault) + socketPathFlagName := "socket-path" flags.StringVar(&cOpts.UDSPath, socketPathFlagName, "", "path to podman socket on remote host. (default '/run/podman/podman.sock' or '/run/user/{uid}/podman/podman.sock)") _ = addCmd.RegisterFlagCompletionFunc(socketPathFlagName, completion.AutocompleteDefault) @@ -141,11 +157,29 @@ func add(cmd *cobra.Command, args []string) error { switch uri.Scheme { case "ssh": + if cmd.Flags().Changed("tls-cert") { + return errors.New("--tls-cert option not supported for ssh scheme") + } + if cmd.Flags().Changed("tls-key") { + return errors.New("--tls-key option not supported for ssh scheme") + } + if cmd.Flags().Changed("tls-ca") { + return errors.New("--tls-ca option not supported for ssh scheme") + } return ssh.Create(entities, sshMode) case "unix": if cmd.Flags().Changed("identity") { return errors.New("--identity option not supported for unix scheme") } + if cmd.Flags().Changed("tls-cert") { + return errors.New("--tls-cert option not supported for unix scheme") + } + if cmd.Flags().Changed("tls-key") { + return errors.New("--tls-key option not supported for unix scheme") + } + if cmd.Flags().Changed("tls-ca") { + return errors.New("--tls-ca option not supported for unix scheme") + } if cmd.Flags().Changed("socket-path") { uri.Path = cmd.Flag("socket-path").Value.String() @@ -169,6 +203,9 @@ func add(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("identity") { return errors.New("--identity option not supported for tcp scheme") } + if cmd.Flags().Changed("tls-cert") != cmd.Flags().Changed("tls-key") { + return errors.New("--tls-cert and --tls-key options must be both provided if one is provided") + } if uri.Port() == "" { return errors.New("tcp scheme requires a port either via --port or in destination URL") } @@ -177,8 +214,11 @@ func add(cmd *cobra.Command, args []string) error { } dst := config.Destination{ - URI: uri.String(), - Identity: cOpts.Identity, + URI: uri.String(), + Identity: cOpts.Identity, + TLSCertFile: cOpts.TLSCertFile, + TLSKeyFile: cOpts.TLSKeyFile, + TLSCAFile: cOpts.TLSCAFile, } connection := args[0] diff --git a/cmd/podman/system/service.go b/cmd/podman/system/service.go index 7806ca2e30..209f630a9a 100644 --- a/cmd/podman/system/service.go +++ b/cmd/podman/system/service.go @@ -36,13 +36,19 @@ Enable a listening service for API access to Podman commands. RunE: service, ValidArgsFunction: common.AutocompleteDefaultOneArg, Example: `podman system service --time=0 unix:///tmp/podman.sock - podman system service --time=0 tcp://localhost:8888`, + podman system service --time=0 tcp://localhost:8888 + podman system service --time=0 --tls-cert=tls.crt --tls-key=tls.key tcp://localhost:8888 + podman system service --time=0 --tls-cert=tls.crt --tls-key=tls.key --tls-client-ca=ca.crt tcp://localhost:8888 + `, } srvArgs = struct { - CorsHeaders string - PProfAddr string - Timeout uint + CorsHeaders string + PProfAddr string + Timeout uint + TLSCertFile string + TLSKeyFile string + TLSClientCAFile string }{} ) @@ -67,6 +73,16 @@ func init() { flags.StringVarP(&srvArgs.PProfAddr, "pprof-address", "", "", "Binding network address for pprof profile endpoints, default: do not expose endpoints") _ = flags.MarkHidden("pprof-address") + + flags.StringVarP(&srvArgs.TLSCertFile, "tls-cert", "", "", + "PEM file containing TLS serving certificate.") + _ = srvCmd.RegisterFlagCompletionFunc("tls-cert", completion.AutocompleteDefault) + flags.StringVarP(&srvArgs.TLSKeyFile, "tls-key", "", "", + "PEM file containing TLS serving certificate private key") + _ = srvCmd.RegisterFlagCompletionFunc("tls-key", completion.AutocompleteDefault) + flags.StringVarP(&srvArgs.TLSClientCAFile, "tls-client-ca", "", "", + "Only trust client connections with certificates signed by this CA PEM file") + _ = srvCmd.RegisterFlagCompletionFunc("tls-client-ca", completion.AutocompleteDefault) } func aliasTimeoutFlag(_ *pflag.FlagSet, name string) pflag.NormalizedName { @@ -100,10 +116,13 @@ func service(cmd *cobra.Command, args []string) error { } return restService(cmd.Flags(), registry.PodmanConfig(), entities.ServiceOptions{ - CorsHeaders: srvArgs.CorsHeaders, - PProfAddr: srvArgs.PProfAddr, - Timeout: time.Duration(srvArgs.Timeout) * time.Second, - URI: apiURI, + CorsHeaders: srvArgs.CorsHeaders, + PProfAddr: srvArgs.PProfAddr, + Timeout: time.Duration(srvArgs.Timeout) * time.Second, + URI: apiURI, + TLSCertFile: srvArgs.TLSCertFile, + TLSKeyFile: srvArgs.TLSKeyFile, + TLSClientCAFile: srvArgs.TLSClientCAFile, }) } diff --git a/docs/source/markdown/podman-system-connection-add.1.md b/docs/source/markdown/podman-system-connection-add.1.md index 2e50d21ad2..716a98acdd 100644 --- a/docs/source/markdown/podman-system-connection-add.1.md +++ b/docs/source/markdown/podman-system-connection-add.1.md @@ -27,6 +27,18 @@ Path to ssh identity file. If the identity file has been encrypted, Podman promp If no identity file is provided and no user is given, Podman defaults to the user running the podman command. Podman prompts for the login password on the remote server. +#### --tls-cert=path + +Path to a PEM file containing the TLS client certificate to present to the server. `--tls-key` must also be provided. + +#### --tls-key=path + +Path to a PEM file containing the private key matching `--tls-cert`. `--tls-cert` must also be provided. + +#### --tls-ca=path + +Path to a PEM file containing the certificate authority bundle to verify the server's certificate against. + #### **--port**, **-p**=*port* Port for ssh destination. The default value is `22`. @@ -56,6 +68,10 @@ Add a named system connection to local tcp socket: ``` $ podman system connection add debug tcp://localhost:8080 ``` +Add a named system connection to remote tcp socket secured via TLS: +``` +$ podman system connection add secure-debug --tls-cert=tls.crt --tls-key=tls.key --tls-ca=ca.crt tcp://podman.example.com:8443 +``` ## SEE ALSO **[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[podman-system-connection(1)](podman-system-connection.1.md)** diff --git a/docs/source/markdown/podman-system-service.1.md b/docs/source/markdown/podman-system-service.1.md index cc1b596987..66d48504b0 100644 --- a/docs/source/markdown/podman-system-service.1.md +++ b/docs/source/markdown/podman-system-service.1.md @@ -70,10 +70,11 @@ To access the API service inside a container: Please note that the API grants full access to all Podman functionality, and thus allows arbitrary code execution as the user running the API, with no ability to limit or audit this access. The API's security model is built upon access via a Unix socket with access restricted via standard file permissions, ensuring that only the user running the service will be able to access it. -We *strongly* recommend against making the API socket available via the network (IE, bindings the service to a *tcp* URL). +TLS can be used to secure this socket by requiring clients to present a certificate signed by a trusted certificate authority ("CA"), as well as to allow the client to verify the identity of the API. +We *strongly* recommend against making the API socket available via the network (IE, bindings the service to a *tcp* URL) without enabling mutual TLS to authenticate the client. Even access via Localhost carries risks - anyone with access to the system will be able to access the API. If remote access is required, we instead recommend forwarding the API socket via SSH, and limiting access on the remote machine to the greatest extent possible. -If a *tcp* URL must be used, using the *--cors* option is recommended to improve security. +If a *tcp* URL must be used without TLS, using the *--cors* option is recommended to improve security. ## OPTIONS @@ -81,6 +82,19 @@ If a *tcp* URL must be used, using the *--cors* option is recommended to improve CORS headers to inject to the HTTP response. The default value is empty string which disables CORS headers. +#### --tls-cert=path + +Path to a PEM file containing the TLS certificate to present to clients. `--tls-key` must also be provided. + +#### --tls-key=path + +Path to a PEM file containing the private key matching `--tls-cert`. `--tls-cert` must also be provided. + +#### --tls-client-ca=path + +Path to a PEM file containing the TLS certificate bundle to validate client connections against. +Connections that present no certificate or a certificate not signed by one of these certificates will be rejected. + #### **--help**, **-h** Print usage statement. diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 41dff40cef..84142e1eb1 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -4,6 +4,7 @@ package server import ( "context" + "crypto/tls" "fmt" "log" "net" @@ -22,6 +23,7 @@ import ( "github.com/containers/podman/v5/pkg/api/server/idle" "github.com/containers/podman/v5/pkg/api/types" "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/containers/podman/v5/pkg/util" "github.com/coreos/go-systemd/v22/daemon" "github.com/gorilla/mux" "github.com/gorilla/schema" @@ -38,6 +40,9 @@ type APIServer struct { CorsHeaders string // Inject Cross-Origin Resource Sharing (CORS) headers PProfAddr string // Binding network address for pprof profiles idleTracker *idle.Tracker // Track connections to support idle shutdown + tlsCertFile string // TLS serving certificate PEM file + tlsKeyFile string // TLS serving certificate private key PEM file + tlsClientCAFile string // TLS client certifiicate CA bundle PEM file } // Number of seconds to wait for next request, if exceeded shutdown server @@ -84,10 +89,13 @@ func newServer(runtime *libpod.Runtime, listener net.Listener, opts entities.Ser Handler: router, IdleTimeout: opts.Timeout * 2, }, - CorsHeaders: opts.CorsHeaders, - Listener: listener, - PProfAddr: opts.PProfAddr, - idleTracker: tracker, + CorsHeaders: opts.CorsHeaders, + Listener: listener, + PProfAddr: opts.PProfAddr, + idleTracker: tracker, + tlsCertFile: opts.TLSCertFile, + tlsKeyFile: opts.TLSKeyFile, + tlsClientCAFile: opts.TLSClientCAFile, } server.BaseContext = func(l net.Listener) context.Context { @@ -98,6 +106,18 @@ func newServer(runtime *libpod.Runtime, listener net.Listener, opts entities.Ser return ctx } + if opts.TLSClientCAFile != "" { + logrus.Debugf("will validate client certs against %s", opts.TLSClientCAFile) + pool, err := util.ReadCertBundle(opts.TLSClientCAFile) + if err != nil { + return nil, err + } + server.TLSConfig = &tls.Config{ + ClientCAs: pool, + ClientAuth: tls.RequireAndVerifyClientCert, + } + } + // Capture panics and print stack traces for diagnostics, // additionally process X-Reference-Id Header to support event correlation router.Use(panicHandler(), referenceIDHandler()) @@ -224,7 +244,15 @@ func (s *APIServer) Serve() error { errChan := make(chan error, 1) s.setupSystemd() go func() { - err := s.Server.Serve(s.Listener) + var err error + if s.tlsClientCAFile != "" || (s.tlsCertFile != "" && s.tlsKeyFile != "") { + if s.tlsCertFile != "" && s.tlsKeyFile != "" { + logrus.Debugf("serving TLS with cert %s and key %s", s.tlsCertFile, s.tlsKeyFile) + } + err = s.Server.ServeTLS(s.Listener, s.tlsCertFile, s.tlsKeyFile) + } else { + err = s.Server.Serve(s.Listener) + } if err != nil && err != http.ErrServerClosed { errChan <- fmt.Errorf("failed to start API service: %w", err) return diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index abf7ab496e..ccbf1d0a51 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -3,6 +3,7 @@ package bindings import ( "bytes" "context" + "crypto/tls" "errors" "fmt" "io" @@ -18,6 +19,7 @@ import ( "github.com/blang/semver/v4" "github.com/containers/common/pkg/ssh" + "github.com/containers/podman/v5/pkg/util" "github.com/containers/podman/v5/version" "github.com/kevinburke/ssh_config" "github.com/sirupsen/logrus" @@ -32,6 +34,7 @@ type APIResponse struct { type Connection struct { URI *url.URL Client *http.Client + tls bool } type valueKey string @@ -92,9 +95,11 @@ func NewConnection(ctx context.Context, uri string) (context.Context, error) { // or unix:///run/podman/podman.sock // or ssh://@[:port]/run/podman/podman.sock func NewConnectionWithIdentity(ctx context.Context, uri string, identity string, machine bool) (context.Context, error) { - var ( - err error - ) + return NewConnectionWithIdentityOrTLS(ctx, uri, identity, "", "", "", machine) +} + +func NewConnectionWithIdentityOrTLS(ctx context.Context, uri string, identity string, tlsCertFile, tlsKeyFile, tlsCAFile string, machine bool) (context.Context, error) { + var err error if v, found := os.LookupEnv("CONTAINER_HOST"); found && uri == "" { uri = v } @@ -103,6 +108,18 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string, identity = v } + if v, found := os.LookupEnv("CONTAINER_TLS_CERT"); found && len(tlsCertFile) == 0 { + tlsCertFile = v + } + + if v, found := os.LookupEnv("CONTAINER_TLS_KEY"); found && len(tlsKeyFile) == 0 { + tlsKeyFile = v + } + + if v, found := os.LookupEnv("CONTAINER_TLS_CA"); found && len(tlsCAFile) == 0 { + tlsCAFile = v + } + _url, err := url.Parse(uri) if err != nil { return nil, fmt.Errorf("value of CONTAINER_HOST is not a valid url: %s: %w", uri, err) @@ -128,7 +145,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string, if !strings.HasPrefix(uri, "tcp://") { return nil, errors.New("tcp URIs should begin with tcp://") } - conn, err := tcpClient(_url) + conn, err := tcpClient(_url, tlsCertFile, tlsKeyFile, tlsCAFile) if err != nil { return nil, newConnectError(err) } @@ -262,11 +279,12 @@ func sshClient(_url *url.URL, uri string, identity string, machine bool) (Connec connection.Client = &http.Client{ Transport: &http.Transport{ DialContext: dialContext, - }} + }, + } return connection, nil } -func tcpClient(_url *url.URL) (Connection, error) { +func tcpClient(_url *url.URL, tlsCertFile, tlsKeyFile, tlsCAFile string) (Connection, error) { connection := Connection{ URI: _url, } @@ -298,11 +316,31 @@ func tcpClient(_url *url.URL) (Connection, error) { } } } + transport := http.Transport{ + DialContext: dialContext, + DisableCompression: true, + } + if len(tlsCAFile) != 0 || len(tlsCertFile) != 0 || len(tlsKeyFile) != 0 { + logrus.Debugf("using TLS cert=%s key=%s ca=%s", tlsCertFile, tlsKeyFile, tlsCAFile) + transport.TLSClientConfig = &tls.Config{} + connection.tls = true + } + if len(tlsCAFile) != 0 { + pool, err := util.ReadCertBundle(tlsCAFile) + if err != nil { + return connection, fmt.Errorf("unable to read CA bundle: %w", err) + } + transport.TLSClientConfig.RootCAs = pool + } + if len(tlsCertFile) != 0 && len(tlsKeyFile) != 0 { + keyPair, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) + if err != nil { + return connection, fmt.Errorf("unable to read TLS key pair: %w", err) + } + transport.TLSClientConfig.Certificates = append(transport.TLSClientConfig.Certificates, keyPair) + } connection.Client = &http.Client{ - Transport: &http.Transport{ - DialContext: dialContext, - DisableCompression: true, - }, + Transport: &transport, } return connection, nil } @@ -383,8 +421,14 @@ func (c *Connection) DoRequest(ctx context.Context, httpBody io.Reader, httpMeth baseURL := "http://d" if c.URI.Scheme == "tcp" { + var scheme string + if c.tls { + scheme = "https" + } else { + scheme = "http" + } // Allow path prefixes for tcp connections to match Docker behavior - baseURL = "http://" + c.URI.Host + c.URI.Path + baseURL = scheme + "://" + c.URI.Host + c.URI.Path } uri := fmt.Sprintf(baseURL+"/v%s/libpod"+endpoint, params...) logrus.Debugf("DoRequest Method: %s URI: %v", httpMethod, uri) diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index 7fcbc64953..abc4617702 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -36,6 +36,9 @@ type PodmanConfig struct { EngineMode EngineMode // ABI or Tunneling mode HooksDir []string Identity string // ssh identity for connecting to server + TLSCertFile string // tls client cert for connecting to server + TLSKeyFile string // tls client cert private key for connection to server + TLSCAFile string // tls certificate authority to verify server connection IsRenumber bool // Is this a system renumber command? If so, a number of checks will be relaxed IsReset bool // Is this a system reset command? If so, a number of checks will be skipped/omitted MaxWorks int // maximum number of parallel threads diff --git a/pkg/domain/entities/types/system.go b/pkg/domain/entities/types/system.go index 6c331cd50e..bcaee2b89d 100644 --- a/pkg/domain/entities/types/system.go +++ b/pkg/domain/entities/types/system.go @@ -9,10 +9,13 @@ import ( // ServiceOptions provides the input for starting an API and sidecar pprof services type ServiceOptions struct { - CorsHeaders string // Cross-Origin Resource Sharing (CORS) headers - PProfAddr string // Network address to bind pprof profiles service - Timeout time.Duration // Duration of inactivity the service should wait before shutting down - URI string // Path to unix domain socket service should listen on + CorsHeaders string // Cross-Origin Resource Sharing (CORS) headers + PProfAddr string // Network address to bind pprof profiles service + Timeout time.Duration // Duration of inactivity the service should wait before shutting down + URI string // Path to unix domain socket service should listen on + TLSCertFile string // Path to serving certificate PEM file + TLSKeyFile string // Path to serving certificate key PEM file + TLSClientCAFile string // Path to client certificate authority } // SystemCheckOptions provides options for checking storage consistency. diff --git a/pkg/domain/infra/runtime_abi.go b/pkg/domain/infra/runtime_abi.go index 21704fa760..3134656991 100644 --- a/pkg/domain/infra/runtime_abi.go +++ b/pkg/domain/infra/runtime_abi.go @@ -18,7 +18,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine, r, err := NewLibpodRuntime(facts.FlagSet, facts) return r, err case entities.TunnelMode: - ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity, facts.MachineMode) + ctx, err := bindings.NewConnectionWithIdentityOrTLS(context.Background(), facts.URI, facts.Identity, facts.TLSCertFile, facts.TLSKeyFile, facts.TLSCAFile, facts.MachineMode) return &tunnel.ContainerEngine{ClientCtx: ctx}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) @@ -32,7 +32,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error) return r, err case entities.TunnelMode: // TODO: look at me! - ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity, facts.MachineMode) + ctx, err := bindings.NewConnectionWithIdentityOrTLS(context.Background(), facts.URI, facts.Identity, facts.TLSCertFile, facts.TLSKeyFile, facts.TLSCAFile, facts.MachineMode) if err != nil { return nil, fmt.Errorf("%w: %s", err, facts.URI) } diff --git a/pkg/domain/infra/runtime_tunnel.go b/pkg/domain/infra/runtime_tunnel.go index a28385890c..8349244d3e 100644 --- a/pkg/domain/infra/runtime_tunnel.go +++ b/pkg/domain/infra/runtime_tunnel.go @@ -17,13 +17,13 @@ var ( connection *context.Context ) -func newConnection(uri string, identity, farmNodeName string, machine bool) (context.Context, error) { +func newConnection(uri string, identity, tlsCertFile, tlsKeyFile, tlsCAFile, farmNodeName string, machine bool) (context.Context, error) { connectionMutex.Lock() defer connectionMutex.Unlock() // if farmNodeName given, then create a connection with the node so that we can send builds there if connection == nil || farmNodeName != "" { - ctx, err := bindings.NewConnectionWithIdentity(context.Background(), uri, identity, machine) + ctx, err := bindings.NewConnectionWithIdentityOrTLS(context.Background(), uri, identity, tlsCertFile, tlsKeyFile, tlsCAFile, machine) if err != nil { return ctx, err } @@ -37,7 +37,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine, case entities.ABIMode: return nil, fmt.Errorf("direct runtime not supported") case entities.TunnelMode: - ctx, err := newConnection(facts.URI, facts.Identity, "", facts.MachineMode) + ctx, err := newConnection(facts.URI, facts.Identity, facts.TLSCertFile, facts.TLSKeyFile, facts.TLSCAFile, "", facts.MachineMode) return &tunnel.ContainerEngine{ClientCtx: ctx}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) @@ -49,7 +49,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error) case entities.ABIMode: return nil, fmt.Errorf("direct image runtime not supported") case entities.TunnelMode: - ctx, err := newConnection(facts.URI, facts.Identity, facts.FarmNodeName, facts.MachineMode) + ctx, err := newConnection(facts.URI, facts.Identity, facts.TLSCertFile, facts.TLSKeyFile, facts.TLSCAFile, facts.FarmNodeName, facts.MachineMode) return &tunnel.ImageEngine{ClientCtx: ctx, FarmNode: tunnel.FarmNode{NodeName: facts.FarmNodeName}}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) diff --git a/pkg/util/tls.go b/pkg/util/tls.go new file mode 100644 index 0000000000..e23ce96ad4 --- /dev/null +++ b/pkg/util/tls.go @@ -0,0 +1,29 @@ +package util + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" +) + +func ReadCertBundle(path string) (*x509.CertPool, error) { + pool := x509.NewCertPool() + caPEM, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading cert bundle %s: %w", path, err) + } + for ix := 0; len(caPEM) != 0; ix++ { + var caDER *pem.Block + caDER, caPEM = pem.Decode(caPEM) + if caDER == nil || caDER.Type != "CERTIFICATE" { + return nil, fmt.Errorf("non-certificate type `%s` PEM data found in cert bundle %s", caDER.Type, path) + } + caCert, err := x509.ParseCertificate(caDER.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing cert bundle at index %d: %w", ix, err) + } + pool.AddCert(caCert) + } + return pool, nil +}