mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
254 lines
7.5 KiB
Go
254 lines
7.5 KiB
Go
//go:build unix
|
|
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Exit codes for executor process communication
|
|
const (
|
|
ExitCodeSuccess = 0
|
|
ExitCodePrivilegeDropFail = 10
|
|
ExitCodeShellExecFail = 11
|
|
ExitCodeValidationFail = 12
|
|
)
|
|
|
|
// ExecutorConfig holds configuration for the executor process
|
|
type ExecutorConfig struct {
|
|
UID uint32
|
|
GID uint32
|
|
Groups []uint32
|
|
WorkingDir string
|
|
Shell string
|
|
Command string
|
|
PTY bool
|
|
}
|
|
|
|
// PrivilegeDropper handles secure privilege dropping in child processes
|
|
type PrivilegeDropper struct{}
|
|
|
|
// NewPrivilegeDropper creates a new privilege dropper
|
|
func NewPrivilegeDropper() *PrivilegeDropper {
|
|
return &PrivilegeDropper{}
|
|
}
|
|
|
|
// CreateExecutorCommand creates a command that spawns netbird ssh exec for privilege dropping
|
|
func (pd *PrivilegeDropper) CreateExecutorCommand(ctx context.Context, config ExecutorConfig) (*exec.Cmd, error) {
|
|
netbirdPath, err := os.Executable()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get netbird executable path: %w", err)
|
|
}
|
|
|
|
if err := pd.validatePrivileges(config.UID, config.GID); err != nil {
|
|
return nil, fmt.Errorf("invalid privileges: %w", err)
|
|
}
|
|
|
|
args := []string{
|
|
"ssh", "exec",
|
|
"--uid", fmt.Sprintf("%d", config.UID),
|
|
"--gid", fmt.Sprintf("%d", config.GID),
|
|
"--working-dir", config.WorkingDir,
|
|
"--shell", config.Shell,
|
|
}
|
|
|
|
for _, group := range config.Groups {
|
|
args = append(args, "--groups", fmt.Sprintf("%d", group))
|
|
}
|
|
|
|
if config.PTY {
|
|
args = append(args, "--pty")
|
|
}
|
|
|
|
if config.Command != "" {
|
|
args = append(args, "--cmd", config.Command)
|
|
}
|
|
|
|
// Log executor args safely - show all args except hide the command value
|
|
safeArgs := make([]string, len(args))
|
|
copy(safeArgs, args)
|
|
for i := 0; i < len(safeArgs)-1; i++ {
|
|
if safeArgs[i] == "--cmd" {
|
|
cmdParts := strings.Fields(safeArgs[i+1])
|
|
safeArgs[i+1] = safeLogCommand(cmdParts)
|
|
break
|
|
}
|
|
}
|
|
log.Tracef("creating executor command: %s %v", netbirdPath, safeArgs)
|
|
return exec.CommandContext(ctx, netbirdPath, args...), nil
|
|
}
|
|
|
|
// DropPrivileges performs privilege dropping with thread locking for security
|
|
func (pd *PrivilegeDropper) DropPrivileges(targetUID, targetGID uint32, supplementaryGroups []uint32) error {
|
|
if err := pd.validatePrivileges(targetUID, targetGID); err != nil {
|
|
return fmt.Errorf("invalid privileges: %w", err)
|
|
}
|
|
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
originalUID := os.Geteuid()
|
|
originalGID := os.Getegid()
|
|
|
|
if originalUID != int(targetUID) || originalGID != int(targetGID) {
|
|
if err := pd.setGroupsAndIDs(targetUID, targetGID, supplementaryGroups); err != nil {
|
|
return fmt.Errorf("set groups and IDs: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := pd.validatePrivilegeDropSuccess(targetUID, targetGID, originalUID, originalGID); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Tracef("successfully dropped privileges to UID=%d, GID=%d", targetUID, targetGID)
|
|
return nil
|
|
}
|
|
|
|
// setGroupsAndIDs sets the supplementary groups, GID, and UID
|
|
func (pd *PrivilegeDropper) setGroupsAndIDs(targetUID, targetGID uint32, supplementaryGroups []uint32) error {
|
|
groups := make([]int, len(supplementaryGroups))
|
|
for i, g := range supplementaryGroups {
|
|
groups[i] = int(g)
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
|
|
if len(groups) == 0 || groups[0] != int(targetGID) {
|
|
groups = append([]int{int(targetGID)}, groups...)
|
|
}
|
|
}
|
|
|
|
if err := syscall.Setgroups(groups); err != nil {
|
|
return fmt.Errorf("setgroups to %v: %w", groups, err)
|
|
}
|
|
|
|
if err := syscall.Setgid(int(targetGID)); err != nil {
|
|
return fmt.Errorf("setgid to %d: %w", targetGID, err)
|
|
}
|
|
|
|
if err := syscall.Setuid(int(targetUID)); err != nil {
|
|
return fmt.Errorf("setuid to %d: %w", targetUID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validatePrivilegeDropSuccess validates that privilege dropping was successful
|
|
func (pd *PrivilegeDropper) validatePrivilegeDropSuccess(targetUID, targetGID uint32, originalUID, originalGID int) error {
|
|
if err := pd.validatePrivilegeDropReversibility(targetUID, targetGID, originalUID, originalGID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := pd.validateCurrentPrivileges(targetUID, targetGID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validatePrivilegeDropReversibility ensures privileges cannot be restored
|
|
func (pd *PrivilegeDropper) validatePrivilegeDropReversibility(targetUID, targetGID uint32, originalUID, originalGID int) error {
|
|
if originalGID != int(targetGID) {
|
|
if err := syscall.Setegid(originalGID); err == nil {
|
|
return fmt.Errorf("privilege drop validation failed: able to restore original GID %d", originalGID)
|
|
}
|
|
}
|
|
if originalUID != int(targetUID) {
|
|
if err := syscall.Seteuid(originalUID); err == nil {
|
|
return fmt.Errorf("privilege drop validation failed: able to restore original UID %d", originalUID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateCurrentPrivileges validates the current UID and GID match the target
|
|
func (pd *PrivilegeDropper) validateCurrentPrivileges(targetUID, targetGID uint32) error {
|
|
currentUID := os.Geteuid()
|
|
if currentUID != int(targetUID) {
|
|
return fmt.Errorf("privilege drop validation failed: current UID %d, expected %d", currentUID, targetUID)
|
|
}
|
|
|
|
currentGID := os.Getegid()
|
|
if currentGID != int(targetGID) {
|
|
return fmt.Errorf("privilege drop validation failed: current GID %d, expected %d", currentGID, targetGID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExecuteWithPrivilegeDrop executes a command with privilege dropping, using exit codes to signal specific failures
|
|
func (pd *PrivilegeDropper) ExecuteWithPrivilegeDrop(ctx context.Context, config ExecutorConfig) {
|
|
log.Tracef("dropping privileges to UID=%d, GID=%d, groups=%v", config.UID, config.GID, config.Groups)
|
|
|
|
// TODO: Implement Pty support for executor path
|
|
if config.PTY {
|
|
config.PTY = false
|
|
}
|
|
|
|
if err := pd.DropPrivileges(config.UID, config.GID, config.Groups); err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "privilege drop failed: %v\n", err)
|
|
os.Exit(ExitCodePrivilegeDropFail)
|
|
}
|
|
|
|
if config.WorkingDir != "" {
|
|
if err := os.Chdir(config.WorkingDir); err != nil {
|
|
log.Debugf("failed to change to working directory %s, continuing with current directory: %v", config.WorkingDir, err)
|
|
}
|
|
}
|
|
|
|
var execCmd *exec.Cmd
|
|
if config.Command == "" {
|
|
os.Exit(ExitCodeSuccess)
|
|
}
|
|
|
|
execCmd = exec.CommandContext(ctx, config.Shell, "-c", config.Command)
|
|
execCmd.Stdin = os.Stdin
|
|
execCmd.Stdout = os.Stdout
|
|
execCmd.Stderr = os.Stderr
|
|
|
|
cmdParts := strings.Fields(config.Command)
|
|
safeCmd := safeLogCommand(cmdParts)
|
|
log.Tracef("executing %s -c %s", execCmd.Path, safeCmd)
|
|
if err := execCmd.Run(); err != nil {
|
|
var exitError *exec.ExitError
|
|
if errors.As(err, &exitError) {
|
|
// Normal command exit with non-zero code - not an SSH execution error
|
|
log.Tracef("command exited with code %d", exitError.ExitCode())
|
|
os.Exit(exitError.ExitCode())
|
|
}
|
|
|
|
// Actual execution failure (command not found, permission denied, etc.)
|
|
log.Debugf("command execution failed: %v", err)
|
|
os.Exit(ExitCodeShellExecFail)
|
|
}
|
|
|
|
os.Exit(ExitCodeSuccess)
|
|
}
|
|
|
|
// validatePrivileges validates that privilege dropping to the target UID/GID is allowed
|
|
func (pd *PrivilegeDropper) validatePrivileges(uid, gid uint32) error {
|
|
currentUID := uint32(os.Geteuid())
|
|
currentGID := uint32(os.Getegid())
|
|
|
|
// Allow same-user operations (no privilege dropping needed)
|
|
if uid == currentUID && gid == currentGID {
|
|
return nil
|
|
}
|
|
|
|
// Only root can drop privileges to other users
|
|
if currentUID != 0 {
|
|
return fmt.Errorf("cannot drop privileges from non-root user (UID %d) to UID %d", currentUID, uid)
|
|
}
|
|
|
|
// Root can drop to any user (including root itself)
|
|
return nil
|
|
}
|