Merge pull request #2688 from fraenki/acme_360b

security/acme-client: additions for release 3.6
This commit is contained in:
Frank Wall
2021-12-08 12:09:44 +01:00
committed by GitHub
10 changed files with 299 additions and 5 deletions
+3
View File
@@ -12,10 +12,13 @@ Plugin Changelog
Added:
* new automation: update local Unifi keystore (#2664)
* add support for dynv6 HTTP API (#2678)
* add support for TLS-ALPN-01 challenge type (#2661)
Fixed:
* fix SFTP upload (#2671)
* fix PHP error when acme.sh deploy hook returns an error (#2674)
* fix path for storing pf config files when using HTTP-01
3.5
@@ -78,6 +78,42 @@
<allownew>true</allownew>
<help>Choose the local HAProxy frontends. They will automatically be configured to redirect acme challenges to the internal acme client. The HAProxy service will automatically be restarted if a certificate was renewed.</help>
</field>
<field>
<label>TLS-ALPN-01</label>
<type>header</type>
<style>method_table method_table_tlsalpn01</style>
</field>
<field>
<id>validation.tlsalpn_service</id>
<label>TLS-ALPN Service</label>
<type>dropdown</type>
</field>
<field>
<label>acme.sh TLS Web Server</label>
<type>header</type>
<style>table_tlsalpn table_tlsalpn_acme</style>
</field>
<field>
<id>validation.tlsalpn_acme_autodiscovery</id>
<label>IP Auto-Discovery</label>
<type>checkbox</type>
<help><![CDATA[The FQDN's used in your certificate must currently point to an official IP address. Choose this option to let OPNsense try to auto-discover these IP addresses. This will lead to a short downtime of the service that is normally used with this IP address.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
</field>
<field>
<id>validation.tlsalpn_acme_interface</id>
<label>Interface</label>
<type>dropdown</type>
<help><![CDATA[The FQDN's used in your certificate must currently point to an official IP address. Choose the interface where this IP address is currently configured. OPNsense will automatically create a temporary port forward to allow the ACME validation to succeed. This will lead to a short downtime of the service that is normally used with this IP address.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
</field>
<field>
<id>validation.tlsalpn_acme_ipaddresses</id>
<label>IP Addresses</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[The FQDN's used in your certificate must currently point to one or more official IP addresses. Enter the all of these IP addresses here. OPNsense will automatically create a temporary port forward to allow the ACME validation to succeed. This will lead to a short downtime of the service that is normally used with these IP addresses.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
<hint>Enter IP addresses here. Finish each with TAB.</hint>
</field>
<field>
<label>DNS-01</label>
<type>header</type>
@@ -1353,4 +1389,14 @@
<label>Secret API Key</label>
<type>password</type>
</field>
<field>
<label>dynv6 HTTP API</label>
<type>header</type>
<style>table_dns table_dns_dynv6</style>
</field>
<field>
<id>validation.dns_dynv6_token</id>
<label>HTTP Token</label>
<type>password</type>
</field>
</form>
@@ -36,6 +36,13 @@
<help><![CDATA[When using HTTP-01 as challenge type, a local webserver is used to provide acme challenge data to the ACME CA. The local webserver is NOT directly exposed to the outside and should NOT use port 80 or any other well-known port. This setting allows you to change the local port of this webserver in case it interferes with another local service. Defaults to port 43580.]]></help>
<advanced>true</advanced>
</field>
<field>
<id>acmeclient.settings.TLSchallengePort</id>
<label>Local TLS ALPN Port</label>
<type>text</type>
<help><![CDATA[The service port when using TLS-ALPN-01 as challenge type. It works similar to the HTTP-01 challenge type. Defaults to port 43581.]]></help>
<advanced>true</advanced>
</field>
<field>
<id>acmeclient.settings.restartTimeout</id>
<label>Automation Timeout</label>
@@ -85,6 +85,9 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
case 'http01':
$this->acme_args[] = '--webroot ' . self::ACME_WEBROOT;
break;
case 'tlsalpn01':
$this->acme_args[] = '--alpn';
break;
}
// Store acme filenames
@@ -0,0 +1,44 @@
<?php
/*
* Copyright (C) 2021 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\AcmeClient\LeValidation;
use OPNsense\AcmeClient\LeValidationInterface;
use OPNsense\Core\Config;
/**
* dynv6 HTTP API
* @package OPNsense\AcmeClient
*/
class DnsDynv6 extends Base implements LeValidationInterface
{
public function prepare()
{
$this->acme_env['DYNV6_TOKEN'] = (string)$this->config->dns_dynv6_token;
}
}
@@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2020 Frank Wall
* Copyright (C) 2020-2021 Frank Wall
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -42,6 +42,8 @@ class HttpOpnsense extends Base implements LeValidationInterface
{
public function prepare()
{
$configdir = (string)sprintf(self::ACME_CONFIG_DIR, $this->cert_id);
// Get configured HTTP port for local lighttpd server.
$configObj = Config::getInstance()->object();
$local_http_port = $configObj->OPNsense->AcmeClient->settings->challengePort;
@@ -0,0 +1,143 @@
<?php
/*
* Copyright (C) 2021 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\AcmeClient\LeValidation;
require_once("interfaces.inc");
use OPNsense\AcmeClient\LeValidationInterface;
use OPNsense\AcmeClient\LeUtils;
use OPNsense\Core\Config;
/**
* Use acme.sh TLS web server for TLS-ALPN-01 validation
* @package OPNsense\AcmeClient
*/
class TlsalpnAcme extends Base implements LeValidationInterface
{
public function prepare()
{
$configdir = (string)sprintf(self::ACME_CONFIG_DIR, $this->cert_id);
// Get configured TLS port for acme.sh web server.
$configObj = Config::getInstance()->object();
$local_tls_port = $configObj->OPNsense->AcmeClient->settings->TLSchallengePort;
$this->acme_args[] = LeUtils::execSafe('--tlsport %s', (string)$local_tls_port);
// Collect all IP addresses here, automatic port forward will be applied for each IP
$iplist = array();
// Add IP addresses from auto-discovery feature
if ($this->config->tlsalpn_acme_autodiscovery == 1) {
$dnslist = explode(',', $this->cert_altnames);
$dnslist[] = $this->cert_name;
foreach ($dnslist as $fqdn) {
// NOTE: This may take some time.
$ip_found = gethostbyname("${fqdn}.");
if (!empty($ip_found)) {
$iplist[] = (string)$ip_found;
}
}
}
// Add IP addresses from user input
$additional_ip = (string)$this->config->tlsalpn_acme_ipaddresses;
if (!empty($additional_ip)) {
foreach (explode(',', $additional_ip) as $ip) {
$iplist[] = $ip;
}
}
// Add IP address from chosen interface
if (!empty((string)$this->config->tlsalpn_acme_interface)) {
$interface_ip = get_interface_ip((string)$this->config->tlsalpn_acme_interface);
if (!empty($interface_ip)) {
$iplist[] = $interface_ip;
}
}
// Check if IPv6 support is enabled
if (isset($configObj->system->ipv6allow) && ($configObj->system->ipv6allow == '1')) {
$_ipv6_enabled = true;
} else {
$_ipv6_enabled = false;
}
// Generate rules for all IP addresses
$anchor_rules = "";
if (!empty($iplist)) {
$dedup_iplist = array_unique($iplist);
// Add one rule for every IP
foreach ($dedup_iplist as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
// IPv4
$_dst = '127.0.0.1';
$_family = 'inet';
LeUtils::log("using IPv4 address: ${ip}");
} elseif (($_ipv6_enabled == true) && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
// IPv6
$_dst = '::1';
$_family = 'inet6';
LeUtils::log("using IPv6 address: ${ip}");
} else {
continue; // skip broken entries
}
$anchor_rules .= "rdr pass ${_family} proto tcp from any to ${ip} port 443 -> ${_dst} port ${local_tls_port}\n";
}
} else {
LeUtils::log_error("no IP addresses found to setup port forward");
return false;
}
// Abort if no rules were generated
if (empty($anchor_rules)) {
LeUtils::log_error("unable to setup a port forward (empty ruleset)");
return false;
}
// Create temporary port forward to allow acme challenges to get through
$anchor_setup = "rdr-anchor \"acme-client\"\n";
file_put_contents("${configdir}/acme_anchor_setup", $anchor_setup);
chmod("${configdir}/acme_anchor_setup", 0600);
mwexec("/sbin/pfctl -f ${configdir}/acme_anchor_setup");
file_put_contents("${configdir}/acme_anchor_rules", $anchor_rules);
chmod("${configdir}/acme_anchor_rules", 0600);
mwexec("/sbin/pfctl -a acme-client -f ${configdir}/acme_anchor_rules");
}
public function cleanup()
{
// Flush OPNsense port forward rules.
mwexec('/sbin/pfctl -a acme-client -F all');
// Workaround to solve disconnection issues reported by some users.
$backend = new \OPNsense\Core\Backend();
$response = $backend->configdRun('filter reload');
return true;
}
}
@@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2020 Frank Wall
* Copyright (C) 2020-2021 Frank Wall
* Copyright (C) 2018 Deciso B.V.
* All rights reserved.
*
@@ -62,6 +62,9 @@ class LeValidationFactory
case 'http01':
$search_name = "http_" . $obj->http_service;
break;
case 'tlsalpn01':
$search_name = "tlsalpn_" . $obj->tlsalpn_service;
break;
}
// Convert to PascalCase
@@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/AcmeClient</mount>
<version>3.1.0</version>
<version>3.2.0</version>
<description>A secure ACME Client plugin</description>
<items>
<settings>
@@ -34,12 +34,18 @@
<stg>Staging Environment</stg>
</OptionValues>
</environment>
<challengePort type="IntegerField">
<challengePort type="PortField">
<default>43580</default>
<MinimumValue>1024</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>Y</Required>
</challengePort>
<TLSchallengePort type="PortField">
<default>43581</default>
<MinimumValue>1024</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>Y</Required>
</TLSchallengePort>
<restartTimeout type="IntegerField">
<default>600</default>
<MinimumValue>10</MinimumValue>
@@ -339,6 +345,7 @@
<OptionValues>
<http01>HTTP-01</http01>
<dns01>DNS-01</dns01>
<tlsalpn01>TLS-ALPN-01</tlsalpn01>
</OptionValues>
</method>
<http_service type="OptionField">
@@ -384,6 +391,28 @@
<multiple>Y</multiple>
<Required>N</Required>
</http_haproxyFrontends>
<tlsalpn_service type="OptionField">
<Required>Y</Required>
<default>acme</default>
<OptionValues>
<acme>acme.sh TLS Web Server (automatic port forward)</acme>
</OptionValues>
</tlsalpn_service>
<tlsalpn_acme_autodiscovery type="BooleanField">
<default>1</default>
<Required>N</Required>
</tlsalpn_acme_autodiscovery>
<tlsalpn_acme_interface type="InterfaceField">
<Required>N</Required>
<default>wan</default>
<filters>
<enable>/^(?!0).*$/</enable>
</filters>
</tlsalpn_acme_interface>
<tlsalpn_acme_ipaddresses type="CSVListField">
<Required>N</Required>
<multiple>Y</multiple>
</tlsalpn_acme_ipaddresses>
<dns_service type="OptionField">
<Required>Y</Required>
<default>dns_freedns</default>
@@ -417,6 +446,7 @@
<dns_duckdns>DuckDNS API</dns_duckdns>
<dns_dyn>Dyn Managed DNS API</dns_dyn>
<dns_dynu>Dynu API</dns_dynu>
<dns_dynv6>dynv6 HTTP API</dns_dynv6>
<dns_euserv>EUserv</dns_euserv>
<dns_freedns>FreeDNS API</dns_freedns>
<dns_gandi_livedns>Gandi LiveDNS API</dns_gandi_livedns>
@@ -1018,6 +1048,9 @@
<dns_zone_key type="TextField">
<Required>N</Required>
</dns_zone_key>
<dns_dynv6_token type="TextField">
<Required>N</Required>
</dns_dynv6_token>
</validation>
</validations>
<actions>
@@ -77,11 +77,20 @@ POSSIBILITY OF SUCH DAMAGE.
} else {
}
});
$("#validation\\.tlsalpn_service").change(function(){
var service_id = 'table_tlsalpn_' + $(this).val();
$(".table_tlsalpn").hide();
if ($("#validation\\.method").val() == 'tlsalpn01') {
$("."+service_id).show();
} else {
}
});
$("#validation\\.method").change(function(){
$(".method_table").hide();
$(".method_table_"+$(this).val()).show();
$("#validation\\.dns_service").change();
$("#validation\\.http_service").change();
$("#validation\\.tlsalpn_service").change();
});
$("#validation\\.method").change();
@@ -103,7 +112,8 @@ POSSIBILITY OF SUCH DAMAGE.
<p>{{ lang._('As defined by the ACME standard, Certificate Authorities (CAs) must validate that you control a domain name. This is done by using "challenges". The following challenge types are supported:') }}</p>
<ul>
<li>{{ lang._('%sDNS-01:%s This is the most reliable challenge type and thus highly recommended when using this plugin. It requires that you control the DNS for your domain name and that your DNS provider is supported both %sby acme.sh%s and this plugin.') | format('<b>', '</b>', '<a href="https://github.com/acmesh-official/acme.sh/wiki/dnsapi" target="_blank">', '</a>') }}</li>
<li>{{ lang._("%sHTTP-01:%s This challenge type usually requires manual configuration and is not recommended. The DNS name used in the certificate must point to the OPNsense host where the ACME Client plugin is running on. The integrated web service will try to guess the correct settings for your setup, but this may not always work out-of-the-box. Furthermore this challenge type cannot be used to create %swildcard certificates with Let's Encrypt%s.") | format('<b>', '</b>', '<a href="https://letsencrypt.org/docs/challenge-types/#http-01-challenge" target="_blank">', '</a>') }}</li>
<li>{{ lang._("%sHTTP-01:%s This challenge type usually requires manual configuration and is not recommended. The DNS name used in the certificate must point to the OPNsense host where the ACME Client plugin is running on. The integrated web service will try to guess the correct settings for your setup, but this may not always work out-of-the-box. Furthermore this challenge type cannot be used to validate %swildcard certificates with Let's Encrypt%s.") | format('<b>', '</b>', '<a href="https://letsencrypt.org/docs/challenge-types/#http-01-challenge" target="_blank">', '</a>') }}</li>
<li>{{ lang._("%sTLS-ALPN-01:%s This works similar to the HTTP-01 challenge type and has the same requirements. It works if port 80 is unavailable. Other challenge types should be preferred. This challenge type cannot be used to validate %swildcard certificates with Let's Encrypt%s.") | format('<b>', '</b>', '<a href="https://letsencrypt.org/docs/challenge-types/#tls-alpn-01" target="_blank">', '</a>') }}</li>
</ul>
<p>{{ lang._('When experiencing issues with a challenge type, try setting the log level to "debug". Please provide full logs when %sreporting issues%s for a challenge type. You should also consider to ask the Certificate Authority for support, if you choose to use a commercial CA.') | format('<a href="https://github.com/opnsense/plugins/issues">', '</a>') }}</p>
</div>