SSH dotfiles: unlocking efficiency

Managing dozens of SSH connections means remembering complex hostnames, multiple keys, and elaborate commands you copy from text files. The .ssh/config file transforms this chaos into memorable aliases that map mental shortcuts to complete configurations, reducing cognitive load so you can focus on actual work rather than SSH incantations.

I used to keep a text file with SSH commands. Each entry was a small novel—hostname, username, port number, identity file path, sometimes a ProxyJump directive for servers behind bastions. I'd copy the command, paste it into the terminal, inevitably get the path wrong, curse, fix it, try again. For deployment servers, I'd type the same 80-character command multiple times daily. The cognitive overhead wasn't the typing—it was remembering which key went with which server, which port, which username.

Then someone showed me their SSH config file. They typed ssh prod and connected to a production server that would have taken me three lines to reach. They typed ssh db and opened a secure tunnel to a remote database. No copy-pasting. No remembering. Just aliases that mapped mental shortcuts to complex configurations.

What is SSH

SSH (Secure Shell) is the network protocol that secures remote server access through strong encryption. Every developer and sysadmin uses it daily—connecting to servers, deploying code, managing infrastructure, accessing remote resources. The protocol itself is straightforward: authenticate, establish encrypted channel, execute commands remotely.

The complexity comes from managing dozens or hundreds of these connections. Different servers require different keys. Some sit behind bastion hosts. Others need port forwarding for database access. Each connection carries its own hostname, username, port, and identity file. Without organisation, this becomes dozens of commands to remember or a text file to copy from.

The .ssh/config file transforms this chaos into simplicity. It's a dotfile that maps memorable aliases to complete SSH configurations, turning labyrinthine commands into single-word shortcuts. This isn't convenience for its own sake—it's reducing cognitive load so you can focus on the actual work rather than SSH incantations.

Specifying identity files for different servers

The first pain point most people encounter is managing multiple SSH keys. You have one key for GitHub, another for your company's servers, a third for AWS instances, perhaps a fourth for client infrastructure. SSH will try keys in sequence, but this creates delays, authentication failures, and the occasional lockout when you've exceeded failed attempt limits.

The IdentityFile directive solves this by mapping specific keys to specific servers. Each host gets exactly the key it expects, every time, automatically. No manual specification. No trying wrong keys. The configuration handles it.

Host server                             # Alias for the remote server.
    HostName server.mycompany.com       # The hostname or IP address of the remote server.
    User myusername                     # The username for the remote server.
    IdentityFile ~/.ssh/my_id_rsa       # The private key for authentication.

Now connecting requires just the alias:

ssh server

Instead of ssh -i ~/.ssh/my_id_rsa myusername@server.mycompany.com, you type two words. The configuration remembers everything else—the hostname, the username, which key to use. You've offloaded that mental overhead to the config file where it belongs.

Enabling agent forwarding

A common workflow involves SSH-ing to a server, then from that server connecting to another—perhaps deploying code from a Git repository that requires authentication, or accessing an internal server from a bastion host. The naive approach copies your private key to the intermediate server. This works, but it's a security disaster waiting to happen. Your private key now lives on a server you don't fully control, creating an additional point of vulnerability.

Agent forwarding solves this elegantly. Your key stays on your local machine. When the remote server needs to authenticate, it forwards the request back to your local SSH agent, which handles authentication without ever exposing the private key. If the remote server gets compromised, the attacker gains no access to your keys—they've evaporated the moment your SSH session closes.

Host deploy-server                      # Alias for the remote deployment server.
    HostName deploy.mycompany.com       # The hostname or IP address of the remote deployment server.
    ForwardAgent yes                    # SSH client is instructed to forward the authentication request from the remote server back to your local machine

When you connect from deploy-server to another server requiring your SSH key, the authentication check forwards back to your local machine where your SSH agent handles it. No keys copied. No additional vulnerability.

Setting up port forwarding

Database access creates its own set of problems. Production databases shouldn't be exposed to the internet—that's basic security. But you need to connect to them for debugging, migrations, or administrative work. The standard solution involves opening database ports to specific IP addresses, configuring firewall rules, managing security groups, and hoping you didn't create a hole an attacker can exploit.

