diff --git a/config/config.go b/config/config.go index 6f68cd332..be9a4397d 100644 --- a/config/config.go +++ b/config/config.go @@ -1251,13 +1251,65 @@ func InitServer(ctx context.Context, currentServers server_structs.ServerType) e if !param.Cache_RunLocation.IsSet() && !param.Origin_RunLocation.IsSet() && param.Xrootd_RunLocation.IsSet() { return errors.New("Xrootd.RunLocation is set, but both modules are enabled. Please set Cache.RunLocation and Origin.RunLocation or disable Xrootd.RunLocation so the default location can be used.") } + } else if param.Server_DropPrivileges.GetBool() { + puser, err := GetPelicanUser() + if err != nil { + return errors.Wrapf(err, "set to drop privileges but no target OS username found for %s", param.Server_UnprivilegedUser.GetString()) + } + // Set up the directories for the server to run as a non-root user; + // for the most part, we need to recursively chown and chmod the directory + // so either root or pelican can access it. + pelicanLocations := []string{} + if currentServers.IsEnabled(server_structs.RegistryType) { + pelicanLocations = append(pelicanLocations, param.Registry_DbLocation.GetString()) + } + if currentServers.IsEnabled(server_structs.OriginType) { + pelicanLocations = append(pelicanLocations, param.Origin_DbLocation.GetString()) + } + if currentServers.IsEnabled(server_structs.DirectorType) { + pelicanLocations = append(pelicanLocations, param.Director_DbLocation.GetString(), param.Director_GeoIPLocation.GetString()) + } + if err = setFileAndDirPerms(pelicanLocations, 0750, 0640, puser.Uid, 0, true); err != nil { + return errors.Wrap(err, "failure when setting up the file permissions for pelican") + } + + pelicanLocationsNoRecursive := []string{} + if (currentServers.IsEnabled(server_structs.OriginType) || currentServers.IsEnabled(server_structs.CacheType)) && param.Shoveler_Enable.GetBool() { + pelicanLocationsNoRecursive = append(pelicanLocationsNoRecursive, param.Shoveler_AMQPTokenLocation.GetString()) + } + if err = setFileAndDirPerms(pelicanLocationsNoRecursive, 0750, 0640, puser.Uid, 0, false); err != nil { + return errors.Wrap(err, "failure when setting up the file permissions for pelican") + } + + pelicanDirs := []string{ + param.Monitoring_DataLocation.GetString(), + } + if currentServers.IsEnabled(server_structs.LocalCacheType) { + pelicanDirs = append(pelicanDirs, param.LocalCache_RunLocation.GetString()) + } + if currentServers.IsEnabled(server_structs.CacheType) && param.Cache_EnableLotman.GetBool() { + pelicanDirs = append(pelicanDirs, param.Lotman_LotHome.GetString(), param.Lotman_DbLocation.GetString()) + } + if (currentServers.IsEnabled(server_structs.OriginType) || currentServers.IsEnabled(server_structs.CacheType)) && param.Shoveler_Enable.GetBool() { + pelicanDirs = append(pelicanDirs, param.Shoveler_QueueDirectory.GetString()) + } + if currentServers.IsEnabled(server_structs.OriginType) { + pelicanDirs = append(pelicanDirs, param.Origin_GlobusConfigLocation.GetString()) + } + if err = setDirPerms(pelicanDirs, 0750, 0640, puser.Uid, puser.Gid, true); err != nil { + return errors.Wrap(err, "failure when setting up the directory permissions for pelican") + } } - if err := os.MkdirAll(param.Monitoring_DataLocation.GetString(), 0750); err != nil { + user, err := GetPelicanUser() + if err != nil { + return errors.Wrap(err, "no OS user found for pelican") + } + if err := MkdirAll(param.Monitoring_DataLocation.GetString(), 0750, user.Uid, user.Gid); err != nil { return errors.Wrapf(err, "failure when creating a directory for the monitoring data") } - if err := os.MkdirAll(param.Shoveler_QueueDirectory.GetString(), 0750); err != nil { + if err := MkdirAll(param.Shoveler_QueueDirectory.GetString(), 0750, user.Uid, user.Gid); err != nil { return errors.Wrapf(err, "failure when creating a directory for the shoveler on-disk queue") } if currentServers.IsEnabled(server_structs.OriginType) { diff --git a/config/init_server_creds.go b/config/init_server_creds.go index e2b30259e..8894ee9f4 100644 --- a/config/init_server_creds.go +++ b/config/init_server_creds.go @@ -189,20 +189,7 @@ func GeneratePrivateKey(keyLocation string, curve elliptic.Curve, allowRSA bool) if keyLocation == "" { return errors.New("failed to generate private key: key location is empty") } - uid, err := GetDaemonUID() - if err != nil { - return err - } - - gid, err := GetDaemonGID() - if err != nil { - return err - } - user, err := GetDaemonUser() - if err != nil { - return err - } - groupname, err := GetDaemonGroup() + user, err := GetPelicanUser() if err != nil { return err } @@ -222,7 +209,7 @@ func GeneratePrivateKey(keyLocation string, curve elliptic.Curve, allowRSA bool) log.Warningf("Will generate a new private key at location: %v", keyLocation) keyDir := filepath.Dir(keyLocation) - if err := MkdirAll(keyDir, 0750, -1, gid); err != nil { + if err := MkdirAll(keyDir, 0750, -1, user.Gid); err != nil { return err } // In this case, the private key file doesn't exist. @@ -235,16 +222,16 @@ func GeneratePrivateKey(keyLocation string, curve elliptic.Curve, allowRSA bool) // Windows does not have "chown", has to work differently currentOS := runtime.GOOS if currentOS == "windows" { - cmd := exec.Command("icacls", keyLocation, "/grant", user+":F") + cmd := exec.Command("icacls", keyLocation, "/grant", user.Username+":F") output, err := cmd.CombinedOutput() if err != nil { return errors.Wrapf(err, "Failed to chown generated key %v to daemon group %v: %s", - keyLocation, groupname, string(output)) + keyLocation, user.Groupname, string(output)) } } else { // Else we are running on linux/mac - if err = os.Chown(keyLocation, uid, gid); err != nil { + if err = os.Chown(keyLocation, user.Uid, user.Gid); err != nil { return errors.Wrapf(err, "Failed to chown generated key %v to daemon group %v", - keyLocation, groupname) + keyLocation, user.Groupname) } } @@ -273,15 +260,7 @@ func generatePrivateKeyToFile(file *os.File, curve elliptic.Curve) error { // for non-production environment so that we can use the private key of the CA // to sign the host certificate func GenerateCACert() error { - gid, err := GetDaemonGID() - if err != nil { - return err - } - groupname, err := GetDaemonGroup() - if err != nil { - return err - } - user, err := GetDaemonUser() + user, err := GetPelicanUser() if err != nil { return err } @@ -306,7 +285,7 @@ func GenerateCACert() error { // No existing CA cert present, generate a new CA root certificate and private key tlsCertDir := filepath.Dir(tlsCACert) - if err := MkdirAll(tlsCertDir, 0755, -1, gid); err != nil { + if err := MkdirAll(tlsCertDir, 0755, user.Uid, user.Gid); err != nil { return err } @@ -364,16 +343,16 @@ func GenerateCACert() error { // Windows does not have "chown", has to work differently currentOS := runtime.GOOS if currentOS == "windows" { - cmd := exec.Command("icacls", tlsCACert, "/grant", user+":F") + cmd := exec.Command("icacls", tlsCACert, "/grant", user.Username+":F") output, err := cmd.CombinedOutput() if err != nil { return errors.Wrapf(err, "Failed to chown generated key %v to daemon group %v: %s", - tlsCACert, groupname, string(output)) + tlsCACert, user.Groupname, string(output)) } } else { // Else we are running on linux/mac - if err = os.Chown(tlsCACert, -1, gid); err != nil { + if err = os.Chown(tlsCACert, user.Uid, user.Gid); err != nil { return errors.Wrapf(err, "Failed to chown generated key %v to daemon group %v", - tlsCACert, groupname) + tlsCACert, user.Groupname) } } @@ -408,7 +387,7 @@ func LoadCertificate(certFile string) (*x509.Certificate, error) { } } if cert == nil { - return nil, fmt.Errorf("Certificate file, %v, contains no certificate", certFile) + return nil, fmt.Errorf("certificate file, %v, contains no certificate", certFile) } return cert, nil } @@ -416,15 +395,7 @@ func LoadCertificate(certFile string) (*x509.Certificate, error) { // Generate a TLS certificate (host certificate) and its private key // for non-production environment if the required TLS files are not present func GenerateCert() error { - gid, err := GetDaemonGID() - if err != nil { - return err - } - groupname, err := GetDaemonGroup() - if err != nil { - return err - } - user, err := GetDaemonUser() + user, err := GetPelicanUser() if err != nil { return err } @@ -483,7 +454,7 @@ func GenerateCert() error { } tlsCertDir := filepath.Dir(tlsCert) - if err := MkdirAll(tlsCertDir, 0755, -1, gid); err != nil { + if err := MkdirAll(tlsCertDir, 0755, user.Uid, user.Gid); err != nil { return err } @@ -554,16 +525,16 @@ func GenerateCert() error { // Windows does not have "chown", has to work differently currentOS := runtime.GOOS if currentOS == "windows" { - cmd := exec.Command("icacls", tlsCert, "/grant", user+":F") + cmd := exec.Command("icacls", tlsCert, "/grant", user.Username+":F") output, err := cmd.CombinedOutput() if err != nil { return errors.Wrapf(err, "Failed to chown generated key %v to daemon group %v: %s", - tlsCert, groupname, string(output)) + tlsCert, user.Groupname, string(output)) } } else { // Else we are running on linux/mac - if err = os.Chown(tlsCert, -1, gid); err != nil { + if err = os.Chown(tlsCert, user.Uid, user.Gid); err != nil { return errors.Wrapf(err, "Failed to chown generated key %v to daemon group %v", - tlsCert, groupname) + tlsCert, user.Groupname) } } @@ -857,20 +828,7 @@ func GenerateSessionSecret() error { return errors.New("Empty filename for Server_SessionSecretFile") } - uid, err := GetDaemonUID() - if err != nil { - return err - } - - gid, err := GetDaemonGID() - if err != nil { - return err - } - user, err := GetDaemonUser() - if err != nil { - return err - } - groupname, err := GetDaemonGroup() + user, err := GetPelicanUser() if err != nil { return err } @@ -891,7 +849,7 @@ func GenerateSessionSecret() error { return errors.Wrap(err, "Failed to load session secret due to I/O error") } keyDir := filepath.Dir(secretLocation) - if err := MkdirAll(keyDir, 0750, -1, gid); err != nil { + if err := MkdirAll(keyDir, 0750, user.Uid, user.Gid); err != nil { return err } @@ -904,16 +862,16 @@ func GenerateSessionSecret() error { // Windows does not have "chown", has to work differently currentOS := runtime.GOOS if currentOS == "windows" { - cmd := exec.Command("icacls", secretLocation, "/grant", user+":F") + cmd := exec.Command("icacls", secretLocation, "/grant", user.Username+":F") output, err := cmd.CombinedOutput() if err != nil { return errors.Wrapf(err, "Failed to chown generated session secret %v to daemon group %v: %s", - secretLocation, groupname, string(output)) + secretLocation, user.Groupname, string(output)) } } else { // Else we are running on linux/mac - if err = os.Chown(secretLocation, uid, gid); err != nil { + if err = os.Chown(secretLocation, user.Uid, user.Gid); err != nil { return errors.Wrapf(err, "Failed to chown generated session secret %v to daemon group %v", - secretLocation, groupname) + secretLocation, user.Groupname) } } diff --git a/config/mkdirall.go b/config/mkdirall.go index fc6e035e7..934cddc59 100644 --- a/config/mkdirall.go +++ b/config/mkdirall.go @@ -21,6 +21,7 @@ package config import ( "os" "os/exec" + "path/filepath" "runtime" "syscall" @@ -101,3 +102,101 @@ func MkdirAll(path string, perm os.FileMode, uid int, gid int) error { } return nil } + +func setFileAndDirPerms(paths []string, dirPerm os.FileMode, perm os.FileMode, uid int, gid int, recursive bool) error { + dirs := map[string]bool{} + for _, path := range paths { + // Create the parent directory if it doesn't exist + dir := filepath.Dir(path) + err := MkdirAll(dir, dirPerm, uid, gid) + if err != nil { + return errors.Wrapf(err, "Failed to create directory %v", dir) + } + // Set the permissions on the parent directory + err = os.Chmod(dir, dirPerm) + if err != nil { + return errors.Wrapf(err, "Failed to set permissions on directory %v", dir) + } + if err = os.Chown(dir, uid, gid); err != nil { + return errors.Wrapf(err, "Failed to chown directory %v", dir) + } + if recursive { + dirs[dir] = true + } + // Skip the file if it doesn't exist + if _, err := os.Stat(path); os.IsNotExist(err) { + continue + } + // Set the permissions on the file + if err = os.Chmod(path, perm); err != nil { + return errors.Wrapf(err, "Failed to set permissions on file %v", path) + } + if err = os.Chown(path, uid, gid); err != nil { + return errors.Wrapf(err, "Failed to chown file %v", path) + } + } + // Set the permissions on all sub-directories, when recursive is set to true + for dir := range dirs { + if err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + itemPerm := perm + if d.IsDir() { + itemPerm = dirPerm + } + if err = os.Chmod(path, itemPerm); err != nil { + return errors.Wrapf(err, "Failed to set permissions on directory %v", path) + } + if err = os.Chown(path, uid, gid); err != nil { + return errors.Wrapf(err, "Failed to chown directory %v", path) + } + return nil + }); err != nil { + return errors.Wrapf(err, "Failed to walk directory %v", dir) + } + } + return nil +} + +func setDirPerms(paths []string, dirPerm os.FileMode, perm os.FileMode, uid int, gid int, recursive bool) error { + dirs := map[string]bool{} + for _, path := range paths { + if path == "" { + continue + } + dirs[path] = true + } + for dir := range dirs { + err := MkdirAll(dir, dirPerm, uid, gid) + if err != nil { + return errors.Wrapf(err, "Failed to create directory %v", dir) + } + err = os.Chmod(dir, dirPerm) + if err != nil { + return errors.Wrapf(err, "Failed to set permissions on directory %v", dir) + } + if err = os.Chown(dir, uid, gid); err != nil { + return errors.Wrapf(err, "Failed to chown directory %v", dir) + } + if err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + itemPerm := perm + if d.IsDir() { + itemPerm = dirPerm + } + if err = os.Chmod(path, itemPerm); err != nil { + return errors.Wrapf(err, "Failed to set permissions on directory %v", path) + } + if err = os.Chown(path, uid, gid); err != nil { + return errors.Wrapf(err, "Failed to chown directory %v", path) + } + return nil + }); err != nil { + return errors.Wrapf(err, "Failed to walk directory %v", dir) + } + } + return nil +} diff --git a/config/privs.go b/config/privs.go index d9e67ba39..8d306b76d 100644 --- a/config/privs.go +++ b/config/privs.go @@ -24,8 +24,11 @@ import ( "runtime" "strconv" "strings" + "sync" "github.com/pkg/errors" + + "github.com/pelicanplatform/pelican/param" ) type User struct { @@ -40,8 +43,11 @@ type User struct { var ( isRootExec bool - xrootdUser User - oa4mpUser User + xrootdUser User + oa4mpUser User + pelicanUser User + + pelicanOnce sync.Once ) func init() { @@ -50,16 +56,20 @@ func init() { xrootdUser = newUser() oa4mpUser = newUser() + pelicanUser = newUser() if isRootExec { xrootdUser = initUserObject("xrootd", nil) oa4mpUser = initUserObject("tomcat", nil) + pelicanUser = initUserObject("root", nil) } else if err != nil { xrootdUser.err = err oa4mpUser.err = err + pelicanUser.err = err } else { xrootdUser = initUserObject(userObj.Username, userObj) oa4mpUser = initUserObject(userObj.Username, userObj) + pelicanUser = initUserObject(userObj.Username, userObj) } } @@ -177,3 +187,12 @@ func GetDaemonGroup() (string, error) { func GetOA4MPUser() (User, error) { return oa4mpUser, oa4mpUser.err } + +func GetPelicanUser() (User, error) { + pelicanOnce.Do(func() { + if param.Server_DropPrivileges.GetBool() { + pelicanUser = initUserObject(param.Server_UnprivilegedUser.GetString(), nil) + } + }) + return pelicanUser, pelicanUser.err +} diff --git a/config/resources/defaults.yaml b/config/resources/defaults.yaml index f39c50d57..786ed779c 100644 --- a/config/resources/defaults.yaml +++ b/config/resources/defaults.yaml @@ -28,6 +28,7 @@ Server: RegistrationRetryInterval: 10s StartupTimeout: 10s UILoginRateLimit: 1 + UnprivilegedUser: pelican Director: DefaultResponse: cache CacheSortMethod: "distance" diff --git a/daemon/launch.go b/daemon/launch.go index 9096e34c3..349998185 100644 --- a/daemon/launch.go +++ b/daemon/launch.go @@ -26,6 +26,7 @@ type ( Launcher interface { Name() string Launch(ctx context.Context) (context.Context, int, error) + KillFunc() func(pid int, sig int) error } DaemonLauncher struct { @@ -34,6 +35,7 @@ type ( Uid int Gid int ExtraEnv []string + InheritFds []int } ) diff --git a/daemon/launch_unix.go b/daemon/launch_unix.go index fee01c518..abf01b9ac 100644 --- a/daemon/launch_unix.go +++ b/daemon/launch_unix.go @@ -42,10 +42,11 @@ import ( type ( launchInfo struct { - ctx context.Context - expiry time.Time - pid int - name string + ctx context.Context + expiry time.Time + pid int + name string + killFunc func(pid int, sig int) error } ) @@ -150,6 +151,13 @@ func (launcher DaemonLauncher) Launch(ctx context.Context) (context.Context, int cmd.Env = append(newEnv, launcher.ExtraEnv...) } + if len(launcher.InheritFds) > 0 { + cmd.ExtraFiles = make([]*os.File, len(launcher.InheritFds)) + for idx, fd := range launcher.InheritFds { + cmd.ExtraFiles[idx] = os.NewFile(uintptr(fd), "") + } + } + if err := cmd.Start(); err != nil { return ctx, -1, err } @@ -162,6 +170,19 @@ func (launcher DaemonLauncher) Launch(ctx context.Context) (context.Context, int return ctx_result, cmd.Process.Pid, nil } +func (launcher DaemonLauncher) KillFunc() func(pid int, sig int) error { + return func(pid int, sig int) error { + process, err := os.FindProcess(pid) + if err != nil { + return errors.Wrapf(err, "unable to find PID %d", pid) + } + if err = process.Signal(syscall.Signal(sig)); err != nil { + return errors.Wrapf(err, "failed to send signal %d to process with pid %d", sig, pid) + } + return nil + } +} + func LaunchDaemons(ctx context.Context, launchers []Launcher, egrp *errgroup.Group) (pids []int, err error) { daemons := make([]launchInfo, len(launchers)) @@ -180,6 +201,7 @@ func LaunchDaemons(ctx context.Context, launchers []Launcher, egrp *errgroup.Gro daemons[idx].ctx = newCtx daemons[idx].pid = pid daemons[idx].name = daemon.Name() + daemons[idx].killFunc = daemon.KillFunc() pids[idx] = pid log.Infoln("Successfully launched", daemon.Name()) metrics.SetComponentHealthStatus(metrics.HealthStatusComponent(metricName), metrics.StatusOK, "") @@ -211,7 +233,7 @@ func LaunchDaemons(ctx context.Context, launchers []Launcher, egrp *errgroup.Gro log.Warnf("Forwarding signal %v to daemons\n", sys_sig) var lastErr error for idx, daemon := range daemons { - if err = syscall.Kill(daemon.pid, sys_sig); err != nil { + if err = daemon.killFunc(daemon.pid, int(sys_sig)); err != nil { lastErr = errors.Wrapf(err, "Failed to forward signal to %s process", launchers[idx].Name()) } daemon.expiry = time.Now().Add(10 * time.Second) @@ -226,7 +248,7 @@ func LaunchDaemons(ctx context.Context, launchers []Launcher, egrp *errgroup.Gro // Kill the daemon if it's still alive exists := checkPIDExists(daemons[chosen].pid) if exists { - if err = syscall.Kill(daemons[chosen].pid, syscall.SIGTERM); err != nil { + if err = daemons[chosen].killFunc(daemons[chosen].pid, int(syscall.SIGTERM)); err != nil { err = errors.Wrapf(err, "Failed to kill %s with pid %d", daemons[chosen].name, daemons[chosen].pid) log.Errorln(err) return err @@ -253,7 +275,7 @@ func LaunchDaemons(ctx context.Context, launchers []Launcher, egrp *errgroup.Gro for idx, daemon := range daemons { // Daemon is expired, clean up if !daemon.expiry.IsZero() && time.Now().After(daemon.expiry) { - if err = syscall.Kill(daemon.pid, syscall.SIGKILL); err != nil { + if err = daemon.killFunc(daemon.pid, int(syscall.SIGKILL)); err != nil { err = errors.Wrapf(err, "Failed to SIGKILL the %s process", launchers[idx].Name()) log.Errorln(err) return err diff --git a/daemon/launch_windows.go b/daemon/launch_windows.go index fa31cd675..8ee8bdbaa 100644 --- a/daemon/launch_windows.go +++ b/daemon/launch_windows.go @@ -36,6 +36,12 @@ func (launcher DaemonLauncher) Launch(ctx context.Context) (context.Context, int return context.Background(), -1, errors.New("launching daemons is not supported on Windows") } +func (launcher DaemonLauncher) KillFunc() func(pid int, sig int) error { + return func(pid int, sig int) error { + return errors.New("killing daemons is not supported on Windows") + } +} + func ForwardCommandToLogger(ctx context.Context, daemonName string, cmdStdout io.ReadCloser, cmdStderr io.ReadCloser) { return } diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 508765cbf..ad812ed03 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -2193,6 +2193,21 @@ type: bool default: false components: ["cache", "director", "origin", "registry"] --- +name: Server.DropPrivileges +description: |+ + If the server has been started with root privileges, drop down to an unprivileged user. +type: bool +default: false +components: ["*"] +--- +name: Server.UnprivilegedUser +description: |+ + The user to run as after dropping root privileges. + This is only relevant if `Server.DropPrivileges` is set to true +type: string +default: pelican +components: ["*"] +--- ################################ # Issuer's Configurations # ################################ diff --git a/images/Dockerfile b/images/Dockerfile index c4b0765ec..4e16590ad 100644 --- a/images/Dockerfile +++ b/images/Dockerfile @@ -168,12 +168,17 @@ ENDRUN ################################################################# FROM hub.opensciencegrid.org/osg-htc/software-base:${OSG_SERIES}-${BASE_OS}-${OSG_REPO} AS pelican-software-base ARG TARGETPLATFORM +ARG PELICAN_USER=pelican WORKDIR /pelican RUN --mount=type=cache,id=dnf-${TARGETPLATFORM},target=/var/cache/dnf,sharing=locked <