When examining an OpenSSH server's process tree, you'll notice a consistent pattern of two processes per authenticated connection:
sshd: root [priv] (PID 26577, parent PID 26135)
sshd: user@pts/N (PID 26582, parent PID 26577)
OpenSSH implements privilege separation as a core security measure. This architecture:
- Creates a privileged monitor process (running as root)
- Spawns an unprivileged child process (running as the authenticated user)
- Minimizes attack surface by limiting root privileges
The privileged process (marked [priv]
) handles:
1. Authentication (PAM, public key verification)
2. Network communication (encryption/decryption)
3. Privileged operations (chroot, setuid)
Meanwhile, the unprivileged process (marked user@pts/N
) manages:
1. User session initialization
2. Shell/command execution
3. Environment setup
You can inspect the forking behavior in OpenSSH's source (serverloop.c):
/* Privilege separation parent */
if (privsep_is_preauth) {
/* Handles pre-auth network communication */
process_network_input();
} else {
/* Unprivileged child process */
start_session();
exec_shell();
}
This design explains several behaviors:
- Why
strace
shows different system calls for each process - How OpenSSH maintains security during credential passing
- Why some debugging requires attaching to both processes
When debugging SSH connection issues:
# Attach to privileged process
sudo strace -p 26577
# Monitor user process (as the connecting user)
strace -p 26582
The dual-process architecture remains consistent across OpenSSH versions, though implementation details may vary slightly between releases.
sshd: test [priv] (PID 26577, running as root)
sshd: test@pts/30 (PID 26582, running as user 'test')
This isn't a bug - it's a deliberate security design called privilege separation. Let's break down what each process does:
sshd: test [priv]
This process handles:
- Network communication (encryption/decryption)
- Pre-authentication operations
- Privileged operations like:
if (authenticated) {
seteuid(target_uid);
setegid(target_gid);
/* Now drop root completely */
setuid(target_uid);
setgid(target_gid);
}
sshd: test@pts/30
This process:
- Runs with the logged-in user's permissions
- Manages the user's shell session
- Handles post-authentication operations
Consider this vulnerability scenario:
// Hypothetical vulnerable code
void process_network_packet() {
char buffer[256];
read_packet(buffer); // Potential buffer overflow
if (is_authenticated) {
launch_shell();
}
}
With privilege separation:
- Exploiting the buffer overflow only compromises the unprivileged process
- Attacker can't escalate to root through the vulnerability
The magic happens through:
// In the privileged parent
int sockpair[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair);
pid_t child = fork();
if (child == 0) {
// Child process - drop privileges
close(sockpair[0]);
drop_privileges();
handle_session(sockpair[1]);
} else {
// Parent process
close(sockpair[1]);
monitor_session(sockpair[0]);
}
Relevant sshd_config directives:
# Disable privilege separation (NOT recommended)
UsePrivilegeSeparation no
# Sandbox options (modern OpenSSH)
UsePrivilegeSeparation sandbox
To see the privilege separation in action:
strace -f -p pgrep -f "sshd: test" -o sshd_trace.txt
You'll observe:
- setuid()/setgid() calls in the privileged process
- IPC communication between the processes