SSH Tips and Tricks: Beyond Basic Connections

SSH is the workhorse of remote access. But most people only scratch the surface. Here’s how to use it like a pro. SSH Config: Stop Typing So Much Instead of: 1 ssh -i ~/.ssh/prod-key.pem -p 2222 ubuntu@ec2-54-123-45-67.compute-1.amazonaws.com Create ~/.ssh/config: H H H o o o s s s t t t H U P I H U I F P U p o s o d s o s d o r s r s e r e t s e e r . o e o t r t n a t r n w i x r d N t g N t a n y a u 2 i i a d i r t J a m b 2 t n m e t d e u d e u 2 y g e p y A r m m n 2 F l F g n p i e t i s o i e a n c u l t y l n l b 2 e a e t a - g s 5 ~ i ~ y t 4 / n / e i - . g . s o 1 s . s n 2 s e s 3 h x h - / a / 4 p m s 5 r p t - o l a 6 d e g 7 - . i . k c n c e o g o y m - m . k p p e u e y t m . e p - e 1 m . a m a z o n a w s . c o m Now just: ...

February 24, 2026 Β· 8 min Β· 1594 words Β· Rob Washington

Ansible Playbook Patterns: Writing Maintainable Automation

Ansible playbooks start simple and grow complex. A quick server setup becomes infrastructure-as-code for dozens of machines. Here are patterns that keep playbooks maintainable as they scale. Project Structure a β”œ β”œ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œ β”‚ β”‚ β”‚ β”œ β”‚ β”‚ β”‚ β”” n ─ ─ ─ ─ ─ s ─ ─ ─ ─ ─ i b a i β”œ β”‚ β”‚ β”‚ β”‚ β”” p β”œ β”œ β”” r β”œ β”œ β”” f l n n ─ ─ l ─ ─ ─ o ─ ─ ─ i e s v ─ ─ a ─ ─ ─ l ─ ─ ─ l / i e y e e b n p β”œ β”” s β”œ β”” b s w d s c n p s l t r ─ ─ t ─ ─ o i e a / o g o / e o o ─ ─ a ─ ─ o t b t m i s . r d g k e s a m n t c y u h g β”œ β”” i h g s . e b o x g f / c o r ─ ─ n o r y r a n r g t s o ─ ─ g s m v s e i t u / t u l e e s o s p a w s p r s / n . _ l e . _ s . y v l b y v . y m a . s m a y m l r y e l r m l s m r s l / l v / e r s . y m l ansible.cfg 1 2 3 4 5 6 7 8 9 [defaults] inventory = inventory/production roles_path = roles host_key_checking = False retry_files_enabled = False [ssh_connection] pipelining = True control_path = /tmp/ansible-%%r@%%h:%%p Inventory Patterns YAML Inventory (Preferred) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # inventory/production/hosts.yml all: children: webservers: hosts: web1.example.com: web2.example.com: vars: http_port: 80 databases: hosts: db1.example.com: postgres_version: 15 db2.example.com: postgres_version: 14 loadbalancers: hosts: lb1.example.com: Group Variables 1 2 3 4 5 6 7 8 9 10 11 12 13 # inventory/production/group_vars/all.yml --- ansible_user: deploy ansible_python_interpreter: /usr/bin/python3 ntp_servers: - 0.pool.ntp.org - 1.pool.ntp.org # inventory/production/group_vars/webservers.yml --- nginx_worker_processes: auto nginx_worker_connections: 1024 app_root: /var/www/app Role Structure r β”œ β”‚ β”œ β”‚ β”œ β”‚ β”œ β”‚ β”œ β”‚ β”œ β”‚ β”” o ─ ─ ─ ─ ─ ─ ─ l ─ ─ ─ ─ ─ ─ ─ e s d β”” v β”” t β”” h β”” t β”” f β”” m β”” / e ─ a ─ a ─ a ─ e ─ i ─ e ─ n f ─ r ─ s ─ n ─ m ─ l ─ t ─ g a s k d p e a i u m / m s m l m l n s s / m n l a a / a e a a g / s a x t i i i r i t i l i / s n n n s n e n - n / . . . / . s x p . y y y y / . a y m m m m c r m l l l l o a l n m f s . . j c 2 o n # # # # # f # D R T H J R e o a a i o f l s n n l a e k d j e u l a l v e e 2 m t a n r e r t s t t v i r e a a a y ( m d r b r p a i l p e l t a e o s a a b s i t t l n a e a e ( t r s n s h t d i ( g s d l h e e o r p w p v e e r i n s i c d t o e e r s n p i , c r t i i y e e o ) t s r c i . t ) y ) defaults/main.yml 1 2 3 4 5 6 --- # Overridable defaults nginx_worker_processes: auto nginx_worker_connections: 768 nginx_keepalive_timeout: 65 nginx_server_tokens: "off" tasks/main.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 --- - name: Install nginx ansible.builtin.apt: name: nginx state: present update_cache: true become: true - name: Configure nginx ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: '0644' become: true notify: Reload nginx - name: Enable and start nginx ansible.builtin.systemd: name: nginx enabled: true state: started become: true handlers/main.yml 1 2 3 4 5 6 7 8 9 10 11 12 --- - name: Reload nginx ansible.builtin.systemd: name: nginx state: reloaded become: true - name: Restart nginx ansible.builtin.systemd: name: nginx state: restarted become: true Task Patterns Idempotent Tasks 1 2 3 4 5 6 7 8 9 10 # Good - idempotent - name: Ensure user exists ansible.builtin.user: name: deploy state: present groups: [sudo, docker] # Avoid - not idempotent - name: Add user ansible.builtin.command: useradd deploy Conditional Execution 1 2 3 4 5 6 7 8 9 10 11 - name: Install package (Debian) ansible.builtin.apt: name: nginx state: present when: ansible_os_family == "Debian" - name: Install package (RedHat) ansible.builtin.dnf: name: nginx state: present when: ansible_os_family == "RedHat" Loops 1 2 3 4 5 6 7 8 9 10 - name: Create users ansible.builtin.user: name: "{{ item.name }}" groups: "{{ item.groups }}" state: present loop: - { name: alice, groups: [developers] } - { name: bob, groups: [developers, sudo] } loop_control: label: "{{ item.name }}" # Cleaner output Blocks for Error Handling 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 - name: Deploy application block: - name: Pull latest code ansible.builtin.git: repo: "{{ app_repo }}" dest: "{{ app_root }}" version: "{{ app_version }}" - name: Install dependencies ansible.builtin.pip: requirements: "{{ app_root }}/requirements.txt" virtualenv: "{{ app_root }}/venv" - name: Run migrations ansible.builtin.command: cmd: "{{ app_root }}/venv/bin/python manage.py migrate" chdir: "{{ app_root }}" rescue: - name: Notify on failure ansible.builtin.debug: msg: "Deployment failed, rolling back" - name: Rollback to previous version ansible.builtin.git: repo: "{{ app_repo }}" dest: "{{ app_root }}" version: "{{ previous_version }}" always: - name: Restart application ansible.builtin.systemd: name: myapp state: restarted Variable Precedence From lowest to highest priority: ...

