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:
Andrew Melnick
2025-07-31 18:51:37 -06:00
parent a118fdf4e2
commit feb36e4fe6
116 changed files with 1848 additions and 616 deletions

View File

@ -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)