diff options
author | Jason T. Greene <jason.greene@redhat.com> | 2022-01-29 03:10:28 -0600 |
---|---|---|
committer | Jason T. Greene <jason.greene@redhat.com> | 2022-02-16 03:49:17 -0600 |
commit | 8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe (patch) | |
tree | 5b81e5c9fccb7f2e44ce8c46729ec2c717507122 /cmd | |
parent | 98962a28f5d89fa7545688bbfca635744279ff7a (diff) | |
download | podman-8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe.tar.gz podman-8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe.tar.bz2 podman-8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe.zip |
Initial implementation of mac forwarding using a privileged docker sock claim helper
Signed-off-by: Jason T. Greene <jason.greene@redhat.com>
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/podman-mac-helper/install.go | 244 | ||||
-rw-r--r-- | cmd/podman-mac-helper/main.go | 149 | ||||
-rw-r--r-- | cmd/podman-mac-helper/service.go | 85 | ||||
-rw-r--r-- | cmd/podman-mac-helper/uninstall.go | 60 |
4 files changed, 538 insertions, 0 deletions
diff --git a/cmd/podman-mac-helper/install.go b/cmd/podman-mac-helper/install.go new file mode 100644 index 000000000..7f623ecb6 --- /dev/null +++ b/cmd/podman-mac-helper/install.go @@ -0,0 +1,244 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" + "text/template" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const ( + rwx_rx_rx = 0755 + rw_r_r = 0644 +) + +const launchConfig = `<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>com.github.containers.podman.helper-{{.User}}</string> + <key>ProgramArguments</key> + <array> + <string>{{.Program}}</string> + <string>service</string> + <string>{{.Target}}</string> + </array> + <key>inetdCompatibility</key> + <dict> + <key>Wait</key> + <false/> + </dict> + <key>UserName</key> + <string>root</string> + <key>Sockets</key> + <dict> + <key>Listeners</key> + <dict> + <key>SockFamily</key> + <string>Unix</string> + <key>SockPathName</key> + <string>/private/var/run/podman-helper-{{.User}}.socket</string> + <key>SockPathOwner</key> + <integer>{{.UID}}</integer> + <key>SockPathMode</key> + <!-- SockPathMode takes base 10 (384 = 0600) --> + <integer>384</integer> + <key>SockType</key> + <string>stream</string> + </dict> + </dict> +</dict> +</plist> +` + +type launchParams struct { + Program string + User string + UID string + Target string +} + +var installCmd = &cobra.Command{ + Use: "install", + Short: "installs the podman helper agent", + Long: "installs the podman helper agent, which manages the /var/run/docker.sock link", + PreRun: silentUsage, + RunE: install, +} + +func init() { + addPrefixFlag(installCmd) + rootCmd.AddCommand(installCmd) +} + +func install(cmd *cobra.Command, args []string) error { + userName, uid, homeDir, err := getUser() + if err != nil { + return err + } + + labelName := fmt.Sprintf("com.github.containers.podman.helper-%s.plist", userName) + fileName := filepath.Join("/Library", "LaunchDaemons", labelName) + + if _, err := os.Stat(fileName); err == nil || !os.IsNotExist(err) { + return errors.New("helper is already installed, uninstall first") + } + + prog, err := installExecutable(userName) + if err != nil { + return err + } + + target := filepath.Join(homeDir, ".local", "share", "containers", "podman", "machine", "podman.sock") + var buf bytes.Buffer + t := template.Must(template.New("launchdConfig").Parse(launchConfig)) + err = t.Execute(&buf, launchParams{prog, userName, uid, target}) + if err != nil { + return err + } + + file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, rw_r_r) + if err != nil { + return errors.Wrap(err, "error creating helper plist file") + } + defer file.Close() + _, err = buf.WriteTo(file) + if err != nil { + return err + } + + if err = runDetectErr("launchctl", "load", fileName); err != nil { + return errors.Wrap(err, "launchctl failed loading service") + } + + return nil +} + +func restrictRecursive(targetDir string, until string) error { + for targetDir != until && len(targetDir) > 1 { + info, err := os.Lstat(targetDir) + if err != nil { + return err + } + if info.Mode()&fs.ModeSymlink != 0 { + return errors.Errorf("symlinks not allowed in helper paths (remove them and rerun): %s", targetDir) + } + if err = os.Chown(targetDir, 0, 0); err != nil { + return errors.Wrap(err, "could not update ownership of helper path") + } + if err = os.Chmod(targetDir, rwx_rx_rx|fs.ModeSticky); err != nil { + return errors.Wrap(err, "could not update permissions of helper path") + } + targetDir = filepath.Dir(targetDir) + } + + return nil +} + +func verifyRootDeep(path string) error { + path = filepath.Clean(path) + current := "/" + segs := strings.Split(path, "/") + depth := 0 + for i := 1; i < len(segs); i++ { + seg := segs[i] + current = filepath.Join(current, seg) + info, err := os.Lstat(current) + if err != nil { + return err + } + + stat := info.Sys().(*syscall.Stat_t) + if stat.Uid != 0 { + return errors.Errorf("installation target path must be solely owned by root: %s is not", current) + } + + if info.Mode()&fs.ModeSymlink != 0 { + target, err := os.Readlink(current) + if err != nil { + return err + } + + targetParts := strings.Split(target, "/") + segs = append(targetParts, segs[i+1:]...) + + if depth++; depth > 1000 { + return errors.New("reached max recursion depth, link structure is cyclical or too complex") + } + + if !filepath.IsAbs(target) { + current = filepath.Dir(current) + i = -1 // Start at 0 + } else { + current = "/" + i = 0 // Skip empty first segment + } + } + } + + return nil +} + +func installExecutable(user string) (string, error) { + // Since the installed executable runs as root, as a precaution verify root ownership of + // the entire installation path, and utilize sticky + read only perms for the helper path + // suffix. The goal is to help users harden against privilege escalation from loose + // filesystem permissions. + // + // Since userpsace package management tools, such as brew, delegate management of system + // paths to standard unix users, the daemon executable is copied into a separate more + // restricted area of the filesystem. + if err := verifyRootDeep(installPrefix); err != nil { + return "", err + } + + targetDir := filepath.Join(installPrefix, "podman", "helper", user) + if err := os.MkdirAll(targetDir, rwx_rx_rx); err != nil { + return "", errors.Wrap(err, "could not create helper directory structure") + } + + // Correct any incorrect perms on previously existing directories and verify no symlinks + if err := restrictRecursive(targetDir, installPrefix); err != nil { + return "", err + } + + exec, err := os.Executable() + if err != nil { + return "", err + } + install := filepath.Join(targetDir, filepath.Base(exec)) + + return install, copyFile(install, exec, rwx_rx_rx) +} + +func copyFile(dest string, source string, perms fs.FileMode) error { + in, err := os.Open(source) + if err != nil { + return err + } + + defer in.Close() + out, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms) + if err != nil { + return err + } + + defer out.Close() + if _, err := io.Copy(out, in); err != nil { + return err + } + + return nil +} diff --git a/cmd/podman-mac-helper/main.go b/cmd/podman-mac-helper/main.go new file mode 100644 index 000000000..8d995519f --- /dev/null +++ b/cmd/podman-mac-helper/main.go @@ -0,0 +1,149 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const ( + defaultPrefix = "/usr/local" + dockerSock = "/var/run/docker.sock" +) + +var installPrefix string + +var rootCmd = &cobra.Command{ + Use: "podman-mac-helper", + Short: "A system helper to manage docker.sock", + Long: `podman-mac-helper is a system helper service and tool for managing docker.sock `, + CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, + SilenceErrors: true, +} + +// Note, this code is security sensitive since it runs under privilege. +// Limit actions to what is strictly necessary, and take appropriate +// safeguards +// +// After installation the service call is ran under launchd in a nowait +// inetd style fashion, so stdin, stdout, and stderr are all pointing to +// an accepted connection +// +// This service is installed once per user and will redirect +// /var/run/docker to the fixed user-assigned unix socket location. +// +// Control communication is restricted to each user specific service via +// unix file permissions + +func main() { + if os.Geteuid() != 0 { + fmt.Printf("This command must be ran as root via sudo or osascript\n") + os.Exit(1) + } + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + } +} + +func getUserInfo(name string) (string, string, string, error) { + // We exec id instead of using user.Lookup to remain compat + // with CGO disabled. + cmd := exec.Command("/usr/bin/id", "-P", name) + output, err := cmd.StdoutPipe() + if err != nil { + return "", "", "", err + } + + if err := cmd.Start(); err != nil { + return "", "", "", err + } + + entry := readCapped(output) + elements := strings.Split(entry, ":") + if len(elements) < 9 || elements[0] != name { + return "", "", "", errors.New("Could not lookup user") + } + + return elements[0], elements[2], elements[8], nil +} + +func getUser() (string, string, string, error) { + name, found := os.LookupEnv("SUDO_USER") + if !found { + name, found = os.LookupEnv("USER") + if !found { + return "", "", "", errors.New("could not determine user") + } + } + + _, uid, home, err := getUserInfo(name) + if err != nil { + return "", "", "", fmt.Errorf("could not lookup user: %s", name) + } + id, err := strconv.Atoi(uid) + if err != nil { + return "", "", "", fmt.Errorf("invalid uid for user: %s", name) + } + if id == 0 { + return "", "", "", fmt.Errorf("unexpected root user") + } + + return name, uid, home, nil +} + +// Used for commands that don't return a proper exit code +func runDetectErr(name string, args ...string) error { + cmd := exec.Command(name, args...) + errReader, err := cmd.StderrPipe() + if err != nil { + return err + } + + err = cmd.Start() + if err == nil { + errString := readCapped(errReader) + if len(errString) > 0 { + re := regexp.MustCompile(`\r?\n`) + err = errors.New(re.ReplaceAllString(errString, ": ")) + } + } + + if werr := cmd.Wait(); werr != nil { + err = werr + } + + return err +} + +func readCapped(reader io.Reader) string { + // Cap output + buffer := make([]byte, 2048) + n, _ := io.ReadFull(reader, buffer) + _, _ = io.Copy(ioutil.Discard, reader) + if n > 0 { + return string(buffer[:n]) + } + + return "" +} + +func addPrefixFlag(cmd *cobra.Command) { + cmd.Flags().StringVar(&installPrefix, "prefix", defaultPrefix, "Sets the install location prefix") +} + +func silentUsage(cmd *cobra.Command, args []string) { + cmd.SilenceUsage = true + cmd.SilenceErrors = true +} diff --git a/cmd/podman-mac-helper/service.go b/cmd/podman-mac-helper/service.go new file mode 100644 index 000000000..65cd89f34 --- /dev/null +++ b/cmd/podman-mac-helper/service.go @@ -0,0 +1,85 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "io" + "io/fs" + "os" + "time" + + "github.com/spf13/cobra" +) + +const ( + trigger = "GO\n" + fail = "NO" + success = "OK" +) + +var serviceCmd = &cobra.Command{ + Use: "service", + Short: "services requests", + Long: "services requests", + PreRun: silentUsage, + Run: serviceRun, + Hidden: true, +} + +func init() { + rootCmd.AddCommand(serviceCmd) +} + +func serviceRun(cmd *cobra.Command, args []string) { + info, err := os.Stdin.Stat() + if err != nil || info.Mode()&fs.ModeSocket == 0 { + fmt.Fprintln(os.Stderr, "This is an internal command that is not intended for standard terminal usage") + os.Exit(1) + } + + os.Exit(service()) +} + +func service() int { + defer os.Stdout.Close() + defer os.Stdin.Close() + defer os.Stderr.Close() + if len(os.Args) < 3 { + fmt.Print(fail) + return 1 + } + target := os.Args[2] + + request := make(chan bool) + go func() { + buf := make([]byte, 3) + _, err := io.ReadFull(os.Stdin, buf) + request <- err == nil && string(buf) == trigger + }() + + valid := false + select { + case valid = <-request: + case <-time.After(5 * time.Second): + } + + if !valid { + fmt.Println(fail) + return 2 + } + + err := os.Remove(dockerSock) + if err == nil || os.IsNotExist(err) { + err = os.Symlink(target, dockerSock) + } + + if err != nil { + fmt.Print(fail) + return 3 + } + + fmt.Print(success) + return 0 +} diff --git a/cmd/podman-mac-helper/uninstall.go b/cmd/podman-mac-helper/uninstall.go new file mode 100644 index 000000000..f72d0efd1 --- /dev/null +++ b/cmd/podman-mac-helper/uninstall.go @@ -0,0 +1,60 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "uninstalls the podman helper agent", + Long: "uninstalls the podman helper agent, which manages the /var/run/docker.sock link", + PreRun: silentUsage, + RunE: uninstall, +} + +func init() { + addPrefixFlag(uninstallCmd) + rootCmd.AddCommand(uninstallCmd) +} + +func uninstall(cmd *cobra.Command, args []string) error { + userName, _, _, err := getUser() + if err != nil { + return err + } + + labelName := fmt.Sprintf("com.github.containers.podman.helper-%s", userName) + fileName := filepath.Join("/Library", "LaunchDaemons", labelName+".plist") + + if err = runDetectErr("launchctl", "unload", fileName); err != nil { + // Try removing the service by label in case the service is half uninstalled + if rerr := runDetectErr("launchctl", "remove", labelName); rerr != nil { + // Exit code 3 = no service to remove + if exitErr, ok := rerr.(*exec.ExitError); !ok || exitErr.ExitCode() != 3 { + fmt.Fprintf(os.Stderr, "Warning: service unloading failed: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Warning: remove also failed: %s\n", rerr.Error()) + } + } + } + + if err := os.Remove(fileName); err != nil { + if !os.IsNotExist(err) { + return errors.Errorf("could not remove plist file: %s", fileName) + } + } + + helperPath := filepath.Join(installPrefix, "podman", "helper", userName) + if err := os.RemoveAll(helperPath); err != nil { + return errors.Errorf("could not remove helper binary path: %s", helperPath) + } + return nil +} |