net/haproxy: add support for built-in OCSP update feature

This commit is contained in:
Frank Wall 2023-12-16 23:17:38 +01:00
parent 13ab16cd96
commit ac824e3d02
12 changed files with 124 additions and 112 deletions

View file

@ -6,12 +6,19 @@ very high loads while needing persistence or Layer7 processing.
Plugin Changelog
================
Added:
* add support for built-in OCSP update feature
Fixed:
* fix typo in cert sync script
Changed:
* move OCSP settings from "Service" to "Global" section
* replace bundled haproxyctl library with haproxy-cli
Removed:
* remove OSCP update cron job
4.1
Fixed:

View file

@ -33,12 +33,6 @@
<type>checkbox</type>
<help><![CDATA[HAProxy will handle service restarts in a way that no connections are dropped. This is the best restart mode, because it has no impact on user experience. That being said, there might be edge cases where seamless reloads lead to unexpected behaviour.]]></help>
</field>
<field>
<id>haproxy.general.storeOcsp</id>
<label>Store OCSP responses</label>
<type>checkbox</type>
<help><![CDATA[Retrieve OCSP data everytime when starting or restarting HAProxy. For every certificate, the OCSP response will be fetched and stored in filesystem, and automatically picked-up by HAProxy on startup. However, depending on the number of certificates and other circumstances, this may noticeably increase the time required to start/restart the HAProxy service. Note that this only updates the OCSP responses once during start/restart, you need to setup a cron job to periodically update this data too.]]></help>
</field>
<field>
<id>haproxy.general.showIntro</id>
<label>Show introduction pages</label>

View file

@ -67,6 +67,28 @@
<help><![CDATA[These lines will be added to the global settings of to the HAProxy configuration file.<br/><div class="text-info"><b>NOTE:</b> The syntax will not be checked, use at your own risk!</div>]]></help>
<advanced>true</advanced>
</field>
<field>
<label>SSL settings</label>
<type>header</type>
</field>
<field>
<id>haproxy.general.tuning.ocspUpdateEnabled</id>
<label>Automatic OCSP updates</label>
<type>checkbox</type>
<help><![CDATA[Enable automatic OCSP response updates. Each OCSP response will be updated at least once an hour, and even more frequently if a given OCSP response has an expire date earlier than this one hour limit.]]></help>
</field>
<field>
<id>haproxy.general.tuning.ocspUpdateMinDelay</id>
<label>Minimum OCSP Interval</label>
<type>text</type>
<help><![CDATA[Sets the minimum interval (in seconds) between two automatic updates of the same OCSP response.]]></help>
</field>
<field>
<id>haproxy.general.tuning.ocspUpdateMaxDelay</id>
<label>Maximum OCSP Interval</label>
<type>text</type>
<help><![CDATA[Sets the maximum interval (in seconds) between two automatic updates of the same OCSP response.]]></help>
</field>
<field>
<label>SSL default settings</label>
<type>header</type>

View file

@ -10,17 +10,6 @@
<type>checkbox</type>
<help><![CDATA[Periodically sync SSL certificate changes into the running HAProxy service. This is most useful when using short-lived Let's Encrypt certificates, but changes to other certificates will also be synced. Note that when using the Let's Encrypt plugin, it is also possible to use an <a target="_blank" href="/ui/acmeclient/actions">Automation</a> instead of this cron job.]]></help>
</field>
<field>
<label>Update OCSP data for SSL certificates</label>
<type>header</type>
<style>table_cron table_cron_updateOcsp</style>
</field>
<field>
<id>haproxy.maintenance.cronjobs.updateOcsp</id>
<label>Enable</label>
<type>checkbox</type>
<help><![CDATA[Periodically fetch OCSP data for all configured SSL certificates. Note that OCSP support needs to be enabled in <a target="_blank" href="/ui/haproxy#general-settings">HAProxy service settings</a>.]]></help>
</field>
<field>
<label>Reload HAProxy service</label>
<type>header</type>

View file