February 24, 2026 Β· 8 min Β· 1621 words Β· Rob Washington

DNS Debugging: Finding Why Your Domain Isn't Working

β€œIt’s always DNS” is a meme because it’s usually true. When something network-related breaks, DNS is the first suspect. Here’s how to investigate. The Essential Tools dig - The DNS Swiss Army Knife 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Basic lookup dig example.com # Specific record type dig example.com MX dig example.com TXT dig example.com CNAME # Short output (just the answer) dig +short example.com # Query a specific nameserver dig @8.8.8.8 example.com # Trace the full resolution path dig +trace example.com nslookup - Quick and Simple 1 2 3 4 5 6 7 8 9 10 # Basic lookup nslookup example.com # Query specific server nslookup example.com 8.8.8.8 # Interactive mode nslookup > set type=MX > example.com host - Even Simpler 1 2 3 4 5 # Basic lookup host example.com # Specific record type host -t MX example.com Common DNS Problems Problem: Domain Not Resolving Symptoms: NXDOMAIN or SERVFAIL errors ...

February 24, 2026 Β· 6 min Β· 1132 words Β· Rob Washington

Log Aggregation Pipelines: From Scattered Files to Searchable Insights

When you have one server, you SSH in and grep the logs. When you have fifty servers, that stops working. Log aggregation is how you make β€œwhat happened?” answerable at scale. The Pipeline Architecture Every log aggregation system follows the same basic pattern: β”Œ β”‚ β”” ─ ─ ─ S ─ ─ o ─ ─ u ─ ─ r ─ β”‚ β”‚ β”” ─ c ─ ─ ─ e ─ ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ─ ─ ─ ─ ─ ─ ─ β–Ά β–Ά β”Œ β”‚ β”” β”Œ β”‚ β”” ─ ─ ─ ─ ─ C ─ ─ ─ ─ o ─ ─ Q ─ ─ l ─ ─ u ─ ─ l ─ ─ e ─ ─ e ─ ─ r ─ ─ c ─ ─ y ─ ─ t ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ┐ β”‚ β”˜ ─ β—€ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” ─ ─ ─ ─ ─ P ─ ─ ─ r ─ ─ ─ o ─ ─ ─ c ─ ─ ─ e ─ ─ ─ s ─ ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ─ ─ ─ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” ─ ─ ─ ─ ─ ─ ─ ─ S ─ ─ ─ t ─ ─ ─ o ─ β”‚ β”˜ ─ r ─ β”‚ ─ e ─ ─ ─ ─ ─ ┐ β”‚ β”˜ Each stage has choices. Let’s walk through them. ...

