Add support for configuring tls verification with machine init

This patch adds a new --tls-verify flag to the `podman machine init`
sub command which matches many of our other commands. This allows the
user to optionally control whether TLS verification is enabled or
disabled for download of the machine image.

The default remains to leave the TLS verification decision to the
backend library which defaults to enabling it, this patch just
allows the user to explicitly set it on the CLI.

Fixes: #26517

Signed-off-by: Lewis Roy <lewis@redhat.com>
This commit is contained in:
Lewis Roy
2025-08-01 22:37:45 +10:00
parent 0c4c9e4fbc
commit 67ec2037c0
10 changed files with 108 additions and 34 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/strongunits"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/cmd/podman/registry"
ldefine "github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/libpod/events"
@ -41,6 +42,7 @@ var (
// Flags which have a meaning when unspecified that differs from the flag default
type InitOptionalFlags struct {
UserModeNetworking bool
tlsVerify bool
}
// maxMachineNameSize is set to thirty to limit huge machine names primarily
@ -154,6 +156,9 @@ func init() {
userModeNetFlagName := "user-mode-networking"
flags.BoolVar(&initOptionalFlags.UserModeNetworking, userModeNetFlagName, false,
"Whether this machine should use user-mode networking, routing traffic through a host user-space process")
flags.BoolVar(&initOptionalFlags.tlsVerify, "tls-verify", true,
"Require HTTPS and verify certificates when contacting registries")
}
func initMachine(cmd *cobra.Command, args []string) error {
@ -219,6 +224,16 @@ func initMachine(cmd *cobra.Command, args []string) error {
}
}
// initOpts.SkipTlsVerify defaults to OptionalBoolUndefined, which means the backend library
// decides whether to verify TLS. We only explicitly set it if the user specifies the
// --tls-verify flag on the CLI.
//
// The flag value from initOptionalFlags.tlsVerify indicates whether TLS verification is desired.
// Since we are converting tlsVerify -> SkipTlsVerify, we must invert the bool accordingly.
if cmd.Flags().Changed("tls-verify") {
initOpts.SkipTlsVerify = types.NewOptionalBool(!initOptionalFlags.tlsVerify)
}
// TODO need to work this back in
// if finished, err := vm.Init(initOpts); err != nil || !finished {
// // Finished = true, err = nil - Success! Log a message with further instructions

View File

@ -1,5 +1,5 @@
####> This option file is used in:
####> podman artifact pull, artifact push, auto update, build, container runlabel, create, farm build, kube play, login, manifest add, manifest create, manifest inspect, manifest push, pull, push, run, search
####> podman artifact pull, artifact push, auto update, build, container runlabel, create, farm build, kube play, login, machine init, manifest add, manifest create, manifest inspect, manifest push, pull, push, run, search
####> If file is edited, make sure the changes
####> are applicable to all of those.
#### **--tls-verify**

View File

@ -119,6 +119,8 @@ means to use the timezone of the machine host.
The timezone setting is not used with WSL. WSL automatically sets the timezone to the same
as the host Windows operating system.
@@option tls-verify
#### **--usb**=*bus=number,devnum=number* or *vendor=hexadecimal,product=hexadecimal*
Assign a USB device from the host to the VM via USB passthrough.

View File

@ -1,6 +1,10 @@
package define
import "net/url"
import (
"net/url"
"github.com/containers/image/v5/types"
)
type InitOptions struct {
PlaybookPath string
@ -21,4 +25,5 @@ type InitOptions struct {
UID string // uid of the user that called machine
UserModeNetworking *bool // nil = use backend/system default, false = disable, true = enable
USBs []string
SkipTlsVerify types.OptionalBool
}

View File

@ -298,18 +298,39 @@ var _ = Describe("run basic podman commands", func() {
It("CVE-2025-6032 regression test - HTTP", func() {
// ensure that trying to pull from a local HTTP server fails and the connection will be rejected
testImagePullTLS(nil)
// ensure that tlsVerify is true by default
testImagePullTLS(nil, nil)
})
It("CVE-2025-6032 regression test - HTTPS unknown cert", func() {
// ensure that trying to pull from an local HTTPS server with invalid certs fails and the connection will be rejected
// ensure that trying to pull from a local HTTPS server with invalid certs fails and the connection will be rejected
// ensure that tlsVerify is true by default
testImagePullTLS(&TLSConfig{
// Key/Cert was generated with:
// openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
// -nodes -keyout test-tls.key -out test-tls.crt -subj "/CN=test.podman.io" -addext "subjectAltName=IP:127.0.0.1"
key: "test-tls.key",
cert: "test-tls.crt",
})
}, nil)
})
It("machine init should not fail on TLS validation with --tls-verfy=false - HTTP", func() {
// ensure that trying to pull from a local HTTP server doesn't fail when --tls-verify=false is set
tlsVerify := false
testImagePullTLS(nil, &tlsVerify)
})
It("machine init should not fail on TLS validation with --tls-verfy=false - HTTPS", func() {
// ensure that trying to pull from a local HTTPS server with invalid certs
// doesn't fail due to tls validation when --tls-verify=false is set
tlsVerify := false
testImagePullTLS(&TLSConfig{
// Key/Cert was generated with:
// openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
// -nodes -keyout test-tls.key -out test-tls.crt -subj "/CN=test.podman.io" -addext "subjectAltName=IP:127.0.0.1"
key: "test-tls.key",
cert: "test-tls.crt",
}, &tlsVerify)
})
})
@ -349,7 +370,7 @@ type TLSConfig struct {
// setup a local webserver in the test and then point podman machine init to it
// to verify the connection details.
func testImagePullTLS(tls *TLSConfig) {
func testImagePullTLS(tls *TLSConfig, tlsVerify *bool) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
Expect(err).ToNot(HaveOccurred())
serverAddr := listener.Addr().String()
@ -377,9 +398,17 @@ func testImagePullTLS(tls *TLSConfig) {
}
}()
name := randomString()
i := new(initMachine)
session, err := mb.setName(name).setCmd(i.withImage("docker://" + serverAddr + "/testimage")).run()
i.withImage("docker://" + serverAddr + "/testimage")
if tlsVerify != nil {
i.withTlsVerify(tlsVerify)
}
name := randomString()
session, err := mb.setName(name).setCmd(i).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(125))
@ -387,7 +416,11 @@ func testImagePullTLS(tls *TLSConfig) {
// Error: wrong manifest type for disk artifact: text/plain
// As such we match the errors strings exactly to ensure we have proper error messages that indicate the TLS error.
expectedErr := "Error: pinging container registry " + serverAddr + ": Get \"https://" + serverAddr + "/v2/\": "
if tls != nil {
switch {
case tlsVerify != nil && *tlsVerify == false: // tls-verify explicitly disabled
expectedErr = "Error: wrong manifest type for disk artifact: text/plain\n"
case tls != nil:
expectedErr += "tls: failed to verify certificate: x509: "
if runtime.GOOS == "darwin" {
// Apple doesn't like such long valid certs so the error is different but the purpose
@ -397,13 +430,18 @@ func testImagePullTLS(tls *TLSConfig) {
} else {
expectedErr += "certificate signed by unknown authority\n"
}
} else {
default:
// With both tlsVerify and tls being nil, a HTTP server will be ran and machine init should
// default to using tlsVerify
expectedErr += "http: server gave HTTP response to HTTPS client\n"
}
Expect(session.errorToString()).To(Equal(expectedErr))
// if the client enforces TLS verification then we should not have received any request
Expect(loggedRequests).To(BeEmpty(), "the server should have not process any request from the client")
if tlsVerify == nil || *tlsVerify == true {
Expect(loggedRequests).To(BeEmpty(), "the server should have not process any request from the client")
}
srv.Close()
Expect(<-serverErr).To(Equal(http.ErrServerClosed))

View File

@ -11,19 +11,19 @@ import (
type initMachine struct {
/*
--cpus uint Number of CPUs (default 1)
--disk-size uint Disk size in GiB (default 100)
--ignition-path string Path to ignition file
--username string Username of the remote user (default "core" for FCOS, "user" for Fedora)
--image-path string Path to bootable image (default "testing")
-m, --memory uint Memory in MiB (default 2048)
--now Start machine now
--rootful Whether this machine should prefer rootful container execution
--playbook string Run an ansible playbook after first boot
--timezone string Set timezone (default "local")
-v, --volume stringArray Volumes to mount, source:target
--volume-driver string Optional volume driver
--cpus uint Number of CPUs (default 1)
--disk-size uint Disk size in GiB (default 100)
--ignition-path string Path to ignition file
--username string Username of the remote user (default "core" for FCOS, "user" for Fedora)
--image-path string Path to bootable image (default "testing")
-m, --memory uint Memory in MiB (default 2048)
--now Start machine now
--rootful Whether this machine should prefer rootful container execution
--playbook string Run an ansible playbook after first boot
--tls-verify Require HTTPS and verify certificates when contacting registries
--timezone string Set timezone (default "local")
-v, --volume stringArray Volumes to mount, source:target
--volume-driver string Optional volume driver
*/
playbook string
cpus *uint
@ -38,6 +38,7 @@ type initMachine struct {
rootful bool
volumes []string
userModeNetworking bool
tlsVerify *bool
cmd []string
}
@ -85,6 +86,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string {
if i.swap != nil {
cmd = append(cmd, "--swap", strconv.Itoa(int(*i.swap)))
}
if i.tlsVerify != nil {
cmd = append(cmd, "--tls-verify="+strconv.FormatBool(*i.tlsVerify))
}
name := m.name
cmd = append(cmd, name)
@ -172,6 +176,11 @@ func (i *initMachine) withRunPlaybook(p string) *initMachine {
return i
}
func (i *initMachine) withTlsVerify(tlsVerify *bool) *initMachine {
i.tlsVerify = tlsVerify
return i
}
func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unused,nolintlint
i.userModeNetworking = r
return i

View File

@ -7,6 +7,7 @@ import (
"runtime"
"strings"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/pkg/machine/compression"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/podman/v5/pkg/machine/ocipull"
@ -22,7 +23,9 @@ func pullOCITestDisk(finalDir string, vmType define.VMType) error {
return err
}
dirs := define.MachineDirs{ImageCacheDir: imageCacheDir}
ociArtPull, err := ocipull.NewOCIArtifactPull(context.Background(), &dirs, "", "e2emachine", vmType, unusedFinalPath)
var skipTlsVerify types.OptionalBool
ociArtPull, err := ocipull.NewOCIArtifactPull(context.Background(), &dirs, "", "e2emachine", vmType, unusedFinalPath, skipTlsVerify)
if err != nil {
return err
}

View File

@ -71,10 +71,8 @@ type DiskArtifactOpts struct {
*/
func NewOCIArtifactPull(ctx context.Context, dirs *define.MachineDirs, endpoint string, vmName string, vmType define.VMType, finalPath *define.VMFile) (*OCIArtifactDisk, error) {
var (
arch string
)
func NewOCIArtifactPull(ctx context.Context, dirs *define.MachineDirs, endpoint string, vmName string, vmType define.VMType, finalPath *define.VMFile, skipTlsVerify types.OptionalBool) (*OCIArtifactDisk, error) {
var arch string
artifactVersion := getVersion()
switch runtime.GOARCH {
@ -108,8 +106,10 @@ func NewOCIArtifactPull(ctx context.Context, dirs *define.MachineDirs, endpoint
imageEndpoint: endpoint,
machineVersion: artifactVersion,
name: vmName,
pullOptions: &PullOptions{},
vmType: vmType,
pullOptions: &PullOptions{
SkipTLSVerify: skipTlsVerify,
},
vmType: vmType,
}
return &ociDisk, nil
}

View File

@ -4,19 +4,20 @@ import (
"context"
"strings"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/podman/v5/pkg/machine/ocipull"
"github.com/containers/podman/v5/pkg/machine/stdpull"
)
func GetDisk(userInputPath string, dirs *define.MachineDirs, imagePath *define.VMFile, vmType define.VMType, name string) error {
func GetDisk(userInputPath string, dirs *define.MachineDirs, imagePath *define.VMFile, vmType define.VMType, name string, skipTlsVerify types.OptionalBool) error {
var (
err error
mydisk ocipull.Disker
)
if userInputPath == "" || strings.HasPrefix(userInputPath, "docker://") {
mydisk, err = ocipull.NewOCIArtifactPull(context.Background(), dirs, userInputPath, name, vmType, imagePath)
mydisk, err = ocipull.NewOCIArtifactPull(context.Background(), dirs, userInputPath, name, vmType, imagePath, skipTlsVerify)
} else {
if strings.HasPrefix(userInputPath, "http") {
// TODO probably should use tempdir instead of datadir
@ -28,5 +29,6 @@ func GetDisk(userInputPath string, dirs *define.MachineDirs, imagePath *define.V
if err != nil {
return err
}
return mydisk.Get()
}

View File

@ -172,7 +172,7 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
// "/path
// "docker://quay.io/something/someManifest
if err := diskpull.GetDisk(opts.Image, dirs, mc.ImagePath, mp.VMType(), mc.Name); err != nil {
if err := diskpull.GetDisk(opts.Image, dirs, mc.ImagePath, mp.VMType(), mc.Name, opts.SkipTlsVerify); err != nil {
return err
}