Merge pull request #28336 from l0rd/import_native_ca

Import host trusted certificates into the guest machine - Windows part
This commit is contained in:
Matt Heon 2026-04-22 13:04:55 -04:00 committed by GitHub
commit 3279767614
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 981 additions and 66 deletions

View file

@ -5,9 +5,6 @@ package machine
import (
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"github.com/containers/podman/v6/cmd/podman/registry"
"github.com/containers/podman/v6/libpod/events"
@ -15,17 +12,13 @@ import (
"github.com/containers/podman/v6/pkg/machine"
"github.com/containers/podman/v6/pkg/machine/define"
"github.com/containers/podman/v6/pkg/machine/shim"
"github.com/containers/podman/v6/pkg/machine/vmconfigs"
"github.com/containers/podman/v6/pkg/specgen"
"github.com/spf13/cobra"
)
type cpOptions struct {
Quiet bool
Machine *vmconfigs.MachineConfig
IsSrc bool
SrcPath string
DestPath string
Quiet bool
IsSrc bool
}
var (
@ -95,11 +88,18 @@ func cp(_ *cobra.Command, args []string) error {
return fmt.Errorf("vm %q is not running", mc.Name)
}
cpOpts.Machine = mc
cpOpts.SrcPath = srcPath
cpOpts.DestPath = destPath
err = localhostSSHCopy(&cpOpts)
sshConfig := mc.SSH
username := sshConfig.RemoteUsername
if mc.HostUser.Rootful {
username = "root"
}
err = machine.LocalhostSSHCopy(username,
sshConfig.IdentityPath,
sshConfig.Port,
srcPath,
destPath,
cpOpts.IsSrc,
cpOpts.Quiet)
if err != nil {
return fmt.Errorf("copy failed: %s", err.Error())
}
@ -109,38 +109,6 @@ func cp(_ *cobra.Command, args []string) error {
return nil
}
// localhostSSHCopy uses scp to copy files from/to a localhost machine using ssh.
func localhostSSHCopy(opts *cpOptions) error {
srcPath := opts.SrcPath
destPath := opts.DestPath
sshConfig := opts.Machine.SSH
username := sshConfig.RemoteUsername
if cpOpts.Machine.HostUser.Rootful {
username = "root"
}
username += "@localhost:"
if opts.IsSrc {
srcPath = username + srcPath
} else {
destPath = username + destPath
}
args := append(
machine.LocalhostSSHArgs(), // Warning: This MUST NOT be generalized to allow communication over untrusted networks.
"-r",
"-i", sshConfig.IdentityPath,
"-P", strconv.Itoa(sshConfig.Port),
srcPath, destPath)
cmd := exec.Command("scp", args...)
if !opts.Quiet {
cmd.Stdout = os.Stdout
}
cmd.Stderr = os.Stderr
return cmd.Run()
}
func resolveMachineName(srcMachine, destMachine string) (string, error) {
if len(srcMachine) > 0 && len(destMachine) > 0 {
return "", errors.New("copying between two machines is unsupported")

View file

@ -170,6 +170,9 @@ func init() {
setDefaultConnectionFlagName := "update-connection"
flags.BoolVarP(&setDefaultSystemConn, setDefaultConnectionFlagName, "u", false, "Set default system connection for this machine")
importNativeCaFlagName := "import-native-ca"
flags.BoolVar(&initOpts.ImportNativeCA, importNativeCaFlagName, cfg.ContainersConfDefaultsRO.Machine.ImportNativeCA, "Import the host trusted CA certificates into the machine")
}
func initMachine(cmd *cobra.Command, args []string) error {

View file

@ -34,6 +34,7 @@ type SetFlags struct {
Rootful bool
UserModeNetworking bool
USBs []string
ImportNativeCA bool
}
func init() {
@ -81,6 +82,10 @@ func init() {
userModeNetFlagName := "user-mode-networking"
flags.BoolVar(&setFlags.UserModeNetworking, userModeNetFlagName, false, // defaults not-relevant due to use of Changed()
"Whether this machine should use user-mode networking, routing traffic through a host user-space process")
importNativeCaFlagName := "import-native-ca"
flags.BoolVar(&setFlags.ImportNativeCA, importNativeCaFlagName, false, // defaults not-relevant due to use of Changed()
"Import the host trusted CA certificates into the machine")
}
func setMachine(cmd *cobra.Command, args []string) error {
@ -120,6 +125,9 @@ func setMachine(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("usb") {
setOpts.USBs = &setFlags.USBs
}
if cmd.Flags().Changed("import-native-ca") {
setOpts.ImportNativeCA = &setFlags.ImportNativeCA
}
// At this point, we have the known changed information, etc
// Walk through changes to the providers if they need them

View file

@ -81,6 +81,13 @@ Registry target must be in the form of `docker://registry/repo/image:version`.
Note: Only images provided by podman will be supported.
#### **--import-native-ca**
Import the host's trusted CA certificates into the machine.
When set to true, the certificates from the host system are imported during machine startup.
This allows containers running in the machine to trust the same Certificate Authorities
as the host OS. The default value is `false`.
#### **--memory**, **-m**=*number*
Memory (in MiB). Note: 1024MiB = 1GiB.

View file

@ -33,6 +33,13 @@ Can only be increased. Only supported for QEMU machines.
Print usage statement.
#### **--import-native-ca**
Import the host's trusted CA certificates into the machine.
When set to true, the certificates from the host system are imported during machine startup.
This allows containers running in the machine to trust the same Certificate Authorities
as the host OS. The default value is `false`.
#### **--memory**, **-m**=*number*
Memory (in MB).

View file

@ -8,6 +8,7 @@ import (
"fmt"
"io/fs"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
@ -81,12 +82,12 @@ func getMachineMountsAndVMType(connectionURI string, parsedConnection *url.URL)
return mc.Mounts, machineProvider.VMType(), nil
}
// isPathAvailableOnMachine checks if a local path is available on the machine through mounted directories.
// IsPathAvailableOnMachine checks if a local path is available on the machine through mounted directories.
// If the path is available, it returns a LocalAPIMap with the corresponding remote path.
func isPathAvailableOnMachine(mounts []*vmconfigs.Mount, vmType define.VMType, path string) (*LocalAPIMap, bool) {
pathABS, err := filepath.Abs(path)
func IsPathAvailableOnMachine(mounts []*vmconfigs.Mount, vmType define.VMType, localPath string) (*LocalAPIMap, bool) {
pathABS, err := filepath.Abs(localPath)
if err != nil {
logrus.Debugf("Failed to get absolute path for %s: %v", path, err)
logrus.Debugf("Failed to get absolute path for %q: %v", localPath, err)
return nil, false
}
@ -107,19 +108,18 @@ func isPathAvailableOnMachine(mounts []*vmconfigs.Mount, vmType define.VMType, p
for _, mount := range mounts {
mountSource := filepath.Clean(mount.Source)
logrus.Debugf("looking if %q is in mount %q (with target %q)", pathABS, mountSource, mount.Target)
relPath, err := filepath.Rel(mountSource, pathABS)
if err != nil {
logrus.Debugf("Failed to get relative path: %v", err)
continue
} else {
logrus.Debugf("Yes, filepath.Rel returned %q", relPath)
}
// If relPath starts with ".." or is absolute, pathABS is not under mountSource
if relPath == "." || (!strings.HasPrefix(relPath, "..") && !filepath.IsAbs(relPath)) {
target := filepath.Join(mount.Target, relPath)
converted_path, err := specgen.ConvertWinMountPath(target)
if err != nil {
logrus.Debugf("Failed to convert Windows mount path: %v", err)
return nil, false
}
// Target is a Linux path, we should use path.Join rather than filepath.Join
converted_path := path.Join(mount.Target, filepath.ToSlash(relPath))
logrus.Debugf("Converted client path: %q", converted_path)
return &LocalAPIMap{
ClientPath: pathABS,
@ -134,7 +134,7 @@ func isPathAvailableOnMachine(mounts []*vmconfigs.Mount, vmType define.VMType, p
// on any currently running machine. It combines machine inspection and path checking.
func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap, bool) {
if err := fileutils.Exists(path); errors.Is(err, fs.ErrNotExist) {
logrus.Debugf("Path %s does not exist locally, skipping machine check", path)
logrus.Debugf("Path %q does not exist locally, skipping machine check", path)
return nil, false
}
@ -155,7 +155,7 @@ func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap,
return nil, false
}
return isPathAvailableOnMachine(mounts, vmType, path)
return IsPathAvailableOnMachine(mounts, vmType, path)
}
// CheckIfImageBuildPathsOnRunningMachine checks if the build context directory and all specified
@ -181,12 +181,12 @@ func CheckIfImageBuildPathsOnRunningMachine(ctx context.Context, containerFiles
// Context directory
if err := fileutils.Lexists(options.ContextDirectory); errors.Is(err, fs.ErrNotExist) {
logrus.Debugf("Path %s does not exist locally, skipping machine check", options.ContextDirectory)
logrus.Debugf("Path %q does not exist locally, skipping machine check", options.ContextDirectory)
return nil, options, false
}
mapping, found := isPathAvailableOnMachine(mounts, vmType, options.ContextDirectory)
mapping, found := IsPathAvailableOnMachine(mounts, vmType, options.ContextDirectory)
if !found {
logrus.Debugf("Path %s is not available on the running machine", options.ContextDirectory)
logrus.Debugf("Path %q is not available on the running machine", options.ContextDirectory)
return nil, options, false
}
options.ContextDirectory = mapping.RemotePath
@ -208,9 +208,9 @@ func CheckIfImageBuildPathsOnRunningMachine(ctx context.Context, containerFiles
continue
}
mapping, found := isPathAvailableOnMachine(mounts, vmType, containerFile)
mapping, found := IsPathAvailableOnMachine(mounts, vmType, containerFile)
if !found {
logrus.Debugf("Path %s is not available on the running machine", containerFile)
logrus.Debugf("Path %q is not available on the running machine", containerFile)
return nil, options, false
}
translatedContainerFiles = append(translatedContainerFiles, mapping.RemotePath)
@ -223,12 +223,12 @@ func CheckIfImageBuildPathsOnRunningMachine(ctx context.Context, containerFiles
continue
default:
if err := fileutils.Lexists(context.Value); errors.Is(err, fs.ErrNotExist) {
logrus.Debugf("Path %s does not exist locally, skipping machine check", context.Value)
logrus.Debugf("Path %q does not exist locally, skipping machine check", context.Value)
return nil, options, false
}
mapping, found := isPathAvailableOnMachine(mounts, vmType, context.Value)
mapping, found := IsPathAvailableOnMachine(mounts, vmType, context.Value)
if !found {
logrus.Debugf("Path %s is not available on the running machine", context.Value)
logrus.Debugf("Path %q is not available on the running machine", context.Value)
return nil, options, false
}
context.Value = mapping.RemotePath

View file

@ -0,0 +1,157 @@
//go:build linux || darwin || freebsd
package localapi
import (
"testing"
"github.com/containers/podman/v6/pkg/machine/define"
"github.com/containers/podman/v6/pkg/machine/vmconfigs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsPathAvailableOnMachine_Unix(t *testing.T) {
tests := []struct {
name string
mounts []*vmconfigs.Mount
vmType define.VMType
localPath string
wantRemotePath string
wantFound bool
}{
{
name: "QEMU - Path within mount",
mounts: []*vmconfigs.Mount{
{
Source: "/home/test",
Target: "/home/user",
},
},
vmType: define.QemuVirt,
localPath: "/home/test/project/file.txt",
wantRemotePath: "/home/user/project/file.txt",
wantFound: true,
},
{
name: "QEMU - Path exactly at mount point",
mounts: []*vmconfigs.Mount{
{
Source: "/home/test",
Target: "/mnt/host",
},
},
vmType: define.QemuVirt,
localPath: "/home/test",
wantRemotePath: "/mnt/host",
wantFound: true,
},
{
name: "QEMU - Path not in any mount",
mounts: []*vmconfigs.Mount{
{
Source: "/home/test",
Target: "/mnt/test",
},
},
vmType: define.QemuVirt,
localPath: "/home/another/file.txt",
wantFound: false,
},
{
name: "QEMU - Multiple mounts, path in second",
mounts: []*vmconfigs.Mount{
{
Source: "/home/first",
Target: "/mnt/first",
},
{
Source: "/home/second",
Target: "/mnt/second",
},
},
vmType: define.QemuVirt,
localPath: "/home/second/data",
wantRemotePath: "/mnt/second/data",
wantFound: true,
},
{
name: "QEMU - Nested subdirectory in mount",
mounts: []*vmconfigs.Mount{
{
Source: "/home/test",
Target: "/home/user",
},
},
vmType: define.QemuVirt,
localPath: "/home/test/deep/nested/path/file.go",
wantRemotePath: "/home/user/deep/nested/path/file.go",
wantFound: true,
},
{
name: "QEMU - Empty mounts list",
mounts: []*vmconfigs.Mount{},
vmType: define.QemuVirt,
localPath: "/home/test/file.txt",
wantFound: false,
},
{
name: "LibKrun - Path within mount",
mounts: []*vmconfigs.Mount{
{
Source: "/Users/developer/projects",
Target: "/mnt/projects",
},
},
vmType: define.LibKrun,
localPath: "/Users/developer/projects/myapp/src/main.go",
wantRemotePath: "/mnt/projects/myapp/src/main.go",
wantFound: true,
},
{
name: "AppleHV - Path within mount",
mounts: []*vmconfigs.Mount{
{
Source: "/Users/developer",
Target: "/home/developer",
},
},
vmType: define.AppleHvVirt,
localPath: "/Users/developer/Documents/code",
wantRemotePath: "/home/developer/Documents/code",
wantFound: true,
},
{
name: "Multiple mounts - first match wins",
mounts: []*vmconfigs.Mount{
{
Source: "/home",
Target: "/mnt/home",
},
{
Source: "/home/user",
Target: "/mnt/user",
},
},
vmType: define.QemuVirt,
localPath: "/home/user/file.txt",
wantRemotePath: "/mnt/home/user/file.txt",
wantFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, found := IsPathAvailableOnMachine(tt.mounts, tt.vmType, tt.localPath)
assert.Equal(t, tt.wantFound, found, "found flag mismatch")
if tt.wantFound {
require.NotNil(t, result, "result should not be nil when found is true")
assert.Equal(t, tt.wantRemotePath, result.RemotePath, "RemotePath mismatch")
} else {
assert.Nil(t, result, "result should be nil when found is false")
}
})
}
}

View file

@ -0,0 +1,168 @@
//go:build windows
package localapi
import (
"testing"
"github.com/containers/podman/v6/pkg/machine/define"
"github.com/containers/podman/v6/pkg/machine/vmconfigs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsPathAvailableOnMachine_Windows(t *testing.T) {
tests := []struct {
name string
mounts []*vmconfigs.Mount
vmType define.VMType
localPath string
wantRemotePath string
wantFound bool
}{
{
name: "WSL - Windows C drive path",
mounts: nil, // WSL doesn't use mounts
vmType: define.WSLVirt,
localPath: "C:\\Users\\test\\project",
wantRemotePath: "/mnt/c/Users/test/project",
wantFound: true,
},
{
name: "WSL - Windows D drive path",
mounts: nil,
vmType: define.WSLVirt,
localPath: "D:\\data\\workspace",
wantRemotePath: "/mnt/d/data/workspace",
wantFound: true,
},
{
name: "HyperV - Path within mount",
mounts: []*vmconfigs.Mount{
{
Source: "C:\\Users\\test",
Target: "/home/user",
},
},
vmType: define.HyperVVirt,
localPath: "C:\\Users\\test\\project\\file.txt",
wantRemotePath: "/home/user/project/file.txt",
wantFound: true,
},
{
name: "HyperV - D drive path",
mounts: []*vmconfigs.Mount{
{
Source: "D:\\Users\\test",
Target: "/home/user",
},
},
vmType: define.HyperVVirt,
localPath: "D:\\Users\\test\\project\\file.txt",
wantRemotePath: "/home/user/project/file.txt",
wantFound: true,
},
{
name: "HyperV - Path exactly at mount point",
mounts: []*vmconfigs.Mount{
{
Source: "C:\\Users\\test",
Target: "/mnt/host",
},
},
vmType: define.HyperVVirt,
localPath: "C:\\Users\\test",
wantRemotePath: "/mnt/host",
wantFound: true,
},
{
name: "HyperV - Path not in any mount",
mounts: []*vmconfigs.Mount{
{
Source: "C:\\Users\\test",
Target: "/mnt/test",
},
},
vmType: define.HyperVVirt,
localPath: "C:\\Users\\another\\file.txt",
wantFound: false,
},
{
name: "HyperV - Path in different drive",
mounts: []*vmconfigs.Mount{
{
Source: "C:\\Users\\test",
Target: "/mnt/test",
},
},
vmType: define.HyperVVirt,
localPath: "D:\\Users\\test\\file.txt",
wantFound: false,
},
{
name: "HyperV - Multiple mounts, path in second",
mounts: []*vmconfigs.Mount{
{
Source: "C:\\Users\\first",
Target: "/mnt/first",
},
{
Source: "C:\\Users\\second",
Target: "/mnt/second",
},
},
vmType: define.HyperVVirt,
localPath: "C:\\Users\\second\\data",
wantRemotePath: "/mnt/second/data",
wantFound: true,
},
{
name: "HyperV - Nested subdirectory in mount",
mounts: []*vmconfigs.Mount{
{
Source: "C:\\Users\\test",
Target: "/home/user",
},
},
vmType: define.HyperVVirt,
localPath: "C:\\Users\\test\\deep\\nested\\path\\file.go",
wantRemotePath: "/home/user/deep/nested/path/file.go",
wantFound: true,
},
{
name: "HyperV - Empty mounts list",
mounts: []*vmconfigs.Mount{},
vmType: define.HyperVVirt,
localPath: "C:\\Users\\test\\file.txt",
wantFound: false,
},
{
name: "HyperV - Path with mixed case drive letter",
mounts: []*vmconfigs.Mount{
{
Source: "C:\\Projects",
Target: "/home/projects",
},
},
vmType: define.HyperVVirt,
localPath: "c:\\Projects\\myapp",
wantRemotePath: "/home/projects/myapp",
wantFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, found := IsPathAvailableOnMachine(tt.mounts, tt.vmType, tt.localPath)
assert.Equal(t, tt.wantFound, found, "found flag mismatch")
if tt.wantFound {
require.NotNil(t, result, "result should not be nil when found is true")
assert.Equal(t, tt.wantRemotePath, result.RemotePath, "RemotePath mismatch")
} else {
assert.Nil(t, result, "result should be nil when found is false")
}
})
}
}

View file

@ -0,0 +1,165 @@
package certificates
import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"github.com/containers/podman/v6/internal/localapi"
"github.com/containers/podman/v6/pkg/machine"
"github.com/containers/podman/v6/pkg/machine/define"
"github.com/containers/podman/v6/pkg/machine/vmconfigs"
"github.com/sirupsen/logrus"
)
const (
// CertificatesFileName is the name of the PEM file containing the host
// trusted CA certificates
CertificatesFileName = "host-ca-certs.pem"
// GuestAnchorsPath is the Fedora folder where the command `update-ca-trust`
// looks for users-defined trusted certificates
GuestAnchorsPath = "/etc/pki/ca-trust/source/anchors"
// UpdateCATrustCommand is the Fedora command to update the system store
UpdateCATrustCommand = "update-ca-trust"
)
// ImportNativeCertificates imports the host's trusted CA certificates into the
// guest machine OS trust store. To do that it:
// - extract all certificates from the host trust store
// - export all the certificates in a single file in the machine data folder
// - check if the file is already mounted in the guest or transfer via SCP
// - update the guest trust store to include the certificates
func ImportNativeCertificates(mc *vmconfigs.MachineConfig, vmType define.VMType) error {
logrus.Debugf("Importing the host CA certificates into machine %q", mc.Name)
// Extract certificates from the host system store
certs := deduplicateCertificates(extractHostCertificates())
if len(certs) == 0 {
logrus.Debugf("No native CA certificates found to import")
return nil
}
logrus.Debugf("Extracted %d host certificates from the system store", len(certs))
// Save the certificates to a PEM file in the machine data folder
certFilePath, err := saveCertificatesToFile(mc, certs)
if err != nil {
return fmt.Errorf("failed to create certs file in machine data folder: %s", err)
}
logrus.Debugf("Saved the certificates to file %q", certFilePath)
// Copy or transfer via SCP the file with the certificates to the anchors
// folder in the guest
err = copyOrTransferFileToGuestAnchorsFolder(mc, vmType, certFilePath)
if err != nil {
return fmt.Errorf("failed to transfer or copy the certs file in the guest anchors folder: %s", err)
}
// Update the CA trust list
if err := runUpdateCATrustInGuest(mc); err != nil {
return fmt.Errorf("failed to update CA trust in guest: %w", err)
}
logrus.Debugf("Successfully imported the host trusted certificates into machine %q", mc.Name)
return nil
}
// saveCertificatesToFile saves the certificates to a PEM file in the machine data directory.
func saveCertificatesToFile(mc *vmconfigs.MachineConfig, certs []*x509.Certificate) (string, error) {
dataDir, err := mc.DataDir()
if err != nil {
return "", fmt.Errorf("failed to get machine data directory: %w", err)
}
// Create the directory if it doesn't exist
certFileDirPath := filepath.Join(dataDir.GetPath(), mc.Name)
if err = os.MkdirAll(certFileDirPath, 0o755); err != nil {
return "", fmt.Errorf("failed to create certificate directory: %w", err)
}
// Save certificates to PEM file in the directory
certFilePath := filepath.Join(certFileDirPath, CertificatesFileName)
if err := saveCertificatesToPEM(certs, certFilePath); err != nil {
return "", fmt.Errorf("failed to save certificates to PEM file: %w", err)
}
return certFilePath, nil
}
// copyOrTransferFileToGuestAnchorsFolder copies or transfers hostFilePath to
// the guest anchor folder, depending on whether the file is already mounted in
// the guest or not.
func copyOrTransferFileToGuestAnchorsFolder(mc *vmconfigs.MachineConfig, vmType define.VMType, hostFilePath string) error {
// Look for the file in the machine mounts
mounts := mc.Mounts
if localMap, ok := localapi.IsPathAvailableOnMachine(mounts, vmType, hostFilePath); ok {
// Copy the mounted file to the guest anchors folder
remotePath := localMap.RemotePath
logrus.Debugf("The certificates file is already mounted in the guest (%q), copy it to the anchors folder", remotePath)
return copyFileToGuestAnchorsFolder(mc, remotePath)
}
// Transfer the certificate file to the guest OS
logrus.Debugf("The certificates file isn't mounted in the guest, transfer it via SCP.")
return machine.LocalhostSSHCopy(
"root", // need root to copy to /etc/pki/ca-trust/source/anchors/
mc.SSH.IdentityPath,
mc.SSH.Port,
hostFilePath,
GuestAnchorsPath,
false,
true)
}
// copyFileToGuestAnchorsFolder runs `sudo cp` in ghe guest to copy guestFilePath
// to /etc/pki/ca-trust/source/anchors/
func copyFileToGuestAnchorsFolder(mc *vmconfigs.MachineConfig, guestFilePath string) error {
logrus.Debugf("Running %q in guest", UpdateCATrustCommand)
return machine.LocalhostSSHSilent(
mc.SSH.RemoteUsername,
mc.SSH.IdentityPath,
mc.Name,
mc.SSH.Port,
[]string{"sudo", "cp", guestFilePath, GuestAnchorsPath},
)
}
// saveCertificatesToPEM exports the certificates in certs to a PEM file
func saveCertificatesToPEM(certs []*x509.Certificate, certsFilePath string) error {
certsFile, err := os.Create(certsFilePath)
if err != nil {
return err
}
defer certsFile.Close()
for _, cert := range certs {
block := &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
if err := pem.Encode(certsFile, block); err != nil {
return err
}
}
return nil
}
// runUpdateCATrustInGuest runs the update-ca-trust command in the guest OS
func runUpdateCATrustInGuest(mc *vmconfigs.MachineConfig) error {
logrus.Debugf("Running %q in guest", UpdateCATrustCommand)
return machine.LocalhostSSHSilent(
mc.SSH.RemoteUsername,
mc.SSH.IdentityPath,
mc.Name,
mc.SSH.Port,
[]string{"sudo", UpdateCATrustCommand},
)
}
// deduplicateCertificates removes duplicate certificates from a slice of certificates.
func deduplicateCertificates(certs []*x509.Certificate) []*x509.Certificate {
seen := make(map[string]bool)
var unique []*x509.Certificate
for _, cert := range certs {
if cert == nil {
continue
}
if seen[string(cert.Signature)] {
continue
}
seen[string(cert.Signature)] = true
unique = append(unique, cert)
}
return unique
}

View file

@ -0,0 +1,13 @@
package certificates
import (
"crypto/x509"
"github.com/sirupsen/logrus"
)
// extractHostCertificates extracts trusted CA certificates from the macOS system keychain
func extractHostCertificates() []*x509.Certificate {
logrus.Warn("extracting trusted CA certificates on macOS is not implemented yet")
return nil
}

View file

@ -0,0 +1,13 @@
package certificates
import (
"crypto/x509"
"github.com/sirupsen/logrus"
)
// extractHostCertificates extracts trusted CA certificates from the FreeBSD system keychain
func extractHostCertificates() []*x509.Certificate {
logrus.Warn("extracting trusted CA certificates on FreeBSD is not implemented yet")
return nil
}

View file

@ -0,0 +1,20 @@
package certificates
import (
"crypto/x509"
"github.com/sirupsen/logrus"
)
// extractHostCertificates retrieved Linux trusted CA certificates from the OS
// trust stores.
func extractHostCertificates() []*x509.Certificate {
// On Linux, system CA certificates are typically stored in:
// - /etc/ssl/certs/ca-certificates.crt (Debian/Ubuntu)
// - /etc/pki/tls/certs/ca-bundle.crt (RHEL/Fedora/CentOS)
// - /etc/ssl/ca-bundle.pem (OpenSUSE)
// - /etc/pki/tls/cacert.pem (OpenELEC)
// - /etc/ssl/cert.pem (Alpine Linux)
logrus.Warn("extracting trusted CA certificates on Linux is not implemented yet")
return nil
}

View file

@ -0,0 +1,26 @@
package certificates
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func verifyCertificateFile(_ string) error {
return nil
}
func TestExtractAndSaveCertificates(t *testing.T) {
certs := extractHostCertificates()
// On Linux extractHostCertificates is not implemented
// therefore `certs` is expected to be empty.
assert.Empty(t, certs)
filePath := filepath.Join(t.TempDir(), "cert.pem")
err := saveCertificatesToPEM(certs, filePath)
assert.NoError(t, err)
err = verifyCertificateFile(filePath)
assert.NoError(t, err)
}

View file

@ -0,0 +1,68 @@
package certificates
import (
"crypto/x509"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDeduplicateCertificates(t *testing.T) {
certA := &x509.Certificate{Signature: []byte("sig-a")}
certB := &x509.Certificate{Signature: []byte("sig-b")}
certC := &x509.Certificate{Signature: []byte("sig-c")}
tests := []struct {
name string
input []*x509.Certificate
expected []*x509.Certificate
}{
{
name: "nil slice",
input: nil,
expected: nil,
},
{
name: "empty slice",
input: []*x509.Certificate{},
expected: nil,
},
{
name: "no duplicates",
input: []*x509.Certificate{certA, certB, certC},
expected: []*x509.Certificate{certA, certB, certC},
},
{
name: "with duplicates",
input: []*x509.Certificate{certA, certB, certA, certC, certB},
expected: []*x509.Certificate{certA, certB, certC},
},
{
name: "all duplicates",
input: []*x509.Certificate{certA, certA, certA},
expected: []*x509.Certificate{certA},
},
{
name: "nil entries are skipped",
input: []*x509.Certificate{nil, certA, nil, certB},
expected: []*x509.Certificate{certA, certB},
},
{
name: "nil and duplicate entries",
input: []*x509.Certificate{nil, certA, certB, nil, certA},
expected: []*x509.Certificate{certA, certB},
},
{
name: "single certificate",
input: []*x509.Certificate{certA},
expected: []*x509.Certificate{certA},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := deduplicateCertificates(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}

View file

@ -0,0 +1,146 @@
package certificates
import (
"crypto/x509"
"errors"
"fmt"
"unsafe"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
var (
predefinedWinStores = []string{
"AAD Token Issuer",
"AuthRoot",
"CA",
"ClientAuthIssuer",
"Disallowed",
"eSIM Certification Authorities",
"FlightRoot",
"My",
"OemEsim",
"PasspointTrustedRoots",
"Remote Desktop",
"REQUEST",
"Root",
"SmartCardRoot",
"TestSignRoot",
"Trust",
"TrustedAppRoot",
"TrustedDevices",
"TrustedPeople",
"TrustedPublisher",
"Windows Live ID Token Issuer",
"WindowsServerUpdateServices",
"UserDS",
}
winStoresLocations = []uint{
windows.CERT_SYSTEM_STORE_CURRENT_USER,
windows.CERT_SYSTEM_STORE_LOCAL_MACHINE,
windows.CERT_SYSTEM_STORE_CURRENT_SERVICE,
windows.CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
}
winStoresLocationsNames = map[uint]string{
windows.CERT_SYSTEM_STORE_CURRENT_USER: "Current User",
windows.CERT_SYSTEM_STORE_LOCAL_MACHINE: "Local Machine",
windows.CERT_SYSTEM_STORE_CURRENT_SERVICE: "Current Service",
windows.CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE: "Local Machine Enterprise",
}
)
// extractHostCertificates extracts trusted CA certificates from the prederfined
// Windows certificate stores
func extractHostCertificates() []*x509.Certificate {
var certificates []*x509.Certificate
for _, location := range winStoresLocations {
for _, store := range predefinedWinStores {
certs := extractFromStore(location, store)
certificates = append(certificates, certs...)
}
}
return certificates
}
// extractFromStore extracts certificates from a specific Windows certificate store
func extractFromStore(storeLocation uint, storeName string) []*x509.Certificate {
storeHandle, err := getStoreHandle(storeLocation, storeName)
if err != nil {
logrus.Debugf("Failed open store %s in %s: %v",
storeName,
winStoresLocationsNames[storeLocation],
err)
return nil
}
defer func(h windows.Handle) {
if err := windows.CertCloseStore(h, 0); err != nil {
logrus.Debugf("Failed to close certificate store: %v", err)
}
}(*storeHandle)
var certs []*x509.Certificate
var ctx *windows.CertContext = nil
for {
ctx, err = windows.CertEnumCertificatesInStore(*storeHandle, ctx)
if err != nil {
if errors.Is(err, windows.Errno(windows.CRYPT_E_NOT_FOUND)) {
// The function reached the end of the store's list
break
}
logrus.Debugf("Failed to enumerate certificates in store %s in %s: %v",
storeName,
winStoresLocationsNames[storeLocation],
err)
return nil
}
if ctx == nil {
break
}
c := parseCertificate(ctx)
if c != nil {
certs = append(certs, c)
}
}
logrus.Debugf("Extracted %d certificates from %s store in %s", len(certs), storeName, winStoresLocationsNames[storeLocation])
return certs
}
// getStoreHandle returns a handle to the certificate store with the given name.
func getStoreHandle(storeLocation uint, storeName string) (*windows.Handle, error) {
storeNameUTF16, err := windows.UTF16PtrFromString(storeName)
if err != nil {
return nil, fmt.Errorf("failed to convert store name to UTF16: %w", err)
}
// storeHandle, err := windows.CertOpenSystemStore(0, storeNameUTF16)
storeHandle, err := windows.CertOpenStore(
windows.CERT_STORE_PROV_SYSTEM,
0,
0,
uint32(storeLocation)|uint32(windows.CERT_STORE_READONLY_FLAG),
uintptr(unsafe.Pointer(storeNameUTF16)))
if err != nil {
return nil, fmt.Errorf("failed to open certificate store: %w", err)
}
return &storeHandle, nil
}
// parseCertificate parses an x509 certificate from a win32 API CertContext
// object. If the certificate cannot be parsed, nil is returned.
func parseCertificate(certCtx *windows.CertContext) *x509.Certificate {
certSize := certCtx.Length
certBytesOrig := unsafe.Slice(certCtx.EncodedCert, certSize)
// Make a copy because certCtx will be overridden
certBytes := make([]byte, certSize)
copy(certBytes, certBytesOrig)
cert, err := x509.ParseCertificate(certBytes) // TODO: if it's not an x509 cert we may want to use a different parising function
if err != nil {
certSubject := unsafe.Slice(certCtx.CertInfo.Subject.Data, certCtx.CertInfo.Subject.Size)
logrus.Debugf("Failed to parse certificate (subject: %s): %v", certSubject, err)
return nil
}
return cert
}

View file

@ -0,0 +1,41 @@
package certificates
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func verifyCertificateFile(filePath string) error {
cmd := exec.Command(
`powershell`,
"-NoProfile",
"-NonInteractive",
"-Command",
"(Get-PfxCertificate -FilePath '"+filePath+"').Verify()",
)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("output: %s, error: %s", out, err)
}
if !strings.Contains(string(out), "True") {
return fmt.Errorf("certificate verification failed")
}
return nil
}
func TestExtractAndSaveCertificates(t *testing.T) {
certs := extractHostCertificates()
assert.NotEmpty(t, certs)
filePath := filepath.Join(t.TempDir(), "cert.pem")
err := saveCertificatesToPEM(certs, filePath)
assert.NoError(t, err)
err = verifyCertificateFile(filePath)
assert.NoError(t, err)
}

View file

@ -26,4 +26,5 @@ type InitOptions struct {
UserModeNetworking *bool // nil = use backend/system default, false = disable, true = enable
USBs []string
SkipTlsVerify types.OptionalBool
ImportNativeCA bool
}

View file

@ -9,4 +9,5 @@ type SetOptions struct {
Rootful *bool
UserModeNetworking *bool
USBs *[]string
ImportNativeCA *bool
}

View file

@ -27,6 +27,7 @@ type initMachine struct {
updateConnection *bool
userModeNetworking bool
tlsVerify *bool
importNativeCA bool
cmd []string
}
@ -83,6 +84,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string {
if i.updateConnection != nil {
cmd = append(cmd, fmt.Sprintf("--update-connection=%s", strconv.FormatBool(*i.updateConnection)))
}
if i.importNativeCA {
cmd = append(cmd, "--import-native-ca")
}
name := m.name
cmd = append(cmd, name)
@ -190,3 +194,8 @@ func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unu
i.userModeNetworking = r
return i
}
func (i *initMachine) withImportNativeCA(r bool) *initMachine {
i.importNativeCA = r
return i
}

View file

@ -10,6 +10,7 @@ type setMachine struct {
memory *uint
rootful bool
userModeNetworking bool
importNativeCA bool
cmd []string
}
@ -31,6 +32,9 @@ func (i *setMachine) buildCmd(m *machineTestBuilder) []string {
if i.userModeNetworking {
cmd = append(cmd, "--user-mode-networking")
}
if i.importNativeCA {
cmd = append(cmd, "--import-native-ca")
}
cmd = append(cmd, m.name)
i.cmd = cmd
return cmd

View file

@ -310,6 +310,53 @@ var _ = Describe("podman machine start", func() {
Expect(err).ToNot(HaveOccurred())
Expect(listings.IsDefault(machineName2)).To(BeTrue())
})
It("machine init --now with --import-native-ca with mounted data folder", func() {
// Windows only
skipIfVmtype(define.QemuVirt, "Importing native certificates isn't supported on Linux")
skipIfVmtype(define.AppleHvVirt, "Importing native certificates isn't supported on macOS")
skipIfVmtype(define.LibKrun, "Importing native certificates isn't supported on macOS")
// Create a new machine
i := initMachine{}
initCommand := i.withImage(mb.imagePath).withImportNativeCA(true).withNow()
m := randomString()
initSession, err := mb.setName(m).setCmd(initCommand).run()
Expect(err).ToNot(HaveOccurred())
Expect(initSession).To(Exit(0))
// Verify that the file in the guest exist
certFilePath := "/etc/pki/ca-trust/source/anchors"
certFileName := "host-ca-certs.pem"
sshMachine := sshMachine{}
sshCertFile, err := mb.setName(m).setCmd(sshMachine.withSSHCommand([]string{"ls", certFilePath})).run()
Expect(err).ToNot(HaveOccurred())
Expect(sshCertFile).To(Exit(0))
Expect(sshCertFile.outputToString()).To(Equal(certFileName))
})
It("machine init --now with --import-native-ca with SCP file transfer", func() {
skipIfVmtype(define.QemuVirt, "Importing native certificates isn't supported on Linux")
skipIfVmtype(define.AppleHvVirt, "Importing native certificates isn't supported on macOS")
skipIfVmtype(define.LibKrun, "Importing native certificates isn't supported on macOS")
skipIfVmtype(define.WSLVirt, "WSL doesn't allow handling volumes (the machine data folder is always mounted")
// Create a new machine
i := initMachine{}
initCommand := i.withImage(mb.imagePath).withImportNativeCA(true).withNow()
// Don't mount any volume to force the transfer the certificates file via SCP
initCommand = initCommand.withVolume("")
m := randomString()
initSession, err := mb.setName(m).setCmd(initCommand).run()
Expect(err).ToNot(HaveOccurred())
Expect(initSession).To(Exit(0))
certFilePath := "/etc/pki/ca-trust/source/anchors"
certFileName := "host-ca-certs.pem"
sshMachine := sshMachine{}
sshCertFile, err := mb.setName(m).setCmd(sshMachine.withSSHCommand([]string{"ls", certFilePath})).run()
Expect(err).ToNot(HaveOccurred())
Expect(sshCertFile).To(Exit(0))
Expect(sshCertFile.outputToString()).To(Equal(certFileName))
})
})
func mapToPort(uris []string) ([]string, error) {

View file

@ -16,6 +16,7 @@ import (
"github.com/containers/podman/v6/cmd/podman/registry"
"github.com/containers/podman/v6/pkg/machine"
"github.com/containers/podman/v6/pkg/machine/certificates"
"github.com/containers/podman/v6/pkg/machine/connection"
machineDefine "github.com/containers/podman/v6/pkg/machine/define"
"github.com/containers/podman/v6/pkg/machine/env"
@ -117,6 +118,7 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
}
mc.Version = vmconfigs.MachineConfigVersion
mc.ImportNativeCA = opts.ImportNativeCA
createOpts := machineDefine.CreateVMOpts{
Name: opts.Name,
@ -655,6 +657,17 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St
return err
}
// Import native CA certificates if enabled
if mc.ImportNativeCA {
if err := certificates.ImportNativeCertificates(mc, mp.VMType()); err != nil {
// Warn the user but continue the machine startup process
logrus.Warnf("Failed to import native CA certificates: %v", err)
fmt.Println("Warning: Failed to import host trusted CA certificates. The machine will start without them.")
} else if !opts.Quiet {
fmt.Println("The host trusted CA certificates have been imported successfully")
}
}
// mount the volumes to the VM
if err := mp.MountVolumesToVM(mc, opts.Quiet); err != nil {
return err
@ -731,6 +744,10 @@ func Set(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machineDefin
mc.Resources.DiskSize = *opts.DiskSize
}
if opts.ImportNativeCA != nil {
mc.ImportNativeCA = *opts.ImportNativeCA
}
if err := mp.SetProviderAttrs(mc, opts); err != nil {
return err
}

View file

@ -37,6 +37,30 @@ func LocalhostSSHWithStdin(username, identityPath, name string, sshPort int, inp
return localhostBuiltinSSH(username, identityPath, name, sshPort, inputArgs, true, stdin)
}
// LocalhostSSHCopy uses scp to copy files from/to a localhost machine using ssh.
func LocalhostSSHCopy(username, identityPath string, sshPort int, srcPath, destPath string, isSrcFromGuest, quiet bool) error {
var src, dest string
if isSrcFromGuest {
src = username + "@localhost:" + srcPath
dest = destPath
} else {
src = srcPath
dest = username + "@localhost:" + destPath
}
args := append(
LocalhostSSHArgs(), // Warning: This MUST NOT be generalized to allow communication over untrusted networks.
"-r",
"-i", identityPath,
"-P", strconv.Itoa(sshPort),
src, dest)
cmd := exec.Command("scp", args...)
if !quiet {
cmd.Stdout = os.Stdout
}
cmd.Stderr = os.Stderr
return cmd.Run()
}
func localhostBuiltinSSH(username, identityPath, name string, sshPort int, inputArgs []string, passOutput bool, stdin io.Reader) error {
config, err := createLocalhostConfig(username, identityPath) // WARNING: This MUST NOT be generalized to allow communication over untrusted networks.
if err != nil {

View file

@ -54,6 +54,8 @@ type MachineConfig struct {
Rosetta bool
Ansible *AnsibleConfig
ImportNativeCA bool
}
type VMProvider interface { //nolint:interfacebloat