net/haproxy: Commit Error in syncCerts.py
Important notices Before you add a new report, we ask you kindly to acknowledge the following:
- [x] I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md
- [x] I have searched the existing issues, open and closed, and I'm convinced that mine is new.
- [x] The title contains the plugin to which this issue belongs
Describe the bug
I am still chasing after #4203 and I narrowed it down to syncCerts.py. It generates an error when attempting to update the HAProxy certificate under /tmp/haproxy/ssl.
# /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py sync
CRT-LIST: /tmp/haproxy/ssl/60294d9f6fa932.93592251.certlist
FRONTEND NAME: frontend_https
FRONTEND ID: 60294d9f6fa932.93592251
NEW / UPDATE: /tmp/haproxy/ssl/6029e37fc87ca.pem
''
"No ongoing transaction! !\nCan't commit /tmp/haproxy/ssl/6029e37fc87ca.pem!\n\n"
Both diff | actions correctly identified which certificate to replace.
# /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py diff
FRONTEND NAME: frontend_https
CONFIG:
CERT (Default):
Serial: 04E5785288966E848BB9839AAE181BBD8F00
Issuer: /C=US/O=Let's Encrypt/CN=E6
Subject: /CN=host1.domain.tld
CERT:
Serial: 049EAE15A1E3248F1B4EA5CFE5A3A2ED5600
Issuer: /C=US/O=Let's Encrypt/CN=E6
Subject: /CN=host2.domain.tld
ACTIVE:
CERT:
Serial: 04E5785288966E848BB9839AAE181BBD8F00
Issuer: /C=US/O=Let's Encrypt/CN=E6
Subject: /CN=host1.domain.tld
CERT:
Serial: 038121C688432012BF2BFDED363F6DEAAC00
Issuer: /C=US/O=Let's Encrypt/CN=E6
Subject: /CN=host2.domain.tld
# /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py actions
FRONTEND: frontend_https
CRT-LIST: /tmp/haproxy/ssl/60294d9f6fa932.93592251.certlist
CERT NEW / UPDATE:
Cert: /tmp/haproxy/ssl/6029e37fc87ca.pem
Serial: 049EAE15A1E3248F1B4EA5CFE5A3A2ED5600
Issuer: /C=US/O=Let's Encrypt/CN=E6
Subject: /CN=host2.domain.tld
CERT ADD: []
CERT REMOVE: []
But transactions show an empty output.
# /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py transactions
## OPEN TRANSACTIONS ##
To Reproduce Steps to reproduce the behavior:
- Have a different active HAProxy certificate from the configuration
- Run
syncCerts.py sync
Expected behavior
I expect an updated certificate under /tmp/haproxy/ssl and it is synced to the running HAProxy process without requiring a restart.
Additional context I am eager to get this working since Let's Encrypt is planning to offer 6-day certificate lifetimes as an option this year.
In case certificate key length is relevant, I have it set to ec-384.
Environment OPNsense 25.1.1-amd64 Intel(R) Core(TM) i5-5300U CPU @ 2.30GHz igb network driver
I believe this is a bug somewhere in cmds.updateSslCrt(). I get a BrokenPipeError after executing that function. Below is my rather lengthy supporting documentation.
I first load the old certificate on HAProxy and modify syncCerts.py to pickle the arguments before it performs cmds.updateSslCrt().
# diff -u /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py /root/haproxy-cert/syncCerts.py
--- /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py 2025-02-23 14:09:24.982477000 -0600
+++ /root/haproxy-cert/syncCerts.py 2025-03-31 18:02:01.689578000 -0500
@@ -22,6 +22,11 @@
def _execute_remote_cmd(self, command_class, **command_args):
con = HaPConn(self.socket)
if con:
+ if command_class == cmds.updateSslCrt:
+ import pickle
+ file = open("/root/haproxy-cert/pickledUpdate", "wb")
+ pickle.dump(command_args, file)
+ file.close()
command_obj = command_class(**command_args)
result = con.sendCmd(command_obj, objectify=True)
con.close()
Then, I run a certificate sync to generate the pickle.
# /root/haproxy-cert/syncCerts.py sync
CRT-LIST: /tmp/haproxy/ssl/60294d9f6fa932.93592251.certlist
FRONTEND NAME: public_https
FRONTEND ID: 60294d9f6fa932.93592251
NEW / UPDATE: /tmp/haproxy/ssl/6029e37fc87ca.pem
''
"No ongoing transaction! !\nCan't commit /tmp/haproxy/ssl/6029e37fc87ca.pem!\n\n"
A second script uses this pickled data and performs cmds.updateSslCrt() first to stage the updated certificate, then it does a cmds.showSslCert(). This second command should succeed since it only shows info on the currently loaded certificate.
# cat /root/haproxy-cert/manualSync.py
#!/usr/bin/env python3
import pickle
from haproxy.conn import HaPConn
from haproxy import cmds
# use pickled data from syncCerts.py cmds.updateSslCrt()
#
f = open('/root/haproxy-cert/pickledUpdate', 'rb')
args = pickle.load(f)
f.close()
con = HaPConn('/var/run/haproxy.socket')
if con:
# update the SSL cert
print(f'using HaPConn {con}')
updateCmd = cmds.updateSslCrt(certfile=args['certfile'], payload=args['payload'])
print(f'sending {updateCmd}')
result = con.sendCmd(updateCmd, objectify=True)
# ------------------------------------
print('-' * 30)
# show existing SSL cert
print(f'using HaPConn {con}')
showCmd = cmds.showSslCert(certfile=args['certfile'])
print(f'sending {showCmd}')
result = con.sendCmd(showCmd, objectify=False)
con.close()
Here is the failure when cmds.showSslCert() is called. I think this failure is due to con.sendCmd() losing its connection to the socket. It would explain why syncCerts.py throws a No ongoing transaction! error; the updated cert is not staged within HAProxy.
# /root/haproxy-cert/manualSync.py
using HaPConn <haproxy.conn.HaPConn object at 0x378b0e6d9850>
sending <haproxy.cmds.updateSslCrt object at 0x378b0db10610>
------------------------------
using HaPConn <haproxy.conn.HaPConn object at 0x378b0e6d9850>
sending <haproxy.cmds.showSslCert object at 0x378b0e61d110>
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/haproxy/conn.py", line 69, in sendCmd
self.sock.send(cmd.getCmd())
TypeError: a bytes-like object is required, not 'str'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/root/haproxy-cert/./manualSync.py", line 35, in <module>
result = con.sendCmd(showCmd, objectify=False)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/haproxy/conn.py", line 71, in sendCmd
self.sock.send(bytearray(cmd.getCmd(), 'ASCII'))
BrokenPipeError: [Errno 32] Broken pipe
So it looks like the cmds.updateSslCrt function is not working properly. I've prepared a fix in the upstream haproxy-cli library:
https://github.com/markt-de/haproxy-cli/commit/73da31e00971395bc48c3a1b1b08827208e10ed3
If you want to test it, then apply this tiny change to the /usr/local/lib/python3.11/site-packages/haproxy/cmds.py file manually.
It will probably take some time for this fix to land in OPNsense, but I'm working on it:
- Released new version on Pypi: https://pypi.org/project/haproxy-cli/
- Submitted FreeBSD port update: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287627
@fraenki, you fixed it! Thank you very much for working on it!
# /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py sync
CRT-LIST: /tmp/haproxy/ssl/60294d9f6fa932.93592251.certlist
FRONTEND NAME: public_https
FRONTEND ID: 60294d9f6fa932.93592251
NEW / UPDATE: /tmp/haproxy/ssl/6029e37fc87ca.pem
'Transaction created for certificate /tmp/haproxy/ssl/6029e37fc87ca.pem!\n\n'
'Committing /tmp/haproxy/ssl/6029e37fc87ca.pem..\nSuccess!\n\n'