Skip to content

Commit

Permalink
Overhaul/rewrite of certificate handling as follows:
Browse files Browse the repository at this point in the history
Change "ca-certs" references to "ca_certs".

New certificates are written to individual files, with an incrementing
number as part of their filename, rather than all being placed in a
single file. This resolves issues caused when certificate files
containing more than a single certificate are placed in /etc/ssl/certs
(by utilities such as "update-ca-certificates" run by ca_certs).

Alpine / Debian / Ubuntu:

The current behaviour, whilst it works, is incorrect with regard to
the design of the underlying OS utilities for managing certificates.
For "remove_defaults" the system-installed certificate files should not
be actually deleted (otherwise it becomes problematic if someone wishes
to later re-enable one or more of them), rather they should be
deactivated and these OSes already provide the means to do so - this MR
modifies the certificate entries in the /etc/ca-certificates.conf file
by prefixing them with "!" - when the update-ca-certificate utility is
then run it will *not* place such delimited certificates into either the
/etc/ssl/certs/ directory (via symlinks) nor add them to the
(re)generated certificates bundle file.

Additionally it is incorrect for added certificates to be placed in the
/usr/share/ca-certificates directory - this location is intended for
standard/"official" certificates, the /usr/local/share/ca-certificates
directory is intended for "local" or "site-specific" certificates and so
this PR adds them there instead - for certs in
/usr/local/share/ca-certificates the update-ca-certificates utility will
automatically use them, there is *no* need to add their filenames to the
/etc/ca-certificates.conf file.

RHEL / Fedora:

For RHEL (and Fedora which I have enabled in this PR) I believe the
existing "remove_defaults" behaviour is also incorrect in deleting
certificates files. However I have not been able to determine the
correct functionality to disable them, therefore this PR generates an
error if certificate deletion is activated for RHEL & Fedora.

FreeBSD:

This PR adds functionality for FreeBSD to add and delete/disable
certificates. For "delete" functionality existing system certificates
are moved to the blacklist directory in order to disable them.

LP: #1599694, #1901915, #1931174
  • Loading branch information
dermotbradley committed Jan 14, 2023
1 parent e776bc4 commit 3a9e211
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 189 deletions.
195 changes: 127 additions & 68 deletions cloudinit/config/cc_ca_certs.py
Expand Up @@ -5,48 +5,76 @@
"""CA Certs: Add ca certificates."""

import os
import shutil
from logging import Logger
from textwrap import dedent

from cloudinit import log as logging
from cloudinit import subp, util
from cloudinit.cloud import Cloud
from cloudinit.config import Config
from cloudinit.config.schema import MetaSchema, get_meta_doc
from cloudinit.settings import PER_INSTANCE

LOG = logging.getLogger(__name__)

DEFAULT_CONFIG = {
"ca_cert_path": "/usr/share/ca-certificates/",
"ca_cert_filename": "cloud-init-ca-certs.crt",
"ca_cert_path": None,
"ca_cert_blacklist_path": None,
"ca_cert_local_path": "/usr/local/share/ca-certificates/",
"ca_cert_filename": "cloud-init-ca-cert-%(number)s.crt",
"ca_cert_config": "/etc/ca-certificates.conf",
"ca_cert_system_path": "/etc/ssl/certs/",
"ca_cert_update_cmd": ["update-ca-certificates"],
}
DISTRO_OVERRIDES = {
"fedora": {
"ca_cert_path": None,
"ca_cert_blacklist_path": None,
"ca_cert_local_path": "/usr/share/pki/ca-trust-source/anchors/",
"ca_cert_filename": "cloud-init-ca-cert-%(number)s.crt",
"ca_cert_config": None,
"ca_cert_update_cmd": ["update-ca-trust"],
},
"freebsd": {
"ca_cert_path": "/usr/share/certs/trusted/",
"ca_cert_blacklist_path": "/usr/share/certs/blacklisted/",
"ca_cert_local_path": "/usr/local/share/certs/",
"ca_cert_filename": "cloud-init-ca-cert-%(number)s.crt",
"ca_cert_config": None,
"ca_cert_update_cmd": ["certctl", "rehash"],
},
"rhel": {
"ca_cert_path": "/usr/share/pki/ca-trust-source/",
"ca_cert_filename": "anchors/cloud-init-ca-certs.crt",
"ca_cert_path": None,
"ca_cert_blacklist_path": None,
"ca_cert_local_path": "/usr/share/pki/ca-trust-source/anchors/",
"ca_cert_filename": "cloud-init-ca-cert-%(number)s.crt",
"ca_cert_config": None,
"ca_cert_system_path": "/etc/pki/ca-trust/",
"ca_cert_update_cmd": ["update-ca-trust"],
}
},
}

