Skip to content

Certificates

NetDefense can keep CAs and certificates consistent across your OPNsense fleet: an internal root or intermediate CA trusted on every firewall, one wildcard certificate everywhere, or a certificate of its own for each firewall. Two snippet types sync them into each device’s trust store (System > Trust):

TypeWhat it syncsWhere it appears on the firewall
TRUST_CAA CA certificate, without its private keySystem > Trust > Authorities
TRUST_CERTA certificate and its private keySystem > Trust > Certificates

Renewal updates the object in place. A renewed certificate or CA keeps its identity on the firewall, so every service bound to it stays bound, and the device reloads those services — see What happens on renewal.

Binding a service is still done on the firewall. NetDefense doesn’t choose which service uses a certificate. Select it once on the device, for the web GUI, an OpenVPN instance, an IPsec connection and so on, and from then on NetDefense renews what’s bound.

Before you start

  • Agent 1.21.0 or later on every device that receives a certificate or CA. A device on an older agent fails its build with TRUST_AGENT_TOO_OLD, and no configuration of any kind reaches it until the agent is upgraded or the snippet no longer applies to it. Upgrading a firewall that sits behind a TLS-inspection proxy needs one extra setting — see Extra CA File.
  • OPNsense 26.1.11 or later. Below it, or when the agent can’t read the release, the device refuses every add or update with TRUST_OPNSENSE_TOO_OLD. Removals still go through.
  • An unencrypted private key for each certificate. OPNsense can’t import an encrypted one — see Private keys.
  • “Allow All Snippet Content” turned on in the plugin’s settings (Services > NetDefense > Settings, under Advanced Settings, Configuration Sync) on any device where NetDefense should add or update a certificate or CA. It’s off by default on a fresh install, and while it’s off the device refuses those changes with TRUST_REJECTED_DANGEROUS. Removals always go through. The refusal names this setting and where to turn it on, with the agent’s own key in parentheses (reject_dangerous_snippets=false); change it on the settings page, not in the agent’s configuration file — see Allow All Snippet Content.
  • A Superuser signed in interactively for every write to either type. A personal access token is refused — see Who can change certificates.
  • The CLI 1.37.0 or later if you author these snippets from the command line (ndcli version; upgrade with brew/scoop).

A certificate or CA is a snippet, just like a firewall rule or an alias — see Snippets if that concept is new. Its content is JSON:

{
"name": "Corp Root CA",
"crt": "-----BEGIN CERTIFICATE-----\nMIIFazCCA1OgAwIBAgIU…\n-----END CERTIFICATE-----\n"
}
{
"name": "fw-wildcard",
"crt": "${fw_wildcard_crt}",
"key": "${fw_wildcard_key}"
}