February 24, 2026 Β· 10 min Β· 1999 words Β· Rob Washington

Configuration Management Principles: Making Deployments Predictable

Most production incidents I’ve debugged came down to configuration. A missing environment variable. A wrong database URL. A feature flag stuck in the wrong state. Code was fine; configuration was the problem. Configuration management is the unsexy work that prevents those 3 AM pages. The Core Principles 1. Separate Configuration from Code Configuration should never be baked into your application binary or container image. Wrong: 1 2 # Hardcoded in code DATABASE_URL = "postgres://prod:password@db.example.com/myapp" Also wrong: ...

February 24, 2026 Β· 7 min Β· 1321 words Β· Rob Washington

Container Orchestration Patterns: Beyond 'Just Deploy It'

Running one container is easy. Running hundreds in production, reliably, at scale? That’s where patterns emerge. These aren’t Kubernetes-specific (though that’s where you’ll see them most). They’re fundamental approaches to composing containers into systems that actually work. The Sidecar Pattern A sidecar is a helper container that runs alongside your main application container, sharing the same pod/network namespace. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 apiVersion: v1 kind: Pod metadata: name: web-app spec: containers: # Main application - name: app image: myapp:1.0 ports: - containerPort: 8080 # Sidecar: log shipper - name: log-shipper image: fluentd:latest volumeMounts: - name: logs mountPath: /var/log/app volumes: - name: logs emptyDir: {} Common sidecar use cases: ...

February 24, 2026 Β· 6 min Β· 1153 words Β· Rob Washington

Blue-Green Deployments: Zero-Downtime Releases Without the Drama

The scariest moment in software delivery used to be clicking β€œdeploy.” Will it work? Will it break? Will you be debugging at 2 AM? Blue-green deployments eliminate most of that fear. Instead of updating your production environment in place, you deploy to an identical standby environment and switch traffic over. If something’s wrong, you switch back. Done. The Core Concept You maintain two identical production environments: Blue: Currently serving live traffic Green: Idle, ready for the next release To deploy: ...

