mirror of
https://github.com/containers/podman.git
synced 2025-10-12 00:35:05 +08:00
Implement TLS API Support
* Added flags to point to TLS PEM files to use for exposing and connecting to an encrypted remote API socket with server and client authentication. * Added TLS fields for system connection ls templates. * Added special "tls" format for system connection ls to list TLS fields in human-readable table format. * Updated remote integration and system tests to allow specifying a "transport" to run the full suite against a unix, tcp, tls, or mtls system service. * Added system tests to verify basic operation of unix, tcp, tls, and mtls services, clients, and connections. Signed-off-by: Andrew Melnick <meln5674.5674@gmail.com>
This commit is contained in:
@ -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/tlsutil"
|
||||
"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
|
||||
@ -76,10 +81,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 {
|
||||
@ -90,6 +98,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 := tlsutil.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())
|
||||
@ -217,7 +237,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
|
||||
|
@ -3,6 +3,7 @@ package bindings
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/containers/podman/v5/pkg/util/tlsutil"
|
||||
"github.com/containers/podman/v5/version"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -33,6 +35,7 @@ type APIResponse struct {
|
||||
type Connection struct {
|
||||
URI *url.URL
|
||||
Client *http.Client
|
||||
tls bool
|
||||
}
|
||||
|
||||
type valueKey string
|
||||
@ -89,7 +92,7 @@ func JoinURL(elements ...string) string {
|
||||
|
||||
// NewConnection creates a new service connection without an identity
|
||||
func NewConnection(ctx context.Context, uri string) (context.Context, error) {
|
||||
return NewConnectionWithIdentity(ctx, uri, "", false)
|
||||
return NewConnectionWithOptions(ctx, Options{URI: uri})
|
||||
}
|
||||
|
||||
// NewConnectionWithIdentity takes a URI as a string and returns a context with the
|
||||
@ -101,14 +104,31 @@ func NewConnection(ctx context.Context, uri string) (context.Context, error) {
|
||||
// or unix:///run/podman/podman.sock
|
||||
// or ssh://<user>@<host>[:port]/run/podman/podman.sock
|
||||
func NewConnectionWithIdentity(ctx context.Context, uri string, identity string, machine bool) (context.Context, error) {
|
||||
var err error
|
||||
if v, found := os.LookupEnv("CONTAINER_HOST"); found && uri == "" {
|
||||
uri = v
|
||||
}
|
||||
return NewConnectionWithOptions(ctx, Options{URI: uri, Identity: identity, Machine: machine})
|
||||
}
|
||||
|
||||
if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identity) == 0 {
|
||||
identity = v
|
||||
type Options struct {
|
||||
URI string
|
||||
Identity string
|
||||
TLSCertFile string
|
||||
TLSKeyFile string
|
||||
TLSCAFile string
|
||||
Machine bool
|
||||
}
|
||||
|
||||
func orEnv(s string, env string) string {
|
||||
if len(s) != 0 {
|
||||
return s
|
||||
}
|
||||
s, _ = os.LookupEnv(env)
|
||||
return s
|
||||
}
|
||||
|
||||
func NewConnectionWithOptions(ctx context.Context, opts Options) (context.Context, error) {
|
||||
var err error
|
||||
|
||||
uri := orEnv(opts.URI, "CONTAINER_HOST")
|
||||
identity := orEnv(opts.Identity, "CONTAINER_SSHKEY")
|
||||
|
||||
_url, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
@ -119,7 +139,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string,
|
||||
var connection Connection
|
||||
switch _url.Scheme {
|
||||
case "ssh":
|
||||
conn, err := sshClient(_url, uri, identity, machine)
|
||||
conn, err := sshClient(_url, uri, identity, opts.Machine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -135,7 +155,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, opts.TLSCertFile, opts.TLSKeyFile, opts.TLSCAFile)
|
||||
if err != nil {
|
||||
return nil, newConnectError(err)
|
||||
}
|
||||
@ -151,7 +171,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string,
|
||||
}
|
||||
ctx = context.WithValue(ctx, versionKey, serviceVersion)
|
||||
|
||||
ctx = context.WithValue(ctx, machineModeKey, machine)
|
||||
ctx = context.WithValue(ctx, machineModeKey, opts.Machine)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
@ -288,7 +308,7 @@ func sshClient(_url *url.URL, uri string, identity string, machine bool) (Connec
|
||||
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,
|
||||
}
|
||||
@ -320,11 +340,34 @@ 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 := tlsutil.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) {
|
||||
return connection, fmt.Errorf("TLS Key and Certificate must both or neither be provided")
|
||||
}
|
||||
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
|
||||
}
|
||||
@ -405,8 +448,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)
|
||||
|
@ -3,6 +3,7 @@ package containers
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -548,9 +549,11 @@ func newUpgradeRequest(ctx context.Context, conn *bindings.Connection, body io.R
|
||||
"Upgrade": []string{"tcp"},
|
||||
}
|
||||
|
||||
// FIXME: This is one giant race condition. Let's hope no-one uses this same client until we're done!
|
||||
var socket net.Conn
|
||||
socketSet := false
|
||||
dialContext := conn.Client.Transport.(*http.Transport).DialContext
|
||||
tlsConfig := conn.Client.Transport.(*http.Transport).TLSClientConfig
|
||||
t := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
c, err := dialContext(ctx, network, address)
|
||||
@ -563,7 +566,33 @@ func newUpgradeRequest(ctx context.Context, conn *bindings.Connection, body io.R
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
DialTLSContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
c, err := dialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg *tls.Config
|
||||
if tlsConfig == nil {
|
||||
cfg = new(tls.Config)
|
||||
} else {
|
||||
cfg = tlsConfig.Clone()
|
||||
}
|
||||
if cfg.ServerName == "" {
|
||||
var firstTLSHost string
|
||||
if firstTLSHost, _, err = net.SplitHostPort(address); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.ServerName = firstTLSHost
|
||||
}
|
||||
c = tls.Client(c, cfg)
|
||||
if !socketSet {
|
||||
socket = c
|
||||
socketSet = true
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
IdleConnTimeout: time.Duration(0),
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
conn.Client.Transport = t
|
||||
response, err := conn.DoRequest(ctx, body, http.MethodPost, path, params, headers)
|
||||
|
@ -37,6 +37,9 @@ type PodmanConfig struct {
|
||||
HooksDir []string
|
||||
CdiSpecDirs []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
|
||||
|
@ -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.
|
||||
|
@ -18,7 +18,14 @@ 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.NewConnectionWithOptions(context.Background(), bindings.Options{
|
||||
URI: facts.URI,
|
||||
Identity: facts.Identity,
|
||||
TLSCertFile: facts.TLSCertFile,
|
||||
TLSKeyFile: facts.TLSKeyFile,
|
||||
TLSCAFile: facts.TLSCAFile,
|
||||
Machine: facts.MachineMode,
|
||||
})
|
||||
return &tunnel.ContainerEngine{ClientCtx: ctx}, err
|
||||
}
|
||||
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)
|
||||
@ -32,7 +39,14 @@ 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.NewConnectionWithOptions(context.Background(), bindings.Options{
|
||||
URI: facts.URI,
|
||||
Identity: facts.Identity,
|
||||
TLSCertFile: facts.TLSCertFile,
|
||||
TLSKeyFile: facts.TLSKeyFile,
|
||||
TLSCAFile: facts.TLSCAFile,
|
||||
Machine: facts.MachineMode,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", err, facts.URI)
|
||||
}
|
||||
|
@ -17,13 +17,20 @@ 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.NewConnectionWithOptions(context.Background(), bindings.Options{
|
||||
URI: uri,
|
||||
Identity: identity,
|
||||
TLSCertFile: tlsCertFile,
|
||||
TLSKeyFile: tlsKeyFile,
|
||||
TLSCAFile: tlsCAFile,
|
||||
Machine: machine,
|
||||
})
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
@ -37,7 +44,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 +56,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)
|
||||
|
32
pkg/util/tlsutil/tls.go
Normal file
32
pkg/util/tlsutil/tls.go
Normal file
@ -0,0 +1,32 @@
|
||||
package tlsutil
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("reading cert bundle %s: non-PEM data found", path)
|
||||
}
|
||||
if caDER.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("reading cert bundle %s: non-certificate type `%s` PEM data found", path, caDER.Type)
|
||||
}
|
||||
caCert, err := x509.ParseCertificate(caDER.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading cert bundle %s: parsing item %d: %w", path, ix, err)
|
||||
}
|
||||
pool.AddCert(caCert)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
Reference in New Issue
Block a user