All posts Feature

Certificate Sync for OPNsense: Renew Once, Every Firewall Follows

FEATURE 8 MIN READ NETDEFENSE TEAM

Renewing a wildcard certificate across twelve OPNsense firewalls takes an afternoon. On each box you import the new certificate, point the web GUI, the OpenVPN server and the IPsec tunnel at it, and restart what needs restarting, copying the private key from machine to machine along the way. Miss one firewall and it serves an expired certificate until someone notices. Internal CAs have the same problem: the root CA that your directory’s LDAPS or your VPN clients depend on gets imported by hand, box by box, and nobody can say which firewalls have the current one.

NetDefense now syncs CAs and certificates into the trust store of every firewall you manage, and renews them in place.

TL;DR

  • TRUST_CA syncs a CA certificate. TRUST_CERT syncs a certificate with its private key.
  • The key lives in a secret variable. It’s never part of snippet content, and it can’t be pulled back from a firewall.
  • A renewal keeps every service bound to the certificate, and only the services that use it reload.
  • Needs agent 1.21.0, OPNsense 26.1.11, and a Superuser to write.

One wildcard certificate on every firewall

Put the private key in a secret variable. Redirect the file into it: the CLI’s no-echo prompt reads a single line, so it can’t take a PEM key.

ndcli bash
$ ndcli variable org create wildcard_key --secret --value-stdin < ./wildcard.key

Build the snippet. jq -Rs reads the certificate file as one string and escapes its line breaks for JSON. key names the variable, never the key itself.

shell bash
$ jq -Rs '{name: "Wildcard example.com", crt: ., key: "${wildcard_key}"}' wildcard.crt > wildcard.json
$ ndcli snippet create wildcard-cert --type TRUST_CERT --file wildcard.json

Attach it to a template and sync:

ndcli bash
$ ndcli template add-snippet base-config wildcard-cert
$ ndcli sync apply --template base-config

Every firewall the template reaches now lists Wildcard example.com under System > Trust > Certificates. Select it once on each box for the web GUI, the OpenVPN server or the IPsec connection. NetDefense doesn’t bind services; it keeps what you bound current.

In the web app, the same snippet comes from Snippets → + Create Snippet → Create from scratch, type Trust Certificate. Paste the PEM, name the key variable, and the form writes the JSON.

Renewal: two updates, no re-binding

When the certificate is renewed with a new key, update the key variable and the certificate back to back:

shell bash
$ ndcli variable org set wildcard_key --value-stdin < ./wildcard-2027.key
$ jq -Rs '{name: "Wildcard example.com", crt: ., key: "${wildcard_key}"}' wildcard-2027.crt > wildcard.json
$ ndcli snippet update-content wildcard-cert ./wildcard.json

A sync that runs between the two updates fails with TRUST_KEY_MISMATCH, and every firewall keeps its current certificate and key. A half-renewed pair never reaches a box. In the web app, Replace certificate (PEM) on the snippet’s page swaps in the new certificate; then edit the key variable.

On the next sync, each firewall replaces the certificate in place. OPNsense keeps the certificate’s internal reference (its refid), so the web GUI, OpenVPN and IPsec stay bound without anyone touching them. The agent then reloads what uses the certificate. We checked each one on lab firewalls:

ServiceWhat happens
OpenVPNThe instance using the certificate restarts, and its tunnels reconnect. Other instances keep running.
IPsecReloads without a restart. Established tunnels stay up, and the new certificate is used when the next IKE SA is set up.
Syslog over TLSRestarts. About 0.3 s of logging is lost.
Web GUIRestarts a few seconds after the sync reports back. Unavailable for about a second; logged-in sessions survive.

Captive portal zones restart too. A setting that NetDefense doesn’t know how to reload is reported, so you can restart it yourself. A sync with nothing new reloads nothing.

A certificate of its own for each firewall

Hostname certificates take one snippet and an override per firewall. Create the organization-level variables first, because an override can only exist under one, then override both for each device:

ndcli bash
$ ndcli variable org create fw_crt --value-stdin < ./default.crt
$ ndcli variable org create fw_key --secret --value-stdin < ./default.key
$ ndcli variable device create fw-hq-01 fw_crt --value-stdin < ./fw-hq-01.crt
$ ndcli variable device create fw-hq-01 fw_key --value-stdin < ./fw-hq-01.key
$ ndcli snippet create fw-cert --type TRUST_CERT --content '{"name": "Firewall certificate", "crt": "${fw_crt}", "key": "${fw_key}"}'

