bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
//go:build darwin
|
||||
|
||||
package main
|
||||
|
||||
// The macOS half of sysservice.go: a launchd daemon in the system domain.
|
||||
//
|
||||
// launchctl is run by its absolute path with a fixed argument list. This code
|
||||
// runs as root, so a launchctl found through PATH would be whatever the
|
||||
// caller's PATH named first.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const launchctlPath = "/bin/launchctl"
|
||||
|
||||
// systemServiceInstalled reports whether the daemon's job file is in place. A
|
||||
// file that cannot be checked is reported as an error, which the caller treats
|
||||
// as installed for the guard (adoptSystemServiceState).
|
||||
func systemServiceInstalled() (bool, error) {
|
||||
_, err := os.Stat(launchdPlistPath)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// darwinOwnerMode is pathOwnerMode for this machine: Lstat, so a symlink is
|
||||
// seen as one rather than followed.
|
||||
func darwinOwnerMode(p string) (uint32, fs.FileMode, error) {
|
||||
fi, err := os.Lstat(p)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, 0, fmt.Errorf("no owner information for %s", p)
|
||||
}
|
||||
return st.Uid, fi.Mode(), nil
|
||||
}
|
||||
|
||||
func installSystemService() error {
|
||||
self, err := resolvedSelf()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot find this program's own path: %w", err)
|
||||
}
|
||||
if err := binaryChainRootOnly(self, darwinOwnerMode); err != nil {
|
||||
return fmt.Errorf("refusing to register %s as the system service: %w.\n"+
|
||||
" The installer's --networked option puts the program in a folder only root can change", self, err)
|
||||
}
|
||||
plist, err := launchdPlist(self)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeRootFile(launchdPlistPath, plist, 0o644); err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", launchdPlistPath, err)
|
||||
}
|
||||
// launchd creates a missing log file readable by everyone. What the bastion
|
||||
// reports about this machine (the agents it sees, the hosts it serves) is
|
||||
// the administrator's to read, so the file is made first, root-only, and
|
||||
// launchd appends to it as it finds it.
|
||||
if f, err := os.OpenFile(darwinDaemonLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
|
||||
_ = f.Close()
|
||||
}
|
||||
_ = os.Chmod(darwinDaemonLog, 0o600)
|
||||
// A job already loaded under this label keeps its old definition until it
|
||||
// is booted out, so a reinstall boots it out first. Not loaded is fine.
|
||||
_ = exec.Command(launchctlPath, "bootout", "system/"+launchdLabel).Run()
|
||||
// bootout returns before launchd has finished removing the job, and a
|
||||
// bootstrap in that window fails. A few short retries cover it.
|
||||
var out []byte
|
||||
for i := 0; i < 5; i++ {
|
||||
out, err = exec.Command(launchctlPath, "bootstrap", "system", launchdPlistPath).CombinedOutput()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("launchctl bootstrap failed: %v: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
_ = exec.Command(launchctlPath, "enable", "system/"+launchdLabel).Run()
|
||||
fmt.Printf("System service installed: launchd daemon %s, running %s as root.\n", launchdLabel, self)
|
||||
fmt.Println(" It starts at boot, before anyone signs in, and launchd restarts it whenever it stops.")
|
||||
fmt.Printf(" Its log is %s.\n", darwinDaemonLog)
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSystemService() error {
|
||||
_ = exec.Command(launchctlPath, "bootout", "system/"+launchdLabel).Run()
|
||||
if err := os.Remove(launchdPlistPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("cannot remove %s: %w", launchdPlistPath, err)
|
||||
}
|
||||
fmt.Printf("System service removed: %s is no longer registered with launchd.\n", launchdLabel)
|
||||
fmt.Println(" The bastion's state under /var/root is left in place.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func printSystemServiceStatus() error {
|
||||
installed, err := systemServiceInstalled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !installed {
|
||||
fmt.Println("No system service is installed on this machine.")
|
||||
return nil
|
||||
}
|
||||
out, err := exec.Command(launchctlPath, "print", "system/"+launchdLabel).CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("System service %s is installed but not loaded (launchctl: %s).\n", launchdLabel, strings.TrimSpace(string(out)))
|
||||
return nil
|
||||
}
|
||||
state, pid := "unknown", ""
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if v, ok := strings.CutPrefix(line, "state = "); ok && state == "unknown" {
|
||||
state = v
|
||||
}
|
||||
if v, ok := strings.CutPrefix(line, "pid = "); ok && pid == "" {
|
||||
pid = v
|
||||
}
|
||||
}
|
||||
if pid != "" {
|
||||
fmt.Printf("System service %s: %s (pid %s).\n", launchdLabel, state, pid)
|
||||
} else {
|
||||
fmt.Printf("System service %s: %s.\n", launchdLabel, state)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeRootFile writes a root-owned file with an exact mode, through a temp
|
||||
// file and a rename in the same directory, so launchd never reads half a job.
|
||||
func writeRootFile(p string, data []byte, mode os.FileMode) error {
|
||||
f, err := os.CreateTemp(filepath.Dir(p), ".privatae-bastion-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := f.Name()
|
||||
defer os.Remove(tmp)
|
||||
if _, err := f.Write(data); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chown(tmp, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmp, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, p)
|
||||
}
|
||||
Reference in New Issue
Block a user