plugins icon indicating copy to clipboard operation
plugins copied to clipboard

net/haproxy: Commit Error in syncCerts.py

Open aque opened this issue 11 months ago • 3 comments

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:

  1. Have a different active HAProxy certificate from the configuration
  2. 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

aque avatar Feb 23 '25 21:02 aque

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

aque avatar Mar 31 '25 23:03 aque

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 avatar Jun 17 '25 20:06 fraenki

@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'

aque avatar Jun 18 '25 23:06 aque