Secure SSH Jump Host Configuration: Evaluating ForceCommand Security for Restricted User Access


2 views

In modern network security architectures, the jump host (or bastion host) pattern is widely adopted for controlled access to internal resources. The typical flow is:

Internet → Bastion → Internal Network

In this specific implementation, we're enforcing strict user-to-machine mapping:

  • User1 → Machine1
  • User2 → Machine2
  • ... (and so on)

The core configuration in /etc/ssh/sshd_config on the bastion host uses Match blocks with ForceCommand:

Match User User1
    ForceCommand ssh User1@Machine1 $SSH_ORIGINAL_COMMAND

Let's examine the security aspects of this approach:

Access Control

The configuration effectively creates a "pass-through" authentication where:

  • Users cannot get an interactive shell on the bastion
  • All commands are immediately forwarded to the target machine
  • The $SSH_ORIGINAL_COMMAND preserves the original command intent

Potential Weaknesses

While generally secure, consider these edge cases:

# File transfer scenarios:
scp -P 22 user1@bastion:/tmp/testfile .

# This would actually attempt to transfer from Machine1
# but might confuse auditing systems

For production environments, consider adding these security measures:

Match User User1
    ForceCommand ssh -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/dev/null \
    -o LogLevel=VERBOSE -i /etc/ssh/jump_host_keys/user1 user1@Machine1 $SSH_ORIGINAL_COMMAND
    PermitTTY no
    X11Forwarding no
    PermitTunnel no
    AllowAgentForwarding no
    AllowTcpForwarding no

Other methods to consider:

Method Pros Cons
ForceCommand Simple, no client changes Limited control over destination
ProxyCommand More flexible routing Requires client config
SSH Certificates Fine-grained access control Complex PKI setup

Ensure proper logging is configured on both bastion and target machines:

# In sshd_config on bastion:
Match User User1
    ForceCommand logger -p authpriv.info "SSH Forward: %u to Machine1"; \
    ssh user1@Machine1 $SSH_ORIGINAL_COMMAND
  • Use separate SSH key pairs for bastion and target machine access
  • Implement command restrictions on target machines using authorized_keys options
  • Regularly audit ForceCommand rules and user assignments
  • Consider pairing with TCP wrappers for additional filtering

The typical jump host (bastion host) setup involves three logical segments:

Internet → Bastion → Internal Network

In this scenario, we're implementing granular access control where each external user gets tunneled to exactly one internal machine. The critical security requirement is preventing any direct access to the bastion itself.

The configuration shown in the original question achieves this through OpenSSH's Match blocks:

Match User User1
    ForceCommand ssh -t User1@Machine1 $SSH_ORIGINAL_COMMAND
Match User User2
    ForceCommand ssh -t User2@Machine2 $SSH_ORIGINAL_COMMAND

Key security aspects:

  • The -t flag forces pseudo-terminal allocation for interactive sessions
  • $SSH_ORIGINAL_COMMAND preserves the original command structure
  • No shell access is granted on the bastion

To test the effectiveness of this setup:

# Attempt to execute local commands on bastion
ssh User1@bastion 'ls /tmp'

# Result: Command gets forwarded to Machine1 instead
# Actual output shows Machine1's /tmp contents

For enhanced security, consider these additions:

Match User *
    # Prevent any shell access
    PermitTTY no
    X11Forwarding no
    PermitTunnel no
    AllowAgentForwarding no
    AllowTcpForwarding no
    
Match User User1
    ForceCommand ssh -o StrictHostKeyChecking=yes -i /path/to/restricted_key User1@Machine1 $SSH_ORIGINAL_COMMAND
    AuthenticationMethods publickey
    PubkeyAuthentication yes
    PasswordAuthentication no

While generally secure, be aware of:

  1. Environment variable leakage (mitigate with PermitUserEnvironment no)
  2. SSH protocol weaknesses (enforce modern protocols with Protocol 2)
  3. Command injection through $SSH_ORIGINAL_COMMAND (validate commands if possible)

For more control, replace ForceCommand with a restricted shell script:

Match User User1
    ForceCommand /usr/local/bin/restricted_ssh_wrapper.sh

# Content of restricted_ssh_wrapper.sh:
#!/bin/bash
# Validate command patterns
case "$SSH_ORIGINAL_COMMAND" in
    "scp"*|"rsync"*|"git-"*)
        ssh -q -i /path/to/key User1@Machine1 "$SSH_ORIGINAL_COMMAND"
        ;;
    *)
        echo "Command not allowed"
        exit 1
        ;;
esac

Essential logging configuration for the bastion host:

# In /etc/ssh/sshd_config
SyslogFacility AUTH
LogLevel VERBOSE

# Example log entry pattern:
# Accepted publickey for User1 from 1.2.3.4 port 12345 ssh2: forced-command "ssh User1@Machine1"