@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/HAProxy</mount>
<version>4.0.0</version>
<version>4.1.0</version>
<description>the HAProxy load balancer</description>
<items>
<general>
@ -27,6 +27,7 @@
<default>0</default>
<Required>Y</Required>
</seamlessReload>
<!-- XXX: old value, required for model migration to 4.1.0 -->
<storeOcsp type="BooleanField">
<default>0</default>
<Required>N</Required>
@ -128,6 +129,24 @@
<customOptions type="TextField">
<Required>N</Required>
</customOptions>
<ocspUpdateEnabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
</ocspUpdateEnabled>
<ocspUpdateMinDelay type="IntegerField">
<default>300</default>
<MinimumValue>1</MinimumValue>
<MaximumValue>86400</MaximumValue>
<ValidationMessage>Please specify a value between 1 and 86400.</ValidationMessage>
<Required>N</Required>
</ocspUpdateMinDelay>
<ocspUpdateMaxDelay type="IntegerField">
<default>3600</default>
<MinimumValue>1</MinimumValue>
<MaximumValue>86400</MaximumValue>
<ValidationMessage>Please specify a value between 1 and 86400.</ValidationMessage>
<Required>N</Required>
</ocspUpdateMaxDelay>
<ssl_defaultsEnabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
@ -3027,6 +3046,7 @@
<ValidationMessage>Related cron not found.</ValidationMessage>
<Required>N</Required>
</syncCertsCron>
<!-- XXX: old value, required for model migration to 4.1.0 -->
<updateOcsp type="BooleanField">
<default>0</default>
<Required>N</Required>

View file

