mirror of
https://github.com/siderolabs/talos.git
synced 2026-05-04 20:06:18 +02:00
Whitelist services which can access the file socket, refuse other connections. Fixes #12701 Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
344 lines
9.7 KiB
Go
344 lines
9.7 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 lifecycle
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/containerd/containerd/v2/client"
|
|
"github.com/containerd/containerd/v2/core/leases"
|
|
"github.com/containerd/containerd/v2/pkg/cio"
|
|
"github.com/containerd/containerd/v2/pkg/oci"
|
|
"github.com/containerd/errdefs"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/siderolabs/go-procfs/procfs"
|
|
|
|
"github.com/siderolabs/talos/internal/app/internal/ctrhelper"
|
|
"github.com/siderolabs/talos/internal/app/machined/pkg/system/pid"
|
|
containerdrunner "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/containerd"
|
|
"github.com/siderolabs/talos/internal/pkg/capability"
|
|
"github.com/siderolabs/talos/internal/pkg/install"
|
|
"github.com/siderolabs/talos/internal/pkg/selinux"
|
|
"github.com/siderolabs/talos/pkg/machinery/api/common"
|
|
configcore "github.com/siderolabs/talos/pkg/machinery/config"
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
)
|
|
|
|
func generateContainerID() (string, error) {
|
|
b := make([]byte, 8)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("failed to generate random ID: %w", err)
|
|
}
|
|
|
|
return fmt.Sprintf("installer-%s", hex.EncodeToString(b)), nil
|
|
}
|
|
|
|
// sendFunc is a callback to stream a message line back to the client.
|
|
type sendFunc func(msg string) error
|
|
|
|
// sendExitCodeFunc is a callback to stream the exit code back to the client.
|
|
type sendExitCodeFunc func(exitCode int32) error
|
|
|
|
// containerRunConfig holds all parameters needed to create and run the installer container.
|
|
type containerRunConfig struct {
|
|
containerdInst *common.ContainerdInstance
|
|
imageRef string
|
|
disk string
|
|
platform string
|
|
cfgContainer configcore.Container
|
|
opts []install.Option
|
|
|
|
send sendFunc
|
|
sendExitCode sendExitCodeFunc
|
|
}
|
|
|
|
// runInstallerContainer creates and runs the installer container synchronously,
|
|
// streaming output lines back to the client via the send callback.
|
|
//
|
|
//nolint:gocyclo,cyclop
|
|
func runInstallerContainer(ctx context.Context, pidRecorder pid.Recorder, rc *containerRunConfig) error {
|
|
options := install.Options{Pull: true} //nolint:staticcheck
|
|
if err := options.Apply(rc.opts...); err != nil {
|
|
return fmt.Errorf("failed to apply install options: %w", err)
|
|
}
|
|
|
|
// connect to containerd
|
|
ctx, detachedCtx, c8dClient, err := ctrhelper.ContainerdInstanceHelper(ctx, rc.containerdInst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer c8dClient.Close() //nolint:errcheck
|
|
|
|
l, err := c8dClient.LeasesService().Create(ctx, leases.WithRandomID())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create lease: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if err := c8dClient.LeasesService().Delete(detachedCtx, l, leases.SynchronousDelete); err != nil {
|
|
log.Printf("failed to delete lease %s: %v", l.ID, err)
|
|
}
|
|
}()
|
|
|
|
ctx = leases.WithLease(ctx, l.ID)
|
|
|
|
img, err := c8dClient.GetImage(ctx, rc.imageRef)
|
|
if err != nil {
|
|
return fmt.Errorf("installer image %q not found in containerd store: %w", rc.imageRef, err)
|
|
}
|
|
|
|
// build container spec
|
|
mounts := buildMounts()
|
|
args := buildInstallerArgs(rc.disk, rc.platform, &options)
|
|
specOpts := buildSpecOpts(img, args, mounts)
|
|
|
|
containerID, err := generateContainerID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate container ID: %v", err)
|
|
}
|
|
|
|
// create container
|
|
ctr, err := c8dClient.NewContainer(ctx, containerID,
|
|
client.WithImage(img),
|
|
client.WithNewSnapshot(containerID, img),
|
|
client.WithNewSpec(specOpts...),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create container: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if cleanupErr := ctr.Delete(detachedCtx, client.WithSnapshotCleanup); cleanupErr != nil {
|
|
log.Printf("failed to delete container %s: %v", ctr.ID(), cleanupErr)
|
|
}
|
|
}()
|
|
|
|
// set up I/O: stdout/stderr -> pipe -> stream to client; stdin <- config bytes
|
|
stdoutR, stdoutW := io.Pipe()
|
|
|
|
var stdinReader interface {
|
|
io.Reader
|
|
WaitAndClose(context.Context, client.Task)
|
|
}
|
|
|
|
if rc.cfgContainer != nil {
|
|
configBytes, cfgErr := rc.cfgContainer.Bytes()
|
|
if cfgErr != nil {
|
|
return fmt.Errorf("failed to serialize config: %w", cfgErr)
|
|
}
|
|
|
|
stdinReader = &containerdrunner.StdinCloser{
|
|
Stdin: bytes.NewReader(configBytes),
|
|
Closer: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
creator := cio.NewCreator(cio.WithStreams(stdinReader, stdoutW, stdoutW))
|
|
|
|
task, err := ctr.NewTask(ctx, creator)
|
|
if err != nil {
|
|
stdoutW.Close() //nolint:errcheck
|
|
|
|
return fmt.Errorf("failed to create task: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if _, delErr := task.Delete(detachedCtx, client.WithProcessKill); delErr != nil && !errdefs.IsNotFound(delErr) {
|
|
log.Printf("failed to delete task: %v", delErr)
|
|
}
|
|
}()
|
|
|
|
if stdinReader != nil {
|
|
go stdinReader.WaitAndClose(ctx, task)
|
|
}
|
|
|
|
if err := task.Start(ctx); err != nil {
|
|
stdoutW.Close() //nolint:errcheck
|
|
|
|
return fmt.Errorf("failed to start task: %w", err)
|
|
}
|
|
|
|
if err := pidRecorder("installer", int32(task.Pid()), false); err != nil {
|
|
return fmt.Errorf("failed to record installer PID: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if err := pidRecorder("installer", 0, true); err != nil {
|
|
log.Printf("failed to clear installer PID record: %v", err)
|
|
}
|
|
}()
|
|
|
|
statusC, err := task.Wait(detachedCtx)
|
|
if err != nil {
|
|
stdoutW.Close() //nolint:errcheck
|
|
|
|
return fmt.Errorf("failed to wait for task: %w", err)
|
|
}
|
|
|
|
// stream output in a goroutine
|
|
sendDone := make(chan error, 1)
|
|
|
|
go func() {
|
|
sendDone <- streamOutput(stdoutR, rc.send)
|
|
}()
|
|
|
|
// wait for task to exit
|
|
exitStatus := <-statusC
|
|
|
|
// close the write end so the reader gets EOF
|
|
stdoutW.Close() //nolint:errcheck
|
|
|
|
// wait for send loop to finish
|
|
if sendErr := <-sendDone; sendErr != nil {
|
|
log.Printf("error streaming output: %v", sendErr)
|
|
}
|
|
|
|
if exitStatus.Error() != nil {
|
|
return fmt.Errorf("task exited with error: %w", exitStatus.Error())
|
|
}
|
|
|
|
exitCode := int32(exitStatus.ExitCode())
|
|
|
|
// send exit code to client
|
|
if err := rc.sendExitCode(exitCode); err != nil {
|
|
return fmt.Errorf("failed to send exit code: %w", err)
|
|
}
|
|
|
|
if exitCode != 0 {
|
|
log.Printf("installer container exited with code %d", exitCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// streamOutput reads from r line by line and sends each line via the send callback.
|
|
func streamOutput(r io.Reader, send sendFunc) error {
|
|
buf := make([]byte, 512)
|
|
|
|
for {
|
|
n, err := r.Read(buf)
|
|
if n > 0 {
|
|
if sendErr := send(string(buf[:n])); sendErr != nil {
|
|
return fmt.Errorf("failed to send message: %w", sendErr)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to read output: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildMounts constructs the OCI mounts for the installer container.
|
|
func buildMounts() []specs.Mount {
|
|
mounts := []specs.Mount{
|
|
{Type: "bind", Destination: "/dev", Source: "/dev", Options: []string{"rbind", "rshared", "rw"}},
|
|
}
|
|
|
|
if _, err := os.Stat(constants.MachineSocketPath); err == nil {
|
|
mounts = append(mounts, specs.Mount{
|
|
Type: "bind", Destination: constants.MachineSocketPath,
|
|
Source: constants.MachineSocketPath, Options: []string{"rbind", "rshared", "ro"},
|
|
})
|
|
}
|
|
|
|
if _, err := os.Stat(constants.EFIVarsMountPoint); err == nil {
|
|
mounts = append(mounts, specs.Mount{
|
|
Type: "efivarfs", Source: "efivarfs",
|
|
Destination: constants.EFIVarsMountPoint,
|
|
Options: []string{"rw", "nosuid", "nodev", "noexec", "relatime"},
|
|
})
|
|
}
|
|
|
|
if _, err := os.Stat(constants.SDStubDynamicInitrdPath); err == nil {
|
|
mounts = append(mounts, specs.Mount{
|
|
Type: "bind", Destination: constants.SDStubDynamicInitrdPath,
|
|
Source: constants.SDStubDynamicInitrdPath, Options: []string{"rbind", "rshared", "ro"},
|
|
})
|
|
}
|
|
|
|
return mounts
|
|
}
|
|
|
|
// buildInstallerArgs constructs the command-line arguments for the installer binary.
|
|
func buildInstallerArgs(disk, platform string, options *install.Options) []string {
|
|
config := constants.ConfigNone
|
|
if c := procfs.ProcCmdline().Get(constants.KernelParamConfig).First(); c != nil {
|
|
config = *c
|
|
}
|
|
|
|
args := []string{
|
|
"/bin/installer",
|
|
"install",
|
|
"--disk=" + disk,
|
|
"--platform=" + platform,
|
|
"--config=" + config,
|
|
"--upgrade=" + strconv.FormatBool(options.Upgrade),
|
|
"--force=" + strconv.FormatBool(options.Force),
|
|
"--zero=" + strconv.FormatBool(options.Zero),
|
|
}
|
|
|
|
for _, arg := range options.ExtraKernelArgs {
|
|
args = append(args, "--extra-kernel-arg", arg)
|
|
}
|
|
|
|
for _, preservedArg := range []string{
|
|
constants.KernelParamSideroLink,
|
|
constants.KernelParamEventsSink,
|
|
constants.KernelParamLoggingKernel,
|
|
constants.KernelParamEquinixMetalEvents,
|
|
constants.KernelParamAuditdDisabled,
|
|
constants.KernelParamDashboardDisabled,
|
|
constants.KernelParamNetIfnames,
|
|
constants.KernelParamEnforceModuleSigVerify,
|
|
} {
|
|
if c := procfs.ProcCmdline().Get(preservedArg).First(); c != nil {
|
|
args = append(args, "--extra-kernel-arg", fmt.Sprintf("%s=%s", preservedArg, *c))
|
|
}
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// buildSpecOpts constructs the OCI spec options for the installer container.
|
|
func buildSpecOpts(img client.Image, args []string, mounts []specs.Mount) []oci.SpecOpts {
|
|
specOpts := []oci.SpecOpts{
|
|
oci.WithImageConfig(img),
|
|
oci.WithProcessArgs(args...),
|
|
oci.WithHostNamespace(specs.NetworkNamespace),
|
|
oci.WithHostNamespace(specs.PIDNamespace),
|
|
oci.WithMounts(mounts),
|
|
oci.WithHostHostsFile,
|
|
oci.WithHostResolvconf,
|
|
oci.WithParentCgroupDevices,
|
|
oci.WithCapabilities(capability.AllGrantableCapabilities()),
|
|
oci.WithMaskedPaths(nil),
|
|
oci.WithReadonlyPaths(nil),
|
|
oci.WithWriteableSysfs,
|
|
oci.WithWriteableCgroupfs,
|
|
oci.WithApparmorProfile(""),
|
|
oci.WithSeccompUnconfined,
|
|
oci.WithAllDevicesAllowed,
|
|
}
|
|
|
|
if selinux.IsEnabled() {
|
|
specOpts = append(specOpts, oci.WithSelinuxLabel(constants.SelinuxLabelInstaller))
|
|
}
|
|
|
|
return specOpts
|
|
}
|