diff --git a/bib/cmd/bootc-image-builder/export_test.go b/bib/cmd/bootc-image-builder/export_test.go index ae62449d2..9b0e6a2c5 100644 --- a/bib/cmd/bootc-image-builder/export_test.go +++ b/bib/cmd/bootc-image-builder/export_test.go @@ -20,11 +20,3 @@ func MockOsGetuid(new func() int) (restore func()) { osGetuid = saved } } - -func MockOsReadFile(new func(string) ([]byte, error)) (restore func()) { - saved := osReadFile - osReadFile = new - return func() { - osReadFile = saved - } -} diff --git a/bib/cmd/bootc-image-builder/main.go b/bib/cmd/bootc-image-builder/main.go index db1ee508d..c78e1da5a 100644 --- a/bib/cmd/bootc-image-builder/main.go +++ b/bib/cmd/bootc-image-builder/main.go @@ -111,20 +111,56 @@ func getContainerSize(imgref string) (uint64, error) { return size, nil } -func makeManifest(c *ManifestConfig, solver *dnfjson.Solver, cacheRoot string) (manifest.OSBuildManifest, map[string][]rpmmd.RepoConfig, error) { +func reposHaveOnlyHostMappedSecrets(repos []rpmmd.RepoConfig) error { + for _, repo := range repos { + if repo.SSLCACert != "" && !strings.Contains(repo.SSLCACert, "etc/rhsm-host/") { + return fmt.Errorf("unsupported cacert %q: not mapped from host", repo.SSLCACert) + } + if repo.SSLClientKey != "" && !strings.Contains(repo.SSLClientKey, "etc/pki/entitlement-host/") { + return fmt.Errorf("unsupported client key %q: not mapped from host", repo.SSLClientKey) + } + if repo.SSLClientCert != "" && !strings.Contains(repo.SSLClientCert, "etc/pki/entitlement-host/") { + return fmt.Errorf("unsupported client cert %q: not mapped from host", repo.SSLClientCert) + } + } + return nil +} + +func makeManifest(c *ManifestConfig, solver *dnfjson.Solver, cacheRoot string) (manifest.OSBuildManifest, error) { mani, err := Manifest(c) if err != nil { - return nil, nil, fmt.Errorf("cannot get manifest: %w", err) + return nil, fmt.Errorf("cannot get manifest: %w", err) } - // depsolve packages + // depsolve packages (only needed for ISO image types) depsolvedSets := make(map[string]dnfjson.DepsolveResult) depsolvedRepos := make(map[string][]rpmmd.RepoConfig) for name, pkgSet := range mani.GetPackageSetChains() { res, err := solver.Depsolve(pkgSet, 0) if err != nil { - return nil, nil, fmt.Errorf("cannot depsolve: %w", err) + return nil, fmt.Errorf("cannot depsolve: %w", err) } + + // We depsolve the container with an alterantive + // rootdir - this means the depsolver will assume any + // required secrets in the found rootdir repos must be + // provided via mtls. However in almost all cases in + // our containers this is unnecessary (and a headache) + // because podman maps our host entitlements into the + // container so we can build it with org.osbuild.rhsm + // + // So we double check here that we only have content + // mapped from the host via podman. Then we just + // switch from org.osbuild.mtls to org.osbuild.rhsm + if err := reposHaveOnlyHostMappedSecrets(res.Repos); err != nil { + return nil, err + } + // we can switch from mtls->rhsm as we valided above that + // only host mounted entitlements are used + for idx := range res.Packages { + res.Packages[idx].Secrets = strings.ReplaceAll(res.Packages[idx].Secrets, "org.osbuild.mtls", "org.osbuild.rhsm") + } + depsolvedSets[name] = *res depsolvedRepos[name] = res.Repos } @@ -146,11 +182,11 @@ func makeManifest(c *ManifestConfig, solver *dnfjson.Solver, cacheRoot string) ( } specs, err := resolver.Finish() if err != nil { - return nil, nil, fmt.Errorf("cannot resolve containers: %w", err) + return nil, fmt.Errorf("cannot resolve containers: %w", err) } for _, spec := range specs { if spec.Arch != c.Architecture { - return nil, nil, fmt.Errorf("image found is for unexpected architecture %q (expected %q), if that is intentional, please make sure --target-arch matches", spec.Arch, c.Architecture) + return nil, fmt.Errorf("image found is for unexpected architecture %q (expected %q), if that is intentional, please make sure --target-arch matches", spec.Arch, c.Architecture) } } containerSpecs[plName] = specs @@ -162,9 +198,9 @@ func makeManifest(c *ManifestConfig, solver *dnfjson.Solver, cacheRoot string) ( } mf, err := mani.Serialize(depsolvedSets, containerSpecs, nil, &opts) if err != nil { - return nil, nil, fmt.Errorf("[ERROR] manifest serialization failed: %s", err.Error()) + return nil, fmt.Errorf("[ERROR] manifest serialization failed: %s", err.Error()) } - return mf, depsolvedRepos, nil + return mf, nil } func saveManifest(ms manifest.OSBuildManifest, fpath string) (err error) { @@ -194,7 +230,7 @@ func saveManifest(ms manifest.OSBuildManifest, fpath string) (err error) { // // TODO: provide a podman progress reader to integrate the podman progress // into our progress. -func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.ProgressBar) ([]byte, *mTLSConfig, error) { +func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.ProgressBar) ([]byte, error) { cntArch := arch.Current() imgref := args[0] @@ -212,7 +248,7 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress if localStorage { fmt.Fprintf(os.Stderr, "WARNING: --local is now the default behavior, you can remove it from the command line\n") } else { - return nil, nil, fmt.Errorf(`--local=false is no longer supported, remove it and make sure to pull the container before running bib: + return nil, fmt.Errorf(`--local=false is no longer supported, remove it and make sure to pull the container before running bib: sudo podman pull %s`, imgref) } } @@ -220,7 +256,7 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress if targetArch != "" { target, err := arch.FromString(targetArch) if err != nil { - return nil, nil, err + return nil, err } if target != arch.Current() { // TODO: detect if binfmt_misc for target arch is @@ -230,7 +266,7 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress // binaries inside our bib container fmt.Fprintf(os.Stderr, "WARNING: target-arch is experimental and needs an installed 'qemu-user' package\n") if slices.Contains(imgTypes, "iso") { - return nil, nil, fmt.Errorf("cannot build iso for different target arches yet") + return nil, fmt.Errorf("cannot build iso for different target arches yet") } cntArch = target } @@ -238,33 +274,33 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress // TODO: add "target-variant", see https://github.com/osbuild/bootc-image-builder/pull/139/files#r1467591868 if err := setup.ValidateHasContainerStorageMounted(); err != nil { - return nil, nil, fmt.Errorf("could not access container storage, did you forget -v /var/lib/containers/storage:/var/lib/containers/storage? (%w)", err) + return nil, fmt.Errorf("could not access container storage, did you forget -v /var/lib/containers/storage:/var/lib/containers/storage? (%w)", err) } imageTypes, err := imagetypes.New(imgTypes...) if err != nil { - return nil, nil, fmt.Errorf("cannot detect build types %v: %w", imgTypes, err) + return nil, fmt.Errorf("cannot detect build types %v: %w", imgTypes, err) } config, err := blueprintload.LoadWithFallback(userConfigFile) if err != nil { - return nil, nil, fmt.Errorf("cannot read config: %w", err) + return nil, fmt.Errorf("cannot read config: %w", err) } pbar.SetPulseMsgf("Manifest generation step") pbar.Start() if err := setup.ValidateHasContainerTags(imgref); err != nil { - return nil, nil, err + return nil, err } cntSize, err := getContainerSize(imgref) if err != nil { - return nil, nil, fmt.Errorf("cannot get container size: %w", err) + return nil, fmt.Errorf("cannot get container size: %w", err) } container, err := podman_container.New(imgref) if err != nil { - return nil, nil, err + return nil, err } defer func() { if err := container.Stop(); err != nil { @@ -278,17 +314,17 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress } else { rootfsType, err = container.DefaultRootfsType() if err != nil { - return nil, nil, fmt.Errorf("cannot get rootfs type for container: %w", err) + return nil, fmt.Errorf("cannot get rootfs type for container: %w", err) } if rootfsType == "" { - return nil, nil, fmt.Errorf(`no default root filesystem type specified in container, please use "--rootfs" to set manually`) + return nil, fmt.Errorf(`no default root filesystem type specified in container, please use "--rootfs" to set manually`) } } // Gather some data from the containers distro sourceinfo, err := osinfo.Load(container.Root()) if err != nil { - return nil, nil, err + return nil, err } buildContainer := container @@ -305,14 +341,14 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress if buildImgref != "" { buildContainer, err = podman_container.New(buildImgref) if err != nil { - return nil, nil, err + return nil, err } startedBuildContainer = true // Gather some data from the containers distro buildSourceinfo, err = osinfo.Load(buildContainer.Root()) if err != nil { - return nil, nil, err + return nil, err } } else { buildImgref = imgref @@ -321,11 +357,11 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress // This is needed just for RHEL and RHSM in most cases, but let's run it every time in case // the image has some non-standard dnf plugins. if err := buildContainer.InitDNF(); err != nil { - return nil, nil, err + return nil, err } solver, err := buildContainer.NewContainerSolver(rpmCacheRoot, cntArch, sourceinfo) if err != nil { - return nil, nil, err + return nil, err } manifestConfig := &ManifestConfig{ @@ -342,17 +378,12 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress UseLibrepo: useLibrepo, } - manifest, repos, err := makeManifest(manifestConfig, solver, rpmCacheRoot) - if err != nil { - return nil, nil, err - } - - mTLS, err := extractTLSKeys(repos) + manifest, err := makeManifest(manifestConfig, solver, rpmCacheRoot) if err != nil { - return nil, nil, err + return nil, err } - return manifest, mTLS, nil + return manifest, nil } func cmdManifest(cmd *cobra.Command, args []string) error { @@ -363,7 +394,7 @@ func cmdManifest(cmd *cobra.Command, args []string) error { } defer pbar.Stop() - mf, _, err := manifestFromCobra(cmd, args, pbar) + mf, err := manifestFromCobra(cmd, args, pbar) if err != nil { return fmt.Errorf("cannot generate manifest: %w", err) } @@ -458,7 +489,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error { manifest_fname := fmt.Sprintf("manifest-%s.json", strings.Join(imgTypes, "-")) pbar.SetMessagef("Generating manifest %s", manifest_fname) - mf, mTLS, err := manifestFromCobra(cmd, args, pbar) + mf, err := manifestFromCobra(cmd, args, pbar) if err != nil { return fmt.Errorf("cannot build manifest: %w", err) } @@ -484,17 +515,6 @@ func cmdBuild(cmd *cobra.Command, args []string) error { osbuildEnv = []string{"OSBUILD_EXPORT_FORCE_NO_PRESERVE_OWNER=1"} } - if mTLS != nil { - envVars, cleanup, err := prepareOsbuildMTLSConfig(mTLS) - if err != nil { - return fmt.Errorf("failed to prepare osbuild TLS keys: %w", err) - } - - defer cleanup() - - osbuildEnv = append(osbuildEnv, envVars...) - } - if experimentalflags.Bool("debug-qemu-user") { osbuildEnv = append(osbuildEnv, "OBSBUILD_EXPERIMENAL=debug-qemu-user") } diff --git a/bib/cmd/bootc-image-builder/mtls.go b/bib/cmd/bootc-image-builder/mtls.go deleted file mode 100644 index ebe650771..000000000 --- a/bib/cmd/bootc-image-builder/mtls.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path" - - "github.com/osbuild/images/pkg/rpmmd" - "github.com/sirupsen/logrus" -) - -type mTLSConfig struct { - key []byte - cert []byte - ca []byte -} - -var osReadFile = os.ReadFile - -func extractTLSKeys(repoSets map[string][]rpmmd.RepoConfig) (*mTLSConfig, error) { - var keyPath, certPath, caPath string - for _, set := range repoSets { - for _, r := range set { - if r.SSLClientKey != "" { - if keyPath != "" && (keyPath != r.SSLClientKey || certPath != r.SSLClientCert || caPath != r.SSLCACert) { - return nil, fmt.Errorf("multiple TLS client keys found, this is currently unsupported") - } - - keyPath = r.SSLClientKey - certPath = r.SSLClientCert - caPath = r.SSLCACert - } - } - } - if keyPath == "" { - return nil, nil - } - - key, err := osReadFile(keyPath) - if err != nil { - return nil, fmt.Errorf("failed to read TLS client key from the container: %w", err) - } - - cert, err := osReadFile(certPath) - if err != nil { - return nil, fmt.Errorf("failed to read TLS client certificate from the container: %w", err) - } - - ca, err := osReadFile(caPath) - if err != nil { - return nil, fmt.Errorf("failed to read TLS CA certificate from the container: %w", err) - } - - return &mTLSConfig{ - key: key, - cert: cert, - ca: ca, - }, nil -} - -// prepareOsbuildMTLSConfig writes the given mTLS keys to the given directory and returns the environment variables -// to set for osbuild -func prepareOsbuildMTLSConfig(mTLS *mTLSConfig) (envVars []string, cleanup func(), err error) { - dir, err := os.MkdirTemp("", "osbuild-mtls") - if err != nil { - return nil, nil, fmt.Errorf("failed to create temporary directory for osbuild mTLS keys: %w", err) - } - - cleanupFn := func() { - if err := os.RemoveAll(dir); err != nil { - logrus.Warnf("prepareOsbuildMTLSConfig: failed to remove temporary directory %s: %v", dir, err) - } - } - - defer func() { - if err != nil { - cleanupFn() - } - }() - - keyPath := path.Join(dir, "client.key") - certPath := path.Join(dir, "client.crt") - caPath := path.Join(dir, "ca.crt") - if err := os.WriteFile(keyPath, mTLS.key, 0600); err != nil { - return nil, nil, fmt.Errorf("failed to write TLS client key for osbuild: %w", err) - } - if err := os.WriteFile(certPath, mTLS.cert, 0600); err != nil { - return nil, nil, fmt.Errorf("failed to write TLS client certificate for osbuild: %w", err) - } - if err := os.WriteFile(caPath, mTLS.ca, 0644); err != nil { - return nil, nil, fmt.Errorf("failed to write TLS CA certificate for osbuild: %w", err) - } - - return []string{ - fmt.Sprintf("OSBUILD_SOURCES_CURL_SSL_CLIENT_KEY=%s", keyPath), - fmt.Sprintf("OSBUILD_SOURCES_CURL_SSL_CLIENT_CERT=%s", certPath), - fmt.Sprintf("OSBUILD_SOURCES_CURL_SSL_CA_CERT=%s", caPath), - }, cleanupFn, nil -} diff --git a/bib/cmd/bootc-image-builder/mtls_test.go b/bib/cmd/bootc-image-builder/mtls_test.go deleted file mode 100644 index 03fac2372..000000000 --- a/bib/cmd/bootc-image-builder/mtls_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path" - "strings" - "testing" - - "github.com/osbuild/images/pkg/rpmmd" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type fakeFileReader struct { - readPaths []string -} - -func (f *fakeFileReader) ReadFile(path string) ([]byte, error) { - f.readPaths = append(f.readPaths, path) - return []byte(fmt.Sprintf("content of %s", path)), nil -} - -func TestExtractTLSKeysHappy(t *testing.T) { - repos := map[string][]rpmmd.RepoConfig{ - "kingfisher": { - { - SSLCACert: "/ca", - SSLClientCert: "/cert", - SSLClientKey: "/key", - }, - }, - } - - fakeReader := &fakeFileReader{} - restore := MockOsReadFile(fakeReader.ReadFile) - defer restore() - - mTLS, err := extractTLSKeys(repos) - require.NoError(t, err) - require.Equal(t, mTLS.ca, []byte("content of /ca")) - require.Equal(t, mTLS.cert, []byte("content of /cert")) - require.Equal(t, mTLS.key, []byte("content of /key")) - require.Len(t, fakeReader.readPaths, 3) - - // also check that adding another repo with same keys still succeeds - repos["toucan"] = repos["kingfisher"] - _, err = extractTLSKeys(repos) - require.NoError(t, err) - require.Len(t, fakeReader.readPaths, 6) -} - -func TestExtractTLSKeysUnhappy(t *testing.T) { - repos := map[string][]rpmmd.RepoConfig{ - "kingfisher": { - { - SSLCACert: "/ca", - SSLClientCert: "/cert", - SSLClientKey: "/key", - }, - }, - - "vulture": { - { - SSLCACert: "/different-ca", - SSLClientCert: "/different-cert", - SSLClientKey: "/different-key", - }, - }, - } - - fakeReader := &fakeFileReader{} - restore := MockOsReadFile(fakeReader.ReadFile) - defer restore() - - _, err := extractTLSKeys(repos) - require.EqualError(t, err, "multiple TLS client keys found, this is currently unsupported") -} - -func TestPrepareOsbuildMTLSConfig(t *testing.T) { - mTLS := mTLSConfig{ - key: []byte("key"), - cert: []byte("cert"), - ca: []byte("ca"), - } - - envVars, cleanup, err := prepareOsbuildMTLSConfig(&mTLS) - require.NoError(t, err) - t.Cleanup(cleanup) - require.Len(t, envVars, 3) - - validateVar := func(envVar, content string) { - for _, e := range envVars { - if strings.HasPrefix(e, envVar+"=") { - envVarParts := strings.SplitN(e, "=", 2) - assert.Len(t, envVarParts, 2) - - actualContent, err := os.ReadFile(envVarParts[1]) - assert.NoError(t, err) - - assert.Equal(t, content, string(actualContent)) - return - } - - } - - assert.Fail(t, "environment variable not found", "%s", envVar) - } - - validateVar("OSBUILD_SOURCES_CURL_SSL_CLIENT_KEY", "key") - validateVar("OSBUILD_SOURCES_CURL_SSL_CLIENT_CERT", "cert") - validateVar("OSBUILD_SOURCES_CURL_SSL_CA_CERT", "ca") -} - -func TestPrepareOsbuildMTLSConfigCleanup(t *testing.T) { - mTLS := mTLSConfig{ - key: []byte("key"), - cert: []byte("cert"), - ca: []byte("ca"), - } - - envVars, cleanup, err := prepareOsbuildMTLSConfig(&mTLS) - require.NoError(t, err) - - // quick and dirty way to get the temporary directory - filepath := strings.SplitN(envVars[0], "=", 2)[1] - tmpdir := path.Dir(filepath) - - // check that the cleanup works - assert.DirExists(t, tmpdir) - cleanup() - assert.NoDirExists(t, tmpdir) -}