Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

edns: implement %certificate kickstart feature #6045

Merged
merged 16 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand it correctly, that there is a period of time, where certificates are not available in the installation environment - right after initramfs switchroot and before this import service is executed?
Is there an option to import the certificates to the stage2 environment even before switchroot to stage2 to ensure the provided certificates are available throughout the whole boot process?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea, I'll think about pushing from initramfs instead of pulling from root.
For just moving files I guess it should be fine, one possible problem is that for other potential actions, like running import tool as in rvykydal@51c67c5#diff-572b90e7fb275a5450a384189ddd4651c02f9ab6059576366cdc25e5cdbec5f9R9
this might be a bit more tricky (running in /sysroot chroot?).
But to be honest I don't know if it is usual / reasonable thing to do so I'll consult dracut people.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create a follow-up issue for this. I think it would be better to handle it separately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


[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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm missing a test for this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I'll add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"""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
Loading