Systemd Service Management: A Practical Guide

Systemd is the init system for most modern Linux distributions. Love it or hate it, you need to know it. Here’s how to manage services effectively. Basic Commands 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Start/stop/restart sudo systemctl start nginx sudo systemctl stop nginx sudo systemctl restart nginx # Reload config without restart sudo systemctl reload nginx # Enable/disable at boot sudo systemctl enable nginx sudo systemctl disable nginx # Check status systemctl status nginx # List all services systemctl list-units --type=service # List failed services systemctl --failed Writing a Service Unit Create /etc/systemd/system/myapp.service: ...

March 13, 2026 Â· 6 min Â· 1118 words Â· Rob Washington

SSH Hardening: Secure Your Servers in 30 Minutes

SSH is the front door to your servers. A weak SSH config is an open invitation to attackers. Here’s how to lock it down properly without locking yourself out. The Bare Minimum 1 2 3 4 5 6 7 8 9 10 11 12 13 # /etc/ssh/sshd_config # Disable root login PermitRootLogin no # Disable password authentication PasswordAuthentication no # Enable key-based auth only PubkeyAuthentication yes # Disable empty passwords PermitEmptyPasswords no 1 2 # Apply changes sudo systemctl restart sshd These four settings stop 99% of automated attacks. ...

March 12, 2026 Â· 7 min Â· 1382 words Â· Rob Washington

SSH Config Mastery: Stop Typing Long Commands

Still typing ssh -i ~/.ssh/my-key.pem -p 2222 user@server.example.com? There’s a better way. The SSH Config File ~/.ssh/config transforms verbose commands into simple ones. # s # s s s B h A h e f f - t p o i e r r r o e ~ d / . s s h / p r o d - k e y . p e m - p 2 2 2 2 d e p l o y @ p r o d . e x a m p l e . c o m Basic Config # H H H o o o ~ s s s / t t t . H U P I H U I H U s p o s o d s o s d d o s s r s e r e t s e e e s e h o t r t n a t r n v t r / d N t g N t N c a d 2 i i a d i a d o m e 2 t n m e t m e n e p 2 y g e p y e v f l 2 F l F e i p o i s o i 1 l g r y l t y l 9 o o e a e 2 p d g . e . ~ i ~ 1 r e / n / 6 x . g . 8 a s . s . m s e s 1 p h x h . l / a / 1 e p m s 0 . r p t 0 c o l a o d e g m - . i k c n e o g y m - . k p e e y m . p e m Now just: ...

March 11, 2026 Â· 15 min Â· 3056 words Â· Rob Washington

Bash Scripting Essentials: From One-Liners to Real Scripts

