Section 9 — SSL, Domains & DNS

9.1 DNS Record Types

RecordPurposeExample
ADomain → IPv4 addressexample.com → 203.0.113.42
AAAADomain → IPv6 addressexample.com → 2001:db8::1
CNAMEAlias to another domainwww → example.com
MXEmail server routing@ → mail.example.com (priority 10)
TXTSPF, DKIM, verificationv=spf1 ip4:203.0.113.42 ~all
NSAuthoritative nameserversns1.cloudflare.com
PTRReverse DNS (IP → domain)Used for email reputation
CAAWhich CAs can issue SSL0 issue "letsencrypt.org"

Setting Up DNS for a New VPS (Example)

Server IP: 203.0.113.42    Domain: mysite.com

DNS Records to create:
Type    Name    Value              TTL
──────────────────────────────────────────
A       @       203.0.113.42       Auto
A       www     203.0.113.42       Auto
A       api     203.0.113.42       Auto
A       mail    203.0.113.42       Auto
CNAME   blog    mysite.com         Auto
MX      @       mail.mysite.com    10
TXT     @       v=spf1 ip4:203.0.113.42 ~all
# Check DNS propagation
dig mysite.com A
nslookup mysite.com 8.8.8.8    # Ask Google DNS
nslookup mysite.com 1.1.1.1    # Ask Cloudflare DNS
# Online: https://dnschecker.org
⏱️
DNS Propagation Time

After changing DNS records, wait 15 minutes to 48 hours. With Cloudflare as your nameserver, it's usually under 5 minutes.

9.2 Cloudflare — Setup & Benefits

Cloudflare is a CDN + DNS provider that sits in front of your server, providing DDoS protection, free SSL, caching, and a Web Application Firewall.

Without Cloudflare:  User → your-server-ip

With Cloudflare:     User → Cloudflare global network → your-server-ip
BenefitWhat It Does
DDoS ProtectionAbsorbs attack traffic before it hits your server
CDNCaches static files at edge locations worldwide
Free SSLAutomatic HTTPS without server-side Certbot
WAFBlocks SQL injection, XSS, and common attacks
AnalyticsTraffic statistics and bot detection
⚠️
Cloudflare SSL Mode — Set to "Full (Strict)"

Never use Flexible SSL with Cloudflare if your server has HTTPS redirects. It creates redirect loops. Use Full (Strict) with a Let's Encrypt cert on your server.

9.3 Let's Encrypt & Certbot

Let's Encrypt is a free Certificate Authority. Certbot automatically requests and renews certificates.

# Install Certbot
sudo apt install certbot

# For Nginx
sudo apt install python3-certbot-nginx
sudo certbot --nginx -d mysite.com -d www.mysite.com

# For Apache
sudo apt install python3-certbot-apache
sudo certbot --apache -d mysite.com -d www.mysite.com

# Standalone (no web server running)
sudo certbot certonly --standalone -d mysite.com

# Webroot method (web server keeps running)
sudo certbot certonly --webroot -w /var/www/mysite -d mysite.com

# Test auto-renewal (certificates renew every 90 days)
sudo certbot renew --dry-run

# Force renew
sudo certbot renew --force-renewal --cert-name mysite.com

# View all certificates + expiry dates
sudo certbot certificates
/etc/letsencrypt/live/mysite.com/
├── cert.pem       ← Your certificate
├── chain.pem      ← Intermediate certificates
├── fullchain.pem  ← cert.pem + chain.pem (use this in Nginx/Apache)
└── privkey.pem    ← Private key — PROTECT THIS!

Auto-Reload Web Server After Renewal

sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh
#!/bin/bash
systemctl reload nginx
# or: systemctl reload apache2
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh

HTTP → HTTPS Redirect

# Nginx
server {
    listen 80;
    server_name mysite.com www.mysite.com;
    return 301 https://$server_name$request_uri;
}
# Apache .htaccess
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

9.4 SSL Troubleshooting

IssueDiagnosisFix
Certificate expiredsudo certbot certificatessudo certbot renew --force-renewal
Mixed content warningHTTP resources on HTTPS pageUse wp search-replace http:// https:// or update app config
Cloudflare redirect loopSSL mode is "Flexible"Change Cloudflare SSL mode to "Full (Strict)"
"Not Secure" badgeHTTP loaded resourcesCheck browser DevTools Network tab for HTTP requests
Wrong domain on certopenssl s_client -connect mysite.com:443Re-run Certbot for the correct domain

