talos/internal/integration/api/selinux.go
Dmitry Sharshakov 1a8cc5f8b2
feat: add SELinux labels to volumes
Label mounted filesystems like ephemeral, overlay mounts, as well as data directories (going to become volumes later).

Signed-off-by: Dmitry Sharshakov <dmitry.sharshakov@siderolabs.com>
2024-11-21 14:23:43 +01:00

266 lines
8.6 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/.
//go:build integration_api
package api
import (
"bytes"
"context"
"io"
"maps"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/siderolabs/go-pointer"
"github.com/siderolabs/go-procfs/procfs"
"golang.org/x/exp/slices"
"github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers"
"github.com/siderolabs/talos/internal/integration/base"
machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine"
"github.com/siderolabs/talos/pkg/machinery/client"
"github.com/siderolabs/talos/pkg/machinery/config/machine"
"github.com/siderolabs/talos/pkg/machinery/constants"
)
// SELinuxSuite ...
type SELinuxSuite struct {
base.APISuite
ctx context.Context //nolint:containedctx
ctxCancel context.CancelFunc
}
// SuiteName ...
func (suite *SELinuxSuite) SuiteName() string {
return "api.SELinuxSuite"
}
// SetupTest ...
func (suite *SELinuxSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 15*time.Second)
if suite.Cluster == nil || suite.Cluster.Provisioner() != base.ProvisionerQEMU {
suite.T().Skip("skipping SELinux test since provisioner is not qemu")
}
}
// TearDownTest ...
func (suite *SELinuxSuite) TearDownTest() {
if suite.ctxCancel != nil {
suite.ctxCancel()
}
}
func (suite *SELinuxSuite) getLabel(nodeCtx context.Context, pid int32) string {
r, err := suite.Client.Read(nodeCtx, filepath.Join("/proc", strconv.Itoa(int(pid)), "attr/current"))
suite.Require().NoError(err)
value, err := io.ReadAll(r)
suite.Require().NoError(err)
suite.Require().NoError(r.Close())
return string(bytes.TrimSpace(value))
}
// TestFileMountLabels reads labels of runtime-created files and mounts from xattrs
// to ensure SELinux labels for files are set when they are created and FS's are mounted with correct labels.
// FIXME: cancel the test in case system was upgraded.
func (suite *SELinuxSuite) TestFileMountLabels() {
workers := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeWorker)
controlplanes := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeControlPlane)
expectedLabelsWorker := map[string]string{
// Mounts
constants.SystemPath: constants.SystemSelinuxLabel,
constants.EphemeralMountPoint: constants.EphemeralSelinuxLabel,
constants.StateMountPoint: constants.StateSelinuxLabel,
constants.SystemEtcPath: constants.SystemEtcSelinuxLabel,
constants.SystemVarPath: constants.SystemVarSelinuxLabel,
constants.RunPath: constants.RunSelinuxLabel,
"/var/run": constants.RunSelinuxLabel,
// Runtime files
constants.APIRuntimeSocketPath: constants.APIRuntimeSocketLabel,
constants.APISocketPath: constants.APISocketLabel,
constants.DBusClientSocketPath: constants.DBusClientSocketLabel,
constants.UdevRulesPath: constants.UdevRulesLabel,
constants.DBusServiceSocketPath: constants.DBusServiceSocketLabel,
constants.MachineSocketPath: constants.MachineSocketLabel,
// Overlays
"/etc/cni": constants.CNISELinuxLabel,
constants.KubernetesConfigBaseDir: constants.KubernetesConfigSELinuxLabel,
"/usr/libexec/kubernetes": constants.KubeletPluginsSELinuxLabel,
"/opt": constants.OptSELinuxLabel,
"/opt/cni": "system_u:object_r:cni_plugin_t:s0",
"/opt/containerd": "system_u:object_r:containerd_plugin_t:s0",
// Directories
"/var/lib/containerd": "system_u:object_r:containerd_state_t:s0",
"/var/lib/kubelet": "system_u:object_r:kubelet_state_t:s0",
}
// Only running on controlplane
expectedLabelsControlPlane := map[string]string{
constants.EtcdPKIPath: constants.EtcdPKISELinuxLabel,
constants.EtcdDataPath: constants.EtcdDataSELinuxLabel,
constants.KubernetesAPIServerConfigDir: constants.KubernetesAPIServerConfigDirSELinuxLabel,
constants.KubernetesAPIServerSecretsDir: constants.KubernetesAPIServerSecretsDirSELinuxLabel,
constants.KubernetesControllerManagerSecretsDir: constants.KubernetesControllerManagerSecretsDirSELinuxLabel,
constants.KubernetesSchedulerConfigDir: constants.KubernetesSchedulerConfigDirSELinuxLabel,
constants.KubernetesSchedulerSecretsDir: constants.KubernetesSchedulerSecretsDirSELinuxLabel,
constants.TrustdRuntimeSocketPath: constants.TrustdRuntimeSocketLabel,
}
maps.Copy(expectedLabelsControlPlane, expectedLabelsWorker)
suite.checkFileLabels(workers, expectedLabelsWorker)
suite.checkFileLabels(controlplanes, expectedLabelsControlPlane)
}
//nolint:gocyclo
func (suite *SELinuxSuite) checkFileLabels(nodes []string, expectedLabels map[string]string) {
paths := make([]string, 0, len(expectedLabels))
for k := range expectedLabels {
paths = append(paths, k)
}
for _, node := range nodes {
nodeCtx := client.WithNode(suite.ctx, node)
cmdline := suite.ReadCmdline(nodeCtx)
seLinuxEnabled := pointer.SafeDeref(procfs.NewCmdline(cmdline).Get(constants.KernelParamSELinux).First()) != ""
if !seLinuxEnabled {
suite.T().Skip("skipping SELinux test since SELinux is disabled")
}
// We should check both folders and their contents for proper labels
for _, dir := range []bool{true, false} {
for path, label := range expectedLabels {
req := &machineapi.ListRequest{
Root: path,
ReportXattrs: true,
}
if dir {
req.Types = []machineapi.ListRequest_Type{machineapi.ListRequest_DIRECTORY}
}
stream, err := suite.Client.LS(nodeCtx, req)
suite.Require().NoError(err)
suite.Require().NoError(helpers.ReadGRPCStream(stream, func(info *machineapi.FileInfo, node string, multipleNodes bool) error {
// E.g. /var/lib should inherit /var label, while /var/run is a new mountpoint
if slices.Contains(paths, info.Name) && info.Name != path {
return nil
}
suite.Require().NotNil(info.Xattrs)
found := false
for _, l := range info.Xattrs {
if l.Name == "security.selinux" {
got := string(bytes.Trim(l.Data, "\x00\n"))
suite.Require().Contains(got, label, "expected %s to have label %s, got %s", path, label, got)
found = true
break
}
}
suite.Require().True(found)
return nil
}))
}
}
}
}
// TestProcessLabels reads labels of system processes from procfs
// to ensure SELinux labels for processes are correctly set
//
//nolint:gocyclo
func (suite *SELinuxSuite) TestProcessLabels() {
nodes := suite.DiscoverNodeInternalIPs(suite.ctx)
for _, node := range nodes {
nodeCtx := client.WithNode(suite.ctx, node)
cmdline := suite.ReadCmdline(nodeCtx)
seLinuxEnabled := pointer.SafeDeref(procfs.NewCmdline(cmdline).Get(constants.KernelParamSELinux).First()) != ""
if !seLinuxEnabled {
suite.T().Skip("skipping SELinux test since SELinux is disabled")
}
r, err := suite.Client.Processes(nodeCtx)
suite.Require().NoError(err)
for _, msg := range r.Messages {
procs := msg.Processes
for _, p := range procs {
switch p.Command {
case "systemd-udevd":
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelUdevd,
)
case "dashboard":
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelDashboard,
)
case "containerd":
if strings.Contains(p.Args, "/system/run/containerd") {
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelSystemRuntime,
)
} else {
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelPodRuntime,
)
}
case "init":
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelMachined,
)
case "kubelet":
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelKubelet,
)
case "apid":
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelApid,
)
case "trustd":
suite.Require().Contains(
suite.getLabel(nodeCtx, p.Pid),
constants.SelinuxLabelTrustd,
)
}
}
}
}
}
// TODO: test for all machined-created files
// TODO: test for system and CRI container labels
// TODO: test labels for unconfined system extensions, pods
// TODO: test for no avc denials in dmesg
// TODO: start a pod and ensure access to restricted resources is denied
func init() {
allSuites = append(allSuites, new(SELinuxSuite))
}