How to Split Large SSH Config Files into Multiple Modular Configurations


2 views

When managing infrastructure at scale, SSH configuration files can quickly become monolithic beasts. A single ~/.ssh/config file containing hundreds or thousands of host definitions creates several practical problems:

  • Difficulty in locating specific host configurations
  • Version control conflicts when multiple team members edit the file
  • Increased risk of syntax errors in large files
  • Poor performance with some SSH clients when parsing huge configs

Since OpenSSH 7.3 (released in 2016), we've had the Include directive. This allows splitting configurations across multiple files while maintaining a single entry point:

# ~/.ssh/config
Include config.d/*.conf
Include config.d/production/*.conf
Include config.d/staging/*.conf

Here's how I organize my SSH configurations:

~/.ssh/
├── config              # Main file with Includes
├── config.d/
│   ├── aws.conf        # AWS hosts
│   ├── gcp.conf        # Google Cloud hosts
│   ├── internal.conf   # Corporate network
│   └── legacy/
│       ├── dc1.conf    # Old datacenter 1
│       └── dc2.conf    # Old datacenter 2
└── config.local        # Machine-specific overrides

For teams managing bastion hosts, this pattern works particularly well:

# ~/.ssh/config.d/bastion.conf
Host bastion-prod
  HostName 203.0.113.1
  User jumpuser
  IdentityFile ~/.ssh/bastion_prod_key

Host *.prod.internal
  ProxyJump bastion-prod
  User admin
  IdentityFile ~/.ssh/prod_key

For older SSH versions, you can use these approaches:

# Option 1: cat in shell rc file
alias ssh='cat ~/.ssh/config.d/* | ssh -F /dev/stdin'

# Option 2: Makefile concatenation
all:
    cat config.d/*.conf > config
  • Use .conf extension for included files for clarity
  • Document your include structure in the main config file
  • Set strict file permissions (chmod 600) on all config files
  • Consider version controlling your SSH config directory

When managing infrastructure at scale, especially with hundreds of remote hosts accessed through bastion servers, SSH configuration files can quickly become unmanageable. A single ~/.ssh/config file growing beyond 1000 lines presents several challenges:

  • Difficult to navigate and maintain
  • High risk of merge conflicts in version control
  • Performance impact on SSH client startup
  • No modularity for team collaboration

Since OpenSSH 7.3 (released 2016), there's been native support for including additional configuration files:

# ~/.ssh/config
Include config.d/*
Include config_bastions
Include config_development

This directive works similarly to how Apache or Nginx handle includes, letting you split configurations logically.

Here's how to structure a modular SSH configuration:

~/.ssh/
├── config              # Main file with includes
├── config.d/
│   ├── 00_common       # Shared settings
│   ├── 10_bastions     # Jump host configurations  
│   ├── 20_production   # Production servers
│   └── 30_development  # Dev environment
└── config_local        # Machine-specific overrides

Example include file (config.d/10_bastions):

# Bastion host configuration
Host bastion-prod
  HostName bastion.example.com
  User ec2-user
  IdentityFile ~/.ssh/prod_bastion_key
  ForwardAgent yes

Host *.prod.internal
  ProxyJump bastion-prod
  User admin
  IdentityFile ~/.ssh/prod_access_key

For systems running older OpenSSH versions, we can use these alternatives:

Option 1: Symbolic Links

cat config.d/* > ~/.ssh/config.tmp
mv ~/.ssh/config.tmp ~/.ssh/config

Option 2: Git Hooks

# .git/hooks/pre-commit
#!/bin/sh
cat config.d/* > config
git add config

For cloud environments where hosts change frequently, consider generating configurations:

#!/bin/bash
# generate_ssh_config.sh
aws ec2 describe-instances \
  --query 'Reservations[*].Instances[*].[Tags[?Key==`Name`].Value[],PublicIpAddress]' \
  --output text | \
while read name ip; do
  echo "Host $name"
  echo "  HostName $ip"
  echo "  User ec2-user"
  echo "  IdentityFile ~/.ssh/aws_key"
done > ~/.ssh/config.d/20_aws_instances
  • Set strict file permissions (chmod 600 ~/.ssh/config*)
  • Use HashKnownHosts to prevent information leakage
  • Consider separate identity files for different environments
  • Regularly audit included files for sensitive information

Modern development tools handle includes well:

  • VS Code: SSH FS extension respects includes
  • JetBrains IDEs: Native SSH config parser understands includes
  • Ansible: ssh_connection plugin follows include directives