Understanding OpenSSH’s Dual-Process Architecture: Why sshd Spawns Two Processes per Connection


1 views

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