Short answer: ~/.ssh/config is the per-user configuration file of the OpenSSH client. A Host block gives a server a short alias and stores its address, user, port and key, so ssh myvps replaces ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@SERVER_IP, and scp, sftp, rsync and git pick up the same settings.
Host myvps
HostName SERVER_IP
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yesAfter saving it, ssh myvps logs in and scp backup.tar.gz myvps:/srv/ uploads. The one rule that explains most surprises: for each option, ssh uses the first value it finds, so specific hosts go at the top and Host * defaults at the bottom. Every directive, error message and output below was checked against OpenSSH 9.2 on Debian 12, most of it with ssh -G, which prints the settings ssh would use without connecting.
Where the file is and how to create it
| System | Per-user file | System-wide file |
|---|---|---|
| Linux, macOS, WSL | ~/.ssh/config | /etc/ssh/ssh_config and /etc/ssh/ssh_config.d/*.conf |
| Windows 10 and 11 | %USERPROFILE%\.ssh\config | %PROGRAMDATA%\ssh\ssh_config |
The file does not exist until you create it. ssh refuses to read it if anyone but you can write to it, so set the modes as well:
mkdir -p ~/.ssh && chmod 700 ~/.ssh
touch ~/.ssh/config && chmod 600 ~/.ssh/configOn Windows, create it in PowerShell with New-Item -ItemType File $env:USERPROFILE\.ssh\config (if ssh-keygen has not created the .ssh folder yet, run mkdir $env:USERPROFILE\.ssh first) and edit it in Notepad or VS Code. It has no extension, and it must be ASCII or UTF-8: Microsoft’s Win32-OpenSSH notes say UTF-16 is not supported, and that is what > redirection writes in Windows PowerShell 5.1. Installing the client, the ssh-agent service and PuTTY keys are covered in SSH on Windows.
A complete config to start from
# Specific hosts first, Host * last
Host myvps
HostName SERVER_IP
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Host bastion
HostName BASTION_IP
User admin
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Host db1
HostName 10.0.0.5
User postgres
ProxyJump bastion
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
AddKeysToAgent yesIndentation is only for readability; a block runs until the next Host or Match line. Keywords are case-insensitive, values are not, and comments go on their own line starting with #. The table lists what each directive does.
| Directive | What it does | Example |
|---|---|---|
| Host | Starts a block for the names you type; several names and * ? wildcards allowed | Host web-1 web-2 |
| HostName | The real address or DNS name to connect to | HostName SERVER_IP |
| User | Login name on the server | User deploy |
| Port | SSH port, default 22 | Port 2222 |
| IdentityFile | Private key to offer; may be repeated | IdentityFile ~/.ssh/id_ed25519 |
| IdentitiesOnly | Offer only the keys named here, not everything in the agent | IdentitiesOnly yes |
| ProxyJump | Reach the host through one or more jump hosts | ProxyJump bastion |
| ServerAliveInterval | Seconds of silence before ssh pings the server; 0 (off) by default | ServerAliveInterval 30 |
| ControlMaster | Share one connection between sessions | ControlMaster auto |
| StrictHostKeyChecking | accept-new saves new host keys but still refuses changed ones | StrictHostKeyChecking accept-new |
| LocalForward | A port forward opened on every login | LocalForward 8080 127.0.0.1:80 |
| DynamicForward | A SOCKS proxy on a local port that sends traffic out through the server | DynamicForward 1080 |
| ConnectTimeout | Seconds to wait for the connection instead of the system's much longer TCP timeout | ConnectTimeout 10 |
| Include | Reads more config files | Include config.d/*.conf |
| Match | A block chosen by conditions instead of the typed name | Match host 10.0.0.* |
First match wins: the order matters
ssh reads the command line first, then ~/.ssh/config from top to bottom, then the system-wide file, and keeps the first value it finds for each option. A common mistake is to put the defaults first:
Host *
User root
Host myvps
HostName 203.0.113.10
User deployuser root
hostname 203.0.113.10User deploy is ignored because Host * already set the user. Move the Host * block to the end and the alias gets deploy. Four more rules complete the picture:
Hostmatches the name you type, not theHostName. A blockHost 203.0.113.10does nothing forssh myvps, even whenmyvpspoints at that address.Match host, below, matches the address. A pattern with!in front is an exception:Host 10.0.0.* !10.0.0.1covers every address you type in that range except10.0.0.1.IdentityFile,LocalForward,RemoteForwardandCertificateFileadd up instead: anIdentityFileinHost *is offered after the host’s own key, not instead of it.- Command-line options beat the file:
ssh -p 22 myvpsuses port 22 for that one call, and-o Option=valueoverrides any directive the same way. ssh -F FILEreads only that file and skips/etc/ssh/ssh_config;ssh -F nonereads no file at all, which is how you test whether the config causes a problem.
See the effective settings with ssh -G
ssh -G ALIAS evaluates every Host and Match block and prints the result, about 80 lowercase lines, then exits without connecting. Filter it for the options you care about:
ssh -G myvps | grep -E '^(hostname|user|port|identityfile|proxyjump) 'In PowerShell, use ssh -G myvps | Select-String '^(hostname|user|port) '. For the example config, with 203.0.113.10 as the server’s address, it prints:
user deploy
hostname 203.0.113.10
port 2222
identityfile ~/.ssh/id_ed25519Ask for db1 and the same filter shows proxyjump bastion and seven identityfile lines, ssh’s default key names, because that block names no key. One caution: ssh -G runs the commands of any Match exec condition, just as a real login would.
To see why a value won, add -v: ssh then names every file it reads and every block that applied, with its line number. For the example config:
ssh -v -G myvps 2>&1 | grep -E 'Reading|Applying'debug1: Reading configuration data /root/.ssh/config
debug1: /root/.ssh/config line 3: Applying options for myvps
debug1: /root/.ssh/config line 21: Applying options for *
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 21: Applying options for *Per-host port, user and key
Port, User and IdentityFile are the three settings people move into the config first, because they are the ones that change between servers. Two details matter with keys:
IdentityFilenames the private key. A.pubfile works there only when the matching private key is loaded in your agent and not present as a file, as with keys kept in a password manager’s agent.- Without
IdentitiesOnly yes, ssh also offers every key in your agent, and a server that allows few attempts disconnects with Too many authentication failures before it reaches the right one.
One block can serve many hosts. Host web-1 web-2 web-3 with HostName %h.example.com sends ssh web-2 to web-2.example.com, because %h stands for the name you typed. The key itself has to be on the server first; ssh-copy-id reads the config too, so ssh-copy-id -i ~/.ssh/id_ed25519.pub myvps installs it on the right port and account. Which port your server really listens on is covered in SSH port 22.
Jump hosts with ProxyJump
ProxyJump bastion makes ssh log in to bastion first and open a tunnel from there to the target, so a server on a private network looks like any other alias. The jump host is reached with its own block, not with the target’s settings, so give it one, as in the example above. Then ssh db1, scp dump.sql db1:/tmp/ and rsync -a ./ db1:/srv/app/ all go through the bastion without extra flags.
- On the command line, the same is
ssh -J admin@BASTION_IP:2222 postgres@10.0.0.5. - Several hops are separated by commas:
ProxyJump hop1,hop2. - ProxyJump needs OpenSSH 7.3 or newer. On older clients write
ProxyCommand ssh -W %h:%p bastioninstead.
Your key never leaves your machine with ProxyJump: the bastion only forwards encrypted traffic. That makes it the safe alternative to agent forwarding, below.
Keep idle sessions alive
Home routers and firewalls drop NAT entries for connections that stay silent, and an idle ssh session then freezes or dies. ServerAliveInterval 30 makes ssh send a small request through the encrypted channel after 30 seconds without traffic. With ServerAliveCountMax 3 (the default), ssh gives up after about 90 seconds without an answer instead of hanging forever. TCPKeepAlive, on by default, works at the TCP level, can be spoofed and usually takes much longer to notice a dead link. When sessions still drop, Broken pipe goes through the server-side causes.
Reuse one connection with ControlMaster
Multiplexing lets the second and every later ssh, scp or rsync to a host ride on the connection that is already open: no new handshake, no second password or 2FA prompt, and scripts with many short ssh calls run much faster.
Host myvps
ControlMaster auto
ControlPath ~/.ssh/cm-%C
ControlPersist 10mauto uses an existing master or becomes one. %C is a hash of the local host, remote host, port and user, so every server and account gets its own socket, and the path stays well below the roughly 100-character limit for socket names. ControlPersist 10m keeps the master open in the background for ten minutes after the last session closes. To manage it:
ssh -O check myvps
ssh -O exit myvpscheck answers Master running (pid=…); without a master it prints Control socket connect(…): No such file or directory. Run exit after you edit the config, because new sessions keep using the old connection and its old settings. Windows’ built-in OpenSSH does not support ControlMaster at all; the Win32-OpenSSH project scope lists it as unsupported, so leave these lines out of a Windows config.
Split the file with Include
Include config.d/*.conf reads more files, relative to ~/.ssh in a user config, in alphabetical order. Put it above the first Host line. An Include below a Host block belongs to that block and is read only when that host matches. We tested it with a work-db entry in config.d/work.conf and the Include line at the end of the file, after Host myvps:
user root
hostname work-dbssh never read the included block, so a real login would look up work-db in DNS and fail. With the Include at the top, the same command printed user dba and hostname 10.0.0.5.
Conditional settings with Match
Match chooses a block by conditions. host compares the address after HostName was applied, originalhost the name you typed, user the remote login and localuser your local account; all matches everything, and ! negates a condition. This sends every connection into the private network through the bastion, whether you type an alias or an address:
Match host 10.0.0.*
ProxyJump bastionWith that block at the end of the example file, ssh -G db1 and ssh -G root@10.0.0.9 both show proxyjump bastion, while bastion itself does not. Match exec "COMMAND" applies a block when a local command exits with 0; it runs on every connection, so keep it fast.
Agent forwarding: per host, never in Host *
ForwardAgent yes lets the server use the keys in your local agent, for example to git pull from a private repository. While you are connected, anyone with root on that server can use your agent to log in wherever your keys are accepted; they cannot copy the keys, but they do not need to. Never set it under Host *. Use ProxyJump to hop between servers, a deploy key on the server for git, and if you do need the agent on a server, forward it for one session with ssh -A myvps instead of a permanent ForwardAgent yes.
Two GitHub accounts on one machine
Host github.com
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yesClone work repositories with the alias as the host: git clone git@github-work:ORG/REPO.git. For an existing checkout, change the remote with git remote set-url origin git@github-work:ORG/REPO.git. IdentitiesOnly yes is what makes this work: without it, ssh can offer a key from your agent before the one you named, GitHub accepts any key it knows, and you land in the wrong account.
Errors caused by the config file
Bad owner or permissions
Bad owner or permissions on /root/.ssh/configssh stops when others can write to the file, or when it belongs to a user other than you or root. In our test, mode 646 failed, while 644 and 600 worked. Fix it with chmod 600 ~/.ssh/config; if the file came from another account, take it over first with sudo chown $USER ~/.ssh/config. The chmod calculator translates the modes if you want to see what each digit allows.
Bad configuration option
/root/.ssh/config: line 2: Bad configuration option: usekeychain
/root/.ssh/config: terminating, 1 bad configuration optionsssh refuses to run at all when one directive is unknown. UseKeychain exists only in Apple’s build, so a config copied from a Mac breaks on Linux and Windows. Put IgnoreUnknown UseKeychain on the first line of the file; it applies only to options that come after it. A garbled first word on line 1 means the encoding is wrong: a UTF-16 file gives line 1: no argument after keyword "\377\376h", and a UTF-8 file with a byte order mark gives Bad configuration option: \357\273\277host. Save it again as UTF-8 without BOM.
Could not resolve hostname
ssh: Could not resolve hostname myvps: Name or service not knownNo block matched, so ssh treated the alias as a DNS name. Check the spelling in the Host line, check with ssh -v -G myvps which files were read, and remember that sudo ssh myvps runs as root and reads /root/.ssh/config, not yours. An alias defined in a file that is included below another Host block fails the same way.
Frequently asked
Where is the SSH config file?
On Linux and macOS it is ~/.ssh/config, on Windows C:\Users\NAME\.ssh\config (%USERPROFILE%\.ssh\config). It does not exist until you create it. The system-wide defaults are in /etc/ssh/ssh_config, or %PROGRAMDATA%\ssh\ssh_config on Windows. On Linux and macOS give your file mode 600, because ssh refuses a config that other users can write to.
Why does ssh ignore a setting in my config file?
ssh keeps the first value it finds for each option, so a Host * block at the top overrides everything below it: move it to the end. Host also matches the name you type, not the HostName it points to. Run ssh -G ALIAS to print the settings ssh will actually use, without connecting.
How do I use a different port or key for one server?
Give the server its own block: Host myvps, then HostName, User, Port 2222, IdentityFile ~/.ssh/id_ed25519 and IdentitiesOnly yes on the lines below. After that ssh myvps, scp file myvps:/srv/ and rsync use that port and key without any flags.