Skip to content

Commit 45e6cf4

Browse files
committed
Fix certificate symlink handling
In response to the identified flaws in the originally delivered fix, for feature enabling http repositories, this commit addresses the following issues: 1. Previously, files installed via RPMs that were originally symlinks were being switched to standard files. This issue has been resolved by preserving symlinks within the /etc/pki directory. Any symlink pointing to a file within the /etc/pki directory (whether present in the source system or installed by a package in the container) will be present in the container, ensuring changes to certificates are properly propagated. 2. Lists of trusted CAs were not being updated, as the update-ca-trust call was missing inside the container. This commit now includes the necessary update-ca-trust call. The solution specification has been modified as follows: - Certificate _files_ in /etc/pki (excluding symlinks) are copied to the container as in the original solution. - Files installed by packages within the container are preserved and given higher priority. - Handling of symlinks is enhanced, ensuring that symlinks within the /etc/pki directory are preserved, while any symlink pointing outside the /etc/pki directory will be copied as a file. - Certificates are updated using `update-ca-trust`.
1 parent 17c88d9 commit 45e6cf4

File tree

2 files changed

+332
-16
lines changed

2 files changed

+332
-16
lines changed

repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,80 @@ def _get_files_owned_by_rpms(context, dirpath, pkgs=None, recursive=False):
345345
return files_owned_by_rpms
346346

347347

348+
def _copy_decouple(srcdir, dstdir):
349+
"""
350+
Copy `srcdir` to `dstdir` while decoupling symlinks.
351+
352+
What we mean by decoupling the `srcdir` is that any symlinks pointing
353+
outside the directory will be copied as regular files. This means that the
354+
directory will become independent from its surroundings with respect to
355+
symlinks. Any symlink (or symlink chains) within the directory will be
356+
preserved.
357+
358+
"""
359+
360+
for root, dummy_dirs, files in os.walk(srcdir):
361+
for filename in files:
362+
relpath = os.path.relpath(root, srcdir)
363+
source_filepath = os.path.join(root, filename)
364+
target_filepath = os.path.join(dstdir, relpath, filename)
365+
366+
# Skip and report broken symlinks
367+
if not os.path.exists(source_filepath):
368+
api.current_logger().warning(
369+
'File {} is a broken symlink! Will not copy the file.'.format(source_filepath))
370+
continue
371+
372+
# Copy symlinks to the target userspace
373+
source_is_symlink = os.path.islink(source_filepath)
374+
pointee = None
375+
if source_is_symlink:
376+
pointee = os.readlink(source_filepath)
377+
378+
# If source file is a symlink within `srcdir` then preserve it,
379+
# otherwise resolve and copy it as a file it points to
380+
if pointee is not None and not pointee.startswith(srcdir):
381+
# Follow the path until we hit a file or get back to /etc/pki
382+
while not pointee.startswith(srcdir) and os.path.islink(pointee):
383+
pointee = os.readlink(pointee)
384+
385+
# Pointee points to a _regular file_ outside /etc/pki so we
386+
# copy it instead
387+
if not pointee.startswith(srcdir) and not os.path.islink(pointee):
388+
source_is_symlink = False
389+
source_filepath = pointee
390+
else:
391+
# pointee points back to /etc/pki
392+
pass
393+
394+
# Ensure parent directory exists
395+
parent_dir = os.path.dirname(target_filepath)
396+
# Note: This is secure because we know that parent_dir is located
397+
# inside of `$target_userspace/etc/pki` which is a directory that
398+
# is not writable by unprivileged users. If this function is used
399+
# elsewhere we may need to be more careful before running `mkdir -p`.
400+
run(['mkdir', '-p', parent_dir])
401+
402+
if source_is_symlink:
403+
# Preserve the owner and permissions of the original symlink
404+
run(['ln', '-s', pointee, target_filepath])
405+
run(['chmod', '--reference={}'.format(source_filepath), target_filepath])
406+
continue
407+
408+
run(['cp', '-a', source_filepath, target_filepath])
409+
410+
348411
def _copy_certificates(context, target_userspace):
349412
"""
350-
Copy the needed certificates into the container, but preserve original ones
413+
Copy certificates from source system into the container, but preserve
414+
original ones
351415
352416
Some certificates are already installed in the container and those are
353417
default certificates for the target OS, so we preserve these.
418+
419+
We respect the symlink hierarchy of the source system within the /etc/pki
420+
folder. Dangling symlinks will be ignored.
421+
354422
"""
355423