Bash scripts automate everything from deployments to backups. Here’s how to write them properly. Script Structure 1 2 3 4 #!/bin/bash set -euo pipefail # Your code here The shebang (#!/bin/bash) tells the system which interpreter to use. The set options make scripts safer: -e: Exit on error -u: Error on undefined variables -o pipefail: Catch errors in pipelines Variables 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Assignment (no spaces!) NAME="Alice" COUNT=42 # Usage echo "Hello, $NAME" echo "Count is ${COUNT}" # Command substitution DATE=$(date +%Y-%m-%d) FILES=$(ls -1 | wc -l) # Default values ENVIRONMENT=${1:-production} # First arg, default "production" LOG_LEVEL=${LOG_LEVEL:-info} # Env var with default Variable Scope 1 2 3 4 5 6 7 8 # Global by default GLOBAL="I'm everywhere" # Local to function my_function() { local LOCAL="I'm only here" echo "$LOCAL" } Conditionals 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 # String comparison if [ "$NAME" = "Alice" ]; then echo "Hi Alice" elif [ "$NAME" = "Bob" ]; then echo "Hi Bob" else echo "Who are you?" fi # Numeric comparison if [ "$COUNT" -gt 10 ]; then echo "More than 10" fi # File tests if [ -f "$FILE" ]; then echo "File exists" fi if [ -d "$DIR" ]; then echo "Directory exists" fi if [ -z "$VAR" ]; then echo "Variable is empty" fi if [ -n "$VAR" ]; then echo "Variable is not empty" fi Comparison Operators String Numeric Meaning = -eq Equal != -ne Not equal -lt Less than -le Less or equal -gt Greater than -ge Greater or equal Modern Syntax 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Double brackets (safer, more features) if [[ "$NAME" == "Alice" ]]; then echo "Hi" fi # Pattern matching if [[ "$FILE" == *.txt ]]; then echo "Text file" fi # Regex if [[ "$EMAIL" =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]]; then echo "Valid email" fi # Logical operators if [[ "$A" == "yes" && "$B" == "yes" ]]; then echo "Both yes" fi if [[ "$A" == "yes" || "$B" == "yes" ]]; then echo "At least one yes" fi Loops 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 35 # For loop over list for NAME in Alice Bob Charlie; do echo "Hello, $NAME" done # For loop over files for FILE in *.txt; do echo "Processing $FILE" done # For loop with range for i in {1..10}; do echo "Number $i" done # C-style for loop for ((i=0; i<10; i++)); do echo "Index $i" done # While loop while [ "$COUNT" -gt 0 ]; do echo "$COUNT" COUNT=$((COUNT - 1)) done # Read lines from file while IFS= read -r line; do echo "Line: $line" done < file.txt # Read from command while read -r file; do echo "Found: $file" done < <(find . -name "*.log") 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 # Basic function greet() { echo "Hello, $1" } greet "World" # With local variables calculate() { local a=$1 local b=$2 echo $((a + b)) } result=$(calculate 5 3) echo "Result: $result" # Return values (0 = success, non-zero = failure) is_valid() { if [[ "$1" =~ ^[0-9]+$ ]]; then return 0 else return 1 fi } if is_valid "42"; then echo "Valid number" fi Arguments 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash # Positional arguments echo "Script: $0" echo "First arg: $1" echo "Second arg: $2" echo "All args: $@" echo "Arg count: $#" # Shift through args while [ $# -gt 0 ]; do echo "Arg: $1" shift done # Getopts for flags while getopts "vf:o:" opt; do case $opt in v) VERBOSE=true ;; f) FILE="$OPTARG" ;; o) OUTPUT="$OPTARG" ;; ?) echo "Usage: $0 [-v] [-f file] [-o output]"; exit 1 ;; esac done 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 #!/bin/bash set -euo pipefail # Trap errors trap 'echo "Error on line $LINENO"; exit 1' ERR # Trap cleanup on exit cleanup() { echo "Cleaning up..." rm -f "$TEMP_FILE" } trap cleanup EXIT # Create temp file TEMP_FILE=$(mktemp) # Check command success if ! command -v docker &> /dev/null; then echo "Docker not installed" exit 1 fi # Or with || docker ps || { echo "Docker not running"; exit 1; } Useful Patterns Check if root 1 2 3 4 if [ "$EUID" -ne 0 ]; then echo "Please run as root" exit 1 fi Confirm before proceeding 1 2 3 4 5 read -p "Are you sure? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi Logging 1 2 3 4 5 6 7 8 LOG_FILE="/var/log/myscript.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } log "Starting script" log "Task completed" Config file 1 2 3 4 5 6 7 8 # config.sh DB_HOST="localhost" DB_PORT=5432 DB_NAME="myapp" # main.sh source ./config.sh echo "Connecting to $DB_HOST:$DB_PORT" Lock file (prevent concurrent runs) 1 2 3 4 5 6 7 8 9 10 11 LOCKFILE="/tmp/myscript.lock" if [ -f "$LOCKFILE" ]; then echo "Script already running" exit 1 fi trap "rm -f $LOCKFILE" EXIT touch "$LOCKFILE" # Your script here Arrays 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # Define array SERVERS=("web1" "web2" "web3") # Access elements echo "${SERVERS[0]}" # First element echo "${SERVERS[@]}" # All elements echo "${#SERVERS[@]}" # Length # Loop over array for server in "${SERVERS[@]}"; do echo "Deploying to $server" done # Add element SERVERS+=("web4") # Associative array (bash 4+) declare -A PORTS PORTS[web]=80 PORTS[api]=8080 echo "${PORTS[web]}" String Operations 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 STR="Hello, World!" # Length echo "${#STR}" # 13 # Substring echo "${STR:0:5}" # Hello # Replace echo "${STR/World/Bash}" # Hello, Bash! # Replace all echo "${STR//o/0}" # Hell0, W0rld! # Remove prefix FILE="document.txt" echo "${FILE%.txt}" # document # Remove suffix PATH="/home/user/file.txt" echo "${PATH##*/}" # file.txt echo "${PATH%/*}" # /home/user Real Script Example 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #!/bin/bash set -euo pipefail # Deploy script with logging and error handling SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOG_FILE="$SCRIPT_DIR/deploy.log" ENVIRONMENT="${1:-staging}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$ENVIRONMENT] $1" | tee -a "$LOG_FILE" } die() { log "ERROR: $1" exit 1 } cleanup() { log "Cleanup complete" } trap cleanup EXIT # Validate environment if [[ ! "$ENVIRONMENT" =~ ^(staging|production)$ ]]; then die "Invalid environment: $ENVIRONMENT" fi # Production confirmation if [ "$ENVIRONMENT" = "production" ]; then read -p "Deploy to PRODUCTION? (yes/no) " confirm [ "$confirm" = "yes" ] || die "Aborted" fi log "Starting deployment to $ENVIRONMENT" # Pull latest code log "Pulling latest code..." git pull origin main || die "Git pull failed" # Build log "Building..." npm run build || die "Build failed" # Deploy log "Deploying..." rsync -avz ./dist/ "deploy@$ENVIRONMENT.example.com:/var/www/" || die "Deploy failed" log "Deployment complete!" Quick Reference 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Variables VAR="value" VAR=${VAR:-default} VAR=$(command) # Conditionals [[ -f file ]] # File exists [[ -d dir ]] # Dir exists [[ -z "$var" ]] # Empty [[ "$a" == "$b" ]] # String equal [[ $n -eq 5 ]] # Numeric equal # Loops for i in list; do ...; done while cond; do ...; done # Functions func() { local x=$1; echo $x; } # Error handling set -euo pipefail trap 'cleanup' EXIT command || { echo "failed"; exit 1; } Bash isn’t pretty, but it’s everywhere. Master these patterns and you can automate anything. ...

