summaryrefslogtreecommitdiff
path: root/src/conf_mode/pki.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/pki.py')
-rwxr-xr-xsrc/conf_mode/pki.py94
1 files changed, 75 insertions, 19 deletions
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 724f97555..869518dd9 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -19,6 +19,7 @@ import os
from sys import argv
from sys import exit
+from vyos.base import Message
from vyos.config import Config
from vyos.config import config_dict_merge
from vyos.configdep import set_dependents
@@ -27,6 +28,8 @@ from vyos.configdict import node_changed
from vyos.configdiff import Diff
from vyos.configdiff import get_config_diff
from vyos.defaults import directories
+from vyos.defaults import internal_ports
+from vyos.defaults import systemd_services
from vyos.pki import encode_certificate
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
@@ -42,9 +45,11 @@ from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
from vyos.utils.file import read_file
+from vyos.utils.network import check_port_availability
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -128,8 +133,20 @@ def certbot_request(name: str, config: dict, dry_run: bool=True):
f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\
f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\
f'{domains}'
+
+ listen_address = None
if 'listen_address' in config:
- tmp += f' --http-01-address {config["listen_address"]}'
+ listen_address = config['listen_address']
+
+ # When ACME is used behind a reverse proxy, we always bind to localhost
+ # whatever the CLI listen-address is configured for.
+ if ('haproxy' in dict_search('used_by', config) and
+ is_systemd_service_running(systemd_services['haproxy']) and
+ not check_port_availability(listen_address, 80)):
+ tmp += f' --http-01-address 127.0.0.1 --http-01-port {internal_ports["certbot_haproxy"]}'
+ elif listen_address:
+ tmp += f' --http-01-address {listen_address}'
+
# verify() does not need to actually request a cert but only test for plausability
if dry_run:
tmp += ' --dry-run'
@@ -150,14 +167,18 @@ def get_config(config=None):
if len(argv) > 1 and argv[1] == 'certbot_renew':
pki['certbot_renew'] = {}
- changed_keys = ['ca', 'certificate', 'dh', 'key-pair', 'openssh', 'openvpn']
+ # Walk through the list of sync_translate mapping and build a list
+ # which is later used to check if the node was changed in the CLI config
+ changed_keys = []
+ for value in sync_translate.values():
+ if value not in changed_keys:
+ changed_keys.append(value)
+ # Check for changes to said given keys in the CLI config
for key in changed_keys:
tmp = node_changed(conf, base + [key], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD)
-
if 'changed' not in pki:
pki.update({'changed':{}})
-
pki['changed'].update({key.replace('-', '_') : tmp})
# We only merge on the defaults of there is a configuration at all
@@ -219,8 +240,8 @@ def get_config(config=None):
continue
path = search['path']
- path_str = ' '.join(path + found_path)
- #print(f'PKI: Updating config: {path_str} {item_name}')
+ path_str = ' '.join(path + found_path).replace('_','-')
+ Message(f'Updating configuration: "{path_str} {item_name}"')
if path[0] == 'interfaces':
ifname = found_path[0]
@@ -230,6 +251,24 @@ def get_config(config=None):
if not D.node_changed_presence(path):
set_dependents(path[1], conf)
+ # 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:
+ continue
+ if not dict_search('system.load_balancing.haproxy', pki):
+ continue
+ used_by = []
+ for cert_list, _ in dict_search_recursive(
+ pki['system']['load_balancing']['haproxy'], 'certificate'):
+ if name in cert_list:
+ used_by.append('haproxy')
+ if used_by:
+ pki['certificate'][name]['acme'].update({'used_by': used_by})
+
return pki
def is_valid_certificate(raw_data):
@@ -321,6 +360,15 @@ def verify(pki):
raise ConfigError(f'An email address is required to request '\
f'certificate for "{name}" via ACME!')
+ listen_address = None
+ if 'listen_address' in cert_conf['acme']:
+ listen_address = cert_conf['acme']['listen_address']
+
+ if 'used_by' not in cert_conf['acme']:
+ if not check_port_availability(listen_address, 80):
+ 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,
# as this is time intensive
@@ -374,27 +422,35 @@ def verify(pki):
for search in sync_search:
for key in search['keys']:
changed_key = sync_translate[key]
-
if changed_key not in pki['changed']:
continue
-
for item_name in pki['changed'][changed_key]:
node_present = False
if changed_key == 'openvpn':
node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
else:
node_present = dict_search_args(pki, changed_key, item_name)
+ # If the node is still present, we can skip the check
+ # as we are not deleting it
+ if node_present:
+ continue
- if not node_present:
- search_dict = dict_search_args(pki['system'], *search['path'])
-
- if not search_dict:
- continue
+ search_dict = dict_search_args(pki['system'], *search['path'])
+ if not search_dict:
+ continue
- for found_name, found_path in dict_search_recursive(search_dict, key):
- if found_name == item_name:
- path_str = " ".join(search['path'] + found_path)
- raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"')
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ # Check if the name matches either by string compare, or beeing
+ # part of a list
+ if ((isinstance(found_name, str) and found_name == item_name) or
+ (isinstance(found_name, list) and item_name in found_name)):
+ # We do not support _ in CLI paths - this is only a convenience
+ # as we mangle all - to _, now it's time to reverse this!
+ path_str = ' '.join(search['path'] + found_path).replace('_','-')
+ object = changed_key.replace('_','-')
+ tmp = f'Embedded PKI {object} with name "{item_name}" is still '\
+ f'in use by CLI path "{path_str}"'
+ raise ConfigError(tmp)
return None
@@ -490,7 +546,7 @@ def generate(pki):
if not ca_cert_present:
tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate')
if not bool(tmp) or tmp != cert_chain_base64:
- print(f'Adding/replacing automatically imported CA certificate for "{cert}" ...')
+ Message(f'Add/replace automatically imported CA certificate for "{cert}"...')
add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64)
return None