356424
target_pki = os.path.join(target_userspace, 'etc', 'pki')
@@ -360,36 +428,56 @@ def _copy_certificates(context, target_userspace):
360428
files_owned_by_rpms = _get_files_owned_by_rpms(target_context, '/etc/pki', recursive=True)
361429
api.current_logger().debug('Files owned by rpms: {}'.format(' '.join(files_owned_by_rpms)))
362430

431+
# Backup container /etc/pki
363432
run(['mv', target_pki, backup_pki])
364-
context.copytree_from('/etc/pki', target_pki)
365433

434+
# Copy source /etc/pki to the container
435+
_copy_decouple('/etc/pki', target_pki)
436+
437+
# Assertion: after running _copy_decouple(), no broken symlinks exist in /etc/pki in the container
438+
# So any broken symlinks created will be by the installed packages.
439+
440+
# Recover installed packages as they always get precedence
366441
for filepath in files_owned_by_rpms:
367442
src_path = os.path.join(backup_pki, filepath)
368443
dst_path = os.path.join(target_pki, filepath)
369444

370445
# Resolve and skip any broken symlinks
371446
is_broken_symlink = False
372-
while os.path.islink(src_path):
373-
# The symlink points to a path relative to the target userspace so
374-
# we need to readjust it
375-
next_path = os.path.join(target_userspace, os.readlink(src_path)[1:])
376-
if not os.path.exists(next_path):
377-
is_broken_symlink = True
378-
379-
# The path original path of the broken symlink in the container
380-
report_path = os.path.join(target_pki, os.path.relpath(src_path, backup_pki))
381-
api.current_logger().warning('File {} is a broken symlink!'.format(report_path))
382-
break
383-
384-
src_path = next_path
447+
pointee = None
448+
if os.path.islink(src_path):
449+
pointee = os.path.join(target_userspace, os.readlink(src_path)[1:])
450+
451+
seen = set()
452+
while os.path.islink(pointee):
453+
# The symlink points to a path relative to the target userspace so
454+
# we need to readjust it
455+
pointee = os.path.join(target_userspace, os.readlink(src_path)[1:])
456+
if not os.path.exists(pointee) or pointee in seen:
457+
is_broken_symlink = True
458+
459+
# The path original path of the broken symlink in the container
460+
report_path = os.path.join(target_pki, os.path.relpath(src_path, backup_pki))
461+
api.current_logger().warning(
462+
'File {} is a broken symlink! Will not copy!'.format(report_path))
463+
break
464+
465+
seen.add(pointee)
385466

386467
if is_broken_symlink:
387468
continue
388469

470+
# Cleanup conflicting files
389471
run(['rm', '-rf', dst_path])
472+
473+
# Ensure destination exists
390474
parent_dir = os.path.dirname(dst_path)
391475
run(['mkdir', '-p', parent_dir])
392-
run(['cp', '-a', src_path, dst_path])
476+
477+
# Copy the new file
478+
run(['cp', '-R', '--preserve=all', src_path, dst_path])
479+
480+
run(['rm', '-rf', backup_pki])
393481

394482

395483
def _prep_repository_access(context, target_userspace):
@@ -401,6 +489,10 @@ def _prep_repository_access(context, target_userspace):
401489
backup_yum_repos_d = os.path.join(target_etc, 'yum.repos.d.backup')
402490

403491
_copy_certificates(context, target_userspace)
492+
# NOTE(dkubek): context.call(['update-ca-trust']) seems to not be working.
493+
# I am not really sure why. The changes to files are not
494+
# being written to disk.
495+
run(["chroot", target_userspace, "/bin/bash", "-c", "su - -c update-ca-trust"])
404496

405497
if not rhsm.skip_rhsm():
406498
run(['rm', '-rf', os.path.join(target_etc, 'rhsm')])

0 commit comments

Comments
 (0)