SSH port forwarding provides a better approach. You create an encrypted tunnel through SSH, forwarding a local port to the remote database port. Your local tools connect to localhost, but the connection routes through the SSH tunnel to the remote database. The database never needs public exposure. All traffic flows through the encrypted SSH connection. Your development tools work as if the database were local.

Host db
    HostName remote.example.com               # The hostname or IP address of the remote server where the database is hosted
    User remote-user                          # The user account on the remote server you will log in as
    LocalForward 3306 127.0.0.1:3306          # Bind the local port 3306 to the remote port 3306
    IdentityFile ~/.ssh/id_rsa                # The path to your private key for authentication
    ServerAliveInterval 120                   # Keep the SSH tunnel alive by sending a packet every 120 seconds
    ServerAliveCountMax 3                     # Number of server alive messages which may be sent without ssh receiving any messages back from the server.

Open the tunnel with the -N flag, which tells SSH you're not running commands—just establishing the port forward:

ssh -N db

The terminal appears to hang. That's expected—the tunnel is open and listening. Your database tools now connect to localhost:3306, and traffic routes securely through the SSH tunnel to the remote database. The ServerAliveInterval settings keep the tunnel alive even during periods of inactivity, preventing annoying disconnections mid-query.

SSH alias for a Git repository

Multiple Git hosting providers create their own friction. GitHub uses one URL format and key. GitLab uses another. Bitbucket uses a third. When you're cloning repositories, the full SSH path becomes tedious to type or remember: git clone git@github.com:organisation/repository.git versus git clone git@gitlab.com:organisation/repository.git. Get the hostname wrong, and authentication fails. Use the wrong key, and you're locked out.

SSH aliases reduce this to manageable shortcuts. Instead of remembering full paths and hostnames, you define an alias like gh for GitHub, gl for GitLab, bb for Bitbucket. The config handles the rest—correct hostname, correct user, correct key.

Host gh                                   # Alias for the remote Git repository.
    HostName github.com                   # The hostname or IP address of the Git server.
    User git                              # The username for the Git repository.
    Port 22                               # SSH port number, if it's not the default port (22).
    IdentityFile ~/.ssh/github_id_rsa     # The private key for authentication.

Cloning becomes trivial:

git clone ssh://gh/path/to/repo.git

The alias handles hostname and authentication. You focus on the repository path. Switch between providers by changing the alias prefix—gh for GitHub, gl for GitLab. The muscle memory stays consistent even as the underlying infrastructure changes.

SSH alias for a bastion (or any other) server

AWS hostnames are cartoonishly long: ec2-192-168-0-1.us-east-1.compute.amazonaws.com. Try typing that accurately multiple times per day. Add in the username, the specific PEM file for that instance, perhaps a non-standard port, and you're looking at commands that span multiple lines or require copy-pasting from documentation.

SSH aliases collapse this to something memorable. ssh bastion instead of the full incantation. The config file remembers the hostname, username, and key. You remember one word.

Host bastion                                                           # Alias for the bastion server.
    HostName ec2-192-168-0-1.us-east-1.compute.amazonaws.com           # The hostname or IP address of the bastion server.
    User myuser                                                        # The username on the bastion server.
    IdentityFile ~/.ssh/bastion.pem                                    # The private key to authenticate to the bastion server.

Connection becomes:

ssh bastion

Two words. The entire AWS hostname, username, and PEM file abstracted away. This pattern applies to any server—production instances, staging environments, database servers, monitoring hosts. Each gets a memorable alias. The config handles complexity.

SSH alias for a server behind a jump host

Internal infrastructure commonly sits behind bastion hosts for security—no direct internet exposure, all access funnelled through a hardened jump server. The traditional approach requires two SSH commands: connect to the bastion, then from there connect to the internal server. You're managing two sessions, two authentications, often with different keys for each hop.

The ProxyJump directive eliminates this friction. You specify the bastion as a jump host in your config, and SSH handles the two-hop connection transparently. You type ssh www and SSH automatically connects through the bastion to reach the internal server. One command. One authentication on your end. The complexity remains in the config where it belongs.

Host www                              # Alias for the internal web server.
    HostName 169.254.1.1              # The hostname or IP address of the web server.
    User webadmin                     # The username on the web server.
    IdentityFile ~/.ssh/web_id_rsa    # The private key to authenticate to the web server.
    ProxyJump bastion                 # Specifies the bastion server as a jump host.

