Files
podman/pkg/machine/e2e/basic_test.go
Lewis Roy 67ec2037c0 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>
2025-08-05 21:02:28 +10:00

449 lines
16 KiB
Go

package e2e_test
import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/containers/podman/v5/pkg/machine/define"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
)
const TESTIMAGE = "quay.io/libpod/testimage:20241011"
var _ = Describe("run basic podman commands", func() {
It("Basic ops", func() {
// golangci-lint has trouble with actually skipping tests marked Skip
// so skip it on cirrus envs and where CIRRUS_CI isn't set.
name := randomString()
i := new(initMachine)
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withNow()).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
bm := basicMachine{}
imgs, err := mb.setCmd(bm.withPodmanCommand([]string{"images", "-q"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(imgs).To(Exit(0))
Expect(imgs.outputToStringSlice()).To(BeEmpty())
newImgs, err := mb.setCmd(bm.withPodmanCommand([]string{"pull", TESTIMAGE})).run()
Expect(err).ToNot(HaveOccurred())
Expect(newImgs).To(Exit(0))
Expect(newImgs.outputToStringSlice()).To(HaveLen(1))
runAlp, err := mb.setCmd(bm.withPodmanCommand([]string{"run", TESTIMAGE, "cat", "/etc/os-release"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(runAlp).To(Exit(0))
Expect(runAlp.outputToString()).To(ContainSubstring("Alpine Linux"))
contextDir := GinkgoT().TempDir()
cfile := filepath.Join(contextDir, "Containerfile")
err = os.WriteFile(cfile, []byte("FROM "+TESTIMAGE+"\nRUN ip addr\n"), 0o644)
Expect(err).ToNot(HaveOccurred())
build, err := mb.setCmd(bm.withPodmanCommand([]string{"build", contextDir})).run()
Expect(err).ToNot(HaveOccurred())
Expect(build).To(Exit(0))
Expect(build.outputToString()).To(ContainSubstring("COMMIT"))
rmCon, err := mb.setCmd(bm.withPodmanCommand([]string{"rm", "-a"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(rmCon).To(Exit(0))
})
It("Volume ops", func() {
tDir, err := filepath.Abs(GinkgoT().TempDir())
Expect(err).ToNot(HaveOccurred())
roFile := filepath.Join(tDir, "attr-test-file")
// Create the file as ready-only, since some platforms disallow selinux attr writes
// The subsequent Z mount should still succeed in spite of that
rf, err := os.OpenFile(roFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o444)
Expect(err).ToNot(HaveOccurred())
rf.Close()
name := randomString()
i := new(initMachine).withImage(mb.imagePath).withNow()
// All other platforms have an implicit mount for the temp area
if isVmtype(define.QemuVirt) {
i.withVolume(tDir)
}
session, err := mb.setName(name).setCmd(i).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
bm := basicMachine{}
// Test relabel works on all platforms
runAlp, err := mb.setCmd(bm.withPodmanCommand([]string{"run", "-v", tDir + ":/test:Z", TESTIMAGE, "ls", "/test/attr-test-file"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(runAlp).To(Exit(0))
// Test overlay works on all platforms except Hyper-V (see #26210)
if !isVmtype(define.HyperVVirt) {
runAlp, err = mb.setCmd(bm.withPodmanCommand([]string{"run", "-v", tDir + ":/test:O", TESTIMAGE, "ls", "/test/attr-test-file"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(runAlp).To(Exit(0))
}
// Test build with --volume option
cf := filepath.Join(tDir, "Containerfile")
err = os.WriteFile(cf, []byte("FROM "+TESTIMAGE+"\nRUN ls /test/attr-test-file\n"), 0o644)
Expect(err).ToNot(HaveOccurred())
build, err := mb.setCmd(bm.withPodmanCommand([]string{"build", "-t", name, "-v", tDir + ":/test", tDir})).run()
Expect(err).ToNot(HaveOccurred())
Expect(build).To(Exit(0))
})
It("Single character volume mount", func() {
name := randomString()
i := new(initMachine).withImage(mb.imagePath).withNow()
session, err := mb.setName(name).setCmd(i).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
bm := basicMachine{}
volumeCreate, err := mb.setCmd(bm.withPodmanCommand([]string{"volume", "create", "a"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(volumeCreate).To(Exit(0))
run, err := mb.setCmd(bm.withPodmanCommand([]string{"run", "-v", "a:/test:Z", TESTIMAGE, "true"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(run).To(Exit(0))
})
It("Volume should be virtiofs", func() {
// In theory this could run on MacOS too, but we know virtiofs works for that now,
// this is just testing linux
skipIfNotVmtype(define.QemuVirt, "This is just adding coverage for virtiofs on linux")
tDir, err := filepath.Abs(GinkgoT().TempDir())
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(filepath.Join(tDir, "testfile"), []byte("some test contents"), 0o644)
Expect(err).ToNot(HaveOccurred())
name := randomString()
i := new(initMachine).withImage(mb.imagePath).withNow()
// Ensure that this is a volume, it may not be automatically on qemu
i.withVolume(tDir)
session, err := mb.setName(name).setCmd(i).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
ssh := new(sshMachine).withSSHCommand([]string{"findmnt", "-no", "FSTYPE", tDir})
findmnt, err := mb.setName(name).setCmd(ssh).run()
Expect(err).ToNot(HaveOccurred())
Expect(findmnt).To(Exit(0))
Expect(findmnt.outputToString()).To(ContainSubstring("virtiofs"))
})
It("Volume should be disabled by command line", func() {
skipIfWSL("Requires standard volume handling")
skipIfVmtype(define.AppleHvVirt, "Skipped on Apple platform")
skipIfVmtype(define.LibKrun, "Skipped on Apple platform")
name := randomString()
i := new(initMachine).withImage(mb.imagePath).withNow()
// Empty arg forces no volumes
i.withVolume("")
session, err := mb.setName(name).setCmd(i).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
ssh9p := new(sshMachine).withSSHCommand([]string{"findmnt", "-no", "FSTYPE", "-t", "9p"})
findmnt9p, err := mb.setName(name).setCmd(ssh9p).run()
Expect(err).ToNot(HaveOccurred())
Expect(findmnt9p).To(Exit(0))
Expect(findmnt9p.outputToString()).To(BeEmpty())
sshVirtiofs := new(sshMachine).withSSHCommand([]string{"findmnt", "-no", "FSTYPE", "-t", "virtiofs"})
findmntVirtiofs, err := mb.setName(name).setCmd(sshVirtiofs).run()
Expect(err).ToNot(HaveOccurred())
Expect(findmntVirtiofs).To(Exit(0))
Expect(findmntVirtiofs.outputToString()).To(BeEmpty())
})
It("Podman ops with port forwarding and gvproxy", func() {
name := randomString()
i := new(initMachine)
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withNow()).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
ctrName := "test"
bm := basicMachine{}
runAlp, err := mb.setCmd(bm.withPodmanCommand([]string{"run", "-dt", "--name", ctrName, "-p", "62544:80",
"--stop-signal", "SIGKILL", TESTIMAGE,
"/bin/busybox-extras", "httpd", "-f", "-p", "80"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(runAlp).To(Exit(0))
_, id, _ := strings.Cut(TESTIMAGE, ":")
testHTTPServer("62544", false, id+"\n")
// Test exec in machine scenario: https://github.com/containers/podman/issues/20821
exec, err := mb.setCmd(bm.withPodmanCommand([]string{"exec", ctrName, "true"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(exec).To(Exit(0))
out, err := pgrep("gvproxy")
Expect(err).ToNot(HaveOccurred())
Expect(out).ToNot(BeEmpty())
rmCon, err := mb.setCmd(bm.withPodmanCommand([]string{"rm", "-af"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(rmCon).To(Exit(0))
testHTTPServer("62544", true, "")
stop := new(stopMachine)
stopSession, err := mb.setCmd(stop).run()
Expect(err).ToNot(HaveOccurred())
Expect(stopSession).To(Exit(0))
// gxproxy should exit after machine is stopped
out, _ = pgrep("gvproxy")
Expect(out).ToNot(ContainSubstring("gvproxy"))
})
It("podman volume on non-standard path", func() {
skipIfWSL("Requires standard volume handling")
dir, err := os.MkdirTemp("", "machine-volume")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(dir)
testString := "abcdefg1234567"
testFile := "testfile"
err = os.WriteFile(filepath.Join(dir, testFile), []byte(testString), 0644)
Expect(err).ToNot(HaveOccurred())
name := randomString()
machinePath := "/does/not/exist"
init := new(initMachine).withVolume(fmt.Sprintf("%s:%s", dir, machinePath)).withImage(mb.imagePath).withNow()
session, err := mb.setName(name).setCmd(init).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
// Must use path.Join to ensure forward slashes are used, even on Windows.
ssh := new(sshMachine).withSSHCommand([]string{"cat", path.Join(machinePath, testFile)})
ls, err := mb.setName(name).setCmd(ssh).run()
Expect(err).ToNot(HaveOccurred())
Expect(ls).To(Exit(0))
Expect(ls.outputToString()).To(ContainSubstring(testString))
})
It("podman build contexts", func() {
skipIfVmtype(define.HyperVVirt, "FIXME: #23429 - Error running podman build with option --build-context on Hyper-V")
name := randomString()
i := new(initMachine)
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withNow()).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
mainContextDir := GinkgoT().TempDir()
cfile := filepath.Join(mainContextDir, "test1")
err = os.WriteFile(cfile, []byte(name), 0o644)
Expect(err).ToNot(HaveOccurred())
additionalContextDir := GinkgoT().TempDir()
cfile = filepath.Join(additionalContextDir, "test2")
err = os.WriteFile(cfile, []byte(name), 0o644)
Expect(err).ToNot(HaveOccurred())
cfile = filepath.Join(mainContextDir, "Containerfile")
err = os.WriteFile(cfile, []byte("FROM "+TESTIMAGE+"\nCOPY test1 /\nCOPY --from=test-context test2 /\n"), 0o644)
Expect(err).ToNot(HaveOccurred())
bm := basicMachine{}
build, err := mb.setCmd(bm.withPodmanCommand([]string{"build", "-t", name, "--build-context", "test-context=" + additionalContextDir, mainContextDir})).run()
if build != nil && build.ExitCode() != 0 {
output := build.outputToString() + build.errorToString()
if strings.Contains(output, "multipart/form-data") &&
strings.Contains(output, "not supported") {
Skip("Build contexts with multipart/form-data are not supported on this version")
}
}
Expect(err).ToNot(HaveOccurred())
Expect(build).To(Exit(0))
Expect(build.outputToString()).To(ContainSubstring("COMMIT"))
run, err := mb.setCmd(bm.withPodmanCommand([]string{"run", name, "cat", "/test1"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(run).To(Exit(0))
Expect(build.outputToString()).To(ContainSubstring(name))
run, err = mb.setCmd(bm.withPodmanCommand([]string{"run", name, "cat", "/test2"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(run).To(Exit(0))
Expect(build.outputToString()).To(ContainSubstring(name))
})
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
// 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 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)
})
})
func testHTTPServer(port string, shouldErr bool, expectedResponse string) {
address := url.URL{
Scheme: "http",
Host: net.JoinHostPort("localhost", port),
}
interval := 250 * time.Millisecond
var err error
var resp *http.Response
for i := 0; i < 6; i++ {
resp, err = http.Get(address.String() + "/testimage-id")
if err != nil && shouldErr {
Expect(err.Error()).To(ContainSubstring(expectedResponse))
return
}
if err == nil {
defer resp.Body.Close()
break
}
time.Sleep(interval)
interval *= 2
}
Expect(err).ToNot(HaveOccurred())
body, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(body)).Should(Equal(expectedResponse))
}
type TLSConfig struct {
key string
cert string
}
// setup a local webserver in the test and then point podman machine init to it
// to verify the connection details.
func testImagePullTLS(tls *TLSConfig, tlsVerify *bool) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
Expect(err).ToNot(HaveOccurred())
serverAddr := listener.Addr().String()
var loggedRequests []string
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
loggedRequests = append(loggedRequests, r.URL.Path)
// don't care about an error, we should never get here
_, _ = w.Write([]byte("Hello"))
})
srv := &http.Server{
Handler: mux,
ErrorLog: log.New(io.Discard, "", 0),
}
defer srv.Close()
serverErr := make(chan error)
go func() {
defer GinkgoRecover()
if tls != nil {
serverErr <- srv.ServeTLS(listener, tls.cert, tls.key)
} else {
serverErr <- srv.Serve(listener)
}
}()
i := new(initMachine)
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))
// Note because we don't run a real registry the error you get when TLS is not checked is:
// 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/\": "
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
// is the same, it rejected a cert which is how we know TLS verification is turned on.
// https://support.apple.com/en-au/102028
expectedErr += "“test.podman.io” certificate is not standards compliant\n"
} else {
expectedErr += "certificate signed by unknown authority\n"
}
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
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))
}