February 24, 2026 Β· 8 min Β· 1600 words Β· Rob Washington

Alerting That Doesn't Suck: From Noise to Signal

The worst oncall shift I ever had wasn’t the one with the outage. It was the one with 47 alerts, none of which mattered, followed by one that did β€” which I almost missed because I’d stopped paying attention. Alert fatigue is real, and it’s a systems problem, not a discipline problem. If your alerts are noisy, the fix isn’t β€œtry harder to pay attention.” The fix is better alerts. ...

February 24, 2026 Β· 5 min Β· 1018 words Β· Rob Washington

Secrets Management in the Modern Stack

We’ve all done it. Committed an API key to git. Hardcoded a database password β€œjust for testing.” Posted a screenshot with credentials visible in the corner. The security community has a name for this: Tuesday. But secrets management doesn’t have to be painful. Let’s walk through the progression from β€œplease no” to β€œactually reasonable” in handling sensitive credentials. The Hierarchy of Secrets (From Worst to Best) Level 0: Hardcoded in Source 1 2 3 # Don't do this. Ever. db_password = "hunter2" api_key = "sk-live-definitely-real-key" This is how breaches happen. Credentials in source code get committed to git, pushed to GitHub, indexed by bots within minutes, and suddenly someone’s mining crypto on your AWS account. ...

February 24, 2026 Β· 5 min Β· 969 words Β· Rob Washington

Terminal Productivity: Tools and Techniques for Speed

