Skip to content

Commit

Permalink
Merge pull request #846 from jlebon/pr/label-from-initrd
Browse files Browse the repository at this point in the history
selinux: perform relabeling from initrd
  • Loading branch information
jlebon authored Dec 6, 2019
2 parents 9278161 + a0fd624 commit a8f91fa
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 163 deletions.
29 changes: 2 additions & 27 deletions doc/operator-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,35 +37,10 @@ When resolving paths, Ignition follows symlinks on all but the last element of a

## SELinux

When using Ignition with distributions which have [SELinux][selinux] enabled, extra care must be taken to prevent Ignition from creating files that lack SELinux labels. Unfortunately, distributions do not typically include SELinux policies in the initramfs where Ignition runs, so any files, directories, and links created by Ignition don't receive the proper default SELinux labels.

A workaround for this issue is to use [`restorecon`][restorecon] in a oneshot systemd unit to relabel files that Ignition has touched. This unit can be set to run after the SELinux policies have loaded, but before services will try to use them.

An example of this unit is as follows:

```
[Unit]
Requires=systemd-udevd.target
After=systemd-udevd.target
Before=sssd.service
DefaultDependencies=no
ConditionFirstBoot=true
[Service]
Type=oneshot
ExecStart=/usr/sbin/restorecon /foo/bar /etc/test /etc/systemd/system/example.service /etc/passwd /etc/group /etc/shadow
[Install]
WantedBy=multi-user.target
```

This unit will vary based on the Ignition config it is being added to and the distribution that Ignition is running on. Notably the paths listed in the unit are all paths that Ignition caused to be modified or created, not just paths listed in `storage.files`. For example, if a new user is created then `/etc/passwd`, `/etc/shadow`, and `/etc/group` will all need to be relabeled.

