This week, I deployed a private n8n automation instance in Azure with a focus on security, auditability, and zero public exposure. Here’s how I solved the HTTPS challenge without storing credentials or opening ports unnecessarily.

Problem Statement

I needed to:

  • Run n8n privately for internal automations
  • Enable HTTPS for browser access and webhook security
  • Use Let’s Encrypt for free TLS certs
  • Avoid storing Azure credentials on the VM
  • Keep the VM locked down with minimal exposure

Azure VM and NSG Setup

  • Deployed Ubuntu VM with n8n running via systemd
  • Configured Azure Network Security Group (NSG) to allow:
    • Port 22 (SSH) and 443 (HTTPS) only
    • Scoped to my static IP
  • Temporarily opened port 80 for Let’s Encrypt HTTP challenge

SSL Issue: Nginx Serving Self-Signed Cert

Despite running Certbot successfully, openssl s_client revealed:

verify error:num=18:self-signed certificate
issuer=CN = myn8n.southcentralus.cloudapp.azure.com

Nginx was still pointing to a self-signed cert:

ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;

Fix

Updated Nginx to use Certbot’s issued certs:

ssl_certificate /etc/letsencrypt/live/myn8n.southcentralus.cloudapp.azure.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myn8n.southcentralus.cloudapp.azure.com/privkey.pem;

Reloaded Nginx:

sudo nginx -t && sudo systemctl reload nginx

Using Managed Identity for NSG Rule Automation

To avoid storing Azure credentials:

  • Enabled System Assigned Managed Identity on the VM
  • Granted Network Contributor on the NSG resource
  • Wrote a script that:
    • Temporarily opens port 80
    • Runs Certbot with HTTP challenge
    • Closes port 80 immediately after

This allows (more) secure, credential-free NSG modification using Azure CLI authenticated via managed identity.

Disabling Certbot’s Systemd Timer

To ensure NSG rules are modified before renewal, I disabled Certbot’s default timer:

sudo systemctl disable certbot.timer
sudo systemctl stop certbot.timer

Instead, I use a cron job that runs the NSG rule script followed by Certbot:

0 3 * * * /home/n8n/scripts/renew-cert.sh >> /var/log/letsencrypt-renew.log 2>&1

Final Validation

Verified with:

openssl s_client -connect myn8n.southcentralus.cloudapp.azure.com:443 -servername myn8n.southcentralus.cloudapp.azure.com

Expected output:

Verify return code: 0 (ok)
issuer=/C=US/O=Let's Encrypt/...

Outcome

  • HTTPS with valid cert
  • No secrets stored on disk
  • Minimal public exposure
  • Fully automated renewals via cron
  • NSG rule automation via managed identity

This setup is ideal for secure, internal automation workflows in Azure—no third-party dependencies, no credential leakage, and full auditability.