File: //proc/self/root/proc/thread-self/root/usr/sbin/cl-post-jwt-update
#!/opt/cloudlinux/venv/bin/python3 -bb
import argparse
import subprocess, os
import pathlib
HOOKS_DIR = '/opt/cloudlinux/rhn_hooks/post.d'
JWT_TOKEN = '/etc/sysconfig/rhn/jwt.token'
# Invoke the edition watcher by ABSOLUTE path so it cannot be hijacked through
# the inherited PATH (this hook runs as root from the JWT-update flow), and run
# it under a fixed, trusted PATH as defense-in-depth for the watcher's own
# child-process lookups (mirrors the clnreg_ks PATH hardening).
_EDITION_WATCHER = '/usr/sbin/cloudlinux-edition-watcher'
_TRUSTED_PATH = '/usr/sbin:/usr/bin:/sbin:/bin'
def _is_ubuntu():
return "ID=ubuntu" in open("/etc/os-release").read()
def _update_edition_user_file():
"""
Save information about current edition in specific
location available for users
"""
if not os.path.exists('/usr/bin/cldetect') or not os.path.exists('/opt/cloudlinux/'):
return
p = subprocess.Popen(['/usr/bin/cldetect', '--detect-edition'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode == 0:
tmp = '/opt/cloudlinux/cl_edition.tmp'
# Refuse to follow a symlink planted at the temp path: drop any
# pre-existing entry, then create it with O_NOFOLLOW|O_EXCL (the real
# guard) so the root hook cannot be tricked into writing through a
# symlink and renaming it into the trusted cl_edition cache (CWE-59).
# cl_edition is intentionally world-readable public edition info (0644).
if os.path.lexists(tmp):
os.unlink(tmp)
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC, 0o644)
try:
# Set the mode on the FD (umask-independent and symlink-safe), not via
# a path-based chmod after the rename. cl_edition is public info (0644).
os.fchmod(fd, 0o644)
os.write(fd, stdout)
finally:
os.close(fd)
os.rename(tmp, '/opt/cloudlinux/cl_edition')
def _is_safe_to_execute(path):
"""
Returns True only if the given path is owned by root and not writable
by group or others, and is not a symlink.
"""
try:
st = os.lstat(path)
except OSError:
return False
if st.st_uid != 0:
return False
if st.st_mode & 0o022:
return False
if os.path.islink(path):
return False
return True
def _trigger_universal_hooks():
"""
Discovers executable files in directory and triggers them all together
Executable files are brought by different rpm packages, depending on which one
requires the action
"""
if not os.path.exists(HOOKS_DIR):
return
if not _is_safe_to_execute(HOOKS_DIR):
return
hooks = [os.path.join(HOOKS_DIR, f) for f in os.listdir(HOOKS_DIR)
if os.path.isfile(os.path.join(HOOKS_DIR, f))]
for hook in hooks:
if not _is_safe_to_execute(hook):
continue
process = subprocess.Popen([hook],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
def _create_symlink(target, linkpath, force=False):
"""
Creates symlink. If force is set to True,
it will overwrite existing symlink or file
"""
target = pathlib.Path(target)
linkpath = pathlib.Path(linkpath)
# if already the same, ignore
if linkpath.resolve() == target.resolve():
return
# remove existing file or link
if force and (linkpath.exists() or linkpath.is_symlink()):
linkpath.unlink()
linkpath.symlink_to(target)
def _run_edition_change_check():
# run interactively
subprocess.run([_EDITION_WATCHER, 'check'],
env=dict(os.environ, PATH=_TRUSTED_PATH))
def _create_repo_auth_symlinks():
"""
Create symlinks for els repos authentication
"""
_create_symlink(JWT_TOKEN, '/etc/dnf/vars/phpelstoken', force=True)
_create_symlink(JWT_TOKEN, '/etc/dnf/vars/altpythonelstoken', force=True)
_create_symlink(JWT_TOKEN, '/etc/dnf/vars/altrubyelstoken', force=True)
_create_symlink(JWT_TOKEN, '/etc/dnf/vars/altnodejselstoken', force=True)
def _create_repo_auth_conf():
"""
Create alt-repo.conf file for els repos authentication on Ubuntu
"""
token = pathlib.Path(JWT_TOKEN).read_text().strip()
if '\n' in token or '\r' in token:
raise ValueError("JWT token contains invalid characters")
conf = f"machine repo.alt.tuxcare.com\nlogin {token}"
auth_file = "/etc/apt/auth.conf.d/alt-repo.conf"
# Remove any pre-existing entry of ANY type at this fixed root path --
# symlink, FIFO, hardlink, or foreign-owned file -- then create fresh with
# O_NOFOLLOW|O_CLOEXEC. This refuses to follow a planted symlink, avoids an
# O_WRONLY open blocking forever on a planted FIFO, and gives the new
# credential a fresh inode rather than writing through an attacker's
# hardlink. os.unlink drops the directory entry (the link, not its target).
# No `.save` backup is kept: apt reads every file in auth.conf.d/, so a
# leftover backup would expose a stale JWT credential to apt.
if os.path.lexists(auth_file):
os.unlink(auth_file)
fd = os.open(auth_file,
os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW | os.O_CLOEXEC,
0o600)
try:
# Re-apply mode: O_CREAT only honours mode on file creation, so a
# pre-existing file with drifted permissions (e.g. 0o644) would not
# be self-healed without an explicit fchmod.
os.fchmod(fd, 0o600)
os.write(fd, conf.encode())
finally:
os.close(fd)
def main(args):
"""
You add your other actions here, but don't forget to handle errors.
"""
_update_edition_user_file()
if _is_ubuntu():
_create_repo_auth_conf()
else:
_create_repo_auth_symlinks()
if args.allow_transition:
_run_edition_change_check()
_trigger_universal_hooks()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--allow-transition', action='store_true', default=False)
main(args=parser.parse_args())