MODULE_DESCRIPTION = """\
This module adds CA certificates to ``/etc/ca-certificates.conf`` and updates
the ssl cert cache using ``update-ca-certificates``. The default certificates
can be removed from the system with the configuration option
``remove_defaults``.
This module adds CA certificates to the system's OpenSSL CA directory and
updates the OpenSSL CA cache using the appropriate OS-specific utility. The
default CA certificates can be disabled from use by the system/OpenSSL with
the configuration option ``remove_defaults``.
.. note::
certificates must be specified using valid yaml. in order to specify a
multiline certificate, the yaml multiline list syntax must be used
.. note::
For Alpine Linux the "remove_defaults" functionality works if the
ca-certificates package is installed but not if the
ca-certificates-bundle package is installed.
Alpine Linux requires the ca-certificates package to be installed in
order to provide the ``update-ca-certificates`` command. This module
will not work as expected if only the ca-certificates-bundle package is
installed.
.. note::
For RHEL and Fedora this module currently only support adding
certificates, there is no support for disabling/deleting the standard
system CA certificates.
"""
distros = ["alpine", "debian", "ubuntu", "rhel"]
distros = ["alpine", "debian", "fedora", "freebsd", "rhel", "ubuntu"]

meta: MetaSchema = {
"id": "cc_ca_certs",
Expand Down Expand Up @@ -79,11 +107,11 @@ def _distro_ca_certs_configs(distro_name):
"""Return a distro-specific ca_certs config dictionary
@param distro_name: String providing the distro class name.
@returns: Dict of distro configurations for ca-cert.
@returns: Dict of distro configurations for ca_cert.
"""
cfg = DISTRO_OVERRIDES.get(distro_name, DEFAULT_CONFIG)
cfg["ca_cert_full_path"] = os.path.join(
cfg["ca_cert_path"], cfg["ca_cert_filename"]
cfg["ca_cert_local_path"], cfg["ca_cert_filename"]
)
return cfg

Expand All @@ -107,88 +135,108 @@ def add_ca_certs(distro_cfg, certs):
"""
if not certs:
return
# First ensure they are strings...
cert_file_contents = "\n".join([str(c) for c in certs])
util.write_file(
distro_cfg["ca_cert_full_path"], cert_file_contents, mode=0o644
)
update_cert_config(distro_cfg)
# Write each certificate to a separate file, maximum 99 certificates.
cert_number = 0
for c in certs:
# First ensure they are strings...
cert_file_contents = str(c)
cert_number += 1
if cert_number == 100:
LOG.error(
"100+ certificates specified, ignoring more than the 1st"
" 99 certs."
)
return

cert_file_name = distro_cfg["ca_cert_full_path"] % {
"number": str(cert_number).zfill(2)
}
util.write_file(cert_file_name, cert_file_contents, mode=0o644)


def disable_default_ca_certs(distro_name, distro_cfg):
"""
Disables all default trusted CA certificates. To actually apply the
changes you must also call L{update_ca_certs}.
@param distro_name: String providing the distro class name.
@param distro_cfg: A hash providing _distro_ca_certs_configs function.
"""
if distro_name == "freebsd":
move_freebsd_ca_certs_to_blacklist_dir(distro_cfg)
elif distro_name in ["alpine", "debian", "ubuntu"]:
disable_system_ca_certs(distro_cfg)

if distro_name in ["debian", "ubuntu"]:
debconf_sel = (
"ca-certificates ca-certificates/trust_new_crts " + "select no"
)
subp.subp(("debconf-set-selections", "-"), debconf_sel)

def update_cert_config(distro_cfg):

def disable_system_ca_certs(distro_cfg):
"""
Update Certificate config file to add the file path managed cloud-init
For every entry in the CA_CERT_CONFIG file prefix the entry with a "!"
in order to disable it.
@param distro_cfg: A hash providing _distro_ca_certs_configs function.
"""
if distro_cfg["ca_cert_config"] is None:
return
if os.stat(distro_cfg["ca_cert_config"]).st_size == 0:
# If the CA_CERT_CONFIG file is empty (i.e. all existing
# CA certs have been deleted) then simply output a single
# line with the cloud-init cert filename.
out = "%s\n" % distro_cfg["ca_cert_filename"]
else:
# Append cert filename to CA_CERT_CONFIG file.
# We have to strip the content because blank lines in the file
# causes subsequent entries to be ignored. (LP: #1077020)
if os.stat(distro_cfg["ca_cert_config"]).st_size != 0:
orig = util.load_file(distro_cfg["ca_cert_config"])
cr_cont = "\n".join(
[
line
for line in orig.splitlines()
if line != distro_cfg["ca_cert_filename"]
]
)
out = "%s\n%s\n" % (cr_cont.rstrip(), distro_cfg["ca_cert_filename"])
out = ""
for line in orig.splitlines():
if line.startswith("#") or line == "":
out += line + "\n"
else:
out += "!" + line + "\n"
util.write_file(distro_cfg["ca_cert_config"], out, omode="wb")


def remove_default_ca_certs(distro_name, distro_cfg):
def move_freebsd_ca_certs_to_blacklist_dir(distro_cfg):
"""
Removes all default trusted CA certificates from the system. To actually
apply the change you must also call L{update_ca_certs}.
For every file in the CA_CERT_PATH directory move it to the
CA_CERT_BLACKLIST_DIR in order to disable it.
@param distro_name: String providing the distro class name.
@param distro_cfg: A hash providing _distro_ca_certs_configs function.
"""
util.delete_dir_contents(distro_cfg["ca_cert_path"])
util.delete_dir_contents(distro_cfg["ca_cert_system_path"])
util.write_file(distro_cfg["ca_cert_config"], "", mode=0o644)

if distro_name in ["debian", "ubuntu"]:
debconf_sel = (
"ca-certificates ca-certificates/trust_new_crts " + "select no"
)
subp.subp(("debconf-set-selections", "-"), debconf_sel)
if (
distro_cfg["ca_cert_config"] is None
or distro_cfg["ca_cert_blacklist_path"] is None
):
return
LOG.debug("Moving system CA certificates to blacklist directory")
shutil.move(
distro_cfg["ca_cert_path"], distro_cfg["ca_cert_blacklist_path"])


def handle(
name: str, cfg: Config, cloud: Cloud, log: Logger, args: list
) -> None:
"""
Call to handle ca-cert sections in cloud-config file.
Call to handle ca_cert sections in cloud-config file.
@param name: The module name "ca-cert" from cloud.cfg
@param name: The module name "ca_cert" from cloud.cfg
@param cfg: A nested dict containing the entire cloud config contents.
@param cloud: The L{CloudInit} object in use.
@param log: Pre-initialized Python logger object to use for logging.
@param args: Any module arguments from cloud.cfg
"""
if "ca-certs" in cfg:
log.warning(
LOG.warning(
"DEPRECATION: key 'ca-certs' is now deprecated. Use 'ca_certs'"
" instead."
)
elif "ca_certs" not in cfg:
log.debug(
LOG.debug(
"Skipping module named %s, no 'ca_certs' key in configuration",
name,
)
return

if "ca-certs" in cfg and "ca_certs" in cfg:
log.warning(
LOG.warning(
"Found both ca-certs (deprecated) and ca_certs config keys."
" Ignoring ca-certs."
)
Expand All @@ -198,26 +246,37 @@ def handle(
# If there is a remove_defaults option set to true, remove the system
# default trusted CA certs first.
if "remove-defaults" in ca_cert_cfg:
log.warning(
LOG.warning(
"DEPRECATION: key 'ca-certs.remove-defaults' is now deprecated."
" Use 'ca_certs.remove_defaults' instead."
)
if ca_cert_cfg.get("remove-defaults", False):
log.debug("Removing default certificates")
remove_default_ca_certs(cloud.distro.name, distro_cfg)
if cloud.distro.name in ["fedora", "rhel"]:
LOG.error(
"Disabling default certificates is not supported for"
" this distro."
)
elif ca_cert_cfg.get("remove-defaults", False):
LOG.debug("Disabling default certificates")
disable_default_ca_certs(cloud.distro.name, distro_cfg)
elif ca_cert_cfg.get("remove_defaults", False):
log.debug("Removing default certificates")
remove_default_ca_certs(cloud.distro.name, distro_cfg)
if cloud.distro.name in ["fedora", "rhel"]:
LOG.error(
"Disabling default certificates is not supported for"
" this distro."
)
else:
LOG.debug("Disabling default certificates")
disable_default_ca_certs(cloud.distro.name, distro_cfg)

# If we are given any new trusted CA certs to add, add them.
if "trusted" in ca_cert_cfg:
trusted_certs = util.get_cfg_option_list(ca_cert_cfg, "trusted")
if trusted_certs:
log.debug("Adding %d certificates" % len(trusted_certs))
LOG.debug("Adding %d certificates" % len(trusted_certs))
add_ca_certs(distro_cfg, trusted_certs)

# Update the system with the new cert configuration.
log.debug("Updating certificates")
LOG.debug("Updating certificates")
update_ca_certs(distro_cfg)


Expand Down

0 comments on commit 3a9e211

Please sign in to comment.