March 5, 2026 Â· 7 min Â· 1438 words Â· Rob Washington

grep, awk, and sed: Text Processing Power Tools

grep, awk, and sed are the foundational text processing tools in Unix. They’re old, they’re cryptic, and they’re incredibly powerful once you learn them. grep: Search and Filter grep searches for patterns in text. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Basic search grep "error" logfile.txt # Case insensitive grep -i "error" logfile.txt # Show line numbers grep -n "error" logfile.txt # Count matches grep -c "error" logfile.txt # Invert (lines NOT matching) grep -v "debug" logfile.txt # Recursive search grep -r "TODO" ./src/ # Only filenames grep -l "password" *.conf Regex with grep 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Extended regex (-E or egrep) grep -E "error|warning|critical" logfile.txt # Word boundary grep -w "fail" logfile.txt # Matches "fail" not "failure" # Line start/end grep "^Error" logfile.txt # Lines starting with Error grep "done$" logfile.txt # Lines ending with done # Any character grep "user.name" logfile.txt # user1name, username, user_name # Character classes grep "[0-9]" logfile.txt # Lines with digits grep "[A-Za-z]" logfile.txt # Lines with letters Context 1 2 3 4 5 6 7 8 # Lines before match grep -B 3 "error" logfile.txt # Lines after match grep -A 3 "error" logfile.txt # Lines before and after grep -C 3 "error" logfile.txt Real Examples 1 2 3 4 5 6 7 8 9 10 11 # Find IP addresses grep -E "\b[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\b" access.log # Find function definitions grep -n "^def \|^function " *.py *.js # Exclude directories grep -r "config" . --exclude-dir={node_modules,.git} # Find files NOT containing pattern grep -L "copyright" *.py sed: Stream Editor sed transforms text line by line. ...

March 5, 2026 Â· 6 min Â· 1216 words Â· Rob Washington

tmux: Terminal Multiplexing for Productivity

SSH into a server, start a long-running process, lose connection, lose everything. tmux solves this by keeping sessions alive independently of your terminal. Why tmux? Persistence: Sessions survive disconnections Multiplexing: Multiple windows and panes in one terminal Remote pairing: Share sessions with teammates Scriptable: Automate complex layouts Basic Concepts t ├ │ │ │ │ └ m ─ ─ u ─ ─ x S ├ │ │ └ S e ─ ─ e s ─ ─ s s s i W ├ └ W i o i ─ ─ i o n n ─ ─ n n d d ( o P P o 2 n w a a w a n n m 1 e e 2 e d ( 1 2 l c i o k l e l e a c t t i a o b n ) o f w i n d o w s ) Getting Started 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Start new session tmux # Start named session tmux new -s myproject # List sessions tmux ls # Attach to session tmux attach -t myproject # Attach to last session tmux attach The Prefix Key All tmux commands start with a prefix (default: Ctrl+b), then a key. ...