Each firewall gets its own pair under the same name, and an override of the secret key variable is secret too. A firewall without overrides gets the organization’s pair. Overrides at OU or template scope work the same way, for one certificate per site.

Override both variables wherever you override one. A firewall with only a new certificate fails its build with TRUST_KEY_MISMATCH instead of receiving a mismatched pair.

An internal CA, trusted everywhere

Sync the root and the issuing CA as two TRUST_CA snippets. A CA is synced without its private key.

shell bash
$ jq -Rs '{name: "Corp Root CA", crt: .}' corp-root.pem > corp-root.json
$ ndcli snippet create corp-root-ca --type TRUST_CA --file corp-root.json
$ jq -Rs '{name: "Corp Issuing CA", crt: .}' corp-issuing.pem > corp-issuing.json
$ ndcli snippet create corp-issuing-ca --type TRUST_CA --file corp-issuing.json

Each change rebuilds the firewall’s system trust store, so LDAPS to your directory, and anything else on the firewall that verifies against that store, trusts what the root signs. Intermediate CAs join the store only when OPNsense’s Store intermediate setting is on, so the root is the one that matters there.

A CA renews in place only with the same subject and key. Moving to a new CA needs a new subject name, such as Corp Root CA G2, in a new snippet. OPNsense matches a certificate to its issuer by name, so two CAs with the same name and different keys can’t coexist, and NetDefense refuses the pair with TRUST_CA_DN_CONFLICT.

Guardrails

  • Only a Superuser writes. Creating, changing or deleting a certificate snippet takes a Superuser signed in interactively, and so does saving any variable it uses. A personal access token is refused even when a Superuser created it, so a leaked CI token can’t swap a certificate or slip in a CA.
  • The key stays where you put it. A private key pasted into snippet content is refused, certificates can’t be pulled from a firewall into a snippet, and errors, task results and logs carry names, never key material. After our lab run, a sweep of device logs, task results and control-plane logs found zero traces of the test keys. Once deployed, the key sits in the firewall’s configuration like any OPNsense certificate key: anyone with root on the box, a full-access remote session included, can read it, and the firewall’s configuration backups carry it.
  • Bad material is refused before it reaches a firewall. NetDefense checks every certificate and key when it builds a device’s configuration. A key that doesn’t belong to its certificate fails with TRUST_KEY_MISMATCH, and the firewall keeps what it has. The same happens to an encrypted key, an RSA-1024 key, or a certificate posing as a CA.
  • Hand-made objects are never touched. NetDefense changes only what it created. A certificate someone added by hand under the same name is reported with NAME_COLLISION_UNMANAGED and left as it is.
  • The agent never trusts a CA you sync. The firewall’s services trust it; the agent’s own connection to NetDefense is checked only against roots built into the agent. In our lab, an endpoint posing as NetDefense with a certificate from a synced CA was refused.

Limits

  • Unencrypted private keys only. openssl pkey removes a passphrase.
  • RSA from 2048 to 4096 bits (CAs up to 8192), or EC on P-256, P-384 or P-521.
  • No CRLs, and no issuing or ACME: bring certificates issued elsewhere.
  • Binding a service to a certificate stays on the firewall.

Getting started

Each firewall needs agent 1.21.0 or later, OPNsense 26.1.11 or later, and Allow All Snippet Content turned on (Services → NetDefense → Settings, Advanced Settings, Configuration Sync, then Apply). Authoring takes a Superuser. From the command line, use the CLI 1.37.0 or later; the web app already has it.

Behind a TLS-inspection proxy? Set extra_ca_file when you upgrade

From 1.21.0, the agent checks its connection to NetDefense only against the public roots built into it, never the firewall’s certificate store. A firewall whose traffic to NetDefense passes through a TLS-inspection proxy disconnects after the upgrade until the proxy’s CA is set in Extra CA File (extra_ca_file) on the firewall. See Extra CA File.

The Certificates guide covers the rest: removal, high availability and every error code.

Join the discussion in our Community

Related reading

Feature RULE and ALIAS Snippets Now Set Every OPNsense Field, and Place Rules Where You Said
Security Superuser Clearance for Snippets That Grant Firewall Admin Rights
Feature Introducing Centralized LDAP and Active Directory Authentication for Your OPNsense Fleet

Enjoyed this post? Explore more in the documentation.