The first is a TRUST_CA with its certificate written into the content; the second, a TRUST_CERT that takes its certificate and its key from variables.

  • name is the object’s description on the firewall, the name System > Trust lists it under. It takes 1 to 255 bytes (UTF-8), with no leading or trailing spaces, no control characters, and no ${. Two snippets of the same type that reach one device can’t share a name, ignoring case (TRUST_NAME_CONFLICT). Changing it later renames the object on the firewall; its bindings stay.
  • crt is exactly one PEM certificate (-----BEGIN CERTIFICATE-----) with nothing around it but white space, or exactly ${variable}, naming a variable that holds one. It never carries a chain or a key: sync each CA of the chain as its own TRUST_CA, and the firewall links each certificate to its issuer. A TRUST_CA certificate must be a CA certificate (basic constraints CA:TRUE).
  • key (TRUST_CERT only) is exactly ${variable}, naming a secret variable that holds the private key. A key is never accepted in the content itself — see Private keys.
  • Leave out uuid. NetDefense assigns it when the snippet is created and refuses one supplied on create (TRUST_UUID_SUPPLIED). It identifies the object on every firewall, so an update may leave it out or send the same one back, but never change it (TRUST_UUID_IMMUTABLE).
  • No other field is accepted, and no field may appear twice.

A file exported by another tool often carries text around the certificate, or a whole chain. openssl x509 -in fullchain.pem -out fw-wildcard.crt writes out the first certificate alone.

A snippet can be created, and attached to a template, before the variables it names exist. A device’s build fails with UNDEFINED_VARIABLES until they do.

JSON needs the PEM’s line breaks escaped, and jq can do it: -R reads the file raw and -s keeps it as one string. Single-quote the jq program so your shell leaves ${...} alone:

Terminal window
jq -Rs '{name: "Corp Root CA", crt: .}' corp-root-ca.pem > corp-root-ca.json
ndcli snippet create corp-root-ca --type TRUST_CA --file corp-root-ca.json
jq -Rs '{name: "fw-wildcard", crt: ., key: "${fw_wildcard_key}"}' fw-wildcard.crt > fw-wildcard.json
ndcli snippet create fw-wildcard --type TRUST_CERT --file fw-wildcard.json

Then attach the snippets to a template, as with any snippet.

Open Snippets → + Create Snippet → Create from scratch, and pick Trust CA or Trust Certificate as the Type. The form asks for the Name (as it will appear on the device), the certificate (Paste PEM, or From a variable for per-device certificates) and, for a certificate, the Private key variable. It writes the JSON for you, with the PEM escaped; Edit as JSON instead shows it. A private key pasted where the certificate goes is refused before anything is sent. Create & Edit saves the snippet.

A certificate’s private key always comes from a variable, never from the snippet content. That variable must be:

  • Secret (SECRET_VARIABLE_REQUIRED). A secret’s value is never shown again: the web app, the CLI, and the API return a placeholder, and the value can only be replaced. The secret flag can’t be added to an existing variable, so set it when you create one.
  • Saved by a Superuser, at every scope that applies to a device, overrides included (SECRET_VARIABLE_CLEARANCE).
  • Used only as a certificate’s key. It can appear in the key of any number of TRUST_CERT snippets, and nowhere else: not in other snippet content, not in a crt, and not as an authentication server’s bind password (SECRET_VARIABLE_MISUSE).
  • An unencrypted PEM key, alone in the value: PRIVATE KEY, RSA PRIVATE KEY, or EC PRIVATE KEY. An encrypted key fails the build with TRUST_KEY_INVALID. openssl pkey writes a key out unencrypted, in a form NetDefense accepts, and asks for the passphrase of an encrypted one.

From the command line, redirect the key file into a new secret variable:

Terminal window
ndcli variable org create fw_wildcard_key --secret --value-stdin < ./fw-wildcard.key

Always use --value-stdin. Without it the CLI prompts for the value, and the prompt reads a single line, which can’t hold a PEM key. To remove a passphrase on the way in, pipe the key through openssl pkey instead of saving a decrypted copy:

Terminal window
openssl pkey -in ./fw-wildcard-encrypted.key | ndcli variable org create fw_wildcard_key --secret --value-stdin

In the web app, open Variables → + Create Variable, turn on Secret and Multi-line value (PEM), and paste the key. The field shows only a summary such as PEM private key, 28 lines, hidden, with Reveal to check it and Clear to start over. The web app refuses a private key in a variable that isn’t secret.

The MCP server’s variable tools refuse to write a secret value at all, so a key can’t be set through an AI assistant. Use the CLI or the web app.

One snippet can give every firewall a certificate of its own: take crt and key from variables, and override both variables for each device. An override at OU or template scope works the same way, for a certificate per site, say.

  1. Create the organization-level variables. An override can only exist under an organization-level variable of the same name, and the organization value is what a device without an override receives (see below). Make the key variable secret now; that can’t be changed later.

    Terminal window
    ndcli variable org create fw_crt --value-stdin < ./fw-default.crt
    ndcli variable org create fw_key --secret --value-stdin < ./fw-default.key
  2. Override both variables for each device. An override of a secret variable is secret too.

    Terminal window
    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 variable ou create and ndcli variable template create override at the other scopes. In the web app, open the variable’s Overrides.

  3. Create one TRUST_CERT with "crt": "${fw_crt}" and "key": "${fw_key}", and attach it to the template those devices use.

A device with no override gets the organization value. Unless the organization’s certificate and key are a matching pair you’re happy to put on that device, its whole build fails (with TRUST_KEY_MISMATCH when only one of the two is overridden, for example), and no configuration reaches it until that’s fixed. Override both variables for every device that receives the snippet.

When a certificate is renewed with a new key, update the key variable and the certificate back to back, as a Superuser. Until both are in, every build that includes them fails with TRUST_KEY_MISMATCH, including a scheduled sync’s. Nothing breaks on the firewall, which keeps its current certificate and key, but no configuration reaches that device until the pair matches again. When the key doesn’t change, update the certificate alone.

  • The key: ndcli variable org set fw_wildcard_key --value-stdin < ./fw-wildcard-2027.key, or the same at the scope of the override you’re renewing (ndcli variable device set fw-hq-01 fw_key --value-stdin < ./fw-hq-01-2027.key). In the web app, Edit the variable or the override.
  • A certificate in a variable: set the variable the same way.
  • A certificate in the content: in the web app, open the snippet, click Replace certificate (PEM), paste the new certificate, click Replace in editor, then Save Changes. The new certificate goes in escaped, and everything else stays as it was. From the command line, rebuild the content with the jq recipe and run ndcli snippet update-content fw-wildcard ./fw-wildcard.json. Leaving uuid out is fine; NetDefense keeps it.

The next sync writes the new certificate over the old one and reloads what uses it.

A CA renews in place only when the new certificate keeps the same subject and the same key, as when it’s reissued with a later expiry. Anything else is refused with TRUST_CA_REKEYED: the certificates the CA issued would no longer chain to it, and OPNsense, which matches issuers by name, wouldn’t notice.

A CA rollover needs a new subject name, for example Corp Root CA G2, in a new TRUST_CA snippet. OPNsense matches a certificate to its issuer by name, so two CAs with the same subject and different keys can’t coexist on a firewall: NetDefense refuses the pair with TRUST_CA_DN_CONFLICT, whether the other CA comes from NetDefense or was added on the firewall by hand.

  1. Create a TRUST_CA for the new CA, and sync it alongside the old one.

  2. Reissue the certificates from the new CA, and renew each one as above.

  3. Detach the old CA once nothing on the firewall uses it, and sync.

OPNsense chooses a certificate’s CA by name, and loosely: a CA qualifies when every value in its subject appears somewhere in the certificate’s issuer name. So a root such as CN=Acme, O=Acme, C=US can take over the certificates its intermediate CN=Acme Issuing CA, O=Acme, C=US issued. NetDefense creates parent CAs first, so that the intermediate wins, and never writes anything that would link a certificate to a CA that didn’t issue it.

When OPNsense links one that way anyway, for example because an overlapping CA was added by hand, the sync reports TRUST_ISSUER_MISLINKED, names that CA, and fails until the CA is removed from the firewall or reissued with a subject that doesn’t overlap. A renewal of a certificate that’s already linked that way still goes through, so the certificate doesn’t expire while you sort it out.

Once a renewed certificate or CA is written, the device reloads each service that uses it:

ServiceEffect
OpenVPNOnly the instances using the certificate restart; their tunnels reconnect.
IPsecReloads. The new certificate is used for new IKE sessions; established tunnels keep running.
Syslog over TLSRestarts, with about a third of a second of logging lost.
Captive portalRestarts.
Web GUIRestarts a few seconds after the sync completes. Unavailable for about a second; sessions are kept.
CA changeThe system trust store is rebuilt automatically. Intermediate CAs join it only if OPNsense’s Store intermediate setting (System > Trust > Settings) is on.
Unknown consumerReported as a warning (TRUST_CONSUMER_UNKNOWN), so you can restart it yourself.
  • Only real changes reload anything. A sync whose certificates and CAs already match what’s on the firewall writes nothing and reloads nothing.
  • A CA renewal also reloads the services that use any certificate the CA issued.
  • A failed reload is a warning (TRUST_RELOAD_FAILED) naming the service, not a failed sync: the new certificate is in place, so restart that service yourself.
  • The web GUI restart is reported as scheduled (TRUST_RELOAD_SCHEDULED) and happens after the sync’s result is sent; the next sync waits until the web GUI answers again. A remote session to the web GUI isn’t cut: its requests fail for about a second, then it carries on.
  • An interrupted sync doesn’t lose its reloads: if the agent stops after writing a renewal, it runs the reloads on its next sync.

Detach the snippet, or its template, and sync: the device removes the object, certificates first, then CAs. A certificate and the CA that issued it go in the same sync.

What’s still in use stays. The device keeps:

  • a certificate a service uses;
  • a CA a service uses, or that issued a certificate or CA still on the firewall, one created by hand included.

A kept object is reported with TRUST_IN_USE, naming what uses it, and the sync fails until it’s unbound; the rest of the configuration still applies. Unbind it on the firewall, then sync again. OPNsense doesn’t stop a CA from being deleted while certificates depend on it; this check is NetDefense’s own.

Removals are never blocked by Allow All Snippet Content or by the OPNsense version.

Removing a device removes the certificates and CAs NetDefense manages on it, except those still in use, which stay on the firewall. See Remove a Device.

Every write to a TRUST_CA or TRUST_CERT snippet (creating, updating, renaming, reprioritizing, or deleting it) needs the Superuser role from an interactive login: ndcli auth login, or the web app. A personal access token is capped at Read-Write and is refused, whatever the role of the account that created it. See Organization Roles for what the roles mean generally.

Every variable these snippets use must be saved by a Superuser too: the key, a crt variable (TRUST_VARIABLE_CLEARANCE), and every override of either. A Read-Write member therefore can’t swap in a certificate or CA of their own.

Read-Write residuals worth knowing about:

  • Attaching is a lower bar. Adding a certificate snippet to a template, adding that template to an OU, or moving a device into such an OU only needs Read-Write, so a Read-Write member can put a Superuser-authored certificate or CA on more devices.
  • Variables can still be changed or deleted. A Read-Write member can set or delete a certificate’s variables. A value they save fails the build of every device that uses it, so this can stop syncs, but it can’t change what’s deployed.

NetDefense controls who can change a certificate or CA. It doesn’t stop everyone with access to a firewall from reading a key deployed there, so plan for this:

  • In NetDefense, a key is stored as written: in its secret variable, and in each sync task that delivers it to a device.
  • On the firewall, OPNsense keeps the private key in its configuration, so the key is also in the firewall’s local configuration backups and in NetDefense’s configuration backups.
  • Read-Write members can reach every deployed key. One signed in interactively can open a remote session, a root shell, on any device whose Remote Access Policy is Full access (the default); a device set to Read-only web UI only or Disabled closes that path. And Read-Write members manage where backups go: they can point the organization’s backups at a bucket and encryption key of their own, and every backup carries the firewall’s full configuration, keys included. Treat Read-Write membership as access to every key you deploy.
  • A read-only web admin session doesn’t show private keys. Their fields are blank, a private key or PKCS#12 bundle can’t be downloaded, and a search of the Certificates or Authorities list is refused with an error, because a search could otherwise recover a key one character at a time.
  • A synced CA is trusted system-wide on the firewall. Anything there that checks certificates against the system trust store, such as an LDAPS connection to a directory, accepts what that CA signs. Sync only CAs you control. The agent’s own connection to NetDefense never trusts a synced CA: it checks NetDefense’s certificate against root certificates built into the agent — see Extra CA File.
  • Errors, results, and logs name certificates, CAs, and variables, never key material.

Reissue a certificate with a new key whenever a device that used it is removed or restored from an old backup, and whenever a configuration backup or its encryption key is exposed.

  • Key types: RSA from 2048 to 4096 bits for a certificate (a CA’s RSA key may be up to 8192 bits), or EC on P-256, P-384, or P-521. A shorter RSA key is refused with TRUST_KEY_WEAK; anything else, such as a longer RSA key, another curve, DSA, Ed25519, or Ed448, with TRUST_KEY_UNSUPPORTED.
  • No CA private keys. A TRUST_CA carries the CA’s certificate only.
  • No CRLs. Certificate revocation lists aren’t synced.
  • No issuing. NetDefense doesn’t create certificates, sign requests, or run ACME; bring certificates issued elsewhere.
  • No service binding. Binding is done on the firewall; NetDefense renews what’s bound.
  • No pull. Certificates and CAs can’t be pulled from a device into a snippet, since a pull would copy the private key off the device in the clear.
  • No adoption. A CA or certificate created by hand on the firewall is never adopted or changed. One with the same name as a NetDefense object, ignoring case, is reported with NAME_COLLISION_UNMANAGED, and the NetDefense object isn’t written until one of the two is renamed or removed.
  • No expiry check. An expired certificate is synced as given; keep track of expiry yourself.

If both firewalls of a CARP/HA pair are managed by NetDefense, and the pair synchronizes Certificates and Authorities (System > High Availability > Settings), the two fight over these objects: each HA synchronization copies the primary’s certificates and CAs over the secondary’s, and the secondary’s next sync puts back what NetDefense delivers to it.

Leave Certificates and Authorities out of HA synchronization on any pair NetDefense manages, and let NetDefense deliver to each firewall.

A hand edit on the firewall to a certificate or CA NetDefense manages doesn’t last: every sync that includes it compares what’s on the firewall with what NetDefense delivers, writes its own version back, and reloads the services that use it. Change the snippet or its variables instead.

A build error fails the whole device build, not just its certificates: no configuration of any kind reaches that device until it’s fixed, and the firewall keeps what it has. The device’s error is TRUST_BUILD_INVALID, or AUTH_BUILD_INVALID when it also has an authentication server problem, with one trust_issues entry per problem naming the code, the snippet, the template, and the object’s name. Messages name things; they never quote a certificate or a key.

CodeMeaningFix
TRUST_AGENT_TOO_OLDThe device’s agent is older than 1.21.0, or its version can’t be read.Upgrade the agent, or keep certificate snippets off that device.
TRUST_CRT_INVALIDcrt isn’t exactly one readable certificate.Put one PEM certificate in crt, or in its variable, with nothing around it.
TRUST_CRT_CONTAINS_KEYThe variable crt names holds a private key.Keep only the certificate there. If the key sat in a variable that isn’t secret, replace it too: anyone who can read that variable has seen it. A secret variable’s value can’t be read back, so a key that sat in one needs no replacing.
TRUST_NOT_CAA TRUST_CA certificate isn’t a CA certificate (no CA:TRUE).Use the CA’s own certificate.
TRUST_KEY_INVALIDThe key isn’t one unencrypted PEM private key: it’s encrypted, damaged, or larger than 8 KiB.Store the unencrypted key in the variable.
TRUST_KEY_MISMATCHThe key doesn’t belong to the certificate.Update the key and the certificate together — see Renewing a certificate.
TRUST_KEY_WEAKThe certificate’s RSA key is shorter than 2048 bits.Reissue it with a supported key — see Limits.
TRUST_KEY_UNSUPPORTEDThe certificate’s key is of a type or size the device doesn’t serve.Reissue it with a supported key — see Limits.
TRUST_NAME_CONFLICTTwo snippets of the same type that reach the device have the same name, ignoring case.Rename one.
TRUST_UUID_CONFLICTTwo certificate snippets that reach the device carry the same uuid.Keep one. For each of the others, first note its content, priority and templates (ndcli template describe lists a template’s snippets), then delete it, create it again without a uuid, set that priority and add it back to those templates.
TRUST_CA_DN_CONFLICTTwo TRUST_CA snippets that reach the device have the same subject name and different keys.Give the new CA a new subject name — see Moving to a new CA.
TRUST_VARIABLE_CLEARANCEA crt variable’s value wasn’t saved by a Superuser.A Superuser saves it again.
TRUST_CONTENT_INVALIDThe stored content no longer passes validation, key isn’t exactly ${variable}, or substituting the variables broke its shape.A Superuser fixes the content and saves the snippet again.
SNIPPET_CLEARANCE_INSUFFICIENTThe snippet wasn’t saved by a Superuser.A Superuser saves it again.
SECRET_VARIABLE_REQUIREDThe key variable isn’t secret.Create a secret variable for the key, point key at it, and replace the key.
SECRET_VARIABLE_CLEARANCEThe key variable’s value wasn’t saved by a Superuser.A Superuser sets it again with --value-stdin.
SECRET_VARIABLE_MISUSEThe key’s variable is also used somewhere other than a certificate’s key, or an authentication server’s bind password is used as a key.Give the key its own variable. A key variable put in crt is reported together with TRUST_CRT_CONTAINS_KEY: set crt to the certificate.

Two more build failures aren’t trust_issues entries:

  • UNDEFINED_VARIABLES: a variable the snippet names doesn’t exist yet. Create it.
  • HASH_SALT_KEY_MISSING: an operational prerequisite on NetDefense’s side wasn’t available. Authoring the snippet differently won’t fix it; contact support if you see it.

Saving a snippet can also be refused outright: TRUST_UUID_SUPPLIED when a create carries a uuid, TRUST_UUID_IMMUTABLE when an update changes it, and a plain message naming the field for content of the wrong shape. A private key anywhere in the content is refused too, with a plain message. TRUST_UUID_IMMUTABLE also answers an update to a snippet whose stored content can’t be read, since its uuid can’t be kept: first note its priority and the templates that hold it (ndcli template describe lists a template’s snippets), then delete it, create it again, set that priority and add it back to those templates.

A device-side refusal fails only that sync, and only its certificate part: the rest of the configuration still applies, and the task ends FAILED. Warnings don’t fail the task. ndcli task describe, or the task’s page in the web app, shows one line per certificate, CA, and reload.

CodeMeaningFix
TRUST_REJECTED_DANGEROUSAllow All Snippet Content is off, so the add or update was refused. The message names the setting and where to turn it on.Turn Allow All Snippet Content on in the plugin’s settings for that device — see Prerequisites.
TRUST_OPNSENSE_TOO_OLDThe device runs OPNsense older than 26.1.11, or the agent can’t read its release. Nothing was written; removals still ran.Upgrade OPNsense — see Firmware Upgrades.
NAME_COLLISION_UNMANAGEDA CA or certificate NetDefense doesn’t manage already has this name, ignoring case. It’s never adopted.Rename or delete it on the firewall, or rename the snippet’s name.
TRUST_CA_REKEYEDA CA update changes the subject or the key.Keep the subject and key, or move to a new CA — see Renewing a CA.
TRUST_CA_DN_CONFLICTAnother CA on the firewall, or in the same sync, has this subject name with a different key.Give the new CA a new subject name, or remove the other CA from the firewall.
TRUST_ISSUER_MISLINKEDOPNsense links the certificate to a CA that didn’t issue it.Remove the CA it names from the firewall, or reissue it with a subject that doesn’t overlap — see A certificate linked to the wrong CA.
TRUST_IN_USEAn object to be removed is still in use, so it was kept.Unbind it on the firewall, then sync again — see Removing a certificate or CA.
TRUST_CONTENT_UNSUPPORTEDThe agent couldn’t accept the certificate content. No certificate or CA was changed in that sync.Check the snippet’s certificate and key; contact support if they look right.
TRUST_IMPORT_FAILEDOPNsense refused the write, or what it stored didn’t read back as sent.Check the certificate and key, and sync again.
TRUST_RELOAD_FAILEDWarning: a service using the renewed object didn’t reload. The object was written.Restart that service on the firewall.
TRUST_RELOAD_SCHEDULEDInformation: the web GUI restarts once the sync’s result is sent.Nothing to do.
TRUST_CONSUMER_UNKNOWNWarning: the object is used by a setting NetDefense doesn’t know how to reload.Restart that service yourself.