mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 08:46:38 +00:00
Complete overhaul
This commit is contained in:
252
client/ssh/server/executor_unix.go
Normal file
252
client/ssh/server/executor_unix.go
Normal file
@@ -0,0 +1,252 @@
|
||||
//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 err := pd.setGroupsAndIDs(targetUID, targetGID, supplementaryGroups); err != nil {
|
||||
return 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 {
|
||||
log.Warnf("Pty requested but executor does not support Pty yet - continuing without Pty")
|
||||
config.PTY = false // Disable Pty and continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user