Connect directly to the internal server via the bastion:

ssh www

SSH establishes the connection to the bastion, authenticates, then immediately establishes the second connection to the internal server—all transparently. You see one authentication prompt. The rest happens behind the scenes. Your internal infrastructure remains protected whilst remaining accessible.


The .ssh/config file transforms SSH from a tool you fight with into infrastructure that stays out of your way. Complex hostnames become memorable aliases. Multiple keys map automatically to the correct servers. Agent forwarding eliminates the need to copy private keys to remote systems. Port forwarding provides secure access to services that shouldn't face the internet. ProxyJump configurations collapse multi-hop connections into single commands.

None of these techniques are complicated individually. Collectively, they represent the difference between wrestling with SSH daily and having SSH simply work. The cognitive overhead vanishes. The text file of commands you used to copy from becomes unnecessary. The risk of using the wrong key or connecting to the wrong server disappears.

Start with the servers you access most frequently. Create aliases for them. Add identity files. Configure forwarding where needed. Each configuration compounds the benefit. Within a week, you'll wonder how you managed without it. Within a month, working on a system without your SSH config will feel like working on someone else's machine—technically possible, but unnecessarily frustrating.

Published on:

Updated on:

Reading time:

7 min read

Article counts:

38 paragraphs, 1,369 words

Topics

TL;DR

The .ssh/config file eliminates SSH complexity through five core patterns. IdentityFile directives map specific keys to specific servers, preventing authentication delays and lockouts from trying wrong keys. ForwardAgent enables secure multi-hop authentication without copying private keys to intermediate servers—keys stay local whilst forwarding authentication requests. LocalForward creates encrypted tunnels for database access, allowing local tools to connect via localhost whilst traffic routes securely through SSH. Git repository aliases collapse provider-specific URLs into consistent shortcuts (gh for GitHub, gl for GitLab). ProxyJump configurations handle bastion hosts transparently, connecting to internal servers with single commands instead of managing two-hop sessions manually. These techniques compound—complex AWS hostnames become two-word commands, multiple authentication steps collapse into one, and the cognitive overhead of remembering connection details vanishes. Start with frequently accessed servers, add configurations incrementally, and within weeks SSH simply works instead of requiring constant attention.

Latest from the blog

15 min read

AWS sub-accounts: isolating resources with Organizations

Most teams dump client resources into their main AWS account, creating an administrative nightmare when projects end or security issues arise. AWS Organizations sub-accounts provide hard security boundaries that separate resources, limit blast radius from incidents, and make cleanup trivial—yet many developers avoid them, assuming the setup complexity outweighs the benefits.

More rabbit holes to fall down

10 min read

Turbocharge development: the magic of SSH port forwarding

Security policies block database ports. Firewalls prevent external connections. Remote services remain inaccessible except through carefully controlled channels. SSH port forwarding creates encrypted tunnels that make distant services appear local—you connect to localhost whilst traffic routes securely to remote resources, maintaining security boundaries without compromising workflow efficiency.
10 min read

SSH keys in 1Password: eliminating the file juggling ritual

SSH keys scattered across machines create a familiar nightmare—copying files between systems, remembering which key lives where, and the inevitable moment when you need to connect from a new laptop without access to your carefully managed ~/.ssh directory. 1Password's SSH agent transforms this by keeping encrypted keys available everywhere whilst ensuring private keys never touch disk outside the vault.
10 min read

The hidden cost of free tooling: when open source becomes technical debt

Adding file compression should have taken a day. Three packages needed different versions of the same streaming library. Three days of dependency archaeology, GitHub issue spelunking, and version juggling later, we manually patched node_modules with a post-install script. Open source is free to download but expensive to maintain.

Further musings for the properly obsessed

15 min read

AWS sub-accounts: isolating resources with Organizations

Most teams dump client resources into their main AWS account, creating an administrative nightmare when projects end or security issues arise. AWS Organizations sub-accounts provide hard security boundaries that separate resources, limit blast radius from incidents, and make cleanup trivial—yet many developers avoid them, assuming the setup complexity outweighs the benefits.
11 min read

The architecture autopsy: when 'we'll refactor later' becomes 'we need a complete rewrite'

