From a867f85e4cb662a17b0738f1f0de4f1485ad925a Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Mon, 4 Nov 2024 22:28:07 +0100 Subject: [PATCH] feat: label system socket and runtime files Set SELinux labels so that services could gain access permissions. Signed-off-by: Dmitry Sharshakov --- internal/app/apid/main.go | 5 + .../app/machined/pkg/controllers/etcd/pki.go | 5 + .../k8s/render_config_static_pods.go | 34 +++--- .../k8s/render_secrets_static_pod.go | 45 ++++---- .../app/machined/pkg/system/services/apid.go | 5 + .../machined/pkg/system/services/machined.go | 5 + .../machined/pkg/system/services/trustd.go | 5 + internal/integration/api/selinux.go | 91 +++++++++++++++- internal/pkg/logind/broker.go | 11 ++ internal/pkg/selinux/policy/policy.33 | Bin 24234 -> 25413 bytes .../policy/selinux/common/classmaps.cil | 99 ++++++++++++++++++ .../selinux/policy/selinux/services/cri.cil | 27 +++++ .../policy/selinux/services/machined.cil | 11 ++ .../selinux/services/system-containers.cil | 12 +++ pkg/machinery/constants/constants.go | 36 +++++++ 15 files changed, 359 insertions(+), 32 deletions(-) diff --git a/internal/app/apid/main.go b/internal/app/apid/main.go index cee48a5080..2785651b81 100644 --- a/internal/app/apid/main.go +++ b/internal/app/apid/main.go @@ -31,6 +31,7 @@ import ( apidbackend "github.com/siderolabs/talos/internal/app/apid/pkg/backend" "github.com/siderolabs/talos/internal/app/apid/pkg/director" "github.com/siderolabs/talos/internal/app/apid/pkg/provider" + "github.com/siderolabs/talos/internal/pkg/selinux" "github.com/siderolabs/talos/pkg/grpc/factory" "github.com/siderolabs/talos/pkg/grpc/middleware/authz" "github.com/siderolabs/talos/pkg/grpc/proxy/backend" @@ -157,6 +158,10 @@ func apidMain() error { return fmt.Errorf("error creating listner: %w", err) } + if err = selinux.SetLabel(constants.APISocketPath, constants.APISocketLabel); err != nil { + return err + } + networkServer := func() *grpc.Server { mode := authz.Disabled if *rbacEnabled { diff --git a/internal/app/machined/pkg/controllers/etcd/pki.go b/internal/app/machined/pkg/controllers/etcd/pki.go index 781148ef1f..8bdd126b4c 100644 --- a/internal/app/machined/pkg/controllers/etcd/pki.go +++ b/internal/app/machined/pkg/controllers/etcd/pki.go @@ -17,6 +17,7 @@ import ( "github.com/siderolabs/gen/optional" "go.uber.org/zap" + "github.com/siderolabs/talos/internal/pkg/selinux" "github.com/siderolabs/talos/pkg/filetree" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/resources/etcd" @@ -92,6 +93,10 @@ func (ctrl *PKIController) Run(ctx context.Context, r controller.Runtime, _ *zap return err } + if err = selinux.SetLabel(constants.EtcdPKIPath, constants.EtcdPKISELinuxLabel); err != nil { + return err + } + if err = os.WriteFile(constants.EtcdCACert, rootScrts.TypedSpec().EtcdCA.Crt, 0o400); err != nil { return fmt.Errorf("failed to write CA certificate: %w", err) } diff --git a/internal/app/machined/pkg/controllers/k8s/render_config_static_pods.go b/internal/app/machined/pkg/controllers/k8s/render_config_static_pods.go index d3fbb27586..d5ec27d0dc 100644 --- a/internal/app/machined/pkg/controllers/k8s/render_config_static_pods.go +++ b/internal/app/machined/pkg/controllers/k8s/render_config_static_pods.go @@ -22,6 +22,7 @@ import ( auditv1 "k8s.io/apiserver/pkg/apis/audit/v1" schedulerv1 "k8s.io/kube-scheduler/config/v1" + "github.com/siderolabs/talos/internal/pkg/selinux" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/resources/k8s" ) @@ -124,17 +125,19 @@ func (ctrl *RenderConfigsStaticPodController) Run(ctx context.Context, r control ) for _, pod := range []struct { - name string - directory string - uid int - gid int - configs []configFile + name string + directory string + selinuxLabel string + uid int + gid int + configs []configFile }{ { - name: "kube-apiserver", - directory: constants.KubernetesAPIServerConfigDir, - uid: constants.KubernetesAPIServerRunUser, - gid: constants.KubernetesAPIServerRunGroup, + name: "kube-apiserver", + directory: constants.KubernetesAPIServerConfigDir, + selinuxLabel: constants.KubernetesAPIServerConfigDirSELinuxLabel, + uid: constants.KubernetesAPIServerRunUser, + gid: constants.KubernetesAPIServerRunGroup, configs: []configFile{ { filename: "admission-control-config.yaml", @@ -147,10 +150,11 @@ func (ctrl *RenderConfigsStaticPodController) Run(ctx context.Context, r control }, }, { - name: "kube-scheduler", - directory: constants.KubernetesSchedulerConfigDir, - uid: constants.KubernetesSchedulerRunUser, - gid: constants.KubernetesSchedulerRunGroup, + name: "kube-scheduler", + directory: constants.KubernetesSchedulerConfigDir, + selinuxLabel: constants.KubernetesSchedulerConfigDirSELinuxLabel, + uid: constants.KubernetesSchedulerRunUser, + gid: constants.KubernetesSchedulerRunGroup, configs: []configFile{ { filename: "scheduler-config.yaml", @@ -163,6 +167,10 @@ func (ctrl *RenderConfigsStaticPodController) Run(ctx context.Context, r control return fmt.Errorf("error creating config directory for %q: %w", pod.name, err) } + if err = selinux.SetLabel(pod.directory, pod.selinuxLabel); err != nil { + return err + } + for _, configFile := range pod.configs { var obj runtime.Object diff --git a/internal/app/machined/pkg/controllers/k8s/render_secrets_static_pod.go b/internal/app/machined/pkg/controllers/k8s/render_secrets_static_pod.go index a9fa9fe6b6..321b4c013d 100644 --- a/internal/app/machined/pkg/controllers/k8s/render_secrets_static_pod.go +++ b/internal/app/machined/pkg/controllers/k8s/render_secrets_static_pod.go @@ -21,6 +21,7 @@ import ( "github.com/siderolabs/gen/xslices" "go.uber.org/zap" + "github.com/siderolabs/talos/internal/pkg/selinux" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/resources/k8s" "github.com/siderolabs/talos/pkg/machinery/resources/secrets" @@ -162,18 +163,20 @@ func (ctrl *RenderSecretsStaticPodController) Run(ctx context.Context, r control } for _, pod := range []struct { - name string - directory string - uid int - gid int - secrets []secret - templates []template + name string + directory string + selinuxLabel string + uid int + gid int + secrets []secret + templates []template }{ { - name: "kube-apiserver", - directory: constants.KubernetesAPIServerSecretsDir, - uid: constants.KubernetesAPIServerRunUser, - gid: constants.KubernetesAPIServerRunGroup, + name: "kube-apiserver", + directory: constants.KubernetesAPIServerSecretsDir, + selinuxLabel: constants.KubernetesAPIServerSecretsDirSELinuxLabel, + uid: constants.KubernetesAPIServerRunUser, + gid: constants.KubernetesAPIServerRunGroup, secrets: []secret{ { getter: func() *x509.PEMEncodedCertificateAndKey { return rootEtcdSecrets.EtcdCA }, @@ -230,10 +233,11 @@ func (ctrl *RenderSecretsStaticPodController) Run(ctx context.Context, r control }, }, { - name: "kube-controller-manager", - directory: constants.KubernetesControllerManagerSecretsDir, - uid: constants.KubernetesControllerManagerRunUser, - gid: constants.KubernetesControllerManagerRunGroup, + name: "kube-controller-manager", + directory: constants.KubernetesControllerManagerSecretsDir, + selinuxLabel: constants.KubernetesControllerManagerSecretsDirSELinuxLabel, + uid: constants.KubernetesControllerManagerRunUser, + gid: constants.KubernetesControllerManagerRunGroup, secrets: []secret{ { getter: func() *x509.PEMEncodedCertificateAndKey { return rootK8sSecrets.IssuingCA }, @@ -258,10 +262,11 @@ func (ctrl *RenderSecretsStaticPodController) Run(ctx context.Context, r control }, }, { - name: "kube-scheduler", - directory: constants.KubernetesSchedulerSecretsDir, - uid: constants.KubernetesSchedulerRunUser, - gid: constants.KubernetesSchedulerRunGroup, + name: "kube-scheduler", + directory: constants.KubernetesSchedulerSecretsDir, + selinuxLabel: constants.KubernetesSchedulerSecretsDirSELinuxLabel, + uid: constants.KubernetesSchedulerRunUser, + gid: constants.KubernetesSchedulerRunGroup, templates: []template{ { filename: "kubeconfig", @@ -274,6 +279,10 @@ func (ctrl *RenderSecretsStaticPodController) Run(ctx context.Context, r control return fmt.Errorf("error creating secrets directory for %q: %w", pod.name, err) } + if err = selinux.SetLabel(pod.directory, pod.selinuxLabel); err != nil { + return err + } + for _, secret := range pod.secrets { certAndKey := secret.getter() diff --git a/internal/app/machined/pkg/system/services/apid.go b/internal/app/machined/pkg/system/services/apid.go index fe01a3102c..d4b657a520 100644 --- a/internal/app/machined/pkg/system/services/apid.go +++ b/internal/app/machined/pkg/system/services/apid.go @@ -32,6 +32,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/containerd" "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/restart" "github.com/siderolabs/talos/internal/pkg/environment" + "github.com/siderolabs/talos/internal/pkg/selinux" "github.com/siderolabs/talos/pkg/conditions" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/resources/network" @@ -96,6 +97,10 @@ func (o *APID) PreFunc(ctx context.Context, r runtime.Runtime) error { return err } + if err := selinux.SetLabel(constants.APIRuntimeSocketPath, constants.APIRuntimeSocketLabel); err != nil { + return err + } + // chown the socket path to make it accessible to the apid if err := os.Chown(constants.APIRuntimeSocketPath, constants.ApidUserID, constants.ApidUserID); err != nil { return err diff --git a/internal/app/machined/pkg/system/services/machined.go b/internal/app/machined/pkg/system/services/machined.go index b6a52219f7..d028d1c439 100644 --- a/internal/app/machined/pkg/system/services/machined.go +++ b/internal/app/machined/pkg/system/services/machined.go @@ -23,6 +23,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/system/health" "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner" "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/goroutine" + "github.com/siderolabs/talos/internal/pkg/selinux" "github.com/siderolabs/talos/pkg/conditions" "github.com/siderolabs/talos/pkg/grpc/factory" "github.com/siderolabs/talos/pkg/grpc/middleware/authz" @@ -160,6 +161,10 @@ func (s *machinedService) Main(ctx context.Context, r runtime.Runtime, logWriter return err } + if err := selinux.SetLabel(constants.MachineSocketPath, constants.MachineSocketLabel); err != nil { + return err + } + // chown the socket path to make it accessible to the apid if err := os.Chown(constants.MachineSocketPath, constants.ApidUserID, constants.ApidUserID); err != nil { return err diff --git a/internal/app/machined/pkg/system/services/trustd.go b/internal/app/machined/pkg/system/services/trustd.go index b21b74e1f6..3eebb34e20 100644 --- a/internal/app/machined/pkg/system/services/trustd.go +++ b/internal/app/machined/pkg/system/services/trustd.go @@ -31,6 +31,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/containerd" "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/restart" "github.com/siderolabs/talos/internal/pkg/environment" + "github.com/siderolabs/talos/internal/pkg/selinux" "github.com/siderolabs/talos/pkg/conditions" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/resources/network" @@ -94,6 +95,10 @@ func (t *Trustd) PreFunc(ctx context.Context, r runtime.Runtime) error { return err } + if err := selinux.SetLabel(constants.TrustdRuntimeSocketPath, constants.TrustdRuntimeSocketLabel); err != nil { + return err + } + // chown the socket path to make it accessible to the apid if err := os.Chown(constants.TrustdRuntimeSocketPath, constants.TrustdUserID, constants.TrustdUserID); err != nil { return err diff --git a/internal/integration/api/selinux.go b/internal/integration/api/selinux.go index 820234e1d2..63078d2c33 100644 --- a/internal/integration/api/selinux.go +++ b/internal/integration/api/selinux.go @@ -18,8 +18,11 @@ import ( "github.com/siderolabs/go-pointer" "github.com/siderolabs/go-procfs/procfs" + "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" ) @@ -64,6 +67,92 @@ func (suite *SELinuxSuite) getLabel(nodeCtx context.Context, pid int32) string { return string(bytes.TrimSpace(value)) } +// TestRuntimeFileLabels reads labels of runtime-created files from xattrs +// to ensure SELinux labels for files are set when they are created. +func (suite *SELinuxSuite) TestRuntimeFileLabels() { + workers := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeWorker) + controlplanes := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeControlPlane) + + expectedLabelsWorker := map[string]string{ + constants.APIRuntimeSocketPath: constants.APIRuntimeSocketLabel, + constants.APISocketPath: constants.APISocketLabel, + constants.DBusClientSocketPath: constants.DBusClientSocketLabel, + constants.UdevRulesPath: constants.UdevRulesLabel, + constants.DBusServiceSocketPath: constants.DBusServiceSocketLabel, + constants.MachineSocketPath: constants.MachineSocketLabel, + } + + expectedLabelsControlPlane := map[string]string{ + constants.APIRuntimeSocketPath: constants.APIRuntimeSocketLabel, + constants.APISocketPath: constants.APISocketLabel, + constants.DBusClientSocketPath: constants.DBusClientSocketLabel, + constants.UdevRulesPath: constants.UdevRulesLabel, + constants.DBusServiceSocketPath: constants.DBusServiceSocketLabel, + constants.MachineSocketPath: constants.MachineSocketLabel, + // Only running on controlplane + constants.EtcdPKIPath: constants.EtcdPKISELinuxLabel, + constants.KubernetesAPIServerConfigDir: constants.KubernetesAPIServerConfigDirSELinuxLabel, + constants.KubernetesAPIServerSecretsDir: constants.KubernetesAPIServerSecretsDirSELinuxLabel, + constants.KubernetesControllerManagerSecretsDir: constants.KubernetesControllerManagerSecretsDirSELinuxLabel, + constants.KubernetesSchedulerConfigDir: constants.KubernetesSchedulerConfigDirSELinuxLabel, + constants.KubernetesSchedulerSecretsDir: constants.KubernetesSchedulerSecretsDirSELinuxLabel, + constants.TrustdRuntimeSocketPath: constants.TrustdRuntimeSocketLabel, + } + + suite.checkFileLabels(workers, expectedLabelsWorker) + suite.checkFileLabels(controlplanes, expectedLabelsControlPlane) +} + +func (suite *SELinuxSuite) checkFileLabels(nodes []string, expectedLabels map[string]string) { + 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 { + 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().Equal(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 // @@ -136,7 +225,7 @@ func (suite *SELinuxSuite) TestProcessLabels() { } } -// TODO: test for file labels +// TODO: test for volume 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 diff --git a/internal/pkg/logind/broker.go b/internal/pkg/logind/broker.go index cdda604800..ed2ce17552 100644 --- a/internal/pkg/logind/broker.go +++ b/internal/pkg/logind/broker.go @@ -19,6 +19,9 @@ import ( "time" "golang.org/x/sync/errgroup" + + "github.com/siderolabs/talos/internal/pkg/selinux" + "github.com/siderolabs/talos/pkg/machinery/constants" ) // DBusBroker implements simplified D-Bus broker which allows to connect @@ -48,11 +51,19 @@ func NewBroker(serviceSocketPath, clientSocketPath string) (*DBusBroker, error) return nil, err } + if err = selinux.SetLabel(serviceSocketPath, constants.DBusServiceSocketLabel); err != nil { + return nil, err + } + broker.listenClient, err = net.Listen("unix", clientSocketPath) if err != nil { return nil, err } + if err = selinux.SetLabel(clientSocketPath, constants.DBusClientSocketLabel); err != nil { + return nil, err + } + return broker, nil } diff --git a/internal/pkg/selinux/policy/policy.33 b/internal/pkg/selinux/policy/policy.33 index 913e3f1981c6205742bf8fc5d07ad39a45cfda7a..dc206fbbdc6ffdd935d7e944feacfb66ced4c376 100644 GIT binary patch literal 25413 zcmbW9Yr7l8k;jjX7jQ@dgamR&=6=hD7%(P~FhHDy#6Tbkxo)&`q|rGO>&9qwZ0Fg1 zaz4Pm*iW->_iOFG*eCn{_4HqANu%R2?We7t>ZgwvN{U87MxBtEW;Nalb*Lv5> z>GIYkmCq>yyQrMS42<+&%};>G;=wv*~>T-K%_5)OlT3 z6z@?UR7GAF&^@SJ732J{7>}yigytVpzM0p9aVZ9ZUE~htW z0#1|Ja$3)(G=b3hd|ph4i1$_Hzz-^P8_xze_M;^xxSLg3 z7uew?cODSx({)p?`kBl=uUF#+;^O}UXb7y?|l0VXe`b5ZZ_6`I=H zE7%mx7P4q#DOewt)8We6GRVBu=Cq-42!vbZ38wP(tG{hpye@7Y_STI;|J^~lc zH}e}sKOg7Sgf@dr$Hm~rd{$2DepM{$Syj*uDlKj=#<<~L*Y zv>XJvg8LQqTx}r>rkvcE&W1zB z0QOzwAXBlJEvrEYdFwE;G8*1Zd!f4!mP|L%uc(2#BU!Nwy-RF@Ye1(u4y>bch(H8P zst3o_Y*xd&7a?_hTLztu%HTPntAc+DrXC$b{OyQ3udBQ`1;270Ia4YI%IUItZjQX^6MVVSBwMww;>sK>WT~z%hC~kyCBd5Pu z5Qcb68^exlU_t)*7Mr}DEKUYpg~5Jsd48MAd_LAZG*%c0q5h6q(LFH2nNqE86t}yw z@Caa7zz&BZO%_L=Z3Y=?i|J>}Vp#-?t-t6<(hJJ;Gnu4urdbPOni|cjn|U?tmtneK z7*)mK1m6MQwXd?B06Pg{Z1Aaa z+OzrisB2eZEVRHvIEvF)v4o{=vfOo5`U@!`^vBAua8fwE+(*ahVm;k?Nnxb)m&lAN zSCvy$?4*mqq6|GepH+2N<&X@fM0I@DLpV(8n&yjq(#8r!K#z;2dZzMDf-f0iQ-PaN z-Ntr@g#h#*>0gy`5624DQT^hinAZ4e@Wc5MWE>+*USZ+H7fP|Nr;Ouy{Q;{m2UCE{ z4fRGbR$(Wx)<72~msFQvr{R&jx~o38Ih=mE#c4jD&u(>PVz*{|&~>O!st2tRiZBVrjkh3;gb zGF1PTieL3@L3a}C9&Gu9a%u(-iCd$pRZi3j_84ob3M&dmX|P6s1g8*WC)xRM7RvtC z8mBHMu*JXhrNudm9@wHwv;NT>k`J**&{xa5Qh}6ci(^J2_0D28E*yg+q2k_Bt8UoQ zWW>df^|g~g!z}M8r@fZny8K#K53`%%_-SBZb=~S?4hd)a6H6#0V_n?VVU=d@qD{ol z-DB(uXz@Aa)E0J;8?(c|YTMUehEUShv

SEu5()wN5#he178)i%D-O;~`{{yri6# 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