Skip to content

Commit

Permalink
Merge pull request #6045 from rvykydal/edns-certs-stage2-w-initramfs
Browse files Browse the repository at this point in the history
edns: implement %certificate kickstart feature
  • Loading branch information
KKoukiou authored Jan 15, 2025
2 parents a54f83d + a9b689b commit 816346f
Show file tree
Hide file tree
Showing 29 changed files with 1,037 additions and 11 deletions.
2 changes: 1 addition & 1 deletion anaconda.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Source0: https://github.com/rhinstaller/%{name}/releases/download/%{name}-%{vers
%define libreportanacondaver 2.0.21-1
%define mehver 0.23-1
%define nmver 1.0
%define pykickstartver 3.58-1
%define pykickstartver 3.61-1
%define pypartedver 2.5-2
%define pythonblivetver 1:3.9.0-1
%define rpmver 4.15.0
Expand Down
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ AC_CONFIG_FILES([Makefile
pyanaconda/modules/boss/kickstart_manager/Makefile
pyanaconda/modules/boss/module_manager/Makefile
pyanaconda/modules/security/Makefile
pyanaconda/modules/security/certificates/Makefile
pyanaconda/modules/timezone/Makefile
pyanaconda/modules/network/Makefile
pyanaconda/modules/network/firewall/Makefile
Expand Down
1 change: 1 addition & 0 deletions data/systemd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dist_systemd_DATA = anaconda.service \
anaconda-nm-config.service \
anaconda-nm-disable-autocons.service \
anaconda-nm-disable-autocons-rhel.service \
anaconda-import-initramfs-certs.service \
anaconda-pre.service \
anaconda-s390-device-config-import.service \
anaconda-fips.service
Expand Down
1 change: 1 addition & 0 deletions data/systemd/anaconda-generator
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ ln -sf "$systemd_dir/anaconda-nm-config.service" "$target_dir/anaconda-nm-config
ln -sf "$systemd_dir/anaconda-nm-disable-autocons.service" "$target_dir/anaconda-nm-disable-autocons.service"
ln -sf "$systemd_dir/anaconda-nm-disable-autocons-rhel.service" "$target_dir/anaconda-nm-disable-autocons-rhel.service"
ln -sf "$systemd_dir/anaconda-pre.service" "$target_dir/anaconda-pre.service"
ln -sf "$systemd_dir/anaconda-import-initramfs-certs.service" "$target_dir/anaconda-import-initramfs-certs.service"
8 changes: 8 additions & 0 deletions data/systemd/anaconda-import-initramfs-certs.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=Import of certificates added in initramfs stage of Anaconda via kickstart
Before=NetworkManager.service
Before=anaconda.target

[Service]
Type=oneshot
ExecStart=/usr/libexec/anaconda/anaconda-import-initramfs-certs
13 changes: 13 additions & 0 deletions docs/release-notes/certificates-import.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
:Type: Kickstart
:Summary: Support certificates import via kickstart file

:Description:
New kickstart section %certificate is supported.
It allows users to securely embed certificates directly within
the kickstart file.

:Links:
- https://issues.redhat.com/browse/RHELBU-2913
- https://issues.redhat.com/browse/INSTALLER-4027
- https://github.com/rhinstaller/anaconda/pull/6045
- https://github.com/pykickstart/pykickstart/pull/517
41 changes: 41 additions & 0 deletions dracut/parse-kickstart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ TMPDIR = "/tmp"
ARPHRD_ETHER = "1"
ARPHRD_INFINIBAND = "32"

CERT_TRANSPORT_DIR = "/run/install/certificates"

# Helper function for reading simple files in /sys
def readsysfile(f):
'''Return the contents of f, or "" if missing.'''
Expand Down Expand Up @@ -403,6 +405,44 @@ def ksnet_to_dracut(args, lineno, net, bootdev=False):

return " ".join(line)


def _dump_certificate(cert, root="/", dump_dir=None):
"""Dump the certificate into specified file."""
dump_dir = dump_dir or cert.dir
if not dump_dir:
log.error("Certificate destination is missing for %s", cert.filename)
return

dst_dir = os.path.join(root, dump_dir.lstrip('/'))
log.debug("Dumping certificate %s into %s.", cert.filename, dst_dir)
if not os.path.exists(dst_dir):
log.debug("Path %s for certificate does not exist, creating.", dst_dir)
os.makedirs(dst_dir)

dst = os.path.join(dst_dir, cert.filename)

if os.path.exists(dst):
log.warning("Certificate file %s already exists, replacing.", dst)

with open(dst, 'w') as f:
f.write(cert.cert)
f.write('\n')


def process_certificates(handler):
"""Import certificates defined in %certificate sections."""
for cert in handler.certificates:
log.info("Processing kickstart certificate %s", cert.filename)

if not cert.filename:
log.error("Missing certificate file name, skipping.")
continue

_dump_certificate(cert)
# Dump for transport to switchroot
_dump_certificate(cert, root=CERT_TRANSPORT_DIR+"/path/")


def process_kickstart(ksfile):
handler = DracutHandler()
try:
Expand All @@ -422,6 +462,7 @@ def process_kickstart(ksfile):
with open(TMPDIR+"/ks.info", "a") as f:
f.write('parsed_kickstart="%s"\n' % processed_file)
log.info("finished parsing kickstart")
process_certificates(handler)
return processed_file, handler.output

if __name__ == '__main__':
Expand Down
3 changes: 3 additions & 0 deletions pyanaconda/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,6 @@ class DisplayModes(Enum):
CATEGORY_SOFTWARE = "SOFTWARE_INSTALLATION"
CATEGORY_BOOTLOADER = "BOOTLOADER_INSTALLATION"
CATEGORY_SYSTEM = "SYSTEM_CONFIGURATION"

# Installation phases
INSTALLATION_PHASE_PREINSTALL = "pre-install"
43 changes: 40 additions & 3 deletions pyanaconda/core/kickstart/specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,14 @@ class KickstartSpecification:
classes that represent them
sections - mapping of kickstart sections names to
classes that represent them
value is a class or a tuple (class, section_data_class)
where section_data_class is a value to be passed to dataObj
class argument (typically the corresponding sections_data class)
sections_data - mapping of kickstart sections data names to
classes that represent them
value is a class or a tuple (class, data_list_name)
where data_list_name is the name of the attribute holding
list of the section data objects of the class
addons - mapping of kickstart addons names to
classes that represent them
Expand All @@ -73,6 +79,25 @@ class NoKickstartSpecification(KickstartSpecification):
pass


class SectionDataListStrWrapper():
"""A wrapper for generating string from a list of kickstart data."""
def __init__(self, data_list, data):
"""Initializer.
:param data_list: list of section data objects
:param data: class required for the object to be included in the string
"""
self._data_list = data_list
self._data = data

def __str__(self):
retval = []
for data_obj in self._data_list:
if isinstance(data_obj, self._data):
retval.append(data_obj.__str__())
return "".join(retval)


class KickstartSpecificationHandler(KickstartHandler):
"""Handler defined by a kickstart specification."""

Expand All @@ -99,8 +124,16 @@ def __init__(self, specification):

def registerSectionData(self, name, data):
"""Register data used by a section."""
obj = data()
setattr(self, name, obj)
if isinstance(data, tuple):
# Multiple data objects (section instances) stored in a list
data, data_list_name = data
data_list = []
setattr(self, data_list_name, data_list)
obj = SectionDataListStrWrapper(data_list, data)
else:
# Single data object for all section instances
obj = data()
setattr(self, name, obj)
self._registerWriteOrder(obj)

def registerAddonData(self, name, data):
Expand All @@ -126,7 +159,11 @@ def __init__(self, handler, specification):
super().__init__(handler)

for section in specification.sections.values():
self.registerSection(section(handler))
if isinstance(section, tuple):
section_cls, data_obj = section
self.registerSection(section_cls(handler, dataObj=data_obj))
else:
self.registerSection(section(handler))

if specification.addons:
self.registerSection(AddonSection(handler))
Expand Down
20 changes: 20 additions & 0 deletions pyanaconda/installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
)
from pyanaconda.modules.common.constants.objects import (
BOOTLOADER,
CERTIFICATES,
FIREWALL,
SCRIPTS,
SNAPSHOT,
Expand Down Expand Up @@ -137,6 +138,20 @@ def _prepare_configuration(self, payload, ksdata):
configuration_queue.queue_started.connect(self._queue_started_cb)
configuration_queue.task_completed.connect(self._task_completed_cb)

# import certificates first
# they may be required for subscription, initramfs regenerating, ... ?
if is_module_available(SECURITY):
certificates_import = TaskQueue(
"Certificates import",
_("Importing certificates"),
CATEGORY_SYSTEM
)
certificates_proxy = SECURITY.get_proxy(CERTIFICATES)
certificates_import.append_dbus_tasks(SECURITY, [
certificates_proxy.InstallWithTask()
])
configuration_queue.append(certificates_import)

# add installation tasks for the Subscription DBus module
if is_module_available(SUBSCRIPTION):
# we only run the tasks if the Subscription module is available
Expand Down Expand Up @@ -440,6 +455,11 @@ def _prepare_installation(self, payload, ksdata):
fips_task = security_proxy.PreconfigureFIPSWithTask(payload.type)
pre_install.append_dbus_tasks(SECURITY, [fips_task])

# Import certificates so they are available for rpm scripts
certificates_proxy = SECURITY.get_proxy(CERTIFICATES)
certificates_task = certificates_proxy.PreInstallWithTask(payload.type)
pre_install.append_dbus_tasks(SECURITY, [certificates_task])

# Install the payload.
pre_install.append(Task(
"Find additional packages & run pre_install()",
Expand Down
2 changes: 2 additions & 0 deletions pyanaconda/kickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ def setupSections(self):
self.registerSection(NullSection(self.handler, sectionOpen="%traceback"))
self.registerSection(NullSection(self.handler, sectionOpen="%packages"))
self.registerSection(NullSection(self.handler, sectionOpen="%addon"))
self.registerSection(NullSection(self.handler, sectionOpen="%certificate"))


class AnacondaKSParser(KickstartParser):
Expand All @@ -258,6 +259,7 @@ def setupSections(self):
self.registerSection(OnErrorScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(UselessSection(self.handler, sectionOpen="%packages"))
self.registerSection(UselessSection(self.handler, sectionOpen="%addon"))
self.registerSection(UselessSection(self.handler, sectionOpen="%certificate"))


def preScriptPass(f):
Expand Down
3 changes: 2 additions & 1 deletion pyanaconda/modules/boss/kickstart_manager/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
)

VALID_SECTIONS_ANACONDA = [
"%pre", "%pre-install", "%post", "%onerror", "%traceback", "%packages", "%addon"
"%certificate", "%pre", "%pre-install", "%post", "%onerror", "%traceback", "%packages",
"%addon"
]


Expand Down
7 changes: 7 additions & 0 deletions pyanaconda/modules/common/constants/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
PARTITIONING_NAMESPACE,
RHSM_NAMESPACE,
RUNTIME_NAMESPACE,
SECURITY_NAMESPACE,
STORAGE_NAMESPACE,
)

Expand Down Expand Up @@ -155,3 +156,9 @@
namespace=RHSM_NAMESPACE,
basename="Syspurpose"
)

# Security objects
CERTIFICATES = DBusObjectIdentifier(
namespace=SECURITY_NAMESPACE,
basename="Certificates"
)
58 changes: 58 additions & 0 deletions pyanaconda/modules/common/structures/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#
# DBus structures for the storage data.
#
# Copyright (C) 2024 Red Hat, Inc. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dasbus.structure import DBusData
from dasbus.typing import * # pylint: disable=wildcard-import

__all__ = ["CertificateData"]


class CertificateData(DBusData):
"""Structure for the certificate data."""

def __init__(self):
self._filename = ""
self._cert = ""
self._dir = ""

@property
def filename(self) -> Str:
"""The certificate file name."""
return self._filename

@filename.setter
def filename(self, value: Str) -> None:
self._filename = value

@property
def cert(self) -> Str:
"""The certificate content."""
return self._cert

@cert.setter
def cert(self, value: Str) -> None:
self._cert = value

@property
def dir(self) -> Str:
"""The certificate directory."""
return self._dir

@dir.setter
def dir(self, value: Str) -> None:
self._dir = value
2 changes: 2 additions & 0 deletions pyanaconda/modules/security/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

SUBDIRS = certificates

pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME)
securitydir = $(pkgpyexecdir)/modules/security
dist_security_DATA = $(wildcard $(srcdir)/*.py)
Expand Down
21 changes: 21 additions & 0 deletions pyanaconda/modules/security/certificates/Makefile.am
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#
# Copyright (C) 2024 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME)
certificatesdir = $(pkgpyexecdir)/modules/security/certificates
dist_certificates_DATA = $(wildcard $(srcdir)/*.py)

MAINTAINERCLEANFILES = Makefile.in
20 changes: 20 additions & 0 deletions pyanaconda/modules/security/certificates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#
# Copyright (C) 2024 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
from pyanaconda.modules.security.certificates.certificates import CertificatesModule

__all__ = ["CertificatesModule"]
Loading

0 comments on commit 816346f

Please sign in to comment.