Best Practices for Running Commands Without Root Privileges in Linux Shell Scripts


12 views

When writing administration scripts, we often face a security dilemma: while the script needs root privileges for some operations, most commands could (and should) run with normal user permissions. Running everything as root creates unnecessary security risks.

The traditional approach would be using su to switch to a specific user account, but this introduces several problems:


# Problematic approach - requires knowing the username
su someuser -c "command"

Most Linux systems come with a built-in nobody user specifically designed for running unprivileged operations. We can leverage this:


#!/bin/bash
# Privileged section
echo "Running as root"

# Switch to nobody for unprivileged operations
su nobody -s /bin/sh -c "echo 'Now running as nobody: \$(whoami)'"

# Back to root for privileged operations
echo "Back to root"

For more controlled environments, you could create and use a temporary user:


#!/bin/bash
# Create temporary user
TMP_USER="scriptuser_$(date +%s)"
useradd -M -s /bin/false $TMP_USER

# Use the temporary user
su $TMP_USER -c "echo 'Running as temporary user'"

# Cleanup
userdel $TMP_USER

For complex scripts, consider dropping privileges programmatically:


#!/bin/bash
drop_privileges() {
    if [ "$(id -u)" -eq 0 ]; then
        local target_user=${1:-nobody}
        local target_gid=$(id -g $target_user)
        local target_uid=$(id -u $target_user)
        
        # Drop group privileges first
        setgroups -g $target_gid
        # Then drop user privileges
        setuidgid $target_user sh -c "$2"
    else
        eval "$2"
    fi
}

# Usage:
drop_privileges nobody "echo 'Running as nobody: \$(whoami)'"
  • Always prefer built-in system users (nobody, daemon) when possible
  • If creating temporary users, ensure proper cleanup even if the script fails
  • Consider using setuidgid from daemontools for more controlled privilege dropping
  • Audit your script to ensure no privilege escalation paths exist

Here's how you might structure a package installation script:


#!/bin/bash
# Root-required operations
apt-get update

# Non-root operations
su nobody -s /bin/sh <<'ENDSCRIPT'
    # Safe environment for downloading
    wget https://example.com/package.tar.gz -O /tmp/package.tar.gz
    tar xzf /tmp/package.tar.gz -C /tmp
ENDSCRIPT

# Back to root for installation
dpkg -i /tmp/package/*.deb
rm -rf /tmp/package*

When writing administrative shell scripts, we often face a dilemma: the script needs root privileges for certain operations, but running the entire script as root creates unnecessary security risks. Any vulnerability or error in non-privileged portions could potentially be exploited with elevated permissions.

The simplest solution is to use sudo -u to temporarily switch to a less privileged user:

#!/bin/bash
# Privileged section
apt-get update

# Drop privileges
sudo -u nobody command_to_run_without_privileges

# Back to root
important_root_command

For more control, create a temporary restricted user:

#!/bin/bash
# Create restricted user
TEMP_USER="scriptuser_$(date +%s)"
useradd --system --shell /bin/false $TEMP_USER

# Use the user
sudo -u $TEMP_USER safe_command

# Clean up
userdel $TEMP_USER

For modern systems, consider capability-based privilege separation:

#!/bin/bash
# First part needs network admin capabilities
/sbin/setcap 'cap_net_admin=+ep' /path/to/network_tool

# Later sections run without capabilities
/sbin/setcap -r /path/to/network_tool
  • Always minimize the time spent with elevated privileges
  • Use trap to ensure cleanup even if script fails
  • Consider using runuser instead of sudo in some cases
  • Log all privilege transitions for security auditing
#!/bin/bash
# Root section
apt-get update

# Non-root section for logging
sudo -u nobody logger "Update completed at $(date)"

# Clean root operation
apt-get upgrade -y