The terminal is where developers live. A few minutes learning shortcuts and tools saves hours over a career. These are the techniques that make the biggest difference. Shell Basics That Pay Off History Navigation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Search history with Ctrl+R # Type partial command, press Ctrl+R repeatedly to cycle # Run last command !! # Run last command with sudo sudo !! # Run last command starting with 'git' !git # Replace text in last command ^typo^fixed # If last command was: git psuh # ^psuh^push runs: git push # Last argument of previous command cd /very/long/path/to/directory ls $_ # $_ = /very/long/path/to/directory Keyboard Shortcuts 1 2 3 4 5 6 7 8 Ctrl+A # Move to beginning of line Ctrl+E # Move to end of line Ctrl+U # Delete from cursor to beginning Ctrl+K # Delete from cursor to end Ctrl+W # Delete word before cursor Alt+B # Move back one word Alt+F # Move forward one word Ctrl+L # Clear screen (keeps current line) Brace Expansion 1 2 3 4 5 6 7 8 9 10 11 # Create multiple directories mkdir -p project/{src,tests,docs} # Create multiple files touch file{1,2,3}.txt # Backup a file cp config.yml{,.bak} # Creates config.yml.bak # Rename with pattern mv file.{txt,md} # Renames file.txt to file.md Essential Tools fzf - Fuzzy Finder 1 2 3 4 5 6 7 8 9 10 11 12 13 # Install brew install fzf # macOS apt install fzf # Debian/Ubuntu # Search files fzf # Pipe anything to fzf cat file.txt | fzf ps aux | fzf # Ctrl+R replacement (add to .bashrc/.zshrc) # Much better history search ripgrep (rg) - Fast Search 1 2 3 4 5 6 7 8 9 10 11 12 13 # Install brew install ripgrep # Search in files (faster than grep) rg "pattern" rg "TODO" --type py rg "function" -g "*.js" # Case insensitive rg -i "error" # Show context rg -C 3 "exception" # 3 lines before and after fd - Better Find 1 2 3 4 5 6 7 8 9 10 # Install brew install fd # Find files fd "pattern" fd -e py # Find .py files fd -t d # Find directories only # Execute on results fd -e log -x rm # Delete all .log files bat - Better Cat 1 2 3 4 5 6 7 8 # Install brew install bat # Syntax highlighting, line numbers, git changes bat file.py # Use as man pager export MANPAGER="sh -c 'col -bx | bat -l man -p'" eza/exa - Better ls 1 2 3 4 5 6 7 8 # Install brew install eza # Tree view with git status eza --tree --level=2 --git # Long format with icons eza -la --icons jq - JSON Processing 1 2 3 4 5 6 7 8 9 10 11 # Pretty print JSON cat data.json | jq . # Extract field curl api.example.com/users | jq '.[0].name' # Filter array jq '.[] | select(.active == true)' # Transform jq '{name: .user.name, email: .user.email}' Tmux Essentials 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # Start new session tmux new -s project # Detach: Ctrl+B, then D # List sessions tmux ls # Attach to session tmux attach -t project # Split panes Ctrl+B % # Vertical split Ctrl+B " # Horizontal split Ctrl+B o # Switch pane Ctrl+B z # Zoom pane (toggle) # Windows Ctrl+B c # New window Ctrl+B n # Next window Ctrl+B p # Previous window Ctrl+B , # Rename window Git Shortcuts 1 2 3 4 5 6 7 8 9 10 11 # .gitconfig [alias] s = status -sb co = checkout br = branch ci = commit ca = commit --amend lg = log --oneline --graph --decorate -20 unstage = reset HEAD -- last = log -1 HEAD aliases = config --get-regexp alias 1 2 3 4 5 6 # Common workflows git s # Short status git lg # Pretty log git co - # Switch to previous branch git stash -u # Stash including untracked git stash pop # Apply and remove stash Shell Aliases 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # Add to .bashrc or .zshrc # Navigation alias ..="cd .." alias ...="cd ../.." alias ~="cd ~" # Safety alias rm="rm -i" alias mv="mv -i" alias cp="cp -i" # Shortcuts alias ll="ls -lah" alias la="ls -la" alias g="git" alias k="kubectl" alias d="docker" alias dc="docker compose" # Quick edits alias zshrc="$EDITOR ~/.zshrc" alias reload="source ~/.zshrc" # Common tasks alias ports="netstat -tulanp" alias myip="curl ifconfig.me" alias weather="curl wttr.in" Functions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # Make directory and cd into it mkcd() { mkdir -p "$1" && cd "$1" } # Extract any archive extract() { case $1 in *.tar.gz) tar xzf "$1" ;; *.tar.bz2) tar xjf "$1" ;; *.tar.xz) tar xJf "$1" ;; *.zip) unzip "$1" ;; *.gz) gunzip "$1" ;; *) echo "Unknown format: $1" ;; esac } # Find and kill process by name killnamed() { ps aux | grep "$1" | grep -v grep | awk '{print $2}' | xargs kill -9 } # Quick HTTP server serve() { python3 -m http.server ${1:-8000} } # Git clone and cd gclone() { git clone "$1" && cd "$(basename "$1" .git)" } Quick One-Liners 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # Find large files find . -type f -size +100M # Disk usage by directory du -sh */ | sort -h # Watch command output watch -n 2 'kubectl get pods' # Run command on file change while inotifywait -e modify file.py; do python file.py; done # Parallel execution cat urls.txt | xargs -P 10 -I {} curl -s {} # Quick calculations echo $((2**10)) # 1024 python3 -c "print(2**100)" # Generate password openssl rand -base64 32 # Quick timestamp date +%Y%m%d_%H%M%S Environment Management 1 2 3 4 5 6 # .envrc with direnv # Auto-load environment per directory echo 'export API_KEY=xxx' > .envrc direnv allow # Now API_KEY is set when you cd into this directory SSH Config 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # ~/.ssh/config Host dev HostName dev.example.com User deploy IdentityFile ~/.ssh/dev_key Host prod HostName prod.example.com User deploy ProxyJump bastion Host * AddKeysToAgent yes IdentitiesOnly yes # Now just: ssh dev Prompt Customization 1 2 3 4 5 # Starship - cross-shell prompt brew install starship echo 'eval "$(starship init zsh)"' >> ~/.zshrc # Shows git status, language versions, errors, duration The Power Combo My essential setup: ...

February 24, 2026 Β· 7 min Β· 1294 words Β· Rob Washington