March 5, 2026 Â· 6 min Â· 1131 words Â· Rob Washington

Cron Jobs Done Right: Scheduling Without the Pain

Cron has been scheduling Unix tasks since 1975. It’s simple, reliable, and will silently fail in ways that waste hours of debugging. Here’s how to use it properly. Cron Syntax │ │ │ │ │ └ ─ │ │ │ │ └ ─ ─ ─ │ │ │ └ ─ ─ ─ ─ ─ │ │ └ ─ ─ ─ ─ ─ ─ ─ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ c ─ ─ ─ ─ ─ o m D M D H M m a o a o i a y n y u n n t r u d o h o t f f ( e ( 0 w 1 m - ( e - o 2 0 e 1 n 3 - k 2 t ) 5 ) h 9 ( ) 0 ( - 1 7 - , 3 1 0 ) a n d 7 a r e S u n d a y ) Common Schedules 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Every minute * * * * * /script.sh # Every 5 minutes */5 * * * * /script.sh # Every hour at minute 0 0 * * * * /script.sh # Daily at 3 AM 0 3 * * * /script.sh # Weekly on Sunday at midnight 0 0 * * 0 /script.sh # Monthly on the 1st at 6 AM 0 6 1 * * /script.sh # Every weekday at 9 AM 0 9 * * 1-5 /script.sh # Every 15 minutes during business hours */15 9-17 * * 1-5 /script.sh The Silent Failure Problem Cron’s default behavior: run command, discard output, send errors to email (which you probably haven’t configured). ...

March 5, 2026 Â· 5 min Â· 1035 words Â· Rob Washington

SSH Config Tips That Save Hours

Every time you type ssh -i ~/.ssh/mykey.pem -p 2222 admin@192.168.1.50, you’re wasting keystrokes. The SSH config file exists to eliminate this friction. Basic Host Aliases Create or edit ~/.ssh/config: H H o o s s t t H U I H U I p o s d s o s d r s e e t s e e o t r n a t r n d N t g N t a d i i a d i m e t n m e t e p y g e p y l F l F 2 o i 2 o i 0 y l 0 y l 3 e 3 e . . 0 ~ 0 ~ . / . / 1 . 1 . 1 s 1 s 3 s 3 s . h . h 5 / 5 / 0 p 1 s r t o a d g _ i k n e g y _ k e y Now just: ...

March 5, 2026 Â· 11 min Â· 2208 words Â· Rob Washington

Systemd Service Hardening: Security Beyond the Defaults

Most systemd service files are written for functionality, not security. The defaults give services more access than they need—full filesystem visibility, network capabilities, and the ability to spawn processes anywhere. A few directives can dramatically reduce the blast radius if that service gets compromised. The Security Baseline Start with this template for any service that doesn’t need special privileges: 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 [Service] # Run as dedicated user User=myservice Group=myservice # Filesystem restrictions ProtectSystem=strict ProtectHome=true PrivateTmp=true ReadWritePaths=/var/lib/myservice # Network restrictions (if service doesn't need network) # PrivateNetwork=true # Capability restrictions NoNewPrivileges=true CapabilityBoundingSet= AmbientCapabilities= # System call filtering SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged @resources # Additional hardening ProtectKernelTunables=true ProtectKernelModules=true ProtectKernelLogs=true ProtectControlGroups=true RestrictRealtime=true RestrictSUIDSGID=true MemoryDenyWriteExecute=true LockPersonality=true Understanding Each Directive Filesystem Protection 1 ProtectSystem=strict Mounts /usr, /boot, and /efi read-only. The strict level also makes /etc read-only. Use full if the service needs to write to /etc. ...

March 5, 2026 Â· 4 min Â· 836 words Â· Rob Washington

Linux Performance Troubleshooting: The First Five Minutes

When a server is slow and people are yelling, you need a systematic approach. Here’s what to run in the first five minutes. The Checklist 1 2 3 4 5 6 7 8 uptime dmesg | tail vmstat 1 5 mpstat -P ALL 1 3 pidstat 1 3 iostat -xz 1 3 free -h sar -n DEV 1 3 Let’s break down what each tells you. 1. uptime 1 2 $ uptime 16:30:01 up 45 days, 3:22, 2 users, load average: 8.42, 6.31, 5.12 Load averages: 1-minute, 5-minute, 15-minute. ...

February 28, 2026 Â· 5 min Â· 1007 words Â· Rob Washington