diff --git a/pkg/agent/config/config.go b/pkg/agent/config/config.go index d5cfa4ebab..b7a03ca701 100644 --- a/pkg/agent/config/config.go +++ b/pkg/agent/config/config.go @@ -200,7 +200,16 @@ func ensureNodePassword(nodePasswordFile string) (string, error) { return "", err } nodePassword := hex.EncodeToString(password) - return nodePassword, os.WriteFile(nodePasswordFile, []byte(nodePassword+"\n"), 0600) + + if err = os.WriteFile(nodePasswordFile, []byte(nodePassword+"\n"), 0600); err != nil { + return nodePassword, err + } + + if err = configureACL(nodePassword); err != nil { + return nodePassword, err + } + + return nodePassword, nil } func upgradeOldNodePasswordPath(oldNodePasswordFile, newNodePasswordFile string) { diff --git a/pkg/agent/config/config_linux.go b/pkg/agent/config/config_linux.go index e2fbf9b71e..04746bb6ea 100644 --- a/pkg/agent/config/config_linux.go +++ b/pkg/agent/config/config_linux.go @@ -23,7 +23,7 @@ func applyCRIDockerdAddress(nodeConfig *config.Node) { } func applyContainerdQoSClassConfigFileIfPresent(envInfo *cmds.Agent, containerdConfig *config.Containerd) { - containerdConfigDir := filepath.Join(envInfo.DataDir, "agent", "etc", "containerd") + containerdConfigDir := filepath.Join(envInfo.DataDir, "agent", "etc", "containerd") blockioPath := filepath.Join(containerdConfigDir, "blockio_config.yaml") @@ -45,3 +45,9 @@ func applyContainerdQoSClassConfigFileIfPresent(envInfo *cmds.Agent, containerdC } } } + +// configureACL will configure an Access Control List for the specified file. +// On Linux, this function is a no-op +func configureACL(file string) error { + return nil +} diff --git a/pkg/agent/config/config_windows.go b/pkg/agent/config/config_windows.go index 226ea7a013..2763ad7693 100644 --- a/pkg/agent/config/config_windows.go +++ b/pkg/agent/config/config_windows.go @@ -6,8 +6,11 @@ package config import ( "path/filepath" + "github.com/k3s-io/k3s/pkg/agent/util/acl" "github.com/k3s-io/k3s/pkg/cli/cmds" "github.com/k3s-io/k3s/pkg/daemons/config" + "github.com/pkg/errors" + "golang.org/x/sys/windows" ) func applyContainerdStateAndAddress(nodeConfig *config.Node) { @@ -22,3 +25,19 @@ func applyCRIDockerdAddress(nodeConfig *config.Node) { func applyContainerdQoSClassConfigFileIfPresent(envInfo *cmds.Agent, containerdConfig *config.Containerd) { // QoS-class resource management not supported on windows. } + +// configureACL will configure an Access Control List for the specified file, +// ensuring that only the LocalSystem and Administrators Group have access to the file contents +func configureACL(file string) error { + // by default Apply will use the current user (LocalSystem in the case of a Windows service) + // as the owner and current user group as the allowed group + // additionally, we define a DACL to permit access to the file to the local system and all administrators + if err := acl.Apply(file, nil, nil, []windows.EXPLICIT_ACCESS{ + acl.GrantSid(windows.GENERIC_ALL, acl.LocalSystemSID()), + acl.GrantSid(windows.GENERIC_ALL, acl.BuiltinAdministratorsSID()), + }...); err != nil { + return errors.Wrapf(err, "failed to configure Access Control List For %s", file) + } + + return nil +} diff --git a/pkg/agent/util/acl/acl_windows.go b/pkg/agent/util/acl/acl_windows.go new file mode 100644 index 0000000000..6d9bab8819 --- /dev/null +++ b/pkg/agent/util/acl/acl_windows.go @@ -0,0 +1,166 @@ +//go:build windows +// +build windows + +package acl + +import ( + "fmt" + "golang.org/x/sys/windows" + "unsafe" +) + +// TODO: Remove in favor of the rancher/permissions repository once that is setup + +func BuiltinAdministratorsSID() *windows.SID { + return mustGetSid(windows.WinBuiltinAdministratorsSid) +} + +func LocalSystemSID() *windows.SID { + return mustGetSid(windows.WinLocalSystemSid) +} + +func mustGetSid(sidType windows.WELL_KNOWN_SID_TYPE) *windows.SID { + sid, err := windows.CreateWellKnownSid(sidType) + if err != nil { + panic(err) + } + return sid +} + +// GrantSid creates an EXPLICIT_ACCESS instance granting permissions to the provided SID. +func GrantSid(accessPermissions windows.ACCESS_MASK, sid *windows.SID) windows.EXPLICIT_ACCESS { + return windows.EXPLICIT_ACCESS{ + AccessPermissions: accessPermissions, + AccessMode: windows.GRANT_ACCESS, + Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT, + Trustee: windows.TRUSTEE{ + TrusteeForm: windows.TRUSTEE_IS_SID, + TrusteeValue: windows.TrusteeValueFromSID(sid), + }, + } +} + +// Apply performs both Chmod and Chown at the same time, where the filemode's owner and group will correspond to +// the provided owner and group (or the current owner and group, if they are set to nil) +func Apply(path string, owner *windows.SID, group *windows.SID, access ...windows.EXPLICIT_ACCESS) error { + if path == "" { + return fmt.Errorf("path cannot be empty") + } + return apply(path, owner, group, access...) +} + +// apply performs a Chmod (if owner and group are provided) and sets a custom ACL based on the provided EXPLICIT_ACCESS rules +// To create EXPLICIT_ACCESS rules, see the helper functions in pkg/access +func apply(path string, owner *windows.SID, group *windows.SID, access ...windows.EXPLICIT_ACCESS) error { + // assemble arguments + args := securityArgs{ + path: path, + owner: owner, + group: group, + access: access, + } + + securityInfo := args.ToSecurityInfo() + if securityInfo == 0 { + // nothing to change + return nil + } + dacl, err := args.ToDACL() + if err != nil { + return err + } + return windows.SetNamedSecurityInfo( + path, + windows.SE_FILE_OBJECT, + securityInfo, + owner, + group, + dacl, + nil, + ) +} + +type securityArgs struct { + path string + + owner *windows.SID + group *windows.SID + + access []windows.EXPLICIT_ACCESS +} + +func (a *securityArgs) ToSecurityInfo() windows.SECURITY_INFORMATION { + var securityInfo windows.SECURITY_INFORMATION + + if a.owner != nil { + // override owner + securityInfo |= windows.OWNER_SECURITY_INFORMATION + } + + if a.group != nil { + // override group + securityInfo |= windows.GROUP_SECURITY_INFORMATION + } + + if len(a.access) != 0 { + // override DACL + securityInfo |= windows.DACL_SECURITY_INFORMATION + securityInfo |= windows.PROTECTED_DACL_SECURITY_INFORMATION + } + + return securityInfo +} + +func (a *securityArgs) ToSecurityAttributes() (*windows.SecurityAttributes, error) { + // define empty security descriptor + sd, err := windows.NewSecurityDescriptor() + if err != nil { + return nil, err + } + err = sd.SetOwner(a.owner, false) + if err != nil { + return nil, err + } + err = sd.SetGroup(a.group, false) + if err != nil { + return nil, err + } + + // define security attributes using descriptor + var sa windows.SecurityAttributes + sa.Length = uint32(unsafe.Sizeof(sa)) + sa.SecurityDescriptor = sd + + if len(a.access) == 0 { + // security attribute should simply inherit parent rules + sa.InheritHandle = 1 + return &sa, nil + } + + // apply provided access rules to the DACL + dacl, err := a.ToDACL() + if err != nil { + return nil, err + } + err = sd.SetDACL(dacl, true, false) + if err != nil { + return nil, err + } + + // set the protected DACL flag to prevent the DACL of the security descriptor from being modified by inheritable ACEs + // (i.e. prevent parent folders from modifying this ACL) + err = sd.SetControl(windows.SE_DACL_PROTECTED, windows.SE_DACL_PROTECTED) + if err != nil { + return nil, err + } + + return &sa, nil +} + +func (a *securityArgs) ToDACL() (*windows.ACL, error) { + if len(a.access) == 0 { + // No rules were specified + return nil, nil + } + return windows.ACLFromEntries(a.access, nil) +}