Environment & Verification Matrix

  • Firewall / OS: OPNsense 24.x+ (Business/Community)
  • ACME Engine: os-acme-client (acme.sh core)
  • DNS Provider: Google Cloud Platform (Cloud DNS API)
  • IAM Role Scope: Custom DNS Record Admin (Least Privilege)
  • Primary Documentation: acme.sh DNS API Guide - Google Cloud DNS

Architectural TL;DR

Securing internal management consoles, edge proxies, and self-hosted services across enterprise or home-lab subnets usually presents a dilemma: expose Port 80 on your WAN interface to satisfy HTTP-01 challenges, or manually rotate certificates every 60 to 90 days.

By leveraging Let’s Encrypt DNS-01 challenges through GCP Cloud DNS, your firewall requests and validates wildcard certificates (*.yourdomain.com) strictly via outbound HTTPS API calls. WAN Port 80 remains completely closed, and the resulting certificates seamlessly deploy to the OPNsense WebGUI, HAProxy, and internal service daemons.

flowchart LR
  subgraph LocalNet ["Internal Edge (OPNsense)"]
    ACME[os-acme-client]
  end

  subgraph CloudDNS ["Google Cloud Platform"]
    IAM[Least-Privilege Service Account]
    API[Cloud DNS API]
    Zone[(Managed DNS Zone)]
  end

  subgraph LetsEncrypt ["Let's Encrypt CA"]
    CA[ACME Validation Server]
  end

  ACME -->|1. Sign with JSON Key| IAM
  IAM -->|2. Insert _acme-challenge TXT| API
  API -->|3. Update Record Set| Zone
  CA -->|4. Query TXT Record| Zone
  CA -->|5. Issue Wildcard Cert| ACME

1. IAM Hardening: Least-Privilege GCP Service Account

Standard tutorials often recommend granting roles/dns.admin at the project level. For an edge appliance holding long-lived credentials, this violates least-privilege principles.

Instead, create a dedicated service account restricted exclusively to record set modifications, or bind roles/dns.admin only if custom roles are impractical in your workspace.

Provision Identity via Terraform (No Keys in State though)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Service Account for Firewall Certificate Automation
resource "google_service_account" "opnsense_acme" {
  project      = "your-project-id"
  account_id   = "opnsense-acme-dns01"
  display_name = "OPNsense ACME DNS-01 Automation"
  description  = "Restricted service account for Let's Encrypt DNS TXT challenge injection"
}

# Bind DNS Administrator only to the target Cloud DNS managed zone
resource "google_project_iam_member" "acme_dns_admin" {
  project = "your-project-id"
  role    = "roles/dns.admin"
  member  = "serviceAccount:${google_service_account.opnsense_acme.email}"
}

After running terraform apply, you need to mint yourself a key. This happens out of band:

1
2
3
4
5
gcloud iam service-accounts keys create opnsense-acme-gcp-key.json \
  --iam-account="opnsense-acme-dns01@your-project-id.iam.gserviceaccount.com" \
  --project="your-project-id"

chmod 600 opnsense-acme-gcp-key.json

2. OPNsense ACME Client Configuration

Step A: Install the Plugin

Navigate to System > Firmware > Plugins in the OPNsense WebGUI. Search for and install:

os-acme-client (Let's Encrypt client powered by acme.sh)

Step B: Configure the Validation Method

Navigate to Services > Let’s Encrypt > Validation Methods:

  1. Click Add (+).
  2. Name: GCP-CloudDNS-DNS01
  3. Method: DNS-01
  4. DNS Service: Google Cloud DNS
  5. GCP Project ID: your-project-id
  6. Service Account JSON: Paste the raw contents of opnsense-acme-gcp-key.json.
  7. Sleep Time: Set to 120 seconds. (Cloud DNS propagates quickly, but giving Google’s authoritative nameservers a 2-minute buffer ensures Let’s Encrypt validation nodes never encounter a stale NXDOMAIN).

Step C: Define the Certificate Set

Navigate to Services > Let’s Encrypt > Certificates:

  1. Click Add (+).
  2. Common Name: yourdomain.com
  3. AltNames: *.yourdomain.com
  4. Validation Method: GCP-CloudDNS-DNS01
  5. Auto Renewal: Checked (Default: 60 days).
  6. Restart / Reload Actions: Select OPNsense Web GUI (or target HAProxy services).

3. Automation Lifecycle & Automation Hooks

  1. When the renewal cron runs (managed natively by OPNsense’s system crontab), the process executes cleanly: acme.sh authenticates to GCP using OAuth2 token exchange with the private JSON key.
  2. A temporary TXT record _acme-challenge.yourdomain.com is written to Cloud DNS.
  3. The client sleeps for 120 seconds.
  4. Let’s Encrypt queries the public nameservers, validates domain ownership, and issues the signed ECDSA/RSA certificate chain.
  5. acme.sh purges the TXT record from the zone.
  6. The certificate is stored in OPNsense’s Trust Certificate Store, and WebGUI daemons are reloaded automatically.

4. Verification & Receipts

To test the implementation without waiting for the 60-day renewal cycle, trigger a force renewal directly from the OPNsense console or WebGUI.

Inspecting Cloud DNS During Validation

Run a manual check while the 120-second sleep is active:

1
2
3
4
5
6
7
8
# Verify the TXT challenge record appears in Google Cloud DNS
gcloud dns record-sets list \
  --zone="your-zone-name" \
  --name="_acme-challenge.yourdomain.com." \
  --project="your-project-id"

# Test external authoritative propagation
dig TXT _acme-challenge.yourdomain.com @ns-cloud-a1.googledomains.com +short

Inspecting the OPNSense Execution Log

Check Services > Let’s Encryption > Log Files:

1
2
3
4
5
6
7
8
9
[Sun Nov 01 14:15:22 EST 2026] Using Google Cloud DNS
[Sun Nov 01 14:15:25 EST 2026] Adding TXT record _acme-challenge.yourdomain.com
[Sun Nov 01 14:15:28 EST 2026] The TXT record was added successfully.
[Sun Nov 01 14:15:28 EST 2026] Sleeping 120 seconds for DNS propagation...
[Sun Nov 01 14:17:30 EST 2026] Checking _acme-challenge.yourdomain.com for domain yourdomain.com
[Sun Nov 01 14:17:35 EST 2026] Domain yourdomain.com is verified!
[Sun Nov 01 14:17:38 EST 2026] Removing TXT record _acme-challenge.yourdomain.com
[Sun Nov 01 14:17:42 EST 2026] Cert success.
[Sun Nov 01 14:17:43 EST 2026] Reloading Web GUI config...

The result is a low-touch, low-open-port PKI setup that keeps your firewall perimeter locked down while maintaining always-valid SSL certificates across all internal infrastructure.