summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2025-05-04 11:35:33 +0200
committerChristian Breunig <christian@breunig.cc>2025-05-04 23:38:29 +0200
commit59957ad694043f41a7b1e9ee740b19c87f297867 (patch)
treebf53840d5fb5636c463ef8df9f8bd4e40d0778ac
parentaff2835d7b6226e4b89f51e3f6133da26f3a07bf (diff)
downloadvyos-1x-59957ad694043f41a7b1e9ee740b19c87f297867.tar.gz
vyos-1x-59957ad694043f41a7b1e9ee740b19c87f297867.zip
haproxy: T7122: always reverse-proxy ACL for certbot
Always enable the ACL entry to reverse-proxy requests to the path "/.well-known/acme-challenge/" when "redirect-http-to-https" is configured for a given HAProxy frontend service. This is an intentional design decision to simplify the implementation and reduce overall code complexity. It poses no risk: a missing path returns a 404, and an unavailable backend yields an error 503. This approach avoids a chicken-and-egg problem where certbot might try to request a certificate via reverse-proxy before the proxy config is actually generated and active. By always routing through HAProxy, we also eliminate downtime as port 80 does not need to be freed for certbot's standalone mode.
-rw-r--r--data/templates/load-balancing/haproxy.cfg.j213
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_haproxy.py142
-rw-r--r--src/conf_mode/load-balancing_haproxy.py9
-rwxr-xr-xsrc/conf_mode/pki.py15
4 files changed, 104 insertions, 75 deletions
diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2
index 400936474..7a6b86c10 100644
--- a/data/templates/load-balancing/haproxy.cfg.j2
+++ b/data/templates/load-balancing/haproxy.cfg.j2
@@ -53,8 +53,8 @@ defaults
# Frontend
{% if service is vyos_defined %}
{% for front, front_config in service.items() %}
-
{% if front_config.redirect_http_to_https is vyos_defined %}
+{% set certbot_backend_name = 'certbot_' ~ front ~ '_backend' %}
frontend {{ front }}-http
mode http
{% if front_config.listen_address is vyos_defined %}
@@ -64,19 +64,14 @@ frontend {{ front }}-http
{% else %}
bind [::]:80 v4v6
{% endif %}
-{% if front_config.ssl.acme_certificate is vyos_defined %}
acl acme_acl path_beg /.well-known/acme-challenge/
- use_backend certbot_{{ front }}_backend if acme_acl
-{% endif %}
+ use_backend {{ certbot_backend_name }} if acme_acl
redirect scheme https code 301 if !acme_acl
-{% endif %}
-{% if front_config.ssl.acme_certificate is vyos_defined %}
-backend certbot_{{ front }}_backend
- server fe_{{ front }}_acme 127.0.0.1:65080
+backend {{ certbot_backend_name }}
+ server acme_{{ front }} 127.0.0.1:{{ get_default_port('certbot_haproxy') }}
{% endif %}
-
frontend {{ front }}
{% set ssl_front = [] %}
{% if front_config.ssl.certificate is vyos_defined and front_config.ssl.certificate is iterable %}
diff --git a/smoketest/scripts/cli/test_load-balancing_haproxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py
index 077f1974f..6a410ffde 100755
--- a/smoketest/scripts/cli/test_load-balancing_haproxy.py
+++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py
@@ -14,11 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import re
+import textwrap
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.template import get_default_port
from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
@@ -131,7 +134,25 @@ ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx
7i7xFz2WEiQeSCPaKYOiqM3t
"""
+haproxy_service_name = 'https_front'
+haproxy_backend_name = 'bk-01'
+def parse_haproxy_config() -> dict:
+ config_str = read_file(HAPROXY_CONF)
+ section_pattern = re.compile(r'^(global|defaults|frontend\s+\S+|backend\s+\S+)', re.MULTILINE)
+ sections = {}
+
+ matches = list(section_pattern.finditer(config_str))
+
+ for i, match in enumerate(matches):
+ section_name = match.group(1).strip()
+ start = match.end()
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(config_str)
+ section_body = config_str[start:end]
+ dedented_body = textwrap.dedent(section_body).strip()
+ sections[section_name] = dedented_body
+
+ return sections
class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
def tearDown(self):
# Check for running process
@@ -146,14 +167,14 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertFalse(process_named_running(PROCESS_NAME))
def base_config(self):
- self.cli_set(base_path + ['service', 'https_front', 'mode', 'http'])
- self.cli_set(base_path + ['service', 'https_front', 'port', '4433'])
- self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'http'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'port', '4433'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'backend', haproxy_backend_name])
- self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http'])
- self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11'])
- self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090'])
- self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'mode', 'http'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'address', '192.0.2.11'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'port', '9090'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'send-proxy'])
self.cli_set(base_path + ['global-parameters', 'max-connections', '1000'])
@@ -167,15 +188,15 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')])
self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')])
- def test_01_lb_reverse_proxy_domain(self):
+ def test_reverse_proxy_domain(self):
domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com']
domain_bk_second = 'n5.example.com'
- frontend = 'https_front'
+ frontend = 'vyos_smoketest'
front_port = '4433'
bk_server_first = '192.0.2.11'
bk_server_second = '192.0.2.12'
- bk_first_name = 'bk-01'
- bk_second_name = 'bk-02'
+ bk_first_name = 'vyosbk-01'
+ bk_second_name = 'vyosbk-02'
bk_server_port = '9090'
mode = 'http'
rule_ten = '10'
@@ -241,9 +262,9 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config)
self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config)
- def test_02_lb_reverse_proxy_cert_not_exists(self):
+ def test_reverse_proxy_cert_not_exists(self):
self.base_config()
- self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
@@ -253,19 +274,19 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.configure_pki()
self.base_config()
- self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
# self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception))
- self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
- self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest'])
+ self.cli_delete(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'smoketest'])
self.cli_commit()
- def test_03_lb_reverse_proxy_ca_not_exists(self):
+ def test_reverse_proxy_ca_not_exists(self):
self.base_config()
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
@@ -275,40 +296,40 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.configure_pki()
self.base_config()
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
# self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception))
- self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+ self.cli_delete(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest'])
self.cli_commit()
- def test_04_lb_reverse_proxy_backend_ssl_no_verify(self):
+ def test_reverse_proxy_backend_ssl_no_verify(self):
# Setup base
self.configure_pki()
self.base_config()
# Set no-verify option
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'no-verify'])
self.cli_commit()
# Test no-verify option
config = read_file(HAPROXY_CONF)
- self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config)
+ self.assertIn(f'server {haproxy_backend_name} 192.0.2.11:9090 send-proxy ssl verify none', config)
# Test setting ca-certificate alongside no-verify option fails, to test config validation
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
- def test_05_lb_reverse_proxy_backend_http_check(self):
+ def test_reverse_proxy_backend_http_check(self):
# Setup base
self.base_config()
# Set http-check
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'method', 'get'])
self.cli_commit()
# Test http-check
@@ -317,8 +338,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('http-check send meth GET', config)
# Set http-check with uri and status
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health'])
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'uri', '/health'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200'])
self.cli_commit()
# Test http-check with uri and status
@@ -328,8 +349,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('http-check expect status 200', config)
# Set http-check with string
- self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success'])
+ self.cli_delete(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'string', 'success'])
self.cli_commit()
# Test http-check with string
@@ -339,11 +360,11 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('http-check expect string success', config)
# Test configuring both http-check & health-check fails validation script
- self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'health-check', 'ldap'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
- def test_06_lb_reverse_proxy_tcp_mode(self):
+ def test_reverse_proxy_tcp_mode(self):
frontend = 'tcp_8443'
mode = 'tcp'
front_port = '8433'
@@ -390,27 +411,27 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'mode {mode}', config)
self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config)
- def test_07_lb_reverse_proxy_http_response_headers(self):
+ def test_reverse_proxy_http_response_headers(self):
# Setup base
self.configure_pki()
self.base_config()
# Set example headers in both frontend and backend
- self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800'])
- self.cli_set(base_path + ['backend', 'bk-01', 'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-response-headers', 'Proxy-Backend-ID', 'value', haproxy_backend_name])
self.cli_commit()
# Test headers are present in generated configuration file
config = read_file(HAPROXY_CONF)
self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config)
- self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config)
+ self.assertIn(f'http-response set-header Proxy-Backend-ID \'{haproxy_backend_name}\'', config)
# Test setting alongside modes other than http is blocked by validation conditions
- self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'tcp'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
- def test_08_lb_reverse_proxy_tcp_health_checks(self):
+ def test_reverse_proxy_tcp_health_checks(self):
# Setup PKI
self.configure_pki()
@@ -458,7 +479,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
config = read_file(HAPROXY_CONF)
self.assertIn(f'option smtpchk', config)
- def test_09_lb_reverse_proxy_logging(self):
+ def test_reverse_proxy_logging(self):
# Setup base
self.base_config()
self.cli_commit()
@@ -477,7 +498,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('log /dev/log local2 warning', config)
# Test backend logging options
- backend_path = base_path + ['backend', 'bk-01']
+ backend_path = base_path + ['backend', haproxy_backend_name]
self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug'])
self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info'])
self.cli_commit()
@@ -488,7 +509,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('log /dev/log local4 info', config)
# Test service logging options
- service_path = base_path + ['service', 'https_front']
+ service_path = base_path + ['service', haproxy_service_name]
self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice'])
self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit'])
self.cli_commit()
@@ -498,16 +519,17 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('log /dev/log local5 notice', config)
self.assertIn('log /dev/log local6 crit', config)
- def test_10_lb_reverse_proxy_http_compression(self):
+ def test_reverse_proxy_http_compression(self):
# Setup base
self.configure_pki()
self.base_config()
# Configure compression in frontend
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'algorithm', 'gzip'])
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/html'])
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/javascript'])
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/plain'])
+ http_comp_path = base_path + ['service', haproxy_service_name, 'http-compression']
+ self.cli_set(http_comp_path + ['algorithm', 'gzip'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/html'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/javascript'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/plain'])
self.cli_commit()
# Test compression is present in generated configuration file
@@ -517,11 +539,11 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('compression type text/html text/javascript text/plain', config)
# Test setting compression without specifying any mime-types fails verification
- self.cli_delete(base_path + ['service', 'https_front', 'http-compression', 'mime-type'])
+ self.cli_delete(base_path + ['service', haproxy_service_name, 'http-compression', 'mime-type'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
- def test_11_lb_haproxy_timeout(self):
+ def test_reverse_proxy_timeout(self):
t_default_check = '5'
t_default_client = '50'
t_default_connect = '10'
@@ -551,7 +573,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['timeout', 'client', t_client])
self.cli_set(base_path + ['timeout', 'connect', t_connect])
self.cli_set(base_path + ['timeout', 'server', t_server])
- self.cli_set(base_path + ['service', 'https_front', 'timeout', 'client', t_front_client])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'timeout', 'client', t_front_client])
self.cli_commit()
@@ -569,5 +591,25 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
for config_entry in config_entries:
self.assertIn(config_entry, config)
+ def test_reverse_proxy_http_redirect(self):
+ self.base_config()
+ self.cli_set(base_path + ['service', haproxy_service_name, 'redirect-http-to-https'])
+
+ self.cli_commit()
+
+ config = parse_haproxy_config()
+ frontend_name = f'frontend {haproxy_service_name}-http'
+ self.assertIn(frontend_name, config.keys())
+ self.assertIn('mode http', config[frontend_name])
+ self.assertIn('bind [::]:80 v4v6', config[frontend_name])
+ self.assertIn('acl acme_acl path_beg /.well-known/acme-challenge/', config[frontend_name])
+ self.assertIn(f'use_backend certbot_{haproxy_service_name}_backend if acme_acl', config[frontend_name])
+ self.assertIn('redirect scheme https code 301 if !acme_acl', config[frontend_name])
+
+ backend_name = f'backend certbot_{haproxy_service_name}_backend'
+ self.assertIn(backend_name, config.keys())
+ port = get_default_port('certbot_haproxy')
+ self.assertIn(f'server acme_https_front 127.0.0.1:{port}', config[backend_name])
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py
index f176009a0..0e959480c 100644
--- a/src/conf_mode/load-balancing_haproxy.py
+++ b/src/conf_mode/load-balancing_haproxy.py
@@ -22,7 +22,6 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configverify import verify_pki_certificate
from vyos.configverify import verify_pki_ca_certificate
-from vyos.defaults import internal_ports
from vyos.utils.dict import dict_search
from vyos.utils.process import call
from vyos.utils.network import check_port_availability
@@ -59,14 +58,6 @@ def get_config(config=None):
with_recursive_defaults=True,
with_pki=True)
- lb['certbot_port'] = internal_ports['certbot_haproxy']
-
- if 'service' in lb:
- for front, front_config in lb['service'].items():
- for cert in dict_search('ssl.certificate', front_config) or []:
- if dict_search(f'pki.certificate.{cert}.acme', lb):
- lb['service'][front]['ssl'].update({'acme_certificate': {}})
-
return lb
def verify(lb):
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index c1ff80d8a..98922595c 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -231,7 +231,7 @@ def get_config(config=None):
path = search['path']
path_str = ' '.join(path + found_path).replace('_','-')
- #print(f'PKI: Updating config: {path_str} {item_name}')
+ print(f'PKI: Updating config: {path_str} {item_name}')
if path[0] == 'interfaces':
ifname = found_path[0]
@@ -241,10 +241,10 @@ def get_config(config=None):
if not D.node_changed_presence(path):
set_dependents(path[1], conf)
- # Check PKI certificates if they are generated by ACME. If they are, traverse
- # the current configutration and determine the service where the certificate
- # is used by. This is needed to check if we might need to start ACME behing
- # a reverse proxy.
+ # Check PKI certificates if they are auto-generated by ACME. If they are,
+ # traverse the current configuration and determine the service where the
+ # certificate is used by.
+ # Required to check if we might need to run certbot behing a reverse proxy.
if 'certificate' in pki:
for name, cert_config in pki['certificate'].items():
if 'acme' not in cert_config:
@@ -252,7 +252,7 @@ def get_config(config=None):
if not dict_search('system.load_balancing.haproxy', pki):
continue
used_by = []
- for cert_list, cli_path in dict_search_recursive(
+ for cert_list, _ in dict_search_recursive(
pki['system']['load_balancing']['haproxy'], 'certificate'):
if name in cert_list:
used_by.append('haproxy')
@@ -356,7 +356,8 @@ def verify(pki):
if 'used_by' not in cert_conf['acme']:
if not check_port_availability(listen_address, 80):
- raise ConfigError(f'Port 80 is not available for ACME challenge for certificate "{name}"!')
+ raise ConfigError('Port 80 is already in use and not available '\
+ f'to provide ACME challenge for "{name}"!')
if 'certbot_renew' not in pki:
# Only run the ACME command if something on this entity changed,