From 13e1d5af4b30bfc9c6a19ce17ff7cb7e5d7144fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Feb 2026 01:14:50 +0100 Subject: [PATCH 1/2] PARTIALLY TESTED: Add --tls-details, use it to affect libimage and the like MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For remote operation, start the remote service with --tls-details: using --tls-details on the client side will only affect client's connection. This should eventually include many more tests - track down all current uses of libpod.Runtime.{SystemContext,imageContext,LibimageRuntime}. That will come later Signed-off-by: Miloslav Trmač --- cmd/podman/root.go | 4 + docs/source/markdown/podman.1.md | 9 + libpod/options.go | 9 + pkg/domain/entities/engine.go | 1 + pkg/domain/infra/runtime_libpod.go | 6 + test/e2e/libpod_suite_remote_test.go | 4 + test/e2e/testdata/tls-details-1.3.yaml | 1 + test/e2e/testdata/tls-details-anything.yaml | 1 + test/e2e/testdata/tls-details-pqc-only.yaml | 3 + test/e2e/tls_test.go | 244 ++++++++++++++++++ test/utils/utils.go | 1 + .../image/v5/pkg/cli/basetls/basetls.go | 219 ++++++++++++++++ .../pkg/cli/basetls/tlsdetails/tlsdetails.go | 59 +++++ vendor/modules.txt | 2 + 14 files changed, 563 insertions(+) create mode 100644 test/e2e/testdata/tls-details-1.3.yaml create mode 100644 test/e2e/testdata/tls-details-anything.yaml create mode 100644 test/e2e/testdata/tls-details-pqc-only.yaml create mode 100644 test/e2e/tls_test.go create mode 100644 vendor/go.podman.io/image/v5/pkg/cli/basetls/basetls.go create mode 100644 vendor/go.podman.io/image/v5/pkg/cli/basetls/tlsdetails/tlsdetails.go diff --git a/cmd/podman/root.go b/cmd/podman/root.go index f2581a2d9c..e4579b25d6 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -547,6 +547,10 @@ func rootFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) { lFlags.StringVar(&podmanConfig.TLSCAFile, tlsCAFileFlagName, podmanConfig.TLSCAFile, "path to TLS certificate Authority PEM file for remote.") _ = cmd.RegisterFlagCompletionFunc(tlsCAFileFlagName, completion.AutocompleteDefault) + tlsDetailsFlagName := "tls-details" + lFlags.StringVar(&podmanConfig.TLSDetailsFile, tlsDetailsFlagName, "", "Path to a containers-tls-details.yaml(5) file") + _ = cmd.RegisterFlagCompletionFunc(tlsDetailsFlagName, 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/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index d5c113a23e..b65e773ac5 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -177,6 +177,15 @@ Path to a PEM file containing the certificate authority bundle to verify the ser Path to a PEM file containing the TLS client certificate to present to the server. `--tls-key` must also be provided. +#### **--tls-details**=*path* + +Path to a `containers-tls-details.yaml(5)` file, affecting TLS behavior throughout the program. + +If not set, defaults to a reasonable default that may change over time (depending on system’s global policy, +version of the program, version of the Go language, and the like). + +Users should generally not use this option unless they have a process to ensure that the configuration will be kept up to date. + #### **--tls-key**=*path* Path to a PEM file containing the private key matching `--tls-cert`. `--tls-cert` must also be provided. diff --git a/libpod/options.go b/libpod/options.go index c907c4fa81..a0416ed248 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -25,6 +25,7 @@ import ( "go.podman.io/common/pkg/config" "go.podman.io/common/pkg/secrets" "go.podman.io/image/v5/manifest" + "go.podman.io/image/v5/pkg/cli/basetls" "go.podman.io/storage" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/idtools" @@ -226,6 +227,14 @@ func WithRegistriesConf(path string) RuntimeOption { } } +// WithBaseTLSConfig sets the TLS _algorithm_ options for the runtime. +func WithBaseTLSConfig(baseTLSConfig *basetls.Config) RuntimeOption { + return func(rt *Runtime) error { + rt.imageContext.BaseTLSConfig = baseTLSConfig.TLSConfig() + return nil + } +} + // WithDatabaseBackend configures the runtime's database backend. func WithDatabaseBackend(value string) RuntimeOption { logrus.Debugf("Setting custom database backend: %q", value) diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index 35c8d5efd8..c98c625c5c 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -40,6 +40,7 @@ type PodmanConfig struct { 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 + TLSDetailsFile string // Path to a containers-tls-details.yaml(5) file 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/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index 1f3c5a54b1..f2f349a1e3 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -18,6 +18,7 @@ import ( "github.com/containers/podman/v6/pkg/util" "github.com/sirupsen/logrus" flag "github.com/spf13/pflag" + "go.podman.io/image/v5/pkg/cli/basetls/tlsdetails" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/types" ) @@ -190,6 +191,11 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo if fs.Changed("registries-conf") { options = append(options, libpod.WithRegistriesConf(cfg.RegistriesConf)) } + baseTLSConfig, err := tlsdetails.BaseTLSFromOptionalFile(cfg.TLSDetailsFile) + if err != nil { + return nil, err + } + options = append(options, libpod.WithBaseTLSConfig(baseTLSConfig)) if cfg.CdiSpecDirs != nil { options = append(options, libpod.WithCDISpecDirs(cfg.CdiSpecDirs)) diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go index 74281c0214..d1d94435a3 100644 --- a/test/e2e/libpod_suite_remote_test.go +++ b/test/e2e/libpod_suite_remote_test.go @@ -66,6 +66,10 @@ func (p *PodmanTestIntegration) StartRemoteService() { if _, found := os.LookupEnv("DEBUG_SERVICE"); found { args = append(args, "--log-level", "trace") } + if p.RemoteTLSDetails != "" { + args = append(args, "--tls-details", p.RemoteTLSDetails) + } + remoteSocket := p.RemoteSocket args = append(args, "system", "service", "--time", "0") diff --git a/test/e2e/testdata/tls-details-1.3.yaml b/test/e2e/testdata/tls-details-1.3.yaml new file mode 100644 index 0000000000..7f1c6e8670 --- /dev/null +++ b/test/e2e/testdata/tls-details-1.3.yaml @@ -0,0 +1 @@ +minVersion: "1.3" diff --git a/test/e2e/testdata/tls-details-anything.yaml b/test/e2e/testdata/tls-details-anything.yaml new file mode 100644 index 0000000000..f61056962b --- /dev/null +++ b/test/e2e/testdata/tls-details-anything.yaml @@ -0,0 +1 @@ +{} # No fields diff --git a/test/e2e/testdata/tls-details-pqc-only.yaml b/test/e2e/testdata/tls-details-pqc-only.yaml new file mode 100644 index 0000000000..7f95360157 --- /dev/null +++ b/test/e2e/testdata/tls-details-pqc-only.yaml @@ -0,0 +1,3 @@ +minVersion: "1.3" +namedGroups: + - "X25519MLKEM768" diff --git a/test/e2e/tls_test.go b/test/e2e/tls_test.go new file mode 100644 index 0000000000..64a68582ee --- /dev/null +++ b/test/e2e/tls_test.go @@ -0,0 +1,244 @@ +//go:build linux || freebsd + +package integration + +import ( + "crypto/tls" + "encoding/pem" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + + . "github.com/containers/podman/v6/test/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// tlsConfigServer returns http.StatusTeapot. So if we see that, that indicates a successful TLS connection. +// But we need to special-case that in podmanFailTLSDetailsWithCAKnowledge. +const teapotRegex = `\b418\b` + +var _ = Describe("--tls-details", func() { + var ( + defaultServer *tlsConfigServer + tls12Server *tlsConfigServer + nonPQCserver *tlsConfigServer + pqcServer *tlsConfigServer + expected []expectedBehavior + ) + + BeforeEach(func() { + defaultServer = newServer(&tls.Config{}) + tls12Server = newServer(&tls.Config{ + MaxVersion: tls.VersionTLS12, + }) + nonPQCserver = newServer(&tls.Config{ + MinVersion: tls.VersionTLS13, + CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384, tls.CurveP521}, + }) + pqcServer = newServer(&tls.Config{ + MinVersion: tls.VersionTLS13, + CurvePreferences: []tls.CurveID{tls.X25519MLKEM768}, + }) + + expected = []expectedBehavior{ + { + server: defaultServer, + tlsDetails: "testdata/tls-details-anything.yaml", + expected: teapotRegex, + }, + { + server: tls12Server, + tlsDetails: "testdata/tls-details-anything.yaml", + expected: teapotRegex, + }, + { + server: nonPQCserver, + tlsDetails: "testdata/tls-details-anything.yaml", + expected: teapotRegex, + }, + { + server: pqcServer, + tlsDetails: "testdata/tls-details-anything.yaml", + expected: teapotRegex, + }, + + { + server: defaultServer, + tlsDetails: "testdata/tls-details-1.3.yaml", + expected: teapotRegex, + }, + { + server: tls12Server, + tlsDetails: "testdata/tls-details-1.3.yaml", + expected: `protocol version not supported`, + }, + { + server: nonPQCserver, + tlsDetails: "testdata/tls-details-1.3.yaml", + expected: teapotRegex, + }, + { + server: pqcServer, + tlsDetails: "testdata/tls-details-1.3.yaml", + expected: teapotRegex, + }, + + { + server: defaultServer, + tlsDetails: "testdata/tls-details-pqc-only.yaml", + expected: teapotRegex, + }, + { + server: tls12Server, + tlsDetails: "testdata/tls-details-pqc-only.yaml", + expected: `protocol version not supported`, + }, + { + server: nonPQCserver, + tlsDetails: "testdata/tls-details-pqc-only.yaml", + expected: `handshake failure`, + }, + { + server: pqcServer, + tlsDetails: "testdata/tls-details-pqc-only.yaml", + expected: teapotRegex, + }, + } + }) + + // FIXME: this should contain many more tests to exercise libimage.Runtime.{SystemContext,imageContext,LibimageRuntime}. + + It("podman --tls-details pull", func() { + caDir := GinkgoT().TempDir() + caPath := filepath.Join(caDir, "ca.crt") + + for _, e := range expected { + err := os.WriteFile(caPath, e.server.certBytes, 0o644) + Expect(err).ToNot(HaveOccurred()) + // --cert-dir is not available in the remote client. + if !IsRemote() { + podmanFailTLSDetails(&e, "pull", "--cert-dir", caDir, "docker://"+e.server.hostPort+"/repo") + } else { + podmanFailTLSDetailsNoCA(&e, "pull", "docker://"+e.server.hostPort+"/repo") + } + } + }) + + It("podman --tls-details push", func() { + podmanTest.AddImageToRWStore(ALPINE) + + caDir := GinkgoT().TempDir() + caPath := filepath.Join(caDir, "ca.crt") + + for _, e := range expected { + err := os.WriteFile(caPath, e.server.certBytes, 0o644) + Expect(err).ToNot(HaveOccurred()) + podmanFailTLSDetails(&e, "push", "--cert-dir", caDir, ALPINE, "docker://"+e.server.hostPort+"/repo") + } + }) + + It("podman --tls-details run", func() { + caDir := GinkgoT().TempDir() + caPath := filepath.Join(caDir, "ca.crt") + + for _, e := range expected { + err := os.WriteFile(caPath, e.server.certBytes, 0o644) + Expect(err).ToNot(HaveOccurred()) + // --cert-dir is not available in the remote client. + if !IsRemote() { + podmanFailTLSDetails(&e, "run", "--cert-dir", caDir, "--rm", "docker://"+e.server.hostPort+"/repo", "true") + } else { + podmanFailTLSDetailsNoCA(&e, "run", "--rm", "docker://"+e.server.hostPort+"/repo", "true") + } + } + }) +}) + +type expectedBehavior struct { + server *tlsConfigServer + tlsDetails string + expected string +} + +// tlsConfigServer serves TLS with a specific configuration. +// It returns StatusTeapot on all requests; we use that to detect that the TLS negotiation succeeded, +// without bothering to actually implement any of the protocols. +type tlsConfigServer struct { + server *httptest.Server + hostPort string + certBytes []byte + certPath string +} + +func newServer(config *tls.Config) *tlsConfigServer { + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusTeapot) + })) + DeferCleanup(server.Close) + + server.TLS = config.Clone() + server.StartTLS() + + certBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: server.Certificate().Raw, + }) + certDir := GinkgoT().TempDir() + certPath := filepath.Join(certDir, "cert.pem") + err := os.WriteFile(certPath, certBytes, 0o644) + Expect(err).ToNot(HaveOccurred()) + + return &tlsConfigServer{ + server: server, + hostPort: server.Listener.Addr().String(), + certBytes: certBytes, + certPath: certPath, + } +} + +// podmanFailTLSDetails runs a podman command with args, setting up --tls-details from e, +// and asserting an expected error. +func podmanFailTLSDetails(e *expectedBehavior, args ...string) { + GinkgoHelper() + podmanFailTLSDetailsWithCAKnowledge(e, true, args...) +} + +// podmanFailTLSDetailsNoCA runs a podman command with args, setting up --tls-details from e, +// and asserting an expected error (assuming that the command does not trust the server’s CA). +func podmanFailTLSDetailsNoCA(e *expectedBehavior, args ...string) { + GinkgoHelper() + podmanFailTLSDetailsWithCAKnowledge(e, false, args...) +} + +// podmanFailTLSDetailsWithCAKnowledge runs a podman command with args, setting up --tls-details from e, +// and asserting an expected error. +// knownCA indicates whether the client trusts the server’s CA. +func podmanFailTLSDetailsWithCAKnowledge(e *expectedBehavior, knownCA bool, args ...string) { + GinkgoHelper() + session := podmanTLSDetailsSessionWithOptions(e, PodmanExecOptions{}, args...) + session.WaitWithDefaultTimeout() + + // Frequently, we don’t expose trusted CA configuration for individual operations, + // especially in the remote CLI. Checking for a successful HTTP connection is better, + // but if we get far enough to be worrying about certificates, + // we already negotiated the TLS version and named group. + expected := e.expected + if expected == teapotRegex && (IsRemote() || !knownCA) { + expected = `certificate signed by unknown authority` + } + Expect(session).Should(ExitWithErrorRegex(125, expected)) +} + +// podmanTLSDetailsSessionWithOptions creates a PodmanSessionIntegration with e.tlsDetails. +func podmanTLSDetailsSessionWithOptions(e *expectedBehavior, options PodmanExecOptions, args ...string) *PodmanSessionIntegration { + GinkgoHelper() + if !IsRemote() { + args = append([]string{"--tls-details", e.tlsDetails}, args...) + } else { + podmanTest.RemoteTLSDetails = e.tlsDetails + podmanTest.RestartRemoteService() + } + return podmanTest.PodmanWithOptions(options, args...) +} diff --git a/test/utils/utils.go b/test/utils/utils.go index 5a0826b8ea..5fb0be861a 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -65,6 +65,7 @@ type PodmanTest struct { RemoteTLSServerCAPool *x509.CertPool RemoteTLSClientCertFile string RemoteTLSClientKeyFile string + RemoteTLSDetails string RemoteTest bool TempDir string } diff --git a/vendor/go.podman.io/image/v5/pkg/cli/basetls/basetls.go b/vendor/go.podman.io/image/v5/pkg/cli/basetls/basetls.go new file mode 100644 index 0000000000..1558e8f4ea --- /dev/null +++ b/vendor/go.podman.io/image/v5/pkg/cli/basetls/basetls.go @@ -0,0 +1,219 @@ +// Package basetls encapsulates a set of base TLS settings (not keys/certificates) +// configured via containers-tls-details.yaml(5). +// +// CLI integration should generally be done using c/image/pkg/cli/basetls/tlsdetails instead +// of using the TLSDetailsFile directly. +package basetls + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "slices" +) + +// Config encapsulates user’s choices about base TLS settings, typically +// configured via containers-tls-details.yaml(5). +// +// Most codebases should pass around the resulting *tls.Config, without depending on this subpackage; +// this primarily exists as a separate type to allow passing the configuration around within (version-matched) RPC systems, +// using the MarshalText/UnmarshalText methods. +type Config struct { + // We keep the text representation because we start with it, and this way we don't have + // to implement formatting back to text. This is an internal detail, so we can change that later. + text TLSDetailsFile + config *tls.Config // Parsed from .text, both match +} + +// TLSDetailsFile contains a set of TLS options. +// +// To consume such a file, most callers should use c/image/pkg/cli/basetls/tlsdetails instead +// of dealing with this type explicitly. +// +// This type is exported primarily to allow creating parameter files programmatically +// (and eventually the tlsdetails subpackage should provide an API to convert this type into +// the appropriate file contents, so that callers don't need to do that manually). +type TLSDetailsFile struct { + // Keep this in sync with docs/containers-tls-details.yaml.5.md ! + + MinVersion string `yaml:"minVersion,omitempty"` // If set, minimum version to use throughout the program. + CipherSuites []string `yaml:"cipherSuites,omitempty"` // If set, allowed TLS cipher suites to use throughout the program. + NamedGroups []string `yaml:"namedGroups,omitempty"` // If set, allowed TLS named groups to use throughout the program. +} + +// NewFromTLSDetails creates a Config from a TLSDetailsFile. +func NewFromTLSDetails(details *TLSDetailsFile) (*Config, error) { + res := Config{ + text: TLSDetailsFile{}, + config: &tls.Config{}, + } + configChanged := false + for _, fn := range []func(input *TLSDetailsFile) (bool, error){ + res.parseMinVersion, + res.parseCipherSuites, + res.parseNamedGroups, + } { + changed, err := fn(details) + if err != nil { + return nil, err + } + if changed { + configChanged = true + } + } + + if !configChanged { + res.config = nil + } + return &res, nil +} + +// tlsVersions maps TLS version strings to their crypto/tls constants. +// We could use the `tls.VersionName` names, but those are verbose and contain spaces; +// similarly the OpenShift enum values (“VersionTLS11”) are unergonomic. +var tlsVersions = map[string]uint16{ + "1.0": tls.VersionTLS10, + "1.1": tls.VersionTLS11, + "1.2": tls.VersionTLS12, + "1.3": tls.VersionTLS13, +} + +func (c *Config) parseMinVersion(input *TLSDetailsFile) (bool, error) { + if input.MinVersion == "" { + return false, nil + } + v, ok := tlsVersions[input.MinVersion] + if !ok { + return false, fmt.Errorf("unrecognized TLS minimum version %q", input.MinVersion) + } + c.text.MinVersion = input.MinVersion + c.config.MinVersion = v + return true, nil +} + +// cipherSuitesByName returns a map from cipher suite name to its ID. +func cipherSuitesByName() map[string]uint16 { + // The Go standard library uses IANA names and already contains the mapping (for relevant values) + // sadly we still need to turn it into a lookup map. + suites := make(map[string]uint16) + for _, cs := range tls.CipherSuites() { + suites[cs.Name] = cs.ID + } + for _, cs := range tls.InsecureCipherSuites() { + suites[cs.Name] = cs.ID + } + return suites +} + +func (c *Config) parseCipherSuites(input *TLSDetailsFile) (bool, error) { + if input.CipherSuites == nil { + return false, nil + } + suitesByName := cipherSuitesByName() + ids := []uint16{} + for _, name := range input.CipherSuites { + id, ok := suitesByName[name] + if !ok { + return false, fmt.Errorf("unrecognized TLS cipher suite %q", name) + } + ids = append(ids, id) + } + c.text.CipherSuites = slices.Clone(input.CipherSuites) + c.config.CipherSuites = ids + return true, nil +} + +// groupsByName maps curve/group names to their tls.CurveID. +// The names match IANA TLS Supported Groups registry. +// +// Yes, the x25519 names differ in capitalization. +// Go’s tls.CurveID has a .String() method, but it +// uses the Go names. +var groupsByName = map[string]tls.CurveID{ + "secp256r1": tls.CurveP256, + "secp384r1": tls.CurveP384, + "secp521r1": tls.CurveP521, + "x25519": tls.X25519, + "X25519MLKEM768": tls.X25519MLKEM768, +} + +func (c *Config) parseNamedGroups(input *TLSDetailsFile) (bool, error) { + if input.NamedGroups == nil { + return false, nil + } + ids := []tls.CurveID{} + for _, name := range input.NamedGroups { + id, ok := groupsByName[name] + if !ok { + return false, fmt.Errorf("unrecognized TLS named group %q", name) + } + ids = append(ids, id) + } + c.text.NamedGroups = slices.Clone(input.NamedGroups) + c.config.CurvePreferences = ids + return true, nil +} + +// TLSConfig returns a *tls.Config matching the provided settings. +// If c contains no settings, it returns nil. +// Otherwise, the returned *tls.Config is freshly allocated and the caller can modify it as needed. +func (c *Config) TLSConfig() *tls.Config { + if c.config == nil { + return nil + } + return c.config.Clone() +} + +// marshaledSerialization is the data we use in MarshalText/UnmarshalText, +// marshaled using JSON. +// +// Note that the file format is using YAML, but we use JSON, to minimize dependencies +// in backend code where we don't need comments and the brackets are not annoying users. +type marshaledSerialization struct { + Version int + Data TLSDetailsFile +} + +const marshaledSerializationVersion1 = 1 + +// MarshalText serializes c to a text representation. +// +// The representation is intended to be reasonably stable across updates to c/image, +// but the consumer must not be older than the producer. +func (c Config) MarshalText() ([]byte, error) { + data := marshaledSerialization{ + Version: marshaledSerializationVersion1, + Data: c.text, + } + return json.Marshal(data) +} + +// UnmarshalText parses the output of MarshalText. +// +// The format is otherwise undocumented and we do not promise ongoing compatibility with producers external to this package. +func (c *Config) UnmarshalText(text []byte) error { + var data marshaledSerialization + + // In the future, this should be an even stricter parser, e.g. refusing duplicate fields + // and requiring a case-sensitive field name match. + decoder := json.NewDecoder(bytes.NewReader(text)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&data); err != nil { + return err + } + if decoder.More() { + return errors.New("unexpected extra data after a JSON object") + } + + if data.Version != marshaledSerializationVersion1 { + return fmt.Errorf("unsupported version %d", data.Version) + } + v, err := NewFromTLSDetails(&data.Data) + if err != nil { + return err + } + *c = *v + return nil +} diff --git a/vendor/go.podman.io/image/v5/pkg/cli/basetls/tlsdetails/tlsdetails.go b/vendor/go.podman.io/image/v5/pkg/cli/basetls/tlsdetails/tlsdetails.go new file mode 100644 index 0000000000..fca6bed917 --- /dev/null +++ b/vendor/go.podman.io/image/v5/pkg/cli/basetls/tlsdetails/tlsdetails.go @@ -0,0 +1,59 @@ +// Package tlsdetails implements the containers-tls-details.yaml(5) file format. +// +// Recommended CLI integration is by a --tls-details flag parsed using BaseTLSFromOptionalFile, with the following documentation: +// +// --tls-details is a path to a containers-tls-details.yaml(5) file, affecting TLS behavior throughout the program. +// +// If not set, defaults to a reasonable default that may change over time (depending on system’s global policy, +// version of the program, version of the Go language, and the like). +// +// Users should generally not use this option unless they have a process to ensure that the configuration will be kept up to date. +package tlsdetails + +import ( + "bytes" + "fmt" + "os" + + "go.podman.io/image/v5/pkg/cli/basetls" + "gopkg.in/yaml.v3" +) + +// BaseTLSFromOptionalFile returns a basetls.Config matching a containers-tls-details.yaml file at the specified path. +// If path is "", it returns a valid basetls.Config with no settings (where config.TLSConfig() will return nil). +func BaseTLSFromOptionalFile(path string) (*basetls.Config, error) { + if path == "" { + return basetls.NewFromTLSDetails(&basetls.TLSDetailsFile{}) + } + return BaseTLSFromFile(path) +} + +// BaseTLSFromFile returns a basetls.Config matching a containers-tls-details.yaml file at the specified path. +func BaseTLSFromFile(path string) (*basetls.Config, error) { + details, err := ParseFile(path) + if err != nil { + return nil, err + } + res, err := basetls.NewFromTLSDetails(details) + if err != nil { + return nil, fmt.Errorf("parsing TLS details %q: %w", path, err) + } + return res, nil +} + +// ParseFile parses a basetls.TLSDetailsFile at the specified path. +// +// Most consumers of the parameter file should use BaseTLSFromFile or BaseTLSFromOptionalFile instead. +func ParseFile(path string) (*basetls.TLSDetailsFile, error) { + var res basetls.TLSDetailsFile + source, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %q: %w", path, err) + } + dec := yaml.NewDecoder(bytes.NewReader(source)) + dec.KnownFields(true) + if err = dec.Decode(&res); err != nil { + return nil, fmt.Errorf("parsing %q: %w", path, err) + } + return &res, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 58a1cd361b..55fc723584 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -889,6 +889,8 @@ go.podman.io/image/v5/pkg/blobinfocache/memory go.podman.io/image/v5/pkg/blobinfocache/none go.podman.io/image/v5/pkg/blobinfocache/sqlite go.podman.io/image/v5/pkg/cli +go.podman.io/image/v5/pkg/cli/basetls +go.podman.io/image/v5/pkg/cli/basetls/tlsdetails go.podman.io/image/v5/pkg/cli/sigstore go.podman.io/image/v5/pkg/cli/sigstore/params go.podman.io/image/v5/pkg/compression From 7fd3be828884d3b6815131906fe64fbce529b494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Feb 2026 02:22:26 +0100 Subject: [PATCH 2/2] Add --tls-details support for (podman login) and (podman logout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miloslav Trmač --- cmd/podman/login.go | 8 ++++++- cmd/podman/logout.go | 10 +++++++- test/e2e/tls_test.go | 56 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/cmd/podman/login.go b/cmd/podman/login.go index 60cc5c7de0..ef85b4f8ef 100644 --- a/cmd/podman/login.go +++ b/cmd/podman/login.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" "go.podman.io/common/pkg/completion" + "go.podman.io/image/v5/pkg/cli/basetls/tlsdetails" "go.podman.io/image/v5/types" ) @@ -65,11 +66,15 @@ func init() { // Implementation of podman-login. func login(cmd *cobra.Command, args []string) error { var skipTLS types.OptionalBool - if cmd.Flags().Changed("tls-verify") { skipTLS = types.NewOptionalBool(!loginOptions.tlsVerify) } + baseTLSConfig, err := tlsdetails.BaseTLSFromOptionalFile(registry.PodmanConfig().TLSDetailsFile) + if err != nil { + return err + } + secretName := cmd.Flag("secret").Value.String() if len(secretName) > 0 { if len(loginOptions.Password) > 0 { @@ -97,6 +102,7 @@ func login(cmd *cobra.Command, args []string) error { sysCtx := &types.SystemContext{ DockerInsecureSkipTLSVerify: skipTLS, + BaseTLSConfig: baseTLSConfig.TLSConfig(), } common.SetRegistriesConfPath(sysCtx) loginOptions.GetLoginSet = cmd.Flag("get-login").Changed diff --git a/cmd/podman/logout.go b/cmd/podman/logout.go index 25db61dfb6..9b298c7dbd 100644 --- a/cmd/podman/logout.go +++ b/cmd/podman/logout.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" "go.podman.io/common/pkg/completion" + "go.podman.io/image/v5/pkg/cli/basetls/tlsdetails" "go.podman.io/image/v5/types" ) @@ -48,7 +49,14 @@ func init() { // Implementation of podman-logout. func logout(_ *cobra.Command, args []string) error { - sysCtx := &types.SystemContext{} + baseTLSConfig, err := tlsdetails.BaseTLSFromOptionalFile(registry.PodmanConfig().TLSDetailsFile) + if err != nil { + return err + } + + sysCtx := &types.SystemContext{ + BaseTLSConfig: baseTLSConfig.TLSConfig(), + } common.SetRegistriesConfPath(sysCtx) return auth.Logout(sysCtx, &logoutOptions, args) } diff --git a/test/e2e/tls_test.go b/test/e2e/tls_test.go index 64a68582ee..a6c7b6cbc6 100644 --- a/test/e2e/tls_test.go +++ b/test/e2e/tls_test.go @@ -9,6 +9,8 @@ import ( "net/http/httptest" "os" "path/filepath" + "regexp" + "slices" . "github.com/containers/podman/v6/test/utils" . "github.com/onsi/ginkgo/v2" @@ -110,6 +112,60 @@ var _ = Describe("--tls-details", func() { // FIXME: this should contain many more tests to exercise libimage.Runtime.{SystemContext,imageContext,LibimageRuntime}. + It("podman --tls-details login", func() { + caDir := GinkgoT().TempDir() + caPath := filepath.Join(caDir, "ca.crt") + + for _, e := range expected { + err := os.WriteFile(caPath, e.server.certBytes, 0o644) + Expect(err).ToNot(HaveOccurred()) + // Not podmanFailTLSDetails because this is a client-side operation, so the + // "if remote, no certificates" conditions don’t apply. + session := podmanTest.Podman([]string{"--tls-details", e.tlsDetails, "login", "--cert-dir", caDir, "-u", "user", "-p", "pass", e.server.hostPort}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitWithErrorRegex(125, e.expected)) + } + }) + + It("podman --tls-details logout", func() { + // Logout only accesses a registry in a very specific situation: + // - The primary auth file does not contain the credentials + // - A secondary auth file does contain them + // - Secondary auth files are _only_ consulted if the primary one is determined + // automatically, not from --authfile. Luckily XDG_RUNTIME_DIR and XDG_CONFIG_HOME + // counts as “automatically”. + xdgRuntimeDir := GinkgoT().TempDir() + Expect(os.MkdirAll(filepath.Join(xdgRuntimeDir, "containers"), 0o700)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(xdgRuntimeDir, "containers", "auth.json"), + []byte(`{"auths":{}}`), 0o600)).To(Succeed()) + + xdgConfigHome := GinkgoT().TempDir() + Expect(os.MkdirAll(filepath.Join(xdgConfigHome, "containers"), 0o700)).To(Succeed()) + secondaryAuthFile := filepath.Join(xdgConfigHome, "containers", "auth.json") + + for _, e := range expected { + Expect(os.WriteFile(secondaryAuthFile, []byte(`{"auths":{"`+e.server.hostPort+`":{"auth":"dXNlcjpwYXNz"}}}`), 0o600)).To(Succeed()) + + // The failure to connect to the registry is not reported to users, it is only visible + // in the debug log. + session := podmanTest.PodmanWithOptions(PodmanExecOptions{ + Env: append(slices.Clone(os.Environ()), + "XDG_RUNTIME_DIR="+xdgRuntimeDir, + "XDG_CONFIG_HOME="+xdgConfigHome), + }, "--log-level", "debug", + "--tls-details", e.tlsDetails, "logout", e.server.hostPort) + session.WaitWithDefaultTimeout() + + // --cert-dir is not available in logout. Compare podmanFailTLSDetailsWithCAKnowledge . + expected := e.expected + if expected == teapotRegex { + expected = `certificate signed by unknown authority` + } + Expect(session).Should(ExitWithErrorRegex(125, + `level=debug msg="Ping https://`+regexp.QuoteMeta(e.server.hostPort)+`/v2/[^\n]*`+expected)) + } + }) + It("podman --tls-details pull", func() { caDir := GinkgoT().TempDir() caPath := filepath.Join(caDir, "ca.crt")