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:

โš ๏ธ 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 publicIpAllocationMethod from '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:

๐Ÿ” Why This Matters

In real-world applications, SSL/TLS encryption is crucial because:


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

  1. Create a directory for SSL certificates:

    sudo mkdir -p /etc/nginx/ssl
    sudo chmod 700 /etc/nginx/ssl
    
  2. Generate 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.crt
    
  3. When 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
  4. Create a Diffie-Hellman group for enhanced security:

    sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
    
  5. Set 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

  1. Create an SSL configuration snippet:

    sudo nano /etc/nginx/snippets/ssl-params.conf
    
  2. Add 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;
    
  3. Create a self-signed certificate snippet:

    sudo nano /etc/nginx/snippets/self-signed.conf
    
  4. Add the certificate paths:

    ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
    
  5. Update the main Nginx configuration to support SSL:

    sudo nano /etc/nginx/sites-available/default
    
  6. Replace 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;
        }
    }
    
  7. Test the Nginx configuration:

    sudo nginx -t
    
  8. If the test passes, reload Nginx:

    sudo systemctl reload nginx
    

Step 3: Update Firewall for HTTPS

  1. Allow HTTPS traffic through the firewall:

    sudo ufw allow 'Nginx HTTPS'
    
  2. Verify firewall rules:

    sudo ufw status
    
  3. IMPORTANT: 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 allow
    

    Option 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

  1. Open your browser and navigate to:

    https://<VM_Public_IP>
    
  2. You’ll see a browser warning about the untrusted certificate - this is expected for self-signed certificates.

  3. Click “Advanced” and “Proceed to [IP address] (unsafe)” to access your site.

  4. 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:

  1. A registered domain name (e.g., example.com, mysite.org)
  2. DNS records configured to point your domain to your VM’s public IP:
    • A record for yourdomain.com โ†’ VM_Public_IP
    • A record for www.yourdomain.com โ†’ VM_Public_IP (optional)
  3. 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

  1. Install Certbot and the Nginx plugin:

    sudo apt update
    sudo apt install certbot python3-certbot-nginx -y
    
  2. Verify Certbot installation:

    certbot --version
    

Step 6: Configure Domain in Nginx

  1. Update Nginx configuration to use your domain name:

    sudo nano /etc/nginx/sites-available/default
    
  2. Replace 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;
        }
    }
    
  3. Test and reload Nginx:

    sudo nginx -t
    sudo systemctl reload nginx
    
  4. Verify your domain is accessible via HTTP:

    curl -I http://yourdomain.com
    

Step 7: Obtain Let’s Encrypt Certificate

  1. Register with Let’s Encrypt (required for first-time use):

    sudo certbot register --register-unsafely-without-email --agree-tos --no-eff-email
    

    You should see: Account registered.

  2. Obtain and install the certificate (replace yourdomain.com with your actual domain):

    sudo certbot --nginx -d yourdomain.com --redirect --non-interactive
    

    Example for DuckDNS domain:

    sudo certbot --nginx -d contact-php-app.duckdns.org --redirect --non-interactive
    
  3. If 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

  1. After Certbot completes, review the updated Nginx configuration:

    sudo nano /etc/nginx/sites-available/default
    
  2. Enhance 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;
        }
    }
    
  3. Test and reload Nginx:

    sudo nginx -t
    sudo systemctl reload nginx
    

Step 9: Set Up Automatic Certificate Renewal

  1. Test the renewal process:

    sudo certbot renew --dry-run
    
  2. If the dry run succeeds, check the automatic renewal timer:

    sudo systemctl status certbot.timer
    
  3. Enable the timer if it’s not already enabled:

    sudo systemctl enable certbot.timer
    sudo systemctl start certbot.timer
    
  4. Verify when the next renewal check will occur:

    sudo systemctl list-timers | grep certbot
    
  5. Create a renewal hook to reload Nginx after renewal:

    sudo nano /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh
    
  6. Add the reload script:

    #!/bin/bash
    /usr/bin/systemctl reload nginx
    
  7. Make the script executable:

    sudo chmod +x /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh
    

๐Ÿงช Final Tests

Test 1: Verify SSL Certificate

  1. 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)
  2. 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

  1. Test your contact form over HTTPS
  2. Access phpMyAdmin at https://yourdomain.com/phpmyadmin
  3. Verify all resources load over HTTPS (no mixed content warnings)

Test 3: Test HTTP to HTTPS Redirect

  1. Try accessing http://yourdomain.com
  2. Should automatically redirect to https://yourdomain.com
  3. Verify redirect works for all pages

Test 4: SSL Security Test

  1. Use SSL Labs test to check your SSL configuration:

    https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com
    
  2. Should achieve A or A+ rating for Let’s Encrypt setup

โœ… Expected Results

๐Ÿ”ง Troubleshooting

Self-Signed Certificate Issues

If browsers reject the self-signed certificate:

Let’s Encrypt Certificate Issues

If Certbot fails to obtain certificate:

If automatic renewal fails:

General SSL Issues

If SSL doesn’t work after configuration:

๐Ÿš€ Optional Challenge

Want to enhance your SSL setup further? Try:

๐Ÿ“š Further Reading

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! ๐Ÿš€