4. SSL/TLS Configuration for Nginx
๐ฏ Goal
Configure SSL/TLS encryption for your LEMP stack application using Nginx SSL offloading, providing secure HTTPS connections for your web application and phpMyAdmin interface.
๐ Prerequisites
Before beginning this tutorial, you should:
- Have completed Tutorial 1: Manual LEMP Stack Installation
- Have a working Nginx web server serving your application
- Have SSH access to your Ubuntu VM
- Understand basic concepts of SSL/TLS encryption
- Recommended: Configure your VM with a static public IP address (see note below)
- For Part 2 (Let’s Encrypt): Own a registered domain name pointing to your VM’s public IP
โ ๏ธ Important: Static IP Recommendation
For SSL certificates, especially Let’s Encrypt, it’s highly recommended to use a static public IP address for your VM instead of dynamic IP allocation. Here’s why:
- Domain DNS consistency: Your domain’s DNS records need to point to a stable IP address
- Certificate renewal: Let’s Encrypt validates domain ownership using your IP address
- Avoided downtime: Dynamic IPs can change during VM restarts, breaking your domain resolution
- Simplified management: No need to update DNS records when IP changes
How to configure static IP:
- In your Azure Bicep template, change
publicIpAllocationMethodfrom'Dynamic'to'Static'- Or in Azure Portal: Go to your VM โ Networking โ Public IP โ Configuration โ Assignment: Static
- Note: Static IPs may incur small additional costs (~$0.005/hour when not attached to running resource)
๐ Learning Objectives
By the end of this tutorial, you will:
- Understand SSL/TLS concepts and certificate types
- Create and configure self-signed certificates for development/testing
- Install and configure Let’s Encrypt certificates for production use
- Configure Nginx SSL offloading with proper security headers
- Implement HTTP to HTTPS redirection for secure access
- Set up automatic certificate renewal for Let’s Encrypt
- Understand SSL security best practices and common configurations
๐ Why This Matters
In real-world applications, SSL/TLS encryption is crucial because:
- It protects data in transit between users and your server
- It’s required for compliance with security standards and regulations
- It builds user trust and improves SEO rankings
- It prevents man-in-the-middle attacks and data interception
- It’s essential for secure login systems and sensitive data handling
- Modern browsers mark non-HTTPS sites as “not secure”
Part 1: Self-Signed Certificates (Development/Testing)
Self-signed certificates are perfect for development environments and internal testing where you need encryption but don’t require public trust.
Step 1: Create Self-Signed SSL Certificate
Create a directory for SSL certificates:
sudo mkdir -p /etc/nginx/ssl sudo chmod 700 /etc/nginx/sslGenerate a private key and self-signed certificate:
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/nginx-selfsigned.key \ -out /etc/nginx/ssl/nginx-selfsigned.crtWhen prompted, enter the following information:
- Country Name: Your country code (e.g.,
US,GB,NO) - State/Province: Your state or province
- City: Your city
- Organization: Your organization name (can be anything for testing)
- Organizational Unit: Your department (optional)
- Common Name: IMPORTANT - Use your VM’s public IP or FQDN
- Email: Your email address
- Country Name: Your country code (e.g.,
Create a Diffie-Hellman group for enhanced security:
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048Set proper permissions for the SSL files:
sudo chmod 600 /etc/nginx/ssl/nginx-selfsigned.key sudo chmod 644 /etc/nginx/ssl/nginx-selfsigned.crt sudo chmod 644 /etc/nginx/ssl/dhparam.pem
๐ก Information
- -x509: Creates a self-signed certificate instead of a certificate request
- -nodes: Creates a key without a passphrase (no encryption)
- -days 365: Certificate valid for one year
- RSA 2048: Uses 2048-bit RSA encryption (secure for most purposes)
- Common Name: Must match the domain/IP you’ll use to access the site
Step 2: Configure Nginx for SSL with Self-Signed Certificate
Create an SSL configuration snippet:
sudo nano /etc/nginx/snippets/ssl-params.confAdd SSL security parameters:
# SSL Security Configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_ecdh_curve secp384r1; ssl_session_timeout 10m; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # Security Headers add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Diffie-Hellman parameter for DHE ciphersuites ssl_dhparam /etc/nginx/ssl/dhparam.pem;Create a self-signed certificate snippet:
sudo nano /etc/nginx/snippets/self-signed.confAdd the certificate paths:
ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt; ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;Update the main Nginx configuration to support SSL:
sudo nano /etc/nginx/sites-available/defaultReplace the configuration with SSL-enabled version:
# HTTP Server - Redirect to HTTPS server { listen 80; server_name _; return 301 https://$server_name$request_uri; } # HTTPS Server server { listen 443 ssl http2; server_name _; # SSL Configuration include snippets/self-signed.conf; include snippets/ssl-params.conf; root /var/www/html; index index.php index.html index.nginx-debian.html; # Main application location / { try_files $uri $uri/ =404; } # PHP processing location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; } # phpMyAdmin with SSL location /phpmyadmin { auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; root /var/www/html; location ~ ^/phpmyadmin/(.+\.php)$ { try_files $uri =404; root /var/www/html; include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* ^/phpmyadmin/(.+\.(jpg|jpeg|gif|css|png|js|ico|html|xml|txt))$ { root /var/www/html; } } # Security: Deny access to sensitive files location ~ /\.(ht|git|svn) { deny all; } }Test the Nginx configuration:
sudo nginx -tIf the test passes, reload Nginx:
sudo systemctl reload nginx
Step 3: Update Firewall for HTTPS
Allow HTTPS traffic through the firewall:
sudo ufw allow 'Nginx HTTPS'Verify firewall rules:
sudo ufw statusIMPORTANT: Update Azure Network Security Group
The tutorial firewall (ufw) only controls traffic within the VM. You must also open port 443 in Azure’s Network Security Group:
Option A: Azure Portal
- Go to Azure Portal โ Your VM โ Networking โ Network Security Group
- Click “Add inbound port rule”
- Set: Source: Any, Source port ranges: *, Destination: Any, Service: HTTPS, Action: Allow
- Priority: 1010, Name: “Allow-HTTPS”
Option B: Azure CLI
# Get your resource group and NSG name az vm show --resource-group <your-rg> --name <your-vm> --query "networkProfile.networkInterfaces[0].id" -o tsv # Add HTTPS rule az network nsg rule create \ --resource-group <your-rg> \ --nsg-name <your-nsg> \ --name Allow-HTTPS \ --protocol tcp \ --priority 1010 \ --destination-port-range 443 \ --access allowOption C: If using Bicep templates Add this security rule to your NSG:
{ name: 'Allow-HTTPS' properties: { priority: 1010 access: 'Allow' direction: 'Inbound' destinationPortRange: '443' protocol: 'Tcp' sourcePortRange: '*' sourceAddressPrefix: '*' destinationAddressPrefix: '*' } }
Step 4: Test Self-Signed SSL Certificate
Open your browser and navigate to:
https://<VM_Public_IP>You’ll see a browser warning about the untrusted certificate - this is expected for self-signed certificates.
Click “Advanced” and “Proceed to [IP address] (unsafe)” to access your site.
Verify HTTPS is working:
- Lock icon should appear in address bar (may show “Not Secure” due to self-signed nature)
- Your contact form should work over HTTPS
- phpMyAdmin should be accessible at
https://<VM_Public_IP>/phpmyadmin
โ Part 1 Complete!
Your site now has SSL encryption using a self-signed certificate. While browsers will show warnings, the traffic between users and your server is encrypted.
Part 2: Let’s Encrypt Certificates (Production)
Let’s Encrypt provides free, automated SSL certificates that are trusted by all major browsers. This requires a registered domain name.
๐จ CRITICAL: Azure Network Security Group Requirement
Before starting Let’s Encrypt setup, you MUST open port 443 (HTTPS) in your Azure Network Security Group, otherwise HTTPS will not work from external clients even though the certificate is installed correctly.
Quick Fix: Go to Azure Portal โ Your VM โ Networking โ Add inbound port rule โ Service: HTTPS โ Allow
This is the most common issue when setting up SSL on Azure VMs!
Prerequisites for Let’s Encrypt
Before proceeding with Part 2, you must have:
- A registered domain name (e.g.,
example.com,mysite.org) - DNS records configured to point your domain to your VM’s public IP:
Arecord foryourdomain.comโVM_Public_IPArecord forwww.yourdomain.comโVM_Public_IP(optional)
- Domain propagation completed (may take up to 24 hours after DNS changes)
โ ๏ธ Important Domain Requirement
Let’s Encrypt cannot issue certificates for IP addresses - you must have a domain name. Free options include:
- Freenom (free domains like .tk, .ml)
- No-IP (free subdomains)
- DuckDNS (free dynamic DNS)
- GitHub Pages custom domains
Step 5: Install Certbot for Let’s Encrypt
Install Certbot and the Nginx plugin:
sudo apt update sudo apt install certbot python3-certbot-nginx -yVerify Certbot installation:
certbot --version
Step 6: Configure Domain in Nginx
Update Nginx configuration to use your domain name:
sudo nano /etc/nginx/sites-available/defaultReplace the server configuration with domain-specific settings:
# HTTP Server - Will be used by Certbot for domain validation server { listen 80; server_name yourdomain.com www.yourdomain.com; # Replace with your domain root /var/www/html; index index.php index.html index.nginx-debian.html; # Let's Encrypt challenge location location /.well-known/acme-challenge/ { root /var/www/html; } # Main application location / { try_files $uri $uri/ =404; } # PHP processing location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; } }Test and reload Nginx:
sudo nginx -t sudo systemctl reload nginxVerify your domain is accessible via HTTP:
curl -I http://yourdomain.com
Step 7: Obtain Let’s Encrypt Certificate
Register with Let’s Encrypt (required for first-time use):
sudo certbot register --register-unsafely-without-email --agree-tos --no-eff-emailYou should see:
Account registered.Obtain and install the certificate (replace
yourdomain.comwith your actual domain):sudo certbot --nginx -d yourdomain.com --redirect --non-interactiveExample for DuckDNS domain:
sudo certbot --nginx -d contact-php-app.duckdns.org --redirect --non-interactiveIf successful, you’ll see a message like:
Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/yourdomain.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/yourdomain.com/privkey.pem This certificate expires on [DATE]. Deploying certificate Successfully deployed certificate for yourdomain.com to /etc/nginx/sites-enabled/default Congratulations! You have successfully enabled HTTPS on https://yourdomain.com
๐ก Information
- –nginx: Automatically configures Nginx with the certificate
- -d yourdomain.com: Specifies the domain names for the certificate
- –redirect: Automatically sets up HTTPโHTTPS redirect
- –non-interactive: Avoids prompts (useful for automation)
- –register-unsafely-without-email: Registers without email (optional but functional)
- Certbot will automatically modify your Nginx configuration
- The certificate is valid for 90 days and will auto-renew
Step 8: Configure Secure Nginx Settings for Let’s Encrypt
After Certbot completes, review the updated Nginx configuration:
sudo nano /etc/nginx/sites-available/defaultEnhance the SSL configuration (Certbot will have created basic SSL settings):
# HTTP Server - Redirect to HTTPS server { listen 80; server_name yourdomain.com www.yourdomain.com; return 301 https://$server_name$request_uri; } # HTTPS Server server { listen 443 ssl http2; server_name yourdomain.com www.yourdomain.com; # Let's Encrypt SSL Certificate (managed by Certbot) ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # Enhanced Security Headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';" always; root /var/www/html; index index.php index.html index.nginx-debian.html; # Main application location / { try_files $uri $uri/ =404; } # PHP processing location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; } # phpMyAdmin with enhanced security location /phpmyadmin { auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; # Additional security for admin area add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always; root /var/www/html; location ~ ^/phpmyadmin/(.+\.php)$ { try_files $uri =404; root /var/www/html; include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* ^/phpmyadmin/(.+\.(jpg|jpeg|gif|css|png|js|ico|html|xml|txt))$ { root /var/www/html; } } # Security: Deny access to sensitive files location ~ /\.(ht|git|svn) { deny all; } }Test and reload Nginx:
sudo nginx -t sudo systemctl reload nginx
Step 9: Set Up Automatic Certificate Renewal
Test the renewal process:
sudo certbot renew --dry-runIf the dry run succeeds, check the automatic renewal timer:
sudo systemctl status certbot.timerEnable the timer if it’s not already enabled:
sudo systemctl enable certbot.timer sudo systemctl start certbot.timerVerify when the next renewal check will occur:
sudo systemctl list-timers | grep certbotCreate a renewal hook to reload Nginx after renewal:
sudo nano /etc/letsencrypt/renewal-hooks/post/nginx-reload.shAdd the reload script:
#!/bin/bash /usr/bin/systemctl reload nginxMake the script executable:
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh
๐งช Final Tests
Test 1: Verify SSL Certificate
Open your browser and navigate to:
- Self-signed:
https://<VM_Public_IP>(will show browser warning) - Let’s Encrypt:
https://yourdomain.com(should show green lock)
- Self-signed:
Check SSL certificate details:
- Click the lock icon in your browser
- Verify certificate information
- For Let’s Encrypt: should show “Certificate (Valid)” with Let’s Encrypt as issuer
Test 2: Test HTTPS Functionality
- Test your contact form over HTTPS
- Access phpMyAdmin at
https://yourdomain.com/phpmyadmin - Verify all resources load over HTTPS (no mixed content warnings)
Test 3: Test HTTP to HTTPS Redirect
- Try accessing
http://yourdomain.com - Should automatically redirect to
https://yourdomain.com - Verify redirect works for all pages
Test 4: SSL Security Test
Use SSL Labs test to check your SSL configuration:
https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.comShould achieve A or A+ rating for Let’s Encrypt setup
โ Expected Results
- HTTPS access works without browser warnings (Let’s Encrypt only)
- All HTTP traffic redirects to HTTPS
- Security headers are present in responses
- SSL certificate automatically renews every 90 days
- phpMyAdmin and contact form work securely over HTTPS
๐ง Troubleshooting
Self-Signed Certificate Issues
If browsers reject the self-signed certificate:
- Verify the Common Name matches your access method (IP or domain)
- Check certificate file permissions:
ls -la /etc/nginx/ssl/ - Ensure Nginx can read the certificate files
Let’s Encrypt Certificate Issues
If Certbot fails to obtain certificate:
- Verify domain DNS points to your VM:
nslookup yourdomain.com - Check domain is accessible:
curl -I http://yourdomain.com - Ensure port 80 is open for domain validation
- Check Certbot logs:
sudo tail -f /var/log/letsencrypt/letsencrypt.log
If automatic renewal fails:
- Test renewal manually:
sudo certbot renew --dry-run - Check timer status:
sudo systemctl status certbot.timer - Verify renewal hook permissions:
ls -la /etc/letsencrypt/renewal-hooks/post/
General SSL Issues
If SSL doesn’t work after configuration:
- Test Nginx configuration:
sudo nginx -t - Check SSL certificate paths in Nginx config
- Verify firewall allows HTTPS:
sudo ufw status - Check Nginx error logs:
sudo tail -f /var/log/nginx/error.log
๐ Optional Challenge
Want to enhance your SSL setup further? Try:
- Implementing HTTP Strict Transport Security (HSTS) preloading
- Setting up SSL certificate monitoring and alerts
- Configuring Certificate Transparency monitoring
- Implementing Content Security Policy (CSP) headers
- Setting up SSL certificate backup and recovery procedures
๐ Further Reading
- Let’s Encrypt Documentation - Official Let’s Encrypt documentation
- Mozilla SSL Configuration Generator - Generate secure SSL configurations
- SSL Labs Best Practices - SSL deployment best practices
- OWASP Transport Layer Protection - OWASP SSL/TLS security guide
- Nginx SSL Termination Guide - Official Nginx HTTPS configuration
Done! ๐
Excellent work! You’ve successfully configured SSL/TLS encryption for your LEMP stack application using both self-signed certificates for development and Let’s Encrypt certificates for production. Your application now provides secure HTTPS connections with automatic certificate renewal and proper security headers! ๐