From ca98e9fdb2daa829100e55b6423b716899bcdb90 Mon Sep 17 00:00:00 2001 From: Andreas Stuerz Date: Tue, 9 Feb 2021 16:00:39 +0100 Subject: [PATCH 1/7] add yaml service template with configured haproxy ssl certificates in config.xml --- net/haproxy/Makefile | 2 +- .../templates/OPNsense/HAProxy/+TARGETS | 1 + .../templates/OPNsense/HAProxy/sslCerts.yaml | 61 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml diff --git a/net/haproxy/Makefile b/net/haproxy/Makefile index 98aea3903..90228279f 100644 --- a/net/haproxy/Makefile +++ b/net/haproxy/Makefile @@ -1,7 +1,7 @@ PLUGIN_NAME= haproxy PLUGIN_VERSION= 2.26 PLUGIN_COMMENT= Reliable, high performance TCP/HTTP load balancer -PLUGIN_DEPENDS= haproxy20 +PLUGIN_DEPENDS= haproxy PLUGIN_MAINTAINER= opnsense@moov.de .include "../../Mk/plugins.mk" diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/+TARGETS b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/+TARGETS index 6e4c913d2..a8fa7728c 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/+TARGETS +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/+TARGETS @@ -1,2 +1,3 @@ haproxy.conf:/usr/local/etc/haproxy.conf rc.conf.d:/etc/rc.conf.d/haproxy +sslCerts.yaml:/usr/local/etc/haproxy/sslCerts.yaml \ No newline at end of file diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml new file mode 100644 index 000000000..0b98ab5e1 --- /dev/null +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml @@ -0,0 +1,61 @@ +# +# Automatically generated configuration. +# Do not edit this file manually. +# +# List all frontends with configured ssl certificates in config.xml +{# ################## #} +{# ##### Macros ##### #} +{# ################## #} +{% macro getCA(refId) -%} +{% set result = '{}' %} +{% for data in helpers.getNodeByTag('ca') if data.refid == refId %} +{{ data.crt -}} +{% else %} +{{ "{}" }} +{% endfor %} +{%- endmacro %} +{% macro getCert(refId, indent=4) -%} +{% for data in helpers.getNodeByTag('cert') if data.refid == refId %} +{% if data.caref %} +{% do data.update({'ca': getCA(data.caref)}) %} +{% else %} +{% do data.update({'ca': {} }) %} +{% endif %} +crt: {{ data.crt }} +key: {{ data.prv }} +ca: {{ data.ca }} +{% endfor %} +{%- endmacro %} +{# ################## #} +{# ##### Main ##### #} +{# ################## #} +{% set enabled_frontends = [] %} +{% set crt_list_template = "/tmp/haproxy/ssl/%s.certlist" %} +{% set cert_template = "/tmp/haproxy/ssl/%s.pem" %} +{% for frontend in helpers.toList('OPNsense.HAProxy.frontends.frontend') %} +{% set certs = [] %} +{% for cert in frontend.get('ssl_default_certificate', '').split(',') + frontend.get('ssl_certificates', '').split(',') if cert %} +{% do certs.append(cert) %} +{% endfor %} +{% do frontend.update({'certs': certs}) %} +{% if frontend.enabled == '1' and frontend.ssl_enabled == '1' and frontend.certs|length > 0 %} +{% do enabled_frontends.append(frontend) %} +{% endif %} +{% endfor %} +{% if helpers.exists('OPNsense.HAProxy.frontends') and enabled_frontends|length > 0 %} +frontends: +{% for frontend in enabled_frontends %} + "{{ frontend.id }}": + name: {{ frontend.name }} + crt_list_path: {{ cert_template % frontend.id }} + certs: +{% for cert_refid in frontend.certs %} + {{ cert_refid }}: + path: {{ cert_template % cert_refid }} + default: {{ "True" if frontend.ssl_default_certificate == cert_refid else "False" }} +{{ getCert(cert_refid) | indent( width=8, indentfirst=True) -}} +{% endfor %} +{% endfor %} +{% else %} +frontends: {} +{% endif %} \ No newline at end of file From 2cf5a36f7d3ef258b06aa9c9e9e14f1c46ee1a63 Mon Sep 17 00:00:00 2001 From: Andreas Stuerz Date: Tue, 9 Feb 2021 17:03:36 +0100 Subject: [PATCH 2/7] add unit tests for haproxy ssl commands --- .../HAProxy/lib/haproxy/tests/test_cmds.py | 133 +++++++++++++++--- 1 file changed, 114 insertions(+), 19 deletions(-) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py index 032954583..5786b6780 100644 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py @@ -18,7 +18,17 @@ class TestCommands(unittest.TestCase): "info" : "show info", "sessions" : "show sess", "servers" : "show stat", - + "show-all-ssl-crt-list" : "show ssl crt-list", + "show-details-ssl-crt-list" : "show ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "show-all-ssl-certs" : "show ssl cert", + "show-details-ssl-certs" : "show ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "add-to-crt-list" : "add ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", + "del-from-crt-list" : "del ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", + "add-ssl-cert" : "new ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "update-ssl-cert" : "set ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem ", + "del-ssl-cert" : "del ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "commit-ssl-cert" : "commit ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "abort-ssl-cert" : "abort ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", } self.Resp = dict([(k, v + "\r\n") for k, v in self.Resp.items()]) @@ -26,48 +36,133 @@ class TestCommands(unittest.TestCase): def test_setServerAgent(self): """Test 'set server agent' command""" args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "up"} - cmdSetServerAgent = cmds.setServerAgent(**args).getCmd() - self.assertEqual(cmdSetServerAgent, self.Resp["set-server-agent"]) + cmdOutput = cmds.setServerAgent(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["set-server-agent"]) def test_setServerHealth(self): """Test 'set server health' command""" args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "stopping"} - cmdSetServerHealth = cmds.setServerHealth(**args).getCmd() - self.assertEqual(cmdSetServerHealth, self.Resp["set-server-health"]) + cmdOutput = cmds.setServerHealth(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["set-server-health"]) def test_setServerState(self): """Test 'set server state' command""" args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "drain"} - cmdSetServerState = cmds.setServerState(**args).getCmd() - self.assertEqual(cmdSetServerState, self.Resp["set-server-state"]) + cmdOutput = cmds.setServerState(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["set-server-state"]) def test_setServerWeight(self): """Test 'set server weight' command""" args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "10"} - cmdSetServerState = cmds.setServerWeight(**args).getCmd() - self.assertEqual(cmdSetServerState, self.Resp["set-server-weight"]) + cmdOutput = cmds.setServerWeight(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["set-server-weight"]) def test_showFrontends(self): """Test 'frontends/backends' commands""" args = {} - cmdFrontends = cmds.showFrontends(**args).getCmd() - self.assertEqual(cmdFrontends, self.Resp["frontends"]) + cmdOutput = cmds.showFrontends(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["frontends"]) def test_showInfo(self): """Test 'show info' command""" - cmdShowInfo = cmds.showInfo().getCmd() - self.assertEqual(cmdShowInfo, self.Resp["info"]) + cmdOutput = cmds.showInfo().getCmd() + self.assertEqual(cmdOutput, self.Resp["info"]) def test_showSessions(self): - """Test 'show info' command""" - cmdShowInfo = cmds.showSessions().getCmd() - self.assertEqual(cmdShowInfo, self.Resp["sessions"]) + """Test 'show sess' command""" + cmdOutput = cmds.showSessions().getCmd() + self.assertEqual(cmdOutput, self.Resp["sessions"]) def test_showServers(self): - """Test 'show info' command""" + """Test 'show stat' command""" args = {"backend": "redis-ro"} - cmdShowInfo = cmds.showServers(**args).getCmd() - self.assertEqual(cmdShowInfo, self.Resp["servers"]) + cmdOutput = cmds.showServers(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["servers"]) + + def test_showAllSslCrtList(self): + """Test 'show ssl crt-list' command""" + cmdOutput = cmds.showAllSslCrtList().getCmd() + self.assertEqual(cmdOutput, self.Resp["show-all-ssl-crt-list"]) + + def test_showDetailsSslCrtList(self): + """Test 'show ssl crt-list ' command""" + args = { + "filename": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + } + cmdOutput = cmds.test_showDetailsSslCrtList(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["show-details-ssl-crt-list"]) + + def test_showAllSslCerts(self): + """Test 'show ssl cert' command""" + cmdOutput = cmds.showAllSslCerts().getCmd() + self.assertEqual(cmdOutput, self.Resp["show-all-ssl-certs"]) + + def test_showDetailsSslCerts(self): + """Test 'show ssl cert ' command""" + args = { + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem" + } + cmdOutput = cmds.showDetailsSslCerts(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["show-details-ssl-certs"]) + + def test_addToSslCrtList(self): + """Test 'add ssl crt-list ' command""" + args = { + "filename": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem" + } + cmdOutput = cmds.addToSslCrtList(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["add-to-crt-list"]) + + def test_delFromSslCrtList(self): + """Test 'del ssl crt-list ' command""" + args = { + "filename": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem" + } + cmdOutput = cmds.delFromSslCrtList(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["del-from-crt-list"]) + + def test_addSslCrt(self): + """Test 'new ssl cert ' command""" + args = { + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + } + cmdOutput = cmds.addSslCrt(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["add-ssl-cert"]) + + def test_updateSslCrt(self): + """Test 'new ssl cert ' command""" + args = { + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + "payload" : "TODO" + } + cmdOutput = cmds.updateSslCrt(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["update-ssl-cert"]) + + def test_delSslCrt(self): + """Test 'del ssl cert ' command""" + args = { + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + } + cmdOutput = cmds.delSslCrt(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["del-ssl-cert"]) + + def test_commitSslCrt(self): + """Test 'commit ssl cert ' command""" + args = { + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + } + cmdOutput = cmds.commitSslCrt(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["commit-ssl-cert"]) + + def test_abortSslCrt(self): + """Test 'abort ssl cert ' command""" + args = { + "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + } + cmdOutput = cmds.abortSslCrt(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["abort-ssl-cert"]) if __name__ == '__main__': unittest.main() From 4da0f2c25f40f4d9060649758ce3d88aa2d45c7b Mon Sep 17 00:00:00 2001 From: Andreas Stuerz Date: Thu, 11 Feb 2021 16:02:07 +0100 Subject: [PATCH 3/7] refactor output mode for socket cmds add ssl handling socket commands --- .../OPNsense/HAProxy/lib/haproxy/cmds.py | 236 ++++++++++++------ .../OPNsense/HAProxy/lib/haproxy/conn.py | 2 +- .../HAProxy/lib/haproxy/tests/test_cmds.py | 236 +++++++++++++----- .../scripts/OPNsense/HAProxy/socketCommand.py | 34 +++ 4 files changed, 381 insertions(+), 127 deletions(-) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py index 0316bd99e..3d13d42a8 100644 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py @@ -1,12 +1,9 @@ -# pylint: disable=locally-disabled, too-few-public-methods, no-self-use, invalid-name """cmds.py - Implementations of the different HAProxy commands""" - import re import csv import json from io import StringIO - class Cmd(): """Cmd - Command base class""" req_args = [] @@ -42,44 +39,201 @@ class Cmd(): """ return self.cmdTxt % self.args + def getBootstrapOutput(self, resObj): + """ Returns results gathered from HAProxy as jquery bootstrap output """ + args = { + "rows": resObj, + "page": int(self.args['page']) if self.args['page'] != None else 1, + "page_rows": int(self.args['page_rows']) if self.args['page_rows'] != None else len(rows), + "search": self.args['search'], + "sort_col": self.args['sort_col'] if self.args['sort_col'] else 'id', + "sort_dir": self.args['sort_dir'], + } + rows = args['rows'] + # search + if args['search']: + filtered_rows = [] + for row in rows: + def inner(row): + for k, v in row.items(): + if args['search'] in v: + return row + return None + + match = inner(row) + if match: + filtered_rows.append(match) + rows = filtered_rows + + # sort + rows.sort(key=lambda k: k[args['sort_col']], reverse=True if args['sort_dir'] == 'desc' else False) + + # pager + total = len(rows) + pages = [rows[i:i + args['page_rows']] for i in range(0, total, args['page_rows'])] + if pages and (args['page'] > len(pages) or args['page'] < 1): + raise KeyError(f"Current page {args['page']} does not exist. Available pages: {len(pages)}") + page = pages[args['page'] - 1] if pages else [] + + return json.dumps({ + "rows": page, + "total": total, + "rowCount": args['page_rows'], + "current": args['page'] + }) + + def getJsonOutput(self, resObj): + """Returns results gathered from HAProxy as json""" + return json.dumps(resObj) + def getResult(self, res): """Returns raw results gathered from HAProxy""" if res == '\n': res = None + + if self.args['output'] == 'json': + return self.getJsonOutput(self.getResultObj(res)) + + if self.args['output'] == 'bootstrap': + return self.getBootstrapOutput(self.getResultObj(res)) + return res def getResultObj(self, res): """Returns refined output from HAProxy, packed inside a Python obj i.e. a dict()""" return res - class setServerAgent(Cmd): - """Set server agent command.""" cmdTxt = "set server %(backend)s/%(server)s agent %(value)s\r\n" req_args = ['backend', 'server', 'value'] helpTxt = "Force a server's agent to a new state." - class setServerHealth(Cmd): - """Set server health command.""" cmdTxt = "set server %(backend)s/%(server)s health %(value)s\r\n" req_args = ['backend', 'server', 'value'] helpTxt = "Force a server's health to a new state." - class setServerState(Cmd): - """Set server state command.""" cmdTxt = "set server %(backend)s/%(server)s state %(value)s\r\n" req_args = ['backend', 'server', 'value'] helpTxt = "Force a server's administrative state to a new state." - class setServerWeight(Cmd): - """Set server weight command.""" cmdTxt = "set server %(backend)s/%(server)s weight %(value)s\r\n" req_args = ['backend', 'server', 'value'] helpTxt = "Force a server's weight to a new state." +class showSslCrtLists(Cmd): + cmdTxt = "show ssl crt-list\r\n" + helpTxt = "Show the list of crt-lists." + + def getResultObj(self, res): + result = { "crt_lists": []} + for line in res.split("\n"): + if line.startswith('/'): + result["crt_lists"].append(line) + return result + +class showSslCrtList(Cmd): + cmdTxt = "show ssl crt-list %(crt_list)s\r\n" + req_args = ['crt_list'] + helpTxt = "Show the the content of a crt-list." + + def getResultObj(self, res): + result = {} + list_id = None + for line in res.split("\n"): + if line.startswith('# '): + list_id = line.split("# ")[1] + result[f"{list_id}"] = [] + + if list_id and line.startswith('/'): + result[f"{list_id}"].append(line) + + if result: + return result + + return {"error": res.strip()} + +class showSslCerts(Cmd): + cmdTxt = "show ssl cert\r\n" + helpTxt = "Display the SSL certificates used in memory." + + def getResultObj(self, res): + result = { + "transaction": [], + "filename": [] + } + for line in res.split("\n"): + if line.startswith('*'): + result['transaction'].append(line) + elif line.startswith('/'): + result['filename'].append(line) + return result + +class showSslCert(Cmd): + cmdTxt = "show ssl cert %(certfile)s\r\n" + req_args = ['certfile'] + helpTxt = "Display the details of a SSL certificate used in memory." + + def getResultObj(self, res): + result = {} + cert_id = None + for line in res.split("\n"): + if line: + key = line.split(":")[0] + val = line.split(":")[1].strip() + + if key == 'Filename': + cert_id = val + result[f"{cert_id}"] = {} + + if cert_id: + result[f"{cert_id}"][key] = val + + if result: + return result + + return {"error": res.strip()} + +class addToSslCrtList(Cmd): + cmdTxt = "add ssl crt-list %(crt_list)s %(certfile)s\r\n" + req_args = ['crt_list', 'certfile'] + helpTxt = "Add a ssl cert to a crt-list." + +class delFromSslCrtList(Cmd): + cmdTxt = "del ssl crt-list %(crt_list)s %(certfile)s\r\n" + req_args = ['crt_list', 'certfile'] + helpTxt = "Delete a ssl cert from a crt-list." + +class newSslCrt(Cmd): + """" Create an empty slot for the certificate in HAProxy’s memory """ + cmdTxt = "new ssl cert %(certfile)s\r\n" + req_args = ['certfile'] + helpTxt = "Create a new certificate file to be used in a crt-list or a directory." + +class updateSslCrt(Cmd): + """" Begin a transaction to upload the certificate into a slot in HAProxy’s memory """ + cmdTxt = "set ssl cert %(certfile)s <<\n%(payload)s\r\n" + req_args = ['certfile', 'payload'] + helpTxt = "Replace a certificate file." + +class delSslCrt(Cmd): + """" Begin a transaction to remove the certificate from a slot in HAProxy’s memory """ + cmdTxt = "del ssl cert %(certfile)s\r\n" + req_args = ['certfile'] + helpTxt = "Delete delete an unused certificate file." + +class commitSslCrt(Cmd): + """ Commit the transaction so HAProxy detects the change. """ + cmdTxt = "commit ssl cert %(certfile)s\r\n" + req_args = ['certfile'] + helpTxt = "Commit a certificate file." + +class abortSslCrt(Cmd): + cmdTxt = "abort ssl cert %(certfile)s\r\n" + req_args = ['certfile'] + helpTxt = "Abort a transaction for a certificate file." class showFBEnds(Cmd): """Base class for getting a listing Frontends and Backends""" @@ -110,19 +264,16 @@ class showFBEnds(Cmd): result.append(e.split(",")[0]) return result - class showFrontends(showFBEnds): """Show frontends command.""" switch = "frontend" helpTxt = "List all Frontends." - class showBackends(showFBEnds): """Show backends command.""" switch = "backend" helpTxt = "List all Backends." - class showInfo(Cmd): """Show info HAProxy command""" cmdTxt = "show info\r\n" @@ -136,7 +287,6 @@ class showInfo(Cmd): return resDict - class showSessions(Cmd): """Show sess HAProxy command""" cmdTxt = "show sess\r\n" @@ -145,7 +295,6 @@ class showSessions(Cmd): def getResultObj(self, res): return res.split('\n') - class baseStat(Cmd): """Base class for stats commands.""" @@ -158,64 +307,11 @@ class baseStat(Cmd): csv_string = StringIO(res) return csv.DictReader(csv_string, delimiter=',') - def getBootstrapOutput(self, **kwargs): - rows = kwargs['rows'] - # search - if kwargs['search']: - filtered_rows = [] - for row in rows: - def inner(row): - for k, v in row.items(): - if kwargs['search'] in v: - return row - return None - - match = inner(row) - if match: - filtered_rows.append(match) - rows = filtered_rows - - # sort - rows.sort(key=lambda k: k[kwargs['sort_col']], reverse=True if kwargs['sort_dir'] == 'desc' else False) - - # pager - total = len(rows) - pages = [rows[i:i + kwargs['page_rows']] for i in range(0, total, kwargs['page_rows'])] - if pages and (kwargs['page'] > len(pages) or kwargs['page'] < 1): - raise KeyError(f"Current page {kwargs['page']} does not exist. Available pages: {len(pages)}") - page = pages[kwargs['page'] - 1] if pages else [] - - return json.dumps({ - "rows": page, - "total": total, - "rowCount": kwargs['page_rows'], - "current": kwargs['page'] - }) - - class showServers(baseStat): """Show all servers. If backend is given, show only servers for this backend. """ cmdTxt = "show stat\r\n" helpTxt = "Lists all servers. Filter for servers in backend, if set." - def getResult(self, res): - if self.args['output'] == 'json': - return json.dumps(self.getResultObj(res)) - - if self.args['output'] == 'bootstrap': - rows = self.getResultObj(res) - args = { - "rows": rows, - "page": int(self.args['page']) if self.args['page'] != None else 1, - "page_rows": int(self.args['page_rows']) if self.args['page_rows'] != None else len(rows), - "search": self.args['search'], - "sort_col": self.args['sort_col'] if self.args['sort_col'] else 'id', - "sort_dir": self.args['sort_dir'], - } - return self.getBootstrapOutput(**args) - - return self.getResultObj(res) - def getResultObj(self, res): servers = [] @@ -234,4 +330,4 @@ class showServers(baseStat): row.move_to_end('id', last=False) servers.append(dict(row)) - return servers + return servers \ No newline at end of file diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py index 962a15cf5..0c38673c1 100644 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py @@ -70,7 +70,7 @@ class HaPConn(object): output = self.sock.recv(const.HAP_BUFSIZE) while output: - res += output.decode('ASCII') + res += output.decode('UTF-8') output = self.sock.recv(const.HAP_BUFSIZE) if objectify: diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py index 5786b6780..01887f583 100644 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py @@ -5,55 +5,178 @@ import sys, os, unittest sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) from haproxy import cmds + class TestCommands(unittest.TestCase): """Tests all of the commands.""" - def setUp(self): - self.Resp = {"disable" : "disable server redis-ro/redis-ro0", - "set-server-agent" : "set server redis-ro/redis-ro0 agent up", - "set-server-health" : "set server redis-ro/redis-ro0 health stopping", - "set-server-state" : "set server redis-ro/redis-ro0 state drain", - "set-server-weight" : "set server redis-ro/redis-ro0 weight 10", - "frontends" : "show stat", - "info" : "show info", - "sessions" : "show sess", - "servers" : "show stat", - "show-all-ssl-crt-list" : "show ssl crt-list", - "show-details-ssl-crt-list" : "show ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", - "show-all-ssl-certs" : "show ssl cert", - "show-details-ssl-certs" : "show ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "add-to-crt-list" : "add ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", - "del-from-crt-list" : "del ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", - "add-ssl-cert" : "new ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "update-ssl-cert" : "set ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem ", - "del-ssl-cert" : "del ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "commit-ssl-cert" : "commit ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "abort-ssl-cert" : "abort ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + def setUp(self): + self.maxDiff = None + self.pem_cert_content = """ + -----BEGIN CERTIFICATE----- + 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 + +VCAIWMkC36Dvg== + -----END CERTIFICATE----- + + -----BEGIN RSA PRIVATE KEY----- + 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 + -----END RSA PRIVATE KEY----- + + + -----BEGIN CERTIFICATE----- + MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw + GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 + MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw + ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 + 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym + oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 + ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN + xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 + dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 + AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw + HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 + BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu + b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu + Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq + hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF + UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 + AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp + DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 + IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf + zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI + PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w + SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em + 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 + WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt + n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= + -----END CERTIFICATE----- + """ + + self.Resp = { + "disable": "disable server redis-ro/redis-ro0", + "set-server-agent": "set server redis-ro/redis-ro0 agent up", + "set-server-health": "set server redis-ro/redis-ro0 health stopping", + "set-server-state": "set server redis-ro/redis-ro0 state drain", + "set-server-weight": "set server redis-ro/redis-ro0 weight 10", + "frontends": "show stat", + "info": "show info", + "sessions": "show sess", + "servers": "show stat", + "show-ssl-crt-lists": "show ssl crt-list", + "show-ssl-crt-list": "show ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "show-ssl-certs": "show ssl cert", + "show-ssl-cert": "show ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "add-to-crt-list": "add ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", + "del-from-crt-list": "del ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", + "new-ssl-cert": "new ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "update-ssl-cert": "set ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem <<\n%s" % self.pem_cert_content, + "del-ssl-cert": "del ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "commit-ssl-cert": "commit ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", + "abort-ssl-cert": "abort ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", } self.Resp = dict([(k, v + "\r\n") for k, v in self.Resp.items()]) def test_setServerAgent(self): """Test 'set server agent' command""" - args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "up"} + args = {"backend": "redis-ro", "server": "redis-ro0", "value": "up"} cmdOutput = cmds.setServerAgent(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["set-server-agent"]) def test_setServerHealth(self): """Test 'set server health' command""" - args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "stopping"} + args = {"backend": "redis-ro", "server": "redis-ro0", "value": "stopping"} cmdOutput = cmds.setServerHealth(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["set-server-health"]) def test_setServerState(self): """Test 'set server state' command""" - args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "drain"} + args = {"backend": "redis-ro", "server": "redis-ro0", "value": "drain"} cmdOutput = cmds.setServerState(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["set-server-state"]) def test_setServerWeight(self): """Test 'set server weight' command""" - args = {"backend": "redis-ro", "server" : "redis-ro0", "value": "10"} + args = {"backend": "redis-ro", "server": "redis-ro0", "value": "10"} cmdOutput = cmds.setServerWeight(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["set-server-weight"]) @@ -79,63 +202,63 @@ class TestCommands(unittest.TestCase): cmdOutput = cmds.showServers(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["servers"]) - def test_showAllSslCrtList(self): + def test_showSslCrtLists(self): """Test 'show ssl crt-list' command""" - cmdOutput = cmds.showAllSslCrtList().getCmd() - self.assertEqual(cmdOutput, self.Resp["show-all-ssl-crt-list"]) + cmdOutput = cmds.showSslCrtLists().getCmd() + self.assertEqual(cmdOutput, self.Resp["show-ssl-crt-lists"]) - def test_showDetailsSslCrtList(self): - """Test 'show ssl crt-list ' command""" + def test_showSslCrtList(self): + """Test 'show ssl crt-list ' command""" args = { - "filename": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", } - cmdOutput = cmds.test_showDetailsSslCrtList(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["show-details-ssl-crt-list"]) + cmdOutput = cmds.showSslCrtList(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["show-ssl-crt-list"]) - def test_showAllSslCerts(self): + def test_showSslCerts(self): """Test 'show ssl cert' command""" - cmdOutput = cmds.showAllSslCerts().getCmd() - self.assertEqual(cmdOutput, self.Resp["show-all-ssl-certs"]) + cmdOutput = cmds.showSslCerts().getCmd() + self.assertEqual(cmdOutput, self.Resp["show-ssl-certs"]) - def test_showDetailsSslCerts(self): + def test_showSslCert(self): """Test 'show ssl cert ' command""" args = { - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem" + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem" } - cmdOutput = cmds.showDetailsSslCerts(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["show-details-ssl-certs"]) + cmdOutput = cmds.showSslCert(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["show-ssl-cert"]) def test_addToSslCrtList(self): - """Test 'add ssl crt-list ' command""" + """Test 'add ssl crt-list ' command""" args = { - "filename": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem" + "crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem" } cmdOutput = cmds.addToSslCrtList(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["add-to-crt-list"]) def test_delFromSslCrtList(self): - """Test 'del ssl crt-list ' command""" + """Test 'del ssl crt-list ' command""" args = { - "filename": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem" + "crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem" } cmdOutput = cmds.delFromSslCrtList(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["del-from-crt-list"]) - def test_addSslCrt(self): + def test_newSslCrt(self): """Test 'new ssl cert ' command""" args = { - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", } - cmdOutput = cmds.addSslCrt(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["add-ssl-cert"]) + cmdOutput = cmds.newSslCrt(**args).getCmd() + self.assertEqual(cmdOutput, self.Resp["new-ssl-cert"]) def test_updateSslCrt(self): - """Test 'new ssl cert ' command""" + """Test 'set ssl cert ' command""" args = { - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", - "payload" : "TODO" + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", + "payload": "%s" % self.pem_cert_content } cmdOutput = cmds.updateSslCrt(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["update-ssl-cert"]) @@ -143,7 +266,7 @@ class TestCommands(unittest.TestCase): def test_delSslCrt(self): """Test 'del ssl cert ' command""" args = { - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", } cmdOutput = cmds.delSslCrt(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["del-ssl-cert"]) @@ -151,7 +274,7 @@ class TestCommands(unittest.TestCase): def test_commitSslCrt(self): """Test 'commit ssl cert ' command""" args = { - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", } cmdOutput = cmds.commitSslCrt(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["commit-ssl-cert"]) @@ -159,10 +282,11 @@ class TestCommands(unittest.TestCase): def test_abortSslCrt(self): """Test 'abort ssl cert ' command""" args = { - "certfile" : "/tmp/haproxy/ssl/601a70e4844b0.pem", + "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", } cmdOutput = cmds.abortSslCrt(**args).getCmd() self.assertEqual(cmdOutput, self.Resp["abort-ssl-cert"]) + if __name__ == '__main__': unittest.main() diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py index fc42c7c14..11a084f85 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py @@ -19,6 +19,17 @@ VALID_COMMANDS = { "show-info": cmds.showInfo, "show-sessions": cmds.showSessions, "show-servers": cmds.showServers, + "show-ssl-crt-lists": cmds.showSslCrtLists, + "show-ssl-crt-list": cmds.showSslCrtList, + "show-ssl-certs": cmds.showSslCerts, + "show-ssl-cert": cmds.showSslCert, + "add-to-crt-list": cmds.addToSslCrtList, + "del-from-crt-list": cmds.delFromSslCrtList, + "new-ssl-cert": cmds.newSslCrt, + "update-ssl-cert": cmds.updateSslCrt, + "del-ssl-cert": cmds.delSslCrt, + "commit-ssl-cert": cmds.commitSslCrt, + "abort-ssl-cert": cmds.abortSslCrt, } def get_args(): @@ -48,6 +59,21 @@ def get_args(): help='Specify value for a set command.', default=None ) + parser.add_argument( + '--payload', + help='Specify payload for a update command. either string or filepath', + default=None + ) + parser.add_argument( + '--crt-list', + help='Set a filepath for a crt-list.', + default=None + ) + parser.add_argument( + '--certfile', + help='Set a filepath for a certificate.', + default=None + ) parser.add_argument( '--output', help='Specify output format.', @@ -89,6 +115,14 @@ def get_args(): return parser.parse_args() args = get_args() +if args.payload and os.path.isfile(args.payload): + with open(args.payload) as payload_file: + payload_content = "" + for line in payload_file: + if line.rstrip(): + payload_content += line + args.payload = payload_content + command_class = VALID_COMMANDS.get(args.command, None) command_args = {key: val for key, val in vars(args).items() if key != "command"} From bd5817d5926ab94cab2c3a7f875538fbdc9b7122 Mon Sep 17 00:00:00 2001 From: Andreas Stuerz Date: Tue, 16 Feb 2021 17:28:25 +0100 Subject: [PATCH 4/7] get current state from local and remote --- .../OPNsense/HAProxy/lib/haproxy/cmds.py | 8 +- .../scripts/OPNsense/HAProxy/socketCommand.py | 3 +- .../scripts/OPNsense/HAProxy/syncCerts.py | 252 ++++++++++++++++++ .../templates/OPNsense/HAProxy/sslCerts.yaml | 2 +- 4 files changed, 258 insertions(+), 7 deletions(-) create mode 100755 net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py index 3d13d42a8..81e8b3351 100644 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py @@ -145,10 +145,10 @@ class showSslCrtList(Cmd): for line in res.split("\n"): if line.startswith('# '): list_id = line.split("# ")[1] - result[f"{list_id}"] = [] + result["certs"] = [] if list_id and line.startswith('/'): - result[f"{list_id}"].append(line) + result["certs"].append(line) if result: return result @@ -186,10 +186,9 @@ class showSslCert(Cmd): if key == 'Filename': cert_id = val - result[f"{cert_id}"] = {} if cert_id: - result[f"{cert_id}"][key] = val + result[key] = val if result: return result @@ -261,6 +260,7 @@ class showFBEnds(Cmd): for e in lines: me = re.match(cl, e) if me: + print(e) result.append(e.split(",")[0]) return result diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py index 11a084f85..fd9b438c0 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py @@ -51,7 +51,7 @@ def get_args(): ) parser.add_argument( '--server-ids', - help='Attempt action on a list of server, specified as a comma seperated list e.g. back1/server1,back2/server3', + help='Attempt action on a list of server, specified as a comma separated list e.g. back1/server1,back2/server3', default=None ) parser.add_argument( @@ -142,7 +142,6 @@ try: if result: print(f"{server_id}: {result.strip()}") con.close() - else: # single con = HaPConn(SOCKET) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py new file mode 100755 index 000000000..7b3d09bbc --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# Sync ssl certificates from a yaml file into haproxy memory +import os +import sys +import argparse +import traceback +import yaml +import ssl +from io import StringIO +import base64 +import OpenSSL + + +sys.path.append(os.path.join(os.path.dirname(__file__), 'lib')) +from haproxy.conn import HaPConn +from haproxy import cmds + + +class Diff: + def __init__(self, local=None, remote=None): + if local is None: + local = [] + if remote is None: + remote = [] + + self.local = local + self.remote = remote + self.state = str(self) + + def show_state(self): + """ Shows current local and remote state """ + print("## STATE ##") + print(str(self)) + + def show_diff(self): + """ Shows what will be synced to target """ + print("## DIFF ##") + print("TODO: Show the diff") + + def sync(self): + print("## SYNC ##") + print("TODO: Sync to target") + + def __iter__(self): + return iter(self.local) + + def __str__(self): + result = "" + for item in self: + result += f"{str(item)}\n" + return result + + +class SyncWithTarget: + """ Base class for sync objects to a target """ + def __init__(self, socket='/var/run/haproxy.socket'): + self.socket = socket + + def execute_remote_cmd(self, command_class, **command_args): + con = HaPConn(self.socket) + if con: + result = con.sendCmd(command_class(**command_args), objectify=True) + con.close() + return result + + def get_remote_state(self, command_class, **command_args): + return self.execute_remote_cmd(command_class, **command_args) + + +class CertList(SyncWithTarget): + """ Represents a haproxy ssl-crt-list """ + def __init__(self, path, certs=None): + super().__init__() + if certs is None: + certs = [] + self.path = path + self.certs = certs + self.local = self.get_local_state() + self.remote = self.get_remote_state(cmds.showSslCrtList, crt_list=self.path) + + def __iter__(self): + return iter(self.local) + + def __str__(self): + result = f"CRT LIST: {self.path}\n" + result += f" LOCAL: {self.local}\n" + result += f" REMOTE: {self.remote}\n" + for cert in self.certs: + result += f"\n{str(cert)}\n" + return result + + def get_local_state(self): + return [f"{repr(cert)}" for cert in self.certs] + + def get_remote_state(self, command_class, **command_args): + crt_list_data = super().get_remote_state(command_class, **command_args) + return crt_list_data.get('certs', {}) + + +class Cert(SyncWithTarget): + """ Represents a haproxy ssl-cert """ + def __init__(self, path, pem): + super().__init__() + self.path = path + self.pem = pem + self.local = self.get_local_state() + self.remote = self.get_remote_state(cmds.showSslCert, certfile=self.path) + + def __repr__(self): + return self.path + + def __str__(self): + result = f" CERT: {self.path}" + result += f"\n LOCAL: {self.local}" + result += f"\n REMOTE: {self.remote}" + return result + + def get_cert_data(self, dump=False, encoding='utf-8'): + result = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, self.pem) + if dump: + result = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, result).decode(encoding) + return result + + def glue(self, components): + return "".join("/{0:s}={1:s}".format(name.decode(), value.decode()) for name, value in components) + + def get_local_state(self): + cert_obj = self.get_cert_data() + return { + "Serial": '%.2x' % cert_obj.get_serial_number(), + "Subject": self.glue(cert_obj.get_subject().get_components()), + "Issuer": self.glue(cert_obj.get_issuer().get_components()) + } + + def get_remote_state(self, command_class, **command_args): + cert_data = super().get_remote_state(command_class, **command_args) + if 'error' in cert_data: + return {} + return { + "Serial": cert_data['Serial'], + "Subject": cert_data['Subject'], + "Issuer": cert_data['Issuer'] + } + +def dict_from_yaml(path): + with open(path, 'r') as yaml_file: + data = yaml.load(yaml_file, Loader=yaml.SafeLoader) + return data + + +def skip_frontend(frontend_id, frontend): + filter_frontend_names = list(filter(None, args.frontends.split(","))) + filter_frontend_ids = list(filter(None, args.frontend_ids.split(","))) + + skip_id = False + if filter_frontend_names and frontend['name'] not in filter_frontend_names: + skip_id = True + + skip_name = False + if filter_frontend_ids and frontend_id not in filter_frontend_ids: + skip_name = True + + return skip_id and skip_name + + +def get_cert_data(cert, dump=False, encoding='utf-8'): + if os.path.isfile(cert): + cert = open(cert).read() + + cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) + if dump: + cert = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, cert).decode(encoding) + + return cert + + +def base64_decode(base64_str, encoding='utf-8'): + if base64_str: + base64_bytes = base64_str.encode(encoding) + message_bytes = base64.b64decode(base64_bytes) + message = message_bytes.decode(encoding) + return message + return '' + +def get_args(): + # noinspection PyTypeChecker + parser = argparse.ArgumentParser( + description=""" + Sync ssl certificates into HAProxy’s memory with certificates read from a configfile. If no frontend filter is + given, all certificates will be synced.""", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + '--config', + help='Path to the ssl certificate information configfile.', + default="/usr/local/etc/haproxy/sslCerts.yaml" + ) + parser.add_argument( + '--frontends', + help='Attempt action on a list of frontend names, specified as a comma separated list.', + default="" + ) + parser.add_argument( + '--frontend_ids', + help='Attempt action on a list of frontend ids, specified as a comma separated list.', + default="" + ) + parser.add_argument( + '--output', + help='Specify output format.', + choices=['json', 'raw'], + default="raw" + ) + parser.add_argument( + '--debug', + type=bool, + help='Show debug output.', + default=False + ) + return parser.parse_args() + + +args = get_args() +config = dict_from_yaml(args.config) + +""" Get ssl crt-list with certificates from configfile""" +crt_lists = [] +for frontend_id, frontend in config['frontends'].items(): + if skip_frontend(id, frontend_id): + continue + + certs = [] + for cert_id, cert_data in frontend['certs'].items(): + crt = base64_decode(cert_data['crt']) + key = base64_decode(cert_data['key']) + ca = base64_decode(cert_data['ca']) + full_cert = crt + key + ca + + certs.append(Cert(path=cert_data['path'], pem=full_cert)) + + crt_lists.append(CertList(path=frontend['crt_list_path'], certs=certs)) + +""" Sync ssl certs from configfile to HaProxy """ +diff = Diff(local=crt_lists) +diff.show_state() +diff.show_diff() +diff.sync() + + +#print(crt_lists) +#print(diff) +#diff.sync() diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml index 0b98ab5e1..56fed4573 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml @@ -47,7 +47,7 @@ frontends: {% for frontend in enabled_frontends %} "{{ frontend.id }}": name: {{ frontend.name }} - crt_list_path: {{ cert_template % frontend.id }} + crt_list_path: {{ crt_list_template % frontend.id }} certs: {% for cert_refid in frontend.certs %} {{ cert_refid }}: From 9df5e84a381f1c333c5093e05adca15a9c4d8ed1 Mon Sep 17 00:00:00 2001 From: Andreas Stuerz Date: Tue, 23 Feb 2021 08:57:37 +0100 Subject: [PATCH 5/7] sync local changes to remote via socket --- .../OPNsense/HAProxy/lib/haproxy/cmds.py | 4 +- .../HAProxy/lib/haproxy/tests/test_cmds.py | 2 +- .../scripts/OPNsense/HAProxy/syncCerts.py | 586 ++++++++++++++---- 3 files changed, 459 insertions(+), 133 deletions(-) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py index 81e8b3351..391527d89 100644 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py @@ -135,7 +135,7 @@ class showSslCrtLists(Cmd): return result class showSslCrtList(Cmd): - cmdTxt = "show ssl crt-list %(crt_list)s\r\n" + cmdTxt = "show ssl crt-list -n %(crt_list)s\r\n" req_args = ['crt_list'] helpTxt = "Show the the content of a crt-list." @@ -330,4 +330,4 @@ class showServers(baseStat): row.move_to_end('id', last=False) servers.append(dict(row)) - return servers \ No newline at end of file + return servers diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py index 01887f583..18a175aee 100644 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py @@ -142,7 +142,7 @@ class TestCommands(unittest.TestCase): "sessions": "show sess", "servers": "show stat", "show-ssl-crt-lists": "show ssl crt-list", - "show-ssl-crt-list": "show ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", + "show-ssl-crt-list": "show ssl crt-list -n /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", "show-ssl-certs": "show ssl cert", "show-ssl-cert": "show ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", "add-to-crt-list": "add ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py index 7b3d09bbc..d8f3d72b0 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py @@ -3,145 +3,452 @@ import os import sys import argparse -import traceback import yaml -import ssl -from io import StringIO import base64 import OpenSSL - +import json +from typing import List sys.path.append(os.path.join(os.path.dirname(__file__), 'lib')) from haproxy.conn import HaPConn from haproxy import cmds -class Diff: - def __init__(self, local=None, remote=None): - if local is None: - local = [] - if remote is None: - remote = [] - - self.local = local - self.remote = remote - self.state = str(self) - - def show_state(self): - """ Shows current local and remote state """ - print("## STATE ##") - print(str(self)) - - def show_diff(self): - """ Shows what will be synced to target """ - print("## DIFF ##") - print("TODO: Show the diff") - - def sync(self): - print("## SYNC ##") - print("TODO: Sync to target") - - def __iter__(self): - return iter(self.local) - - def __str__(self): - result = "" - for item in self: - result += f"{str(item)}\n" - return result - - class SyncWithTarget: """ Base class for sync objects to a target """ + def __init__(self, socket='/var/run/haproxy.socket'): self.socket = socket - def execute_remote_cmd(self, command_class, **command_args): + def _execute_remote_cmd(self, command_class, **command_args): con = HaPConn(self.socket) if con: - result = con.sendCmd(command_class(**command_args), objectify=True) + command_obj = command_class(**command_args) + result = con.sendCmd(command_obj, objectify=True) con.close() return result - def get_remote_state(self, command_class, **command_args): - return self.execute_remote_cmd(command_class, **command_args) + def _calc_diff(self): + """ return needed operations to get remote object in sync """ + raise Exception("need to be implemented!") + + def diff_list(self, first: List, second: List): + second = set(second) + return [item for item in first if item not in second] + + +class Diff(SyncWithTarget): + """ Represents a full diff to sync with remote """ + + def __init__(self, crt_lists=None): + super().__init__() + if crt_lists is None: + crt_lists = [] + self._crt_lists = crt_lists + self._diff = self._calc_diff() + self._status = self._get_status() + self._transactions = self._get_transactions() + + @property + def diff(self): + return self._diff + + @property + def crt_lists(self): + return self._crt_lists + + @property + def transactions(self): + return self._transactions + + @property + def status(self): + return self._status + + def _calc_diff(self): + result = {} + for crt_list in self: + result[crt_list.frontend_id] = crt_list.diff + return result + + def abort(self, output_format): + """ Abort transactions""" + aborted = [] + for certfile in self.transactions: + certfile = certfile.replace('*/', "/") + + output = self._execute_remote_cmd(cmds.abortSslCrt, certfile=certfile) + aborted.append({ + "cert": certfile, + "output": output, + }) + + if output_format == 'json': + print(json.dumps({'abort': aborted})) + + if output_format == 'raw': + for item in aborted: + print(f"ABORT transaction: {item['cert']}") + print(f" {repr(item['output'])}") + + def _get_transactions(self): + """ get open transactions""" + return self._execute_remote_cmd(cmds.showSslCerts)['transaction'] + + def _get_status(self): + status = {} + crt_list: CertList + for crt_list in self.crt_lists: + status[crt_list.frontend_id] = { + "frontend_name": crt_list.frontend_name, + "path": crt_list.path, + "local_certs": crt_list.local, + "local_default": crt_list.local_default, + "remote_certs": crt_list.remote, + "remote_default": crt_list.remote_default, + } + cert: Cert + status[crt_list.frontend_id]['certs'] = {} + for cert in crt_list.certs: + status[crt_list.frontend_id]['certs'][cert.cert_id] = { + 'path': cert.path, + 'local': cert.local, + 'remote': cert.local, + } + return status + + def show_status(self, output_format): + """ Shows current local and remote state """ + if output_format == 'json': + print(json.dumps(self.status)) + + if output_format == 'raw': + print("## STATUS ##") + for frontend_id, crt_list in self.status.items(): + print(f"CRT_LIST: {crt_list['path']}") + print(f" FRONTEND NAME: {crt_list['frontend_name']}") + print(f" FRONTEND ID: {frontend_id}") + print(f" LOCAL CERTS: {crt_list['local_certs']}") + print(f" REMOTE CERTS: {crt_list['remote_certs']}") + print(f" LOCAL DEFAULT: {crt_list['local_default']}") + print(f" REMOTE DEFAULT: {crt_list['remote_default']}") + + for cert_id, cert in crt_list['certs'].items(): + print() + print(f" CERT: {cert['path']}") + print(f" LOCAL: {cert['local']}") + print(f" REMOTE: {cert['remote']}") + print() + + def show_diff(self, output_format): + """ Shows what will be synced to target """ + if output_format == 'json': + print(json.dumps(self.diff)) + + if output_format == 'raw': + print("## DIFF ##") + for frontend_id, diff in self.diff.items(): + print(f"CRT LIST: {diff['path']}") + print(f" FRONTEND NAME: {diff['frontend_name']}") + print(f" FRONTEND ID: {diff['frontend_id']}") + for update in diff['update']: + print(f" CERT UPDATE:") + print(f" Cert: {update['certfile']}") + print(f" Serial: {update['meta']['Serial']}") + print(f" Issuer: {update['meta']['Issuer']}") + print(f" Subject: {update['meta']['Subject']}") + else: + if not diff['update']: + print(f" CERT UPDATE: []") + print(f" CERT ADD : {diff['add']}") + print(f" CERT DEL : {diff['del']}") + + def show_transactions(self, output_format): + + if output_format == 'json': + print(json.dumps({'transactions': self.transactions})) + + if output_format == 'raw': + print("## OPEN TRANSACTIONS ##") + for cert in self.transactions: + print(cert) + + def sync(self, output_format): + """ Sync to target """ + sync = {} + certs_to_delete = [] + for frontend_id, diff in self.diff.items(): + sync[frontend_id] = { + 'frontend_name': diff['frontend_name'], + 'frontend_id': diff['frontend_id'], + 'path': diff['path'], + 'add': [], + 'remove': [], + 'update': [], + 'del': [] + } + + # update cert content + for cert in diff['update']: + messages = [] + if cert['certfile'] in diff['add']: + output = self._execute_remote_cmd(cmds.newSslCrt, certfile=cert['certfile']) + messages.append(output) + + output = self._execute_remote_cmd(cmds.updateSslCrt, certfile=cert['certfile'], payload=cert['pem']) + messages.append(output) + + output = self._execute_remote_cmd(cmds.commitSslCrt, certfile=cert['certfile']) + messages.append(output) + + sync[frontend_id]['update'].append({ + 'cert': cert['certfile'], + 'messages': messages + }) + + # add to crt-list + for cert in diff['add']: + messages = [] + output = self._execute_remote_cmd(cmds.addToSslCrtList, crt_list=diff['path'], certfile=cert) + messages.append(output) + sync[frontend_id]['add'].append({ + 'cert': cert, + 'messages': messages + }) + + # remove from crt-list + for cert in diff['del']: + messages = [] + output = self._execute_remote_cmd(cmds.delFromSslCrtList, crt_list=diff['path'], certfile=cert) + messages.append(output) + certs_to_delete.append(cert.split(":")[0]) + sync[frontend_id]['remove'].append({ + 'cert': cert, + 'messages': messages + }) + + # delete unused certs operation - haproxy does not allow to delete certs in use + for cert in certs_to_delete: + messages = [] + output = self._execute_remote_cmd(cmds.delSslCrt, certfile=cert) + messages.append(output) + sync[frontend_id]['del'].append({ + 'cert': cert, + 'messages': messages + }) + + if output_format == 'json': + print(json.dumps(self.diff)) + + if output_format == 'raw': + print("## SYNC ##") + for frontend_id, crt_list in sync.items(): + print(f"CRT-LIST: {crt_list['path']}") + print(f" FRONTEND NAME: {crt_list['frontend_name']}") + print(f" FRONTEND ID: {crt_list['frontend_id']}") + for cert in crt_list['update']: + print(f" UPDATE: {cert['cert']}") + for message in cert['messages']: + print(" " + repr(message)) + for cert in crt_list['add']: + print(f" ADD: {cert['cert']}") + for message in cert['messages']: + print(" " + repr(message)) + + for cert in crt_list['remove']: + print(f" REMOVE: {cert['cert']}") + for message in cert['messages']: + print(" " + repr(message)) + + for cert in crt_list['del']: + print(f" DEL: {cert['cert']}") + for message in cert['messages']: + print(" " + repr(message)) + print() + + def __iter__(self): + return iter(self._crt_lists) + + def __str__(self): + return self.status class CertList(SyncWithTarget): """ Represents a haproxy ssl-crt-list """ - def __init__(self, path, certs=None): + + def __init__(self, path, frontend_id=None, frontend_name=None, certs=None, default_cert=None): super().__init__() if certs is None: certs = [] - self.path = path - self.certs = certs - self.local = self.get_local_state() - self.remote = self.get_remote_state(cmds.showSslCrtList, crt_list=self.path) + self._path = path + self._certs = certs + self._frontend_name = frontend_name + self._frontend_id = frontend_id + self._local_default = default_cert + self._local = self._get_local_state() + self._remote_ln = self._get_remote_state(cmds.showSslCrtList, crt_list=self._path) + self._remote = [cert_ln.split(":")[0] for cert_ln in self._remote_ln] + self._diff = self._calc_diff() + + @property + def path(self): + return self._path + + @property + def frontend_name(self): + return self._frontend_name + + @property + def frontend_id(self): + return self._frontend_id + + @property + def certs(self): + return self._certs + + @property + def local_default(self): + return self._local_default + + @property + def remote_default(self): + return next(iter(self._remote), None) + + @property + def local(self): + return self._local + + @property + def remote_ln(self): + """ Certs with line number""" + return self._remote_ln + + @property + def remote(self): + """ + if default certs are different return remote certs with line numbers, so they are deleted in the crt list. + This ensures that the default cert is always on top. + """ + if self._local_default is not None and self.local_default != self.remote_default: + return self._remote_ln + return self._remote + + @property + def diff(self): + return self._diff + + def _calc_diff(self): + """ return needed operations to get remote object in sync """ + diff = { + 'frontend_name': self.frontend_name, + 'frontend_id': self.frontend_id, + 'path': self.path, + 'add': [], + 'del': [], + 'update': [] + } + # skip when there is no remote crt list + if self.remote is None: + return diff + + # certs to add, delete and update on the remote target + diff['add'] = self.diff_list(self.local, self.remote) + diff['del'] = self.diff_list(self.remote, self.local) + diff['update'] = [cert.diff for cert in self.certs if cert.diff] + + return diff + + def _get_local_state(self): + return [f"{repr(cert)}" for cert in self._certs] + + def _get_remote_state(self, command_class, **command_args): + crt_list_data = self._execute_remote_cmd(command_class, **command_args) + return crt_list_data.get('certs', None) def __iter__(self): - return iter(self.local) - - def __str__(self): - result = f"CRT LIST: {self.path}\n" - result += f" LOCAL: {self.local}\n" - result += f" REMOTE: {self.remote}\n" - for cert in self.certs: - result += f"\n{str(cert)}\n" - return result - - def get_local_state(self): - return [f"{repr(cert)}" for cert in self.certs] - - def get_remote_state(self, command_class, **command_args): - crt_list_data = super().get_remote_state(command_class, **command_args) - return crt_list_data.get('certs', {}) + return iter(self._local) class Cert(SyncWithTarget): """ Represents a haproxy ssl-cert """ - def __init__(self, path, pem): + + def __init__(self, path, pem, cert_id=None): super().__init__() - self.path = path - self.pem = pem - self.local = self.get_local_state() - self.remote = self.get_remote_state(cmds.showSslCert, certfile=self.path) + self._path = path + self._pem = pem + self._cert_id = cert_id + self._local = self._get_local_state() + self._remote = self._get_remote_state(cmds.showSslCert, certfile=self._path) + self._diff = self._calc_diff() + + @property + def path(self): + return self._path + + @property + def cert_id(self): + return self._cert_id + + @property + def pem(self): + return self._pem.replace("\n\n", "\n") + + @property + def local(self): + return self._local + + @property + def remote(self): + return self._remote + + @property + def diff(self): + return self._diff def __repr__(self): - return self.path + return self._path - def __str__(self): - result = f" CERT: {self.path}" - result += f"\n LOCAL: {self.local}" - result += f"\n REMOTE: {self.remote}" - return result - - def get_cert_data(self, dump=False, encoding='utf-8'): + def _get_cert_data(self, dump=False, encoding='utf-8'): result = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, self.pem) if dump: result = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, result).decode(encoding) return result - def glue(self, components): + def _glue(self, components): return "".join("/{0:s}={1:s}".format(name.decode(), value.decode()) for name, value in components) - def get_local_state(self): - cert_obj = self.get_cert_data() + def _get_local_state(self): + cert_obj = self._get_cert_data() return { - "Serial": '%.2x' % cert_obj.get_serial_number(), - "Subject": self.glue(cert_obj.get_subject().get_components()), - "Issuer": self.glue(cert_obj.get_issuer().get_components()) + "Serial": '%.2x'.upper() % cert_obj.get_serial_number(), + "Subject": self._glue(cert_obj.get_subject().get_components()), + "Issuer": self._glue(cert_obj.get_issuer().get_components()) } - def get_remote_state(self, command_class, **command_args): - cert_data = super().get_remote_state(command_class, **command_args) + def _get_remote_state(self, command_class, **command_args): + cert_data = self._execute_remote_cmd(command_class, **command_args) + if 'error' in cert_data: - return {} + return cert_data + + if cert_data['Status'] == 'Empty': + return {'Status': cert_data['Status']} + return { - "Serial": cert_data['Serial'], - "Subject": cert_data['Subject'], - "Issuer": cert_data['Issuer'] + "Serial": cert_data.get('Serial', None), + "Subject": cert_data.get('Subject', None), + "Issuer": cert_data.get('Issuer', None), } + def _calc_diff(self): + result = {} + if self._remote != self._local: + result['certfile'] = self.path + result['pem'] = self.pem + result['meta'] = self.local + return result + + def dict_from_yaml(path): with open(path, 'r') as yaml_file: data = yaml.load(yaml_file, Loader=yaml.SafeLoader) @@ -152,15 +459,15 @@ def skip_frontend(frontend_id, frontend): filter_frontend_names = list(filter(None, args.frontends.split(","))) filter_frontend_ids = list(filter(None, args.frontend_ids.split(","))) - skip_id = False - if filter_frontend_names and frontend['name'] not in filter_frontend_names: - skip_id = True + if not filter_frontend_ids and not filter_frontend_names: + return False - skip_name = False - if filter_frontend_ids and frontend_id not in filter_frontend_ids: - skip_name = True + if filter_frontend_ids and frontend_id in filter_frontend_ids: + return False + if filter_frontend_names and frontend['name'] in filter_frontend_names: + return False - return skip_id and skip_name + return True def get_cert_data(cert, dump=False, encoding='utf-8'): @@ -182,6 +489,7 @@ def base64_decode(base64_str, encoding='utf-8'): return message return '' + def get_args(): # noinspection PyTypeChecker parser = argparse.ArgumentParser( @@ -190,6 +498,12 @@ def get_args(): given, all certificates will be synced.""", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) + parser.add_argument( + 'command', + choices=['status', 'diff', 'sync', 'transactions', 'abort'], + nargs='+', + help="Execute one or more operations." + ) parser.add_argument( '--config', help='Path to the ssl certificate information configfile.', @@ -201,7 +515,7 @@ def get_args(): default="" ) parser.add_argument( - '--frontend_ids', + '--frontend-ids', help='Attempt action on a list of frontend ids, specified as a comma separated list.', default="" ) @@ -211,42 +525,54 @@ def get_args(): choices=['json', 'raw'], default="raw" ) - parser.add_argument( - '--debug', - type=bool, - help='Show debug output.', - default=False - ) return parser.parse_args() +def get_crt_lists_from_config(configfile): + """ Get ssl crt-list with certificates from configfile""" + config = dict_from_yaml(configfile) + crt_lists = [] + for frontend_id, frontend in config['frontends'].items(): + if skip_frontend(frontend_id, frontend): + continue + + certs = [] + default_cert = None + for cert_id, cert_data in frontend['certs'].items(): + crt = base64_decode(cert_data['crt']) + key = base64_decode(cert_data['key']) + ca = base64_decode(cert_data['ca']) + full_cert = crt + key + ca + + if cert_data['default']: + default_cert = cert_data['path'] + + certs.append(Cert(path=cert_data['path'], pem=full_cert, cert_id=cert_id)) + + params = { + 'path': frontend['crt_list_path'], + 'frontend_id': frontend_id, + 'frontend_name': frontend['name'], + 'certs': certs, + 'default_cert': default_cert + } + crt_lists.append(CertList(**params)) + + return crt_lists + + args = get_args() -config = dict_from_yaml(args.config) - -""" Get ssl crt-list with certificates from configfile""" -crt_lists = [] -for frontend_id, frontend in config['frontends'].items(): - if skip_frontend(id, frontend_id): - continue - - certs = [] - for cert_id, cert_data in frontend['certs'].items(): - crt = base64_decode(cert_data['crt']) - key = base64_decode(cert_data['key']) - ca = base64_decode(cert_data['ca']) - full_cert = crt + key + ca - - certs.append(Cert(path=cert_data['path'], pem=full_cert)) - - crt_lists.append(CertList(path=frontend['crt_list_path'], certs=certs)) +crt_lists = get_crt_lists_from_config(args.config) +diff = Diff(crt_lists=crt_lists) """ Sync ssl certs from configfile to HaProxy """ -diff = Diff(local=crt_lists) -diff.show_state() -diff.show_diff() -diff.sync() - - -#print(crt_lists) -#print(diff) -#diff.sync() +if "status" in args.command: + diff.show_status(args.output) +if "diff" in args.command: + diff.show_diff(args.output) +if "abort" in args.command: + diff.abort(args.output) +if "transactions" in args.command: + diff.show_transactions(args.output) +if "sync" in args.command: + diff.sync(args.output) From 7231fcfa0dc514619d0d0c3b2c9697a0722d25fa Mon Sep 17 00:00:00 2001 From: Andreas Stuerz Date: Tue, 23 Feb 2021 10:15:07 +0100 Subject: [PATCH 6/7] add config.d services --- .../scripts/OPNsense/HAProxy/syncCerts.py | 1 - .../conf/actions.d/actions_haproxy.conf | 26 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py index d8f3d72b0..87632bda0 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py @@ -164,7 +164,6 @@ class Diff(SyncWithTarget): print(f" CERT DEL : {diff['del']}") def show_transactions(self, output_format): - if output_format == 'json': print(json.dumps({'transactions': self.transactions})) diff --git a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf index ce1ef790b..7301e83e8 100644 --- a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf +++ b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf @@ -74,4 +74,28 @@ message:change haproxy state for multiple server command:/usr/local/opnsense/scripts/OPNsense/HAProxy/socketCommand.py parameters: set-server-weight --server-ids %s --value %s type:script_output -message:change haproxy weight for multiple server \ No newline at end of file +message:change haproxy weight for multiple server + +[cert_diff] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py +parameters: diff --output json --frontends %s +type:script_output +message:Show diff between configured ssl certificates and certs from HAProxy memory for multiple frontends + +[cert_sync] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py +parameters: sync --frontends %s --output json +type:script_output +message:Sync ssl certificates into HAProxy memory for multiple frontends + +[cert_diff_bulk] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py diff --output json +parameters: +type:script_output +message:Show diff between configured ssl certificates and certs from HAProxy memory for all frontends + +[cert_sync_bulk] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py sync --output json +parameters: +type:script_output +message:Sync ssl certificates into HAProxy memory for all frontends From 1c5346467de9870019d26b5a4741656826a9cda4 Mon Sep 17 00:00:00 2001 From: Andreas Stuerz Date: Tue, 23 Feb 2021 16:36:02 +0100 Subject: [PATCH 7/7] fix status remote cert display add description for cert_sync_bulk --- .../src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py | 4 ++-- .../src/opnsense/service/conf/actions.d/actions_haproxy.conf | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py index 87632bda0..0427b2e7e 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py @@ -113,7 +113,7 @@ class Diff(SyncWithTarget): status[crt_list.frontend_id]['certs'][cert.cert_id] = { 'path': cert.path, 'local': cert.local, - 'remote': cert.local, + 'remote': cert.remote, } return status @@ -260,7 +260,7 @@ class Diff(SyncWithTarget): print(" " + repr(message)) for cert in crt_list['del']: - print(f" DEL: {cert['cert']}") + print(f"\n DEL: {cert['cert']}") for message in cert['messages']: print(" " + repr(message)) print() diff --git a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf index 7301e83e8..48bcc629d 100644 --- a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf +++ b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf @@ -99,3 +99,4 @@ command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py sync --output parameters: type:script_output message:Sync ssl certificates into HAProxy memory for all frontends +description:Sync ssl certificates changes into HAProxy memory