This is an old revision of the document!
====== pfxexx (What is this?) ======
important box
PFX exporter for OPNsense's ACME plugin's certificates. A bash script.
Background
The ACME client on OPNsense has a lot of useful utilities (such as installing certificates on Synology's DSM and automated push over SSH that though doable on other systems, each user admin has to come up with their own scheme. It's not perfect, as everything-else-OPNsense, it's poorly documented but it does the job nevertheless.
Certain servers require PFX certificates and will refuse other formats. PFX are bundle files, that usually include the whole chain of certificates (public keys) as well as the private key of the issued certificate. Since they have a private key, they also often have a password protecting them. You may skip this password but you will still have to press return/enter to accept it IF the recipient server tolerates it, most will not and they will refuse it outright.
How does it work?
The original idea for this was to add an automation per certificate so the process would focus only on that certificate, but there's no documentation about it (on OPNsense's documentation at least) so instead it works in bulk.
The ACME plugin creates a random directory under /var/etc/acme-client/certs
where it puts the public keys and a matching directory on /var/etc/acme-client/keys
where the private keys are stored. The randomness of this directory is in part why a targeted approach couldn't be made.
So instead the script will list the subdirectories of /var/etc/acme-client/certs
to confirm there's a cert.pem
and chaim.pem
in it, then search for a private.key
file the private keys subdirectory. If all files are found, then it will decode the common name of the certificate and use that name to name the new file with the extension .pfx which will be placed on /var/etc/acme-client/pfx
which it will protect using the password stored in the file indicated in the variable pfile
, by default this is: /var/etc/acme-client/pfiles/std
Installation
There's nothing to install, except maybe bash since it's a bash script. It will fail in the default OPNsense shell, assuming the commands were run directly. Since everything is in /var/etc/acme-client
it fel like the best place for it in /var/etc/acme-client/scripts
. However, like VyOS and pfSense, OPNsense overwrites the filesystem un updates except for a few safe places. /config
on VyOS; /root
is on of them on OPNsense and pfSense.
You can download it straight from this page or copy and paste it on the terminal (the code is below.), to download it (assuming you'd use
/var/etc/acme-client/scripts':
1. Create a save location if it doesn't already exist:
mkdir -p
/var/etc/acme-client/scripts
2. Download
curl -o <destination-filename(-and-path)> <source-URL> e.g;
curl -o /var/etc/acme-client/scripts/pfxexx https://ref.vitanetworks.link/_export/code/en/utility-scripts/pfxexx?codeblock=0
===== Options/Syntax =====
Neither of the only two options affect how the script works, they only affect the amount of
stdout.
==== –unmute ====
The script normally discards all output to avoid slowing down firewalls and reduce wearing of flash storage. This option eliminates the discarding of the minimal data it would otherwise output.
==== –debug ====
This is for testing the script itself, not so much about certificates. Pretty much a useless option for most people.
===== Requirements =====
The script must be edited or at least review to verify the variables are correct at least reviewed edite hardcoded with the variab
======
<code bash pfxexx>
#!/usr/bin/env bash
printOPTIONS() {
cat « _options
┌────────────────────────────────────────────────────────────────────────────i─┐
│ pfxexx — PFX Exporter for the ACME plugin on OPNsense │
│ Copyright (C) 2025 Gustavo Domínguez │
│ GNU General Public License version 3 │
├─────────────────────────────────────────────────────────OPTIONS/REQUIREMENTS─┤
│ There are no real options*, only requirements: │
│ 1. The script requires bash to run. To install run: 'pkg install -y bash'. │
│ 2. Variables must be reviewed or changed before running the script. │
│ - Likely the most important will be the password file, the "pfile" from │
│ which a password will be read in order to set it on PFX exports. │
│ │
│ *: except for –unmute and –debug, neither of which affects how the script │
│ works in terms of exporting PFXs. See more info at: │
│ https://ref.vitanetworks.link/en/utility-scripts/pfxexx │
├────────────────────────────────────────────────────────────────────────────@─┤
│ Gustavo Domínguez deliver@senseivita.com │
│ senseivita.com | antipostal.com | vitanetworks.link │
└──────────────────────────────────────────────────────────────────────────────┘
_options
}
restoreOptions(){ set +e ; set +x ; set +v;}
enableDebugOptions(){ set -e ; set -x ; set -v;}
trap restoreOptions ERR EXIT
if $1 =~ "debug"; then enableDebugOptions; fi
verifiedPieces=
cbn='/var/etc/acme-client/certs'
kbn='/var/etc/acme-client/keys'
ebn='/var/etc/acme-client/pfx'
cnlist=''
pfile='/var/etc/acme-client/pfiles/std'
main() {
checkPfxExportDir() { if ! [[ -d "$ebn" ]]; then mkdir -p "$ebn" fi }
candidates=( "$(find "$cbn" -type d -mindepth 1 -print0 | xargs -0 basename -s "$cbn")" ) echo "${candidates[*]}"
partsCheck() { for i in "${candidates[@]}"; do echo "$i" if [[ -f "$cbn/$i/cert.pem" ]]; then echo "$cbn/$i/cert.pem" if [[ -f "$cbn/$i/chain.pem" ]]; then echo "$cbn/$i/chain.pem" if [[ -f "$kbn/$i/private.key" ]]; then echo "$kbn/$i/private.key" verifiedPieces+=( "$i" ) else continue; fi else continue; fi else continue; fi done }
exportPFXs() { for iset in "${verifiedPieces[@]}"; do cn=$(openssl x509 -noout -subject -in "$cbn/$iset/cert.pem" | awk '{print $3}') cnlist+=( "$cn" ) openssl pkcs12 -export -out "$ebn/$cn.pfx" -inkey "$kbn/$iset/private.key" -in "$cbn/$iset/cert.pem" -certfile "$cbn/$iset/chain.pem" -password file:"$pfile" done }
printResuts() { echo Found the following certificates: printf '%s\n' "${cnlist[*]}" }
if checkPfxExportDir; then
if partsCheck; then if exportPFXs; then echo "Finished successfully." printResuts else if [[ $1 =~ "debug" ]]; then echo "Failed exportPFXs"; fi exit fi else if [[ $1 =~ "debug" ]]; then echo "Failed partsCheck"; fi exit fi
else
if [[ $1 =~ "debug" ]]; then echo "Failed checkPfxExportDir"; fi exit
fi }
while [ "$1" != "" ]; do
case "$1" in -h|--help|--options|help) printOPTIONS ;; --debug|debug) shift; main debug ;; --unmute) main ;; *) main > /dev/null 2>&1 ;; esac shift
done </code>