# MAIN KICKSTART # Author: Linuxfabrik GmbH, Zurich, Switzerland # Contact: info (at) linuxfabrik (dot) ch # https://www.linuxfabrik.ch/ # License: The Unlicense, see LICENSE file. # This kickstart file consists of two parts. # * The main part, which is quite minimal, # * and a dynamic part (/tmp/dynamic.ks) that gets written during the %pre script. # The %pre script starts by parsing some options from the kernel cmdline and # detecting the RHEL version. # It then fills /tmp/dynamic.ks depending on these variables. # The order of the commands is relatively compatible with the output of # https://access.redhat.com/labs/kickstartconfig/ # See the README for more details. # System language lang en_US.UTF-8 # Keyboard layouts keyboard us # System timezone timezone Europe/Zurich --utc # Shutdown after installation shutdown # Use text mode install and CDROM installation media text cdrom # This command is required when performing an unattended installation on a system with previously # initialized disks. zerombr # Automatically creates partitions required by your hardware platform, eg /boot/efi or biosboot reqpart # Network information network --hostname=localhost.localdomain # note: do not set any other network options. dhcp is the default anyway, and setting them here disregards the ip and nameserver settings on the kernel command line # network --bootproto=dhcp --device=link --activate --onboot=on # Do not configure the X Window System skipx # Disable the Setup Agent on first boot firstboot --disable # State of SELinux on the installed system selinux --enforcing # Firewalling should be done later on by the admin. Required for the cloud firewall --disable # System services services --disabled="kdump" --enabled="NetworkManager,sshd" # Disable kdump by default, frees up some memory %addon com_redhat_kdump --disable %end %include /tmp/dynamic.ks %pre --logfile /tmp/kickstart.install.pre.log --erroronfail # fedora doesn't have platform-python and rhel8 only has platform-python, so we'll have to # detect what we have PYTHON=$(ls /usr/libexec/platform-python /usr/bin/python3 2> /dev/null | head -n1) cat << EOT > /tmp/pre-script.py #!$PYTHON import os import re import stat import sys def isblockdevice(path): return os.path.exists(path) and stat.S_ISBLK(os.stat(path).st_mode) lfkeys = [ 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDuYw1auj8Lo6l5Ie8H7q419pKNjD3LSDZpFLNI0KehO chris.drescher@linuxfabrik.ch', 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDaWaDOEGqhZy1XD3a0IJKPvuB0wz2Yzc7Y8b2E91PPDSfi2wczUjZ9T2f8J7fw46i6O8dUFdsle8HePlVt1f6P+SPr84KIvte4sCXPGjHO7UlP+0biPl1FJbe+LU6akGVgAhc37CTn7h10COim5TpIdmPfn8A1y0Y8G3GNTVELSC4GG6rJLgme++OTkNlenH+L7KSobQE1v+MS4mRjrg+qitgPzBv1VgTWJff4d3vdEtb9zwVdzZzyXcdP5/nWH54iDaZawLsyvPXG2VDQq9bUn4CbZ+/ldrVg+yi8Y5RlSLFlbaX2XKnqf8mC3fpszXrmZ93d/idvCLDQ2ijV1FQzYLNg9nstV6VHCek1+g0u3oQ17CyRRCuxSRf3kDagO/+FMGwIliQTSX8rTx0epYzj4vUKA0nYIHXhwklUTG2PFNgJ1Nfxllqblij/PbFfoJCCp+st7/ewYYiclV6jrAg01/bqAvrdS8PW6JQpTIeuJRZhkrvCxgzDsuYE+EqTHxyBs7Uxu9D8QvgZEYiwuClRE92xPNmuEk/BDpX9s8mcXtamp57AUESRFWPFUZoDFuHRjHz56lXJ0UIfDR7XDZHumdQRt6jh7Sj0YGVN3Es9UIqka9dZRiXLdZ9q/C0IReZKtcKDSIn/xB1Kt2qAIRLpSG3IX8JmONOa1h/Gns1NKw== markus.frei@linuxfabrik.ch', 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM5DxWzuUlSfdHHE1c4oQ2cbC0TyjXkVCuNKBJvn7TvX navid.sassan@linuxfabrik.ch', ] lftype='minimal' lfdisk = None for guess in ['vda', 'sda', 'nvme0n1']: if isblockdevice('/dev/{}'.format(guess)): lfdisk = guess break print('Linuxfabrik Kickstart: Set kernel cmdline arguments') with open('/proc/cmdline', 'r') as file: cmdline = file.read() for item in cmdline.split(" "): if item.split("=")[0] == "lftype": lftype=item.split("=")[1].strip() elif item.split("=")[0] == "lfdisk": lfdisk=item.split("=")[1].strip() if not lfdisk: print('Linuxfabrik Kickstart: Lfdisk is not given and no disk to install to could be guessed. Aborting...') sys.exit(1) print('Linuxfabrik Kickstart: Lftype: {}, lfdisk: {}'.format(lftype, lfdisk)) print('Linuxfabrik Kickstart: Set bootloader') # * console=ttyS0,115200n8: # required for cloud images. # see https://docs.openstack.org/image-guide/openstack-images.html#ensure-image-writes-boot-log-to-console # * no_timer_check: # see https://opendev.org/openstack/diskimage-builder/commit/1ec93f43a8736171cb78382fd6184f03c001771b # * net.ifnames=0: # for cloud images, 'eth0' _is_ the predictable device name # * audit=1 audit_backlog_limit=8192: # required by CIS # * vsyscall=none: # due to https://www.stigviewer.com/stig/red_hat_enterprise_linux_8/2020-11-25/finding/V-230278 bootloader_cis = 'bootloader --location=mbr --boot-drive={} --append="audit=1 audit_backlog_limit=8192 vsyscall=none"'.format(lfdisk) bootloader_cloud = 'bootloader --location=mbr --boot-drive={} --append="console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 vsyscall=none"'.format(lfdisk) bootloader_cloud_cis = 'bootloader --location=mbr --boot-drive={} --append="console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 audit=1 audit_backlog_limit=8192 vsyscall=none"'.format(lfdisk) bootloader_minimal = 'bootloader --location=mbr --boot-drive={} --append="vsyscall=none"'.format(lfdisk) print('Linuxfabrik Kickstart: Set packages') packages = [ '@core', '-plymouth', # No need for plymouth. Also means anaconda won't put rhgb/quiet on kernel command line ] packages_cis = [] packages_cloud = [ 'cloud-init', 'cloud-utils-growpart', 'dhcp-client', 'qemu-guest-agent', 'rng-tools', 'tuned', '-aic94xx-firmware', '-alsa-firmware', '-alsa-lib', '-alsa-tools-firmware', '-biosdevname', '-iprutils', '-ivtv-firmware', '-iwl100-firmware', '-iwl1000-firmware', '-iwl105-firmware', '-iwl135-firmware', '-iwl2000-firmware', '-iwl2030-firmware', '-iwl3160-firmware', '-iwl3945-firmware', '-iwl4965-firmware', '-iwl5000-firmware', '-iwl5150-firmware', '-iwl6000-firmware', '-iwl6000g2a-firmware', '-iwl6000g2b-firmware', '-iwl6050-firmware', '-iwl7260-firmware', '-langpacks-*', '-langpacks-en', '-libertas-sd8686-firmware', '-libertas-sd8787-firmware', '-libertas-usb8388-firmware', ] packages_cloud_cis = packages_cloud packages_minimal = [] print('Linuxfabrik Kickstart: Set partitioning schema') part_cis = [ 'logvol / --fstype="xfs" --size=4096 --vgname=rl --name=root', 'logvol /home --fstype="xfs" --size=1024 --vgname=rl --name=home --fsoptions="nodev,nosuid"', 'logvol /tmp --fstype="xfs" --size=1024 --vgname=rl --name=tmp --fsoptions="nodev,noexec,nosuid"', 'logvol /var --fstype="xfs" --size=4096 --vgname=rl --name=var --fsoptions="nodev,nosuid"', 'logvol /var/log --fstype="xfs" --size=2048 --vgname=rl --name=var_log --fsoptions="nodev,noexec,nosuid"', 'logvol /var/log/audit --fstype="xfs" --size=512 --vgname=rl --name=var_log_audit --fsoptions="nodev,noexec,nosuid"', 'logvol /var/tmp --fstype="xfs" --size=1024 --vgname=rl --name=var_tmp --fsoptions="nodev,noexec,nosuid"', 'logvol swap --fstype="swap" --recommended --vgname=rl --name=swap', ] part_cloud = [ 'logvol / --fstype="xfs" --size=1024 --vgname=rl --name=root --grow', 'logvol swap --fstype="swap" --recommended --vgname=rl --name=swap', ] part_cloud_cis = part_cis part_minimal = part_cloud print('Linuxfabrik Kickstart: Define users') users_cis = [ { 'name': 'linuxfabrik', 'cmd': 'user --name=linuxfabrik --groups=wheel --password=password --plaintext', 'keys': lfkeys, }, { 'name': 'root', 'cmd': 'rootpw --plaintext --lock password', 'keys': [], }, ] users_cloud = [ { 'name': 'linuxfabrik', 'cmd': 'user --name=linuxfabrik --groups=wheel --lock', 'keys': [], }, { 'name': 'root', 'cmd': 'rootpw --plaintext --lock password', 'keys': [], }, ] users_cloud_cis = users_cloud users_minimal = users_cis print('Linuxfabrik Kickstart: Create post scripts') post_cis = ''' # allow password-less sudo cat > /etc/sudoers.d/linuxfabrik << EOF %linuxfabrik ALL=(ALL) NOPASSWD: ALL EOF ''' post_cloud = ''' # setup systemd to boot to the right runlevel echo "Setting default runlevel to multiuser text mode" rm -f /etc/systemd/system/default.target ln -s /lib/systemd/system/multi-user.target /etc/systemd/system/default.target # this is installed by default but we don't need it in virt # Commenting out the following for =1234504 # rpm works just fine for removing this, no idea why yum can't cope echo "Removing linux-firmware package" rpm --erase linux-firmware # Remove firewalld; was supposed to be optional in F18+, but is pulled in # in install/image building. echo "Removing firewalld" # FIXME! clean_requirements_on_remove is the default with yum, but may # not work when package was installed by Anaconda instead of command line. # Also -- check if this is still even needed with new anaconda -- disabled # firewall should _not_ pull in this package. # yum -C -y remove "firewalld*" --setopt="clean_requirements_on_remove=1" yum --cacheonly -y erase "firewalld*" # Another one needed at install time but not after that, and it pulls # in some unneeded deps (like, newt and slang) echo "Removing authconfig" yum --cacheonly -y erase authconfig echo "Removing avahi" yum --cacheonly -y remove avahi\\* echo "Installing cloud-init cloud-utils-growpart rng-tools tuned" # these tools always fail to install in the %packages section, so try this here yum -y install cloud-init cloud-utils-growpart rng-tools tuned # Since the scriptlets from yum install are running in chroot, they can't do a daemon-reload # ("Running in chroot, ignoring command 'daemon-reload'"), so a "systemctl enable --now" would # fail. "systemctl enable" is enough here. systemctl enable cloud-init systemctl enable cloud-init-local systemctl enable cloud-config systemctl enable cloud-final systemctl enable rngd echo "Getty fixes" # although we want console output going to the serial console, we don't # actually have the opportunity to login there. FIX. # we don't really need to auto-spawn _any_ gettys. sed --in-place 's/^#NAutoVTs=.*/NAutoVTs=0/' /etc/systemd/logind.conf echo "Network fixes" # initscripts don't like this file to be missing. # and https://bugzilla.redhat.com/show_bug.cgi?id=1204612 cat > /etc/sysconfig/network << EOF NETWORKING=yes NOZEROCONF=yes DEVTIMEOUT=10 EOF echo "Truncate /etc/resolv.conf" # Anaconda is writing an /etc/resolv.conf from the install environment. # The system should start out with an empty file, otherwise cloud-init # will try to use this information and may error: # https://bugs.launchpad.net/cloud-init/+bug/1670052 truncate --size=0 /etc/resolv.conf echo "Disable predictable network interface names" # For cloud images, 'eth0' _is_ the predictable device name, since # we don't want to be tied to specific virtual (!) hardware rm -f /etc/udev/rules.d/70* ln -s /dev/null /etc/udev/rules.d/80-net-name-slot.rules echo 'Set generic localhost names (/etc/hosts)' cat > /etc/hosts << EOF 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 EOF # Because memory is scarce resource in most cloud/virt environments, # and because this impedes forensics, we are differing from the Fedora # default of having /tmp on tmpfs. # However, there is no need to disable the mount, as we are already overwriting the options # for /tmp above. # Masking the mount now would lead to a missing /tmp. # echo "Disabling tmpfs for /tmp." # systemctl mask tmp.mount echo 'Set tuned profile to virtual-guest' echo 'virtual-guest' > /etc/tuned/active_profile echo 'Make sure firstboot does not start' echo 'RUN_FIRSTBOOT=NO' > /etc/sysconfig/firstboot echo 'Adjust sudoers for the linuxfabrik user' cat > /etc/sudoers.d/linuxfabrik << EOF %linuxfabrik ALL=(ALL) NOPASSWD: ALL EOF sed --in-place 's/name: cloud-user/name: linuxfabrik/g' /etc/cloud/cloud.cfg echo 'Cleaning up old yum repodata' yum clean all echo 'Increase DHCP client retry/timeouts' # change dhcp client retry/timeouts to resolve =6866 cat >> /etc/dhcp/dhclient.conf << EOF timeout 300; retry 60; EOF echo 'Adjust console output in /etc/default/grub and regenerate grub config' # If init script messages need to be seen on the serial console as well, it should be made # the primary by swapping the order of the console parameters: sed --in-place 's/console=ttyS0,115200n8 console=tty0/console=tty0 console=ttyS0,115200n8/' /etc/default/grub if [ -d /sys/firmware/efi/ ]; then grub_config_file=$(find /boot/efi -name grub.cfg) else grub_config_file=$(find /boot/ -name grub.cfg) fi if [ -z "$grub_config_file" ]; then echo 'Could not find a grub.cfg in /boot. Skipping grub2-mkconfig' else grub2-mkconfig --output=$grub_config_file fi echo "Removing random-seed so it's not the same in every image" rm -f /var/lib/systemd/random-seed echo 'Remove /etc/machine-id' # https://git.resf.org/sig_core/kickstarts/src/branch/r8/Rocky-8-GenericCloud-LVM.ks cat /dev/null > /etc/machine-id echo 'Remove various other log / cache files' rm -f /root/.wget-hsts rm -rf /root/anaconda-ks.cfg rm -rf /root/install.log rm -rf /root/install.log.syslog rm -rf /var/lib/yum/* rm -rf /var/log/anaconda* rm -rf /var/log/yum.log rm -rf /var/tmp/* find /var/log -type f -exec truncate --size=0 {} \\; export HISTSIZE=0 cat /dev/null > ~/.bash_history history -c echo 'Fixing SELinux contexts' touch /var/log/cron touch /var/log/boot.log mkdir -p /var/cache/yum # ignore return code because UEFI systems with vfat filesystems # that don't support selinux will give us errors like # "Could not set context for /boot/efi/EFI/BOOT/BOOTX64.EFI: Operation not supported" /usr/sbin/fixfiles -R -a restore || true ''' post_cloud_cis = post_cloud post_minimal = post_cis print('Linuxfabrik Kickstart: Act on cmdargs & discovery for {}'.format(lftype)) if lftype == 'cloud': bootloader = bootloader_cloud packages += packages_cloud part = part_cloud post = post_cloud users = users_cloud elif lftype == 'cloud-cis': bootloader = bootloader_cloud_cis packages += packages_cloud_cis part = part_cloud_cis post = post_cloud_cis users = users_cloud_cis elif lftype == 'cis': bootloader = bootloader_cis packages += packages_cis part = part_cis post = post_cis users = users_cis elif lftype == 'minimal': bootloader = bootloader_minimal packages += packages_minimal part = part_minimal post = post_minimal users = users_minimal # version specifics with open('/etc/redhat-release') as file: rhel_version = int(re.findall('[0-9]{1,2}', file.read())[0]) print('Linuxfabrik Kickstart: Detected OS version {}'.format(rhel_version)) if rhel_version == 7: try: packages.remove('dhcp-client') # does not exist on RHEL7 except ValueError: pass # means it is already removed print('Linuxfabrik Kickstart: Write dynamic.ks') dynamic = [] keypost = [] dynamic.append('# System users') for user in users: dynamic.append(user['cmd']) for key in user['keys']: if rhel_version == 7: # sshkey only exists for RHEL8+. instead manually add the keys if user['name'] == 'root': keypost.append('mkdir -m0700 /root/.ssh') keypost.append('echo "{}" >> /root/.ssh/authorized_keys'.format(key)) keypost.append('restorecon -F /root/.ssh/authorized_keys') else: keypost.append('mkdir -m0700 /home/{}/.ssh'.format(user['name'])) keypost.append('echo "{}" >> "/home/{}/.ssh/authorized_keys"'.format(key, user['name'])) keypost.append('chown -R {}: /home/{}/.ssh'.format(user['name'], user['name'])) keypost.append('restorecon -F "/home/{}/.ssh/authorized_keys"'.format(user['name'])) else: dynamic.append('sshkey --user={} "{}"'.format(user['name'], key)) dynamic.append('# Only touch $lfdisk. This setting is also respected by zerombr (confirmed on RHEL8 and 9)') dynamic.append('ignoredisk --only-use={}'.format(lfdisk)) dynamic.append('# System bootloader configuration') dynamic.append(bootloader) dynamic.append('# Do not remove any partitions, but initializes a disk (or disks) by creating a default disk label') dynamic.append('clearpart --all --drives={} --initlabel --disklabel gpt'.format(lfdisk)) dynamic.append('# Partitioning') dynamic.append('volgroup rl --pesize=4096 pv.0') dynamic.append('part pv.0 --fstype=lvmpv --ondisk={} --size=1 --grow'.format(lfdisk)) dynamic.append('part /boot --fstype=xfs --ondisk={} --size=1024 --asprimary'.format(lfdisk)) dynamic.extend(part) dynamic.append('%packages') dynamic.extend(packages) dynamic.append('%end') dynamic.append('%post --erroronfail') dynamic.append(post) dynamic.append('%end\n') # actually write to file with open('/tmp/dynamic.ks', 'w') as file: file.write('\n'.join(dynamic)) if rhel_version == 7: with open('/usr/share/anaconda/post-scripts/70-install-ssh-keys.ks', 'a') as file: file.write('%post\n') file.write('\n'.join(keypost)) file.write('%end\n') print('Linuxfabrik Kickstart: Wrote dynamic kickstart to /tmp/dynamic.ks') EOT $PYTHON /tmp/pre-script.py %end %post --nochroot root_mount=$(awk '/rl-root/{print $2}' /proc/mounts) echo "Copying /tmp/dynamic.ks to $root_mount/root/" cp /tmp/dynamic.ks $root_mount/root/ [ -f /usr/share/anaconda/post-scripts/70-install-ssh-keys.ks ] && cp /usr/share/anaconda/post-scripts/70-install-ssh-keys.ks $root_mount/root/ %end