Early architectural decisions compound over time, creating irreversible constraints that transform minor technical debt into catastrophic system failures. Understanding how seemingly innocent choices cascade into complete rewrites reveals why future-proofing architecture requires balancing immediate needs with long-term reversibility.
19 min read

The symptom-fix trap: Why patching consequences breeds chaos

In the relentless pressure to ship features and fix bugs quickly, development teams fall into a destructive pattern of treating symptoms rather than root causes. This reactive approach creates cascading technical debt, multiplies maintenance costs, and transforms codebases into brittle systems that break under the weight of accumulated shortcuts.
9 min read

The 2038 problem: when time runs out

At exactly 03:14:07 UTC on January 19, 2038, a significant portion of the world's computing infrastructure will experience temporal catastrophe. Unlike Y2K, this isn't a formatting problem - it's mathematics meets physics, and we can't patch the fundamental laws of binary arithmetic.
20 min read

The velocity trap: when speed metrics destroy long-term performance

Velocity metrics were meant to help teams predict and improve, but they have become weapons of productivity theatre that incentivise gaming the system while destroying actual productivity. Understanding how story points, velocity tracking, and sprint metrics create perverse incentives is essential for building truly effective development teams.
18 min read

Sprint overcommitment: the quality tax nobody measures

Three features in parallel, each "nearly done". The authentication refactor sits at 85% complete. The payment integration passed initial testing. The dashboard redesign awaits final review. None will ship this sprint—all will introduce bugs next sprint. Research shows teams planning above 70% capacity experience 60% more defects whilst delivering 40% less actual value.
12 min read

Technical debt triage: making strategic compromises

Simple CSV export: one day estimated, three weeks actual. User data spread across seven tables with inconsistent types—strings, epochs, ISO 8601 timestamps. Technical debt's real cost isn't messy code; it's velocity degradation. Features take weeks instead of days. Developers spend 17 hours weekly on maintenance from accumulated debt.
10 min read

Environment reproducibility: Docker vs. Nix vs. Vagrant

Production threw segmentation faults in unchanged code. Four hours revealed the cause: Node.js 18.16.0 versus 18.17.1—a patch version difference in native addon handling exposing a memory corruption issue. Environment drift creates space for bugs to hide. Docker, Nix, and Vagrant solve reproducibility at different levels with distinct trade-offs.
9 min read

Reproducible development environments: the Nix approach

Dozens of Go microservices in Docker, almost a dozen Node.js UI applications, PostgreSQL, Redis. Extensive setup process. Docker Desktop, Go 1.21 specifically, Node.js 18 specifically, PostgreSQL 14, build tools differing between macOS and Linux. When it breaks, debugging requires understanding which layer failed. Developers spend 10% of working time fighting environment issues.
10 min read

Avoiding overkill: embracing simplicity

A contact form implemented with React, Redux, Webpack, TypeScript, and elaborate CI/CD pipelines—2.3MB production bundle for three fields and a submit button. Two days to set up the development environment. Thirty-five minutes to change placeholder text. This is overengineering: enterprise solutions applied to problems that need HTML and a server script.
10 min read

Terminal multiplexing: beyond the basics

Network drops during critical database migrations. SSH connections terminate mid-deployment. Terminal crashes destroy hours of workspace setup. tmux decouples your terminal interface from persistent sessions that continue running independently—network failures become irrelevant interruptions rather than catastrophic losses, whilst organised workspaces survive crashes and reconnections.
9 min read

Streamlining local development with Dnsmasq

Testing on localhost hides entire categories of bugs—cookie scope issues, CORS policies, authentication flows that behave differently on real domains. These problems surface after deployment, when fixing them costs hours instead of minutes. Dnsmasq eliminates this gap by making local development behave like production, turning any custom domain into localhost whilst preserving domain-based security policies.
11 min read

Dotfiles: why and how

Working on someone else's machine feels like writing with their hands—common commands fail, shortcuts vanish, and everything feels wrong. Dotfiles transform this by capturing your accumulated workflow optimisation in version-controlled configuration files, turning any terminal into your terminal within minutes rather than days of manual reconfiguration.
10 min read

Downtime of uptime percentages, deciphering the impact

Understanding the real-world implications of uptime percentages is paramount for businesses and consumers alike. What might seem like minor decimal differences in uptime guarantees can translate to significant variations in service availability, impacting operations, customer experience, and bottom lines.