Section 10 — Security Hardening

10.1 UFW Firewall

UFW (Uncomplicated Firewall) is the standard firewall tool on Ubuntu. Block everything you don't need.

🚨
Allow SSH BEFORE Enabling UFW

If you enable UFW without allowing SSH first, you will lock yourself out of the server. This requires a console login to fix.

# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing

# ALWAYS allow SSH first
sudo ufw allow ssh          # Port 22
sudo ufw allow 22/tcp       # Same — explicit

# Allow web traffic
sudo ufw allow http         # Port 80
sudo ufw allow https        # Port 443
sudo ufw allow 'Nginx Full' # Ports 80 + 443 (Nginx profile)

# Allow from specific IP only
sudo ufw allow from 203.0.113.10 to any port 3306   # MySQL from one IP
sudo ufw allow from 203.0.113.10 to any port 22     # SSH from office IP

# Enable UFW
sudo ufw enable

# Check status
sudo ufw status verbose

# Delete a rule
sudo ufw delete allow 3306

# Standard production setup
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

10.2 Fail2ban — Brute Force Protection

Fail2ban monitors log files and automatically bans IPs after too many failed attempts.

sudo apt install fail2ban

# Create local config (never edit jail.conf directly)
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 3600   ; Ban for 1 hour
findtime = 600    ; 10-minute window
maxretry = 5      ; Ban after 5 failures

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 3      ; Stricter for SSH

[nginx-http-auth]
enabled = true
logpath = %(nginx_error_log)s
maxretry = 6

[apache-auth]
enabled = true
logpath = %(apache_error_log)s
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check what jails are active
sudo fail2ban-client status

# Check who's banned for SSH
sudo fail2ban-client status sshd

# Unban an IP
sudo fail2ban-client set sshd unbanip 203.0.113.42

10.3 SSH Hardening

sudo nano /etc/ssh/sshd_config
# Recommended settings
Port 2222                  # Change default port (optional but helpful)
PermitRootLogin no         # Never login as root via SSH
PasswordAuthentication no  # Use keys only (set AFTER setting up keys!)
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
ClientAliveInterval 300    # Disconnect idle sessions after 5 min
ClientAliveCountMax 2
AllowUsers ubuntu developer  # Only allow specific users
Protocol 2
# Test config before restarting
sudo sshd -t             # No output = OK

sudo systemctl restart sshd

Setting Up SSH Key Authentication

# On your LOCAL machine — generate key pair
ssh-keygen -t ed25519 -C "your-email@example.com"
# Creates: ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public)

# Copy public key to server
ssh-copy-id ubuntu@server-ip

# Or manually:
cat ~/.ssh/id_ed25519.pub | ssh ubuntu@server-ip \
  "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

# Now test key-based login
ssh ubuntu@server-ip   # Should NOT ask for password

# Only THEN disable password auth in sshd_config
PasswordAuthentication no

10.4 Server Security Checklist

Section 11 — Troubleshooting Master Guide

11.1 The Debugging Mindset

Before panicking, follow this structured debugging approach:

Problem appears
     ↓
1. WHAT exactly is the symptom? (404? 502? Server down? Slow?)
     ↓
2. WHEN did it start? (After deploy? After reboot? Randomly?)
     ↓
3. WHAT changed? (Code? Config? Server update? DNS?)
     ↓
4. CHECK THE LOGS first — they almost always tell you the cause
     ↓
5. ISOLATE the problem (web server? PHP? DB? DNS? App code?)
     ↓
6. FIX and TEST
     ↓
7. DOCUMENT what caused it and how you fixed it

11.2 Log File Locations — Where to Look First

# Priority order when debugging:

# 1. Web server error log (MOST important — start here)
sudo tail -n 50 /var/log/nginx/error.log
sudo tail -n 50 /var/log/apache2/error.log

# 2. Site-specific error log
sudo tail -n 50 /var/log/nginx/mysite-error.log

# 3. PHP-FPM log
sudo tail -n 50 /var/log/php8.2-fpm.log

# 4. Application logs
tail -n 50 /var/www/mylaravelapp/storage/logs/laravel.log
pm2 logs my-node-app