@ -0,0 +1,59 @@
<?php
/**
* Copyright (C) 2023 Frank Wall
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
namespace OPNsense\HAProxy\Migrations;
use OPNsense\Base\BaseModelMigration;
use OPNsense\Core\Config;
use OPNsense\Cron\Cron;
class M4_1_0 extends BaseModelMigration
{
public function run($model)
{
// Get old OCSP config item and map to new value
$old_ocsp = (string)$model->general->storeOcsp;
$model->general->tuning->ocspUpdateEnabled = $old_ocsp;
// Remove obsolete OCSP cron job
if ((string)$model->maintenance->cronjobs->updateOcspCron != "") {
$cron_uuid = (string)$model->maintenance->cronjobs->updateOcspCron;
$model->maintenance->cronjobs->updateOcspCron = "";
// Delete the cronjob item
$mdlCron = new Cron();
if ($mdlCron->jobs->job->del($cron_uuid)) {
$mdlCron->serializeToConfig();
$model->serializeToConfig($validateFullModel = false, $disable_validation = true);
Config::getInstance()->save();
}
}
}
}

View file

@ -95,7 +95,12 @@ foreach ($configNodes as $key => $value) {
file_put_contents($output_pem_filename, $pem_content);
chmod($output_pem_filename, 0600);
echo "exported $type to " . $output_pem_filename . "\n";
$crtlist[] = $output_pem_filename;
// Check if automatic OCSP updates are enabled.
if (isset($configObj->OPNsense->HAProxy->general->tuning->ocspUpdateEnabled) and ($configObj->OPNsense->HAProxy->general->tuning->ocspUpdateEnabled == '1')) {
$crtlist[] = $output_pem_filename . " ocsp-update on";
} else {
$crtlist[] = $output_pem_filename;
}
} else {
// In contrast to certificates, CA/CRL content needs to be put in a single file.
// A list of individual files is not supported by HAproxy.

View file

@ -22,11 +22,6 @@ find /var/haproxy -type d -exec chmod 550 {} \;
/usr/local/opnsense/scripts/OPNsense/HAProxy/exportErrorFiles.php > /dev/null 2>&1
/usr/local/opnsense/scripts/OPNsense/HAProxy/exportMapFiles.php > /dev/null 2>&1
# update OCSP data
if [ "${haproxy_ocsp}" == "YES" ]; then
/usr/local/opnsense/scripts/OPNsense/HAProxy/updateOcsp.sh > /dev/null 2>&1
fi
# deploy new config
case "$1" in
deploy)

View file

@ -1,76 +0,0 @@
#!/bin/sh
# This file is based on:
# https://github.com/acmesh-official/acme.sh/blob/master/deploy/haproxy.sh
#
# Copyright (C) 2021 Neil Pang
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
HAPROXY_DIR="/tmp/haproxy/ssl"
HAPROXY_SOCKET="/var/run/haproxy.socket"
for _pem in "$HAPROXY_DIR"/*.pem; do
cert_file="$(basename "$_pem")"
_issuer="${HAPROXY_DIR}/${cert_file%.pem}.issuer"
_ocsp="${_pem}.ocsp"
cert_cn="$(openssl x509 -in "$_pem" -noout -text | sed -nE 's/.*Subject:.*CN = ([^,]*)(,.*)?$/\1/p')"
if [ ! -f "$_issuer" ]; then
continue
fi
if [ -r "${_issuer}" ]; then
_ocsp_url="$(openssl x509 -noout -ocsp_uri -in "$_pem")"
if [ -n "$_ocsp_url" ]; then
_ocsp_host="$(echo "$_ocsp_url" | cut -d/ -f3)"
subjectdn="$(openssl x509 -in "$_issuer" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)"
issuerdn="$(openssl x509 -in "$_issuer" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)"
if [ "$subjectdn" = "$issuerdn" ]; then
_cafile_argument="-CAfile \"${_issuer}\""
else
_cafile_argument=""
fi
_openssl_version=$(openssl version | cut -d' ' -f2)
_openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
_openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
_header_sep="="
else
_header_sep=" "
fi
_openssl_ocsp_cmd="openssl ocsp \
-issuer \"${_issuer}\" \
-cert \"${_pem}\" \
-url \"${_ocsp_url}\" \
-header Host${_header_sep}\"${_ocsp_host}\" \
-respout \"${_ocsp}\" \
-verify_other \"${_issuer}\" \
${_cafile_argument} \
| grep -q \"${_pem}: good\""
eval "${_openssl_ocsp_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
echo "Updating OCSP stapling failed with return code ${_ret}"
else
_update="$(openssl enc -base64 -A -in "${_ocsp}")"
if ! echo "set ssl ocsp-response ${_update}" | socat stdio $HAPROXY_SOCKET; then
echo "Updating haproxy OCSP stapling via socket failed"
fi
fi
fi
fi
done

View file

@ -126,10 +126,3 @@ command:/usr/bin/diff -Naur /usr/local/etc/haproxy.conf /usr/local/etc/haproxy.c
parameters:
type:script_output
message:diff haproxy config
[update_ocsp]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/updateOcsp.sh
parameters:
type:script_output
description:Update HAProxy OCSP data
message:update haproxy ocsp data

View file

@ -982,6 +982,15 @@ global
{% if helpers.exists('OPNsense.HAProxy.general.tuning.maxConnections') %}
maxconn {{OPNsense.HAProxy.general.tuning.maxConnections}}
{% endif %}
{# # check if OCSP is enabled #}
{% if OPNsense.HAProxy.general.tuning.ocspUpdateEnabled|default('') == '1' %}
{% if helpers.exists('OPNsense.HAProxy.general.tuning.ocspUpdateMinDelay') %}
tune.ssl.ocsp-update.mindelay {{OPNsense.HAProxy.general.tuning.ocspUpdateMinDelay}}
{% endif %}
{% if helpers.exists('OPNsense.HAProxy.general.tuning.ocspUpdateMaxDelay') %}
tune.ssl.ocsp-update.maxdelay {{OPNsense.HAProxy.general.tuning.ocspUpdateMaxDelay}}
{% endif %}
{% endif %}
{% if helpers.exists('OPNsense.HAProxy.general.tuning.maxDHSize') %}
tune.ssl.default-dh-param {{OPNsense.HAProxy.general.tuning.maxDHSize}}
{% endif %}

View file

@ -3,11 +3,6 @@ haproxy_enable=YES
haproxy_setup="/usr/local/opnsense/scripts/OPNsense/HAProxy/setup.sh"
haproxy_pidfile="/var/run/haproxy.pid"
haproxy_config="/usr/local/etc/haproxy.conf"
{% if helpers.exists('OPNsense.HAProxy.general.storeOcsp') and OPNsense.HAProxy.general.storeOcsp|default("0") == "1" %}
haproxy_ocsp=YES
{% else %}
haproxy_ocsp=NO
{% endif %}
{% if helpers.exists('OPNsense.HAProxy.general.gracefulStop') and OPNsense.HAProxy.general.gracefulStop|default("0") == "1" %}
haproxy_hardstop=NO
{% else %}