Overview and What You Will Learn
SSH is the primary tool for accessing every Linux server in production. Every Razorpay engineer, every Swiggy DevOps team member, every Zerodha infrastructure operator uses SSH daily. But most engineers only know the basics — ssh user@host — and miss the features that make SSH faster, safer, and far more powerful.
By the end of this lab you will:
- Generate and deploy Ed25519 SSH key pairs correctly
- Configure
~/.ssh/configfor one-command server access - Set up ProxyJump for accessing private servers through a bastion host
- Use SSH port forwarding to access internal services securely
- Harden
sshd_configto production security standards - Troubleshoot SSH connection failures systematically
Why This Matters in Production
A production environment at a company like Hotstar has hundreds of servers — none reachable directly from the internet. Engineers connect through a bastion host (jump server) that is the single entry point. Without proper SSH config, every connection requires a complex command with multiple flags. With proper SSH config, it is ssh payment-prod-db-1 and you are connected.
More critically: misconfigured SSH is one of the most common attack vectors against production servers. Enabling password authentication, allowing root login, and using weak key algorithms are mistakes that put entire infrastructure at risk.
Core Principles
SSH directory structure and required permissions:
+------------------------------------------+| ~/.ssh/ chmod 700 || || config chmod 600 || id_ed25519 chmod 600 || id_ed25519.pub chmod 644 || authorized_keys chmod 600 || known_hosts chmod 600 |+------------------------------------------+ SSH enforces these permissions. Wrong permissionsmean SSH silently refuses to use the key or config.SSH authentication flow:
+------------------+ +------------------+| Client | | Server (sshd) || | | || 1. Connect | -------> | Listen :22 || | | || 2. Key exchange | <------> | Exchange keys || Encrypted | | Session key set || tunnel open | | || | | || 3. Send username | -------> | Check auth methods|| | | || 4. Key auth: | -------> | Check || Sign challenge| | authorized_keys || with priv key | | Verify signature || | | || 5. Shell access | <------> | Spawn bash |+------------------+ +------------------+Detailed Step-by-Step Practical Lab
Milestone 1 — Generate an Ed25519 key pair
Ed25519 is the preferred key algorithm in 2024. It is faster, more secure, and produces shorter keys than RSA-4096 while providing equivalent or better security.
## Generate Ed25519 key pairssh-keygen -t ed25519 -C "rahul@devops.in"## Generating public/private ed25519 key pair.## Enter file in which to save the key (~/.ssh/id_ed25519): [press Enter]## Enter passphrase (empty for no passphrase): [type a strong passphrase]## Enter same passphrase again:## Your identification has been saved in /home/rahul/.ssh/id_ed25519## Your public key has been saved in /home/rahul/.ssh/id_ed25519.pub ## Verify the files were created with correct permissionsls -la ~/.ssh/## -rw------- id_ed25519 (600 -- private key)## -rw-r--r-- id_ed25519.pub (644 -- public key) ## View the public key (safe to share)cat ~/.ssh/id_ed25519.pub## ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... rahul@devops.in ## Copy public key to a serverssh-copy-id -i ~/.ssh/id_ed25519.pub rahul@10.0.1.50## Number of key(s) added: 1## Now try logging into the machine: ssh rahul@10.0.1.50 ## Test key-based loginssh rahul@10.0.1.50## Should connect without password (passphrase if set)COMMON MISTAKE / WARNING**Common Mistake:** Generating a key without a passphrase for "convenience." If your laptop is stolen or compromised, an unprotected private key gives instant access to every server where your public key is installed. Always use a strong passphrase and ssh-agent to avoid typing it repeatedly.
Milestone 2 — Configure ~/.ssh/config for fast access
The SSH config file lets you define aliases, set defaults, and configure complex connection options so they become a single command.
## Create or edit the config filemkdir -p ~/.ssh && chmod 700 ~/.sshnano ~/.ssh/configchmod 600 ~/.ssh/config## ~/.ssh/config -- complete production example ## Global defaults applied to all connectionsHost * ServerAliveInterval 60 ServerAliveCountMax 3 AddKeysToAgent yes IdentityFile ~/.ssh/id_ed25519 ## Bastion / jump serverHost bastion HostName 52.66.100.200 User rahul Port 22 IdentityFile ~/.ssh/id_ed25519 ## Production servers (accessed via bastion)Host payment-prod-* User rahul ProxyJump bastion IdentityFile ~/.ssh/id_ed25519 Host payment-prod-api-1 HostName 10.0.1.50 Host payment-prod-db-1 HostName 10.0.2.100 ## Staging environmentHost staging-* User deploy ProxyJump bastion IdentityFile ~/.ssh/id_ed25519_staging Host staging-api HostName 10.0.10.50## Now connect with just the aliasssh payment-prod-api-1## Automatically: user=rahul, jumps via bastion, uses correct key ## No more typing:## ssh -i ~/.ssh/id_ed25519 -J rahul@52.66.100.200 rahul@10.0.1.50Milestone 3 — Set up ProxyJump for bastion host access
Most production environments have no direct internet access to application servers. A bastion (jump) host is the single entry point.
## One-time: copy your key to the bastionssh-copy-id -i ~/.ssh/id_ed25519.pub rahul@52.66.100.200 ## Then copy your key to the internal server via the bastionssh-copy-id -i ~/.ssh/id_ed25519.pub -o ProxyJump=rahul@52.66.100.200 rahul@10.0.1.50 ## Test the jump connectionssh -J rahul@52.66.100.200 rahul@10.0.1.50## -J flag: jump through the specified host ## With config file set up, just use the aliasssh payment-prod-api-1 ## ProxyJump chains (multiple hops)ssh -J bastion1,bastion2 rahul@internal-server ## Agent forwarding (use your local keys on the jump server)## WARNING: only use on trusted hostsssh -A rahul@52.66.100.200Milestone 4 — Use SSH port forwarding
SSH port forwarding creates encrypted tunnels to access internal services without exposing them directly.
## Local forwarding: access remote PostgreSQL on localhost:5433## Connects to 10.0.2.100:5432 through the SSH server at 10.0.1.50ssh -L 5433:10.0.2.100:5432 rahul@10.0.1.50 ## In another terminal, connect to the database as if it were localpsql -h localhost -p 5433 -U postgres mydb ## Local forward: access internal Grafana dashboardssh -L 3000:localhost:3000 rahul@10.0.1.50## Then open http://localhost:3000 in your browser ## Run in background (-f) and stay alive (-N no command)ssh -fN -L 5433:10.0.2.100:5432 rahul@10.0.1.50 ## Remote forwarding: expose local port on remote server## Useful for webhooks during developmentssh -R 8080:localhost:3000 rahul@10.0.1.50## Anyone hitting 10.0.1.50:8080 reaches your localhost:3000 ## Dynamic SOCKS proxy: route all traffic through serverssh -D 1080 rahul@10.0.1.50## Configure browser to use SOCKS5 proxy at localhost:1080Milestone 5 — Manage ssh-agent for passphrase convenience
## Start ssh-agent (usually started automatically by desktop environment)eval $(ssh-agent -s)## Agent pid 12345 ## Add your private key (prompts for passphrase once)ssh-add ~/.ssh/id_ed25519 ## List loaded keysssh-add -l## 256 SHA256:abc123... rahul@devops.in (ED25519) ## Add key that expires after 8 hours (re-auth after workday)ssh-add -t 8h ~/.ssh/id_ed25519 ## Remove all keys from agentssh-add -D ## On macOS: integrate with Keychain (passphrase remembered across reboots)## In ~/.ssh/config:## Host *## UseKeychain yes## AddKeysToAgent yesMilestone 6 — Harden sshd_config for production
## Edit the SSH server configurationsudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backupsudo nano /etc/ssh/sshd_config## /etc/ssh/sshd_config -- production hardening settings ## Change default port to reduce automated scan noise## (Security through obscurity -- not a substitute for real security)Port 22 ## Disable root login completelyPermitRootLogin no ## Disable password authentication -- keys onlyPasswordAuthentication noChallengeResponseAuthentication no ## Allow only specific usersAllowUsers rahul deploy jenkins ## Limit authentication attemptsMaxAuthTries 3LoginGraceTime 30 ## Disconnect idle sessions after 10 minutesClientAliveInterval 300ClientAliveCountMax 2 ## Use modern key exchange algorithms onlyKexAlgorithms curve25519-sha256,diffie-hellman-group14-sha256 ## Disable X11 forwarding (not needed on servers)X11Forwarding no ## Disable TCP forwarding if not needed (restrict to specific hosts if needed)## AllowTcpForwarding no ## Log level for auditLogLevel VERBOSE## Validate config before applyingsudo sshd -t## No output = valid config ## Apply changessudo systemctl reload sshd ## Verify from ANOTHER terminal that you can still connect## before closing current sessionssh rahul@10.0.1.50Production Best Practices and Common Pitfalls
| Scenario | Wrong | Correct |
|---|---|---|
| New server setup | Leave PasswordAuthentication yes | Disable password auth immediately |
| Team access | Share one SSH key | Each engineer has their own key |
| Debugging connection | Guess at the problem | ssh -vvv user@host for full debug |
| Accessing internal DB | Open DB port to internet | Use SSH port forwarding |
| Config file change | Apply without testing | Test with sshd -t, keep second session open |
Quick Reference and Troubleshooting Commands
| Task | Command |
|---|---|
| Generate key | ssh-keygen -t ed25519 -C "email" |
| Copy key to server | ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host |
| Connect via bastion | ssh -J bastion user@internal-host |
| Local port forward | ssh -L localport:remotehost:remoteport user@host |
| Debug connection | ssh -vvv user@host |
| Test sshd config | sudo sshd -t |
| Load key in agent | ssh-add ~/.ssh/id_ed25519 |
| List loaded keys | ssh-add -l |
PLACEMENT PRO TIP**Tip:** The `ServerAliveInterval 60` setting in `~/.ssh/config` sends a keepalive packet every 60 seconds. Without it, SSH connections through NAT or firewalls with idle timeouts drop silently — and you discover the connection is dead only when you try to type something.
COMMON MISTAKE / WARNING**Common Mistake:** Running `ssh -A` (agent forwarding) habitually on all connections. Agent forwarding exposes your ssh-agent socket on the remote server. Any user with root on that server can use your agent to impersonate you elsewhere. Always use `ProxyJump` instead — it achieves the same result without exposing your agent.
COMMON MISTAKE / WARNING**Common Mistake:** Running `ssh -A` (agent forwarding) on every connection out of habit. Agent forwarding exposes your local ssh-agent socket on the remote server. Root on that server can silently use your keys to access other servers as you. Use `ProxyJump` in `~/.ssh/config` instead — same result, no agent exposure.
REMEMBER THIS**Remember:** Always keep a second SSH terminal open when testing new sshd_config changes. If you lock yourself out by misconfiguring authentication, the existing session keeps you connected so you can fix the error without needing console access.
COMMON MISTAKE / WARNING**Security:** Never use `ssh -A` (agent forwarding) to untrusted servers. Agent forwarding allows the remote server to use your ssh-agent — meaning anyone with root on that server can use your keys to connect to other servers as you. Use `ProxyJump` instead, which does not expose your agent to intermediate hosts.