If tooling is being used to generate Ignition configs, the tooling _should_ generate such a unit when creating a config for distributions which rely on SELinux.
Ignition fully supports distributions which have [SELinux][selinux] enabled. It requires that the distribution ships the [`setfiles`][setfiles] utility. The kernel must be at least v5.5 or alternatively have [this patch](https://lore.kernel.org/selinux/20190912133007.27545-1-jlebon@redhat.com/T/#u) backported.

[selinux]: https://selinuxproject.org/page/Main_Page
[restorecon]: https://linux.die.net/man/8/restorecon
[setfiles]: https://linux.die.net/man/8/setfiles

## Partition Reuse Semantics

Expand Down
23 changes: 10 additions & 13 deletions internal/distro/distro.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ var (
udevadmCmd = "udevadm"
usermodCmd = "usermod"
useraddCmd = "useradd"

// The restorecon tool is embedded inside of a systemd unit
// and as such requires the absolute path
restoreconCmd = "/usr/sbin/restorecon"
setfilesCmd = "setfiles"

// Filesystem tools
btrfsMkfsCmd = "mkfs.btrfs"
Expand Down Expand Up @@ -75,15 +72,15 @@ func DiskByPartUUIDDir() string { return diskByPartUUIDDir }
func KernelCmdlinePath() string { return kernelCmdlinePath }
func SystemConfigDir() string { return fromEnv("SYSTEM_CONFIG_DIR", systemConfigDir) }

func GroupaddCmd() string { return groupaddCmd }
func MdadmCmd() string { return mdadmCmd }
func MountCmd() string { return mountCmd }
func SgdiskCmd() string { return sgdiskCmd }
func ModprobeCmd() string { return modprobeCmd }
func UdevadmCmd() string { return udevadmCmd }
func UsermodCmd() string { return usermodCmd }
func UseraddCmd() string { return useraddCmd }
func RestoreconCmd() string { return restoreconCmd }
func GroupaddCmd() string { return groupaddCmd }
func MdadmCmd() string { return mdadmCmd }
func MountCmd() string { return mountCmd }
func SgdiskCmd() string { return sgdiskCmd }
func ModprobeCmd() string { return modprobeCmd }
func UdevadmCmd() string { return udevadmCmd }
func UsermodCmd() string { return usermodCmd }
func UseraddCmd() string { return useraddCmd }
func SetfilesCmd() string { return setfilesCmd }

func BtrfsMkfsCmd() string { return btrfsMkfsCmd }
func Ext4MkfsCmd() string { return ext4MkfsCmd }
Expand Down
117 changes: 16 additions & 101 deletions internal/exec/stages/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ package files
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/coreos/ignition/v2/config/v3_1_experimental/types"
"github.com/coreos/ignition/v2/internal/distro"
Expand All @@ -32,9 +29,6 @@ import (

const (
name = "files"

// see https://github.com/systemd/systemd/commit/65e183d7899eb3725d3009196ac4decf1090b580
relabelExtraDir = "/run/systemd/relabel-extra.d"
)

var (
Expand Down Expand Up @@ -87,17 +81,8 @@ func (s stage) Run(config types.Config) error {
return fmt.Errorf("failed to create units: %v", err)
}

// add systemd unit to relabel files
if err := s.addRelabelUnit(); err != nil {
return fmt.Errorf("failed to add relabel unit: %v", err)
}

// Add a file in /run/systemd/relabel-extra.d/ with paths that need to be relabeled
// as early as possible (e.g. systemd units so systemd can read them while building its
// graph). These are relabeled very early (right after policy load) so it cannot relabel
// across mounts. Only relabel things in /etc here.
if err := s.addRelabelExtraFile(); err != nil {
return fmt.Errorf("failed to write systemd relabel file: %v", err)
if err := s.relabelFiles(); err != nil {
return fmt.Errorf("failed to handle relabeling: %v", err)
}

return nil
Expand All @@ -106,23 +91,11 @@ func (s stage) Run(config types.Config) error {
// checkRelabeling determines whether relabeling is supported/requested so that
// we only collect filenames if we need to.
func (s *stage) checkRelabeling() error {
if !distro.SelinuxRelabel() || distro.RestoreconCmd() == "" {
if !distro.SelinuxRelabel() {
s.Logger.Debug("compiled without relabeling support, skipping")
return nil
}

path, err := s.JoinPath(distro.RestoreconCmd())
if err != nil {
return fmt.Errorf("error resolving path for %s: %v", distro.RestoreconCmd(), err)
}

_, err = os.Lstat(path)
if err != nil && os.IsNotExist(err) {
return fmt.Errorf("targeting root without %s, cannot relabel", distro.RestoreconCmd())
} else if err != nil {
return fmt.Errorf("error checking for %s in root: %v", distro.RestoreconCmd(), err)
}

// initialize to non-nil (whereas a nil slice means not to append, even
// though they're functionally equivalent)
s.toRelabel = []string{}
Expand All @@ -137,82 +110,24 @@ func (s *stage) relabeling() bool {
// relabel adds one or more paths to the list of paths that need relabeling.
func (s *stage) relabel(paths ...string) {
if s.toRelabel != nil {
s.toRelabel = append(s.toRelabel, paths...)
for _, path := range paths {
s.toRelabel = append(s.toRelabel, filepath.Join(s.DestDir, path))
}
}
}

// addRelabelUnit creates and enables a runtime systemd unit to run restorecon
// if there are files that need to be relabeled.
func (s *stage) addRelabelUnit() error {
if len(s.toRelabel) == 0 {
// relabelFiles relabels all the files that were marked for relabeling using
// the libselinux APIs.
func (s *stage) relabelFiles() error {
if s.toRelabel == nil || len(s.toRelabel) == 0 {
return nil
}
contents := `[Unit]
Description=Relabel files created by Ignition
DefaultDependencies=no
After=local-fs.target
Before=sysinit.target systemd-sysctl.service
ConditionSecurity=selinux
ConditionPathExists=/etc/selinux/ignition.relabel
OnFailure=emergency.target
OnFailureJobMode=replace-irreversibly
[Service]
Type=oneshot
ExecStart=` + distro.RestoreconCmd() + ` -0vRif /etc/selinux/ignition.relabel
ExecStart=/usr/bin/rm /etc/selinux/ignition.relabel
RemainAfterExit=yes`

// create the unit file itself
unit := types.Unit{
Name: "ignition-relabel.service",
Contents: &contents,
}

if err := s.writeSystemdUnit(unit, true); err != nil {
return err
}

if err := s.EnableRuntimeUnit(unit, "sysinit.target"); err != nil {
return err
}

// and now create the list of files to relabel
etcRelabelPath, err := s.JoinPath("etc/selinux/ignition.relabel")
if err != nil {
return err
}
f, err := os.Create(etcRelabelPath)
if err != nil {
return err
}
defer f.Close()

// yes, apparently the final \0 is needed
_, err = f.WriteString(strings.Join(s.toRelabel, "\000") + "\000")
return err
}

// addRelabelExtraFile writes a file to /run/systemd/relabel-extra.d/ with a list of files
// that should be relabeled immediately after policy load. In our case that's everything we
// wrote under /etc. This ensures systemd can access the files when building it's graph.
func (s stage) addRelabelExtraFile() error {
relabelFilePath := filepath.Join(relabelExtraDir, "ignition.relabel")
s.Logger.Info("adding relabel-extra.d/ file: %q", relabelFilePath)
defer s.Logger.Info("finished adding relabel file")

relabelFileContents := ""
for _, file := range s.toRelabel {
if strings.HasPrefix(file, "/etc") {
relabelFileContents += file + "\n"
}
}
if relabelFileContents == "" {
return nil
}
if err := os.MkdirAll(relabelExtraDir, 0755); err != nil {
return err
}
// We could go further here and use the `setfscreatecon` API so that we
// atomically create the files from the start with the right label, but (1)
// atomicity isn't really necessary here since there is not even a policy
// loaded and hence no MAC enforced, and (2) we'd still need after-the-fact
// labeling for files created by processes we call out to, like `useradd`.

return ioutil.WriteFile(relabelFilePath, []byte(relabelFileContents), 0644)
return s.RelabelFiles(s.toRelabel)
}
27 changes: 5 additions & 22 deletions internal/exec/stages/files/filesystemEntries.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,31 +254,14 @@ func (s *stage) relabelDirsForFile(path string) error {
if !s.relabeling() {
return nil
}
// relabel from the first parent dir that we'll have to create --
// alternatively, we could make `MkdirForFile` fancier instead of
// using `os.MkdirAll`, though that's quite a lot of levels to plumb
// through
relabelFrom := path
dir := filepath.Dir(path)
for {
exists := true
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
exists = false
} else if err != nil {
return err
}

// we're done on the first hit -- also sanity check we didn't
// somehow get all the way up to /sysroot
if exists || dir == s.DestDir {
break
}
relabelFrom = dir
dir = filepath.Dir(dir)
missing_dir, err := util.FindFirstMissingDirForFile(path)
if err != nil {
return err
}
// trim off prefix since this needs to be relative to the sysroot
s.relabel(relabelFrom[len(s.DestDir):])

// trim off prefix since this needs to be relative to the sysroot
s.relabel(missing_dir[len(s.DestDir):])
return nil
}

Expand Down
27 changes: 27 additions & 0 deletions internal/exec/stages/mount/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ func (s stage) mountFs(fs types.Filesystem) error {
return err
}

var firstMissing string
if distro.SelinuxRelabel() {
var err error
firstMissing, err = util.FindFirstMissingDirForFile(path)
if err != nil {
return err
}
}

if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return err
Expand All @@ -119,13 +128,31 @@ func (s stage) mountFs(fs types.Filesystem) error {
return err
}

if distro.SelinuxRelabel() {
if err := s.RelabelFiles([]string{firstMissing}); err != nil {
return err
}
}

args := translateOptionSliceToString(fs.MountOptions, ",")
cmd := exec.Command(distro.MountCmd(), "-o", args, "-t", *fs.Format, fs.Device, path)
if _, err := s.Logger.LogCmd(cmd,
"mounting %q at %q with type %q and options %q", fs.Device, path, *fs.Format, args,
); err != nil {
return err
}

if distro.SelinuxRelabel() {
// relabel the root of the disk if it's fresh
if isEmpty, err := util.DirIsEmpty(path); err != nil {
return fmt.Errorf("Checking if directory %s is empty: %v", path, err)
} else if isEmpty {
if err := s.RelabelFiles([]string{path}); err != nil {
return err
}
}
}

return nil
}

Expand Down
41 changes: 41 additions & 0 deletions internal/exec/util/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,47 @@ func MkdirForFile(path string) error {
return os.MkdirAll(filepath.Dir(path), DefaultDirectoryPermissions)
}

// FindFirstMissingDirForFile returns the first component which was found to be
// missing for the path.
func FindFirstMissingDirForFile(path string) (string, error) {
entry := path
dir := filepath.Dir(path)
for {
exists := true
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
exists = false
} else if err != nil {
return "", err
}

// also sanity check we didn't somehow get all the way up to /sysroot
if dir == "/" {
return "", fmt.Errorf("/ doesn't seem to exist")
}
if exists {
return entry, nil
}
entry = dir
dir = filepath.Dir(dir)
}
}

// DirIsEmpty checks whether a directory is empty.
// Adapted from https://stackoverflow.com/a/30708914
func DirIsEmpty(dirpath string) (bool, error) {
dfd, err := os.Open(dirpath)
if err != nil {
return false, err
}
defer dfd.Close()

_, err = dfd.Readdirnames(1)
if err == io.EOF {
return true, nil
}
return false, err
}

// getFileOwner will return the uid and gid for the file at a given path. If the
// file doesn't exist, or some other error is encountered when running stat on
// the path, 0, 0, and 0 will be returned.
Expand Down
Loading

0 comments on commit a8f91fa

Please sign in to comment.