talos/pkg/imager/profile/input.go
Andrey Smirnov 9be7bc0250
fix: don't set xattrs while decompressing extensions
When decompressing extensions, we might not be able to set xattrs (e.g.
running rootless), so instead of setting xattrs, save them in memory and
push to mksquashfs as pseudo definitions.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
(cherry picked from commit d697f5538a7a624a1ac7bafdfebc67dd9418c434)
2026-04-15 18:38:38 +04:00

375 lines
11 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package profile
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/github"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/google"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/siderolabs/gen/value"
"golang.org/x/sync/errgroup"
"github.com/siderolabs/talos/internal/pkg/measure"
"github.com/siderolabs/talos/internal/pkg/secureboot/pesign"
"github.com/siderolabs/talos/pkg/archiver"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/azure"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/file"
"github.com/siderolabs/talos/pkg/images"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
)
const (
arm64 = "arm64"
amd64 = "amd64"
)
// Input describes inputs for image generation.
type Input struct {
// Kernel is a vmlinuz file.
Kernel FileAsset `yaml:"kernel"`
// Initramfs is a initramfs file (without system extensions).
Initramfs FileAsset `yaml:"initramfs"`
// SDStub is a sd-stub file (only for SecureBoot).
SDStub FileAsset `yaml:"sdStub,omitempty"`
// SDBoot is a sd-boot file (only for SecureBoot).
SDBoot FileAsset `yaml:"sdBoot,omitempty"`
// Base installer image to mutate.
BaseInstaller ContainerAsset `yaml:"baseInstaller,omitempty"`
// ImageCache is an image cache to inject into the asset.
ImageCache ContainerAsset `yaml:"imageCache,omitempty"`
// OverlayInstaller is an overlay image to inject into the installer.
//
// OverlayInstaller architecture should match the output installer architecture.
OverlayInstaller ContainerAsset `yaml:"overlayInstaller,omitempty"`
// SecureBoot is a section with secureboot keys, only for SecureBoot enabled builds.
SecureBoot *SecureBootAssets `yaml:"secureboot,omitempty"`
// SystemExtensions is a list of system extensions to install.
SystemExtensions []ContainerAsset `yaml:"systemExtensions,omitempty"`
}
// FileAsset describes a file asset.
type FileAsset struct {
// Path to the file.
Path string `yaml:"path"`
}
// ContainerAsset describes a container asset.
type ContainerAsset struct {
// ImageRef is a reference to the container image.
ImageRef string `yaml:"imageRef"`
// ForceInsecure forces insecure registry communication.
ForceInsecure bool `yaml:"forceInsecure,omitempty"`
// TarballPath is a path to the .tar format container image contents.
//
// If TarballPath is set, ImageRef is ignored.
TarballPath string `yaml:"tarballPath,omitempty"`
// OCIPath is a path to the OCI format container image contents.
//
// If OCIPath is set, ImageRef is ignored.
OCIPath string `yaml:"ociPath,omitempty"`
}
// SecureBootAssets describes secureboot assets.
type SecureBootAssets struct {
// SecureBoot signing key & cert.
SecureBootSigner SigningKeyAndCertificate `yaml:"secureBootSigner"`
// PCR signing key.
PCRSigner SigningKey `yaml:"pcrSigner"`
// Optional, auto-enrollment paths.
PlatformKeyPath string `yaml:"platformKeyPath,omitempty"`
KeyExchangeKeyPath string `yaml:"keyExchangeKeyPath,omitempty"`
SignatureKeyPath string `yaml:"signatureKeyPath,omitempty"`
// Optional, auto-enrollment include well-known UEFI (Microsoft) certs.
IncludeWellKnownCerts bool `yaml:"includeWellKnownCerts,omitempty"`
}
// SigningKeyAndCertificate describes a signing key & certificate.
type SigningKeyAndCertificate struct {
// File-based.
//
// Static key and certificate paths.
KeyPath string `yaml:"keyPath,omitempty"`
CertPath string `yaml:"certPath,omitempty"`
// Azure.
//
// Azure Vault URL and certificate ID, key will be found from the certificate.
AzureVaultURL string `yaml:"azureVaultURL,omitempty"`
AzureCertificateID string `yaml:"azureCertificateID,omitempty"`
// AWS.
//
// AWS KMS Key ID, ACM certificate ARN, and region.
// Support local cert file for legacy use cases.
AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"`
AwsRegion string `yaml:"awsRegion,omitempty"`
AwsCertPath string `yaml:"awsCertPath,omitempty"`
AwsCertARN string `yaml:"awsCertARN,omitempty"`
}
// SigningKey describes a signing key.
type SigningKey struct {
// File-based.
//
// Static key path.
KeyPath string `yaml:"keyPath,omitempty"`
// Azure.
//
// Azure Vault URL and key ID.
// AzureKeyVersion might be left empty to use the latest key version.
AzureVaultURL string `yaml:"azureVaultURL,omitempty"`
AzureKeyID string `yaml:"azureKeyID,omitempty"`
AzureKeyVersion string `yaml:"azureKeyVersion,omitempty"`
// AWS.
//
// AWS KMS Key ID and region.
AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"`
AwsRegion string `yaml:"awsRegion,omitempty"`
}
// GetSigner returns the signer.
func (key SigningKey) GetSigner(ctx context.Context) (measure.RSAKey, error) {
switch {
case key.KeyPath != "":
return file.NewPCRSigner(key.KeyPath)
case key.AzureVaultURL != "" && key.AzureKeyID != "":
return azure.NewPCRSigner(ctx, key.AzureVaultURL, key.AzureKeyID, key.AzureKeyVersion)
case key.AwsKMSKeyID != "":
return aws.NewPCRSigner(ctx, key.AwsKMSKeyID, key.AwsRegion)
default:
return nil, errors.New("unsupported PCR signer")
}
}
// GetSigner returns the signer.
func (keyAndCert SigningKeyAndCertificate) GetSigner(ctx context.Context) (pesign.CertificateSigner, error) {
switch {
case keyAndCert.KeyPath != "" && keyAndCert.CertPath != "":
return file.NewSecureBootSigner(keyAndCert.CertPath, keyAndCert.KeyPath)
case keyAndCert.AzureVaultURL != "" && keyAndCert.AzureCertificateID != "":
return azure.NewSecureBootSigner(ctx, keyAndCert.AzureVaultURL, keyAndCert.AzureCertificateID, keyAndCert.AzureCertificateID)
case keyAndCert.AwsKMSKeyID != "" && keyAndCert.AwsCertARN != "":
return aws.NewSecureBootACMSigner(ctx, keyAndCert.AwsKMSKeyID, keyAndCert.AwsRegion, keyAndCert.AwsCertARN)
case keyAndCert.AwsKMSKeyID != "" && keyAndCert.AwsCertPath != "":
return aws.NewSecureBootSigner(ctx, keyAndCert.AwsKMSKeyID, keyAndCert.AwsRegion, keyAndCert.AwsCertPath)
default:
return nil, errors.New("unsupported PCR signer")
}
}
const defaultSecureBootPrefix = "/secureboot"
// FillDefaults fills default values for the input.
//
//nolint:gocyclo,cyclop
func (i *Input) FillDefaults(arch, version string, secureboot bool) {
var (
zeroFileAsset FileAsset
zeroContainerAsset ContainerAsset
)
if i.Kernel == zeroFileAsset {
i.Kernel.Path = fmt.Sprintf(constants.KernelAssetPath, arch)
}
if i.Initramfs == zeroFileAsset {
i.Initramfs.Path = fmt.Sprintf(constants.InitramfsAssetPath, arch)
}
if i.BaseInstaller == zeroContainerAsset {
i.BaseInstaller.ImageRef = fmt.Sprintf("%s:%s", images.DefaultInstallerImageRepository, version)
if quirks.New(version).SupportsUnifiedInstaller() {
i.BaseInstaller.ImageRef = fmt.Sprintf("%s-base:%s", images.DefaultInstallerImageRepository, version)
}
}
if i.SDStub == zeroFileAsset {
i.SDStub.Path = fmt.Sprintf(constants.SDStubAssetPath, arch)
}
if i.SDBoot == zeroFileAsset {
i.SDBoot.Path = fmt.Sprintf(constants.SDBootAssetPath, arch)
}
if secureboot {
if i.SecureBoot == nil {
i.SecureBoot = &SecureBootAssets{}
}
if value.IsZero(i.SecureBoot.SecureBootSigner) {
i.SecureBoot.SecureBootSigner.KeyPath = filepath.Join(defaultSecureBootPrefix, constants.SecureBootSigningKeyAsset)
i.SecureBoot.SecureBootSigner.CertPath = filepath.Join(defaultSecureBootPrefix, constants.SecureBootSigningCertAsset)
}
if value.IsZero(i.SecureBoot.PCRSigner) {
i.SecureBoot.PCRSigner.KeyPath = filepath.Join(defaultSecureBootPrefix, constants.PCRSigningKeyAsset)
}
if i.SecureBoot.PlatformKeyPath == "" {
if platformKeyPath := filepath.Join(defaultSecureBootPrefix, constants.PlatformKeyAsset); fileExists(platformKeyPath) {
i.SecureBoot.PlatformKeyPath = platformKeyPath
}
}
if i.SecureBoot.KeyExchangeKeyPath == "" {
if keyExchangeKeyPath := filepath.Join(defaultSecureBootPrefix, constants.KeyExchangeKeyAsset); fileExists(keyExchangeKeyPath) {
i.SecureBoot.KeyExchangeKeyPath = keyExchangeKeyPath
}
}
if i.SecureBoot.SignatureKeyPath == "" {
if signatureKeyPath := filepath.Join(defaultSecureBootPrefix, constants.SignatureKeyAsset); fileExists(signatureKeyPath) {
i.SecureBoot.SignatureKeyPath = signatureKeyPath
}
}
}
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// Pull the container asset to the path.
func (c *ContainerAsset) Pull(ctx context.Context, arch string, printf func(string, ...any)) (v1.Image, error) {
if c.TarballPath != "" {
return nil, errors.New("pulling tarball container image is not supported")
}
if c.OCIPath != "" {
printf("using OCI image from %s...", c.OCIPath)
return c.pullFromOCI(arch)
}
printf("pulling %s...", c.ImageRef)
opts := []crane.Option{
crane.WithPlatform(&v1.Platform{
Architecture: arch,
OS: "linux",
}),
crane.WithContext(ctx),
crane.WithAuthFromKeychain(
authn.NewMultiKeychain(
authn.DefaultKeychain,
github.Keychain,
google.Keychain,
),
),
}
if c.ForceInsecure {
opts = append(opts, crane.Insecure)
}
img, err := crane.Pull(c.ImageRef, opts...)
if err != nil {
return nil, fmt.Errorf("error pulling image %s: %w", c.ImageRef, err)
}
return img, nil
}
func (c *ContainerAsset) pullFromOCI(arch string) (v1.Image, error) {
ociLayout, err := layout.FromPath(c.OCIPath)
if err != nil {
return nil, fmt.Errorf("error opening OCI layout: %w", err)
}
ociIndex, err := ociLayout.ImageIndex()
if err != nil {
return nil, fmt.Errorf("error opening OCI index: %w", err)
}
ociManifest, err := ociIndex.IndexManifest()
if err != nil {
return nil, fmt.Errorf("error opening OCI manifest: %w", err)
}
for _, manifest := range ociManifest.Manifests {
if manifest.Platform == nil {
continue
}
if manifest.Platform.OS == "linux" && manifest.Platform.Architecture == arch {
img, err := ociLayout.Image(manifest.Digest)
if err != nil {
return nil, fmt.Errorf("error opening OCI image: %w", err)
}
return img, nil
}
}
return nil, fmt.Errorf("no OCI image found for %s", arch)
}
// Extract the container asset to the path.
func (c *ContainerAsset) Extract(
ctx context.Context,
destination, arch string,
printf func(string, ...any),
xattrsMap map[string]string,
) error {
if c.TarballPath != "" {
in, err := os.Open(c.TarballPath)
if err != nil {
return err
}
defer in.Close() //nolint:errcheck
printf("extracting %s...", c.TarballPath)
return archiver.Untar(ctx, in, destination, xattrsMap)
}
img, err := c.Pull(ctx, arch, printf)
if err != nil {
return err
}
r, w := io.Pipe()
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
if exportErr := crane.Export(img, w); exportErr != nil {
w.CloseWithError(exportErr)
return exportErr
}
w.Close() //nolint:errcheck
return nil
})
eg.Go(func() error {
if untarErr := archiver.Untar(ctx, r, destination, xattrsMap); untarErr != nil {
r.CloseWithError(untarErr)
return untarErr
}
return nil
})
return eg.Wait()
}