# 5. Service status
sudo systemctl status nginx
sudo systemctl status php8.2-fpm
sudo systemctl status mysql

# 6. System logs
sudo journalctl -u nginx -n 50
sudo tail -n 50 /var/log/syslog

# 7. Disk space (often forgotten!)
df -h

# 8. Memory
free -h

11.3 HTTP Status Code Reference

CodeNameCommon CauseFix
200OK✅ Success—
201CreatedResource created (API POST)—
301Moved PermanentlyDomain migration, HTTP→HTTPSEnsure redirect is correct
302Found (Temp)Temporary redirect—
304Not ModifiedCached content still valid—
400Bad RequestMalformed request, invalid JSONCheck request format/headers
401UnauthorizedNot logged in, invalid tokenCheck auth headers/session
403ForbiddenWrong permissions, .htaccess denychown, chmod, check VHost config
404Not FoundFile missing, routing not set upCheck URL, file, try_files
405Method Not AllowedGET on a POST-only routeCheck API route method
413Payload Too LargeFile too bigIncrease PHP/Nginx upload limits
422Unprocessable EntityValidation failedCheck input data
429Too Many RequestsRate limit hitWait, or increase limit
500Internal Server ErrorPHP error, .htaccess errorCheck error.log
502Bad GatewayPHP-FPM/Node.js not runningRestart upstream service
503Service UnavailableOverloaded or maintenance modeCheck resources, php artisan up
504Gateway TimeoutUpstream too slowIncrease timeout, fix slow query

11.4 Debugging 500 Internal Server Error

💡
500 is the Most Common Error for Freshers

Don't panic. Open the error log. The log will almost always tell you exactly what went wrong and on which line.

What Log SaysCauseFix
PHP Fatal error: Uncaught Error...PHP code errorFix the code on the line mentioned
PHP Parse error: syntax error...Syntax error in PHP fileRun php -l problematic-file.php
Invalid command 'RewriteEngine'mod_rewrite not enabledsudo a2enmod rewrite && reload apache2
Call to undefined function curl_init()Missing PHP extensionsudo apt install php8.2-curl
Unable to write to storage/Wrong permissionschmod -R 775 storage
No application encryption keyLaravel APP_KEY missingphp artisan key:generate
SQLSTATE[HY000] Connection refusedMySQL not running or wrong credsCheck MySQL status, check .env
# Debugging commands
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/apache2/error.log
tail -n 100 /var/www/myapp/storage/logs/laravel.log

# Test PHP file syntax
php -l /var/www/myapp/routes/web.php

11.5 Debugging 502 Bad Gateway

502 means Nginx received no response from the upstream application (PHP-FPM or Node.js).

# Check Nginx error log first
sudo tail -n 50 /var/log/nginx/error.log
# Look for: "connect() failed (111: Connection refused) while connecting to upstream"

# For PHP-FPM:
sudo systemctl status php8.2-fpm
sudo systemctl restart php8.2-fpm
ls -la /run/php/php8.2-fpm.sock    # Does the socket exist?

# For Node.js:
pm2 status                          # Is app running?
pm2 restart my-app
sudo ss -tlnp | grep :3000          # Is something on port 3000?

# Check Nginx config matches reality
# Config says: fastcgi_pass unix:/run/php/php8.2-fpm.sock
# Does the socket file actually exist?

11.6 Debugging 403 Forbidden

CauseCheckFix
Wrong ownershipls -la /var/www/mysite/sudo chown -R www-data:www-data /var/www/mysite
Permissions too strictls -la /var/www/mysite/sudo chmod -R 755 /var/www/mysite
Apache AllowOverride NoneCheck VirtualHost configChange to AllowOverride All
Directory listing disabled + no indexls /var/www/mysite/Create an index.php file
.htaccess Deny rulecat /var/www/mysite/.htaccessRemove or fix the Deny rule

11.7 Debugging 404 Not Found

CauseFix
File genuinely missingUpload the file to the correct path
Laravel: DocumentRoot not pointing to public/Fix VirtualHost/server block DocumentRoot
WordPress: mod_rewrite not enabledsudo a2enmod rewrite + AllowOverride All
Nginx: try_files missingAdd try_files $uri $uri/ /index.php?$query_string
React Router: refresh returns 404Add try_files $uri $uri/ /index.html to Nginx
Domain pointing to wrong serverCheck dig mysite.com A

11.8 Troubleshooting Checklist Template

When anything breaks on a server, work through this list systematically:

Section 12 — Deployment Workflows

12.1 Git for Deployments

sudo apt install git

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

# Clone a repository
git clone https://github.com/yourorg/project.git
git clone git@github.com:yourorg/project.git   # SSH (no password prompts)

# Pull latest changes
git pull origin main

git status            # Check what changed
git log --oneline -20 # Recent commits
git branch            # Current branch

Set Up SSH Key for GitHub/GitLab

# Generate key on server
ssh-keygen -t ed25519 -C "server@mysite.com"
# Press Enter for all prompts

# View public key
cat ~/.ssh/id_ed25519.pub
# Add this key to GitHub: Settings → SSH Keys → New SSH Key

# Test
ssh -T git@github.com
# "Hi username! You've successfully authenticated"

12.2 Laravel Deployment Workflow

#!/bin/bash
# /usr/local/bin/deploy-laravel.sh
# Run: sudo /usr/local/bin/deploy-laravel.sh

set -e   # Exit on any error
APP_DIR="/var/www/mylaravelapp"

echo "Starting Laravel deployment..."
cd $APP_DIR

# Enable maintenance mode
php artisan down --message="Deploying update. Back in a minute." --retry=60

# Pull latest code
git fetch origin
git reset --hard origin/main

# Install dependencies (no dev, optimize autoloader)
composer install --no-dev --optimize-autoloader

# Run migrations
php artisan migrate --force

# Clear and rebuild all caches
php artisan config:clear
php artisan config:cache
php artisan route:clear
php artisan route:cache
php artisan view:clear
php artisan view:cache
php artisan cache:clear

# Restart queue workers (picks up new code)
php artisan queue:restart

# Fix permissions
chown -R www-data:www-data $APP_DIR/storage $APP_DIR/bootstrap/cache

# Disable maintenance mode
php artisan up
echo "Deployment complete! ✓"
sudo chmod +x /usr/local/bin/deploy-laravel.sh
sudo /usr/local/bin/deploy-laravel.sh

12.3 WordPress Deployment Workflow

# ALWAYS BACKUP BEFORE UPDATING
# Backup database
mysqldump -u root -p wordpress_db > /backup/wp_$(date +%Y%m%d).sql

# Backup files
tar -czvf /backup/wp_files_$(date +%Y%m%d).tar.gz /var/www/myclientsite

# Update via WP-CLI
cd /var/www/myclientsite
sudo -u www-data wp core update
sudo -u www-data wp plugin update --all
sudo -u www-data wp theme update --all

# Clear cache
sudo -u www-data wp cache flush
sudo -u www-data wp rewrite flush

# Verify site works
curl -I https://myclientsite.com

12.4 React App Deployment

#!/bin/bash
# /usr/local/bin/deploy-react.sh

APP_DIR="/var/www/react-app"
WEB_DIR="/var/www/myreactapp"

echo "Deploying React app..."
cd $APP_DIR

git fetch origin
git reset --hard origin/main

npm install --production=false
npm run build

cp -r build/* $WEB_DIR/
chown -R www-data:www-data $WEB_DIR
echo "React deployment complete! ✓"
# Or: build locally, sync to server
npm run build
rsync -avz --delete ./build/ ubuntu@server-ip:/var/www/myreactapp/

12.5 Node.js App Deployment

#!/bin/bash
# /usr/local/bin/deploy-node.sh

APP_DIR="/var/www/nodeapp"
APP_NAME="my-api"

echo "Deploying Node.js app..."
cd $APP_DIR

# Pull latest code
git fetch origin
git reset --hard origin/main

# Install dependencies
npm install --production

# Zero-downtime reload with PM2
pm2 reload $APP_NAME

echo "Node.js deployment complete! ✓"
sudo chmod +x /usr/local/bin/deploy-node.sh
sudo /usr/local/bin/deploy-node.sh

Rollback Strategy

# Git rollback to previous commit
cd /var/www/mylaravelapp

# Find the last good commit
git log --oneline -10

# Roll back to a specific commit
git reset --hard <commit-hash>

# Restore caches
php artisan config:cache
php artisan route:cache
php artisan up   # Remove maintenance mode