summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-08-10 18:32:47 +0000
committerChristian Breunig <christian@breunig.cc>2026-08-10 21:05:13 +0200
commit5faebdb613ae83947f0428951f6e68d6b66da7db (patch)
treec6b5812abb287f30e6a452f2866fbd37066051cb
parent0cfdd6a869772defbd6ca7778273bdab4b85dfe7 (diff)
downloadvyos-1x-5faebdb613ae83947f0428951f6e68d6b66da7db.tar.gz
vyos-1x-5faebdb613ae83947f0428951f6e68d6b66da7db.zip
pki: T9135: preserve every certificate in an ACME chain, not just the first
certbot's chain.pem commonly holds more than one certificate - e.g. the immediate intermediate plus its own issuing root - but the synthetic CA entry built from it only ever kept the first, silently dropping the rest before find_chain() ever saw them. This left a shorter chain than certbot itself actually has, e.g. requiring a root to also be configured manually to reach the same result certbot's own data already provides. Parse every certificate block in chain.pem and emit one synthetic entry per certificate, numbering entries after the first so each is still its own addressable, non-redundant, non-settable object exactly like before.
-rw-r--r--python/vyos/config.py8
-rw-r--r--python/vyos/pki.py45
-rwxr-xr-xsrc/op_mode/pki.py6
3 files changed, 36 insertions, 23 deletions
diff --git a/python/vyos/config.py b/python/vyos/config.py
index e79f659b7..113136901 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -360,7 +360,6 @@ class Config(object):
if pki_dict:
if 'certificate' in pki_dict:
from vyos.defaults import directories
- from vyos.pki import AUTOCHAIN_PREFIX
from vyos.pki import acme_chain_ca_entry
from vyos.pki import acme_chain_redundant
vyos_certbot_dir = directories['certbot']
@@ -385,10 +384,9 @@ class Config(object):
leaf_cert = cert_conf.get('certificate')
if leaf_cert and 'acme' in cert_conf and not acme_chain_redundant(
leaf_cert, real_ca_certs):
- chain_entry = acme_chain_ca_entry(vyos_certbot_dir, certificate)
- if chain_entry:
- ca_dict = pki_dict.setdefault('ca', {})
- autochain_name = f'{AUTOCHAIN_PREFIX}{certificate}'
+ ca_dict = pki_dict.setdefault('ca', {})
+ for autochain_name, chain_entry in acme_chain_ca_entry(
+ vyos_certbot_dir, certificate).items():
# A real, manually-configured CLI CA object
# with this name wins over the synthetic one
if autochain_name not in ca_dict:
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index 787b3fdbf..af0dee626 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -482,25 +482,42 @@ def find_chain(cert, ca_certs):
# without it ever being a real, settable, or deletable CLI object.
AUTOCHAIN_PREFIX = 'AUTOCHAIN_'
-def acme_chain_ca_entry(vyos_certbot_dir: str, cert_name: str) -> dict | None:
- """Build a synthetic 'pki ca <name>'-shaped dict entry
- (`{'certificate': <base64>}`) for an ACME certificate's intermediate
- CA chain, read live from certbot's own chain.pem.
-
- Returns None if chain.pem is not (yet) present - e.g. the certificate
- has not been issued yet, or a prior request failed - so callers can
- skip this certificate's chain gracefully instead of crashing.
+def acme_chain_ca_entry(vyos_certbot_dir: str, cert_name: str) -> dict:
+ """Build synthetic 'pki ca <name>'-shaped dict entries
+ (`{<name>: {'certificate': <base64>}, ...}`) for every certificate in
+ an ACME certificate's chain, read live from certbot's own chain.pem.
+
+ certbot commonly writes more than just the immediate intermediate to
+ chain.pem (e.g. the intermediate's own issuing root too), and every
+ one of them is needed for find_chain() to walk the full chain - a
+ single 'pki ca' object only ever holds one certificate, so each one
+ gets its own synthetic entry here.
+
+ Returns an empty dict if chain.pem is not (yet) present - e.g. the
+ certificate has not been issued yet, or a prior request failed - so
+ callers can skip this certificate's chain gracefully instead of
+ crashing.
"""
+ import re
from vyos.utils.file import read_file
tmp = read_file(f'{vyos_certbot_dir}/live/{cert_name}/chain.pem',
defaultonfailure=None)
if tmp is None:
- return None
- tmp = load_certificate(tmp, wrap_tags=False)
- if not tmp:
- return None
- chain_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1])
- return {'certificate': chain_base64}
+ return {}
+
+ entries = {}
+ for block in re.findall(
+ r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----',
+ tmp, re.DOTALL):
+ cert = load_certificate(block, wrap_tags=False)
+ if not cert:
+ continue
+ index = len(entries) + 1
+ name = f'{AUTOCHAIN_PREFIX}{cert_name}' if index == 1 else \
+ f'{AUTOCHAIN_PREFIX}{cert_name}_{index}'
+ chain_base64 = "".join(encode_certificate(cert).strip().split("\n")[1:-1])
+ entries[name] = {'certificate': chain_base64}
+ return entries
def acme_chain_redundant(leaf_cert_base64: str, ca_certs: dict) -> bool:
"""Return True if one of the already-configured CAs in ca_certs (a
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 91a83ea70..78b896edb 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -151,7 +151,6 @@ def get_config_ca_certificate(name=None):
# object, but consumers (find_chain(), "show pki ca") should see it
# the same way they'd see a manually-configured CA.
from vyos.defaults import directories
- from vyos.pki import AUTOCHAIN_PREFIX
from vyos.pki import acme_chain_ca_entry
from vyos.pki import acme_chain_redundant
vyos_certbot_dir = directories['certbot']
@@ -167,9 +166,8 @@ def get_config_ca_certificate(name=None):
continue
if acme_chain_redundant(leaf_cert, real_ca_certs):
continue
- chain_entry = acme_chain_ca_entry(vyos_certbot_dir, cert_name)
- if chain_entry:
- autochain_name = f'{AUTOCHAIN_PREFIX}{cert_name}'
+ for autochain_name, chain_entry in acme_chain_ca_entry(
+ vyos_certbot_dir, cert_name).items():
# A real, manually-configured CLI CA object with this name
# wins over the synthetic one
if autochain_name not in ca_certs: