#!/bin/bash # Build-time provisioning of the VM exam host. Runs once, inside the VM, as the first-boot script of # the Ubuntu cloud image (cloud-init user-data), and powers the machine off when it is through; what # is on the disk then is the base disk every VM exam host starts from. # # The result: Ubuntu with containerd, kubeadm, kubelet and kubectl as apt packages (held), the # control-plane images pulled, Falco with the modern eBPF probe, the AppArmor tools, and `candidate` # with passwordless sudo. No cluster yet: every host makes its own on its first boot # (guest/firstboot.sh), under its own name. exec > >(tee -a /var/log/ksim-provision.log /dev/ttyS0) 2>&1 set -euxo pipefail trap 'echo KSIM-PROVISION-FAILED line $LINENO; sync; poweroff -f' ERR export DEBIAN_FRONTEND=noninteractive modprobe 9p; modprobe 9pnet_virtio mkdir -p /mnt/build && mount -t 9p -o trans=virtio,version=9p2000.L,ro build /mnt/build . /mnt/build/env # K8S_VERSION=1.35.8 K8S_MINOR=v1.35 # --- nothing that works behind the candidate's back systemctl disable --now unattended-upgrades.service apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true apt-get purge -y snapd unattended-upgrades >/dev/null 2>&1 || true # --- kernel settings Kubernetes wants, and the modules the host needs at every boot printf 'overlay\nbr_netfilter\n9p\n9pnet_virtio\n' > /etc/modules-load.d/ksim.conf modprobe overlay; modprobe br_netfilter printf 'net.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\n' > /etc/sysctl.d/99-kubernetes.conf sysctl --system >/dev/null swapoff -a; sed -i '/\sswap\s/d' /etc/fstab # --- packages: Kubernetes and Falco from their own repositories mkdir -p /etc/apt/keyrings curl -fsSL "https://pkgs.k8s.io/core:/stable:/$K8S_MINOR/deb/Release.key" | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/$K8S_MINOR/deb/ /" > /etc/apt/sources.list.d/kubernetes.list curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" > /etc/apt/sources.list.d/falcosecurity.list apt-get update apt-get install -y --no-install-recommends containerd apparmor-utils jq vim tmux bash-completion conntrack socat strace \ "kubelet=$K8S_VERSION-*" "kubeadm=$K8S_VERSION-*" "kubectl=$K8S_VERSION-*" apt-mark hold kubelet kubeadm kubectl curl -fsSL -o /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" && chmod 755 /usr/local/bin/yq # --- containerd: systemd cgroups, and Docker Hub through the simulator's cache mkdir -p /etc/containerd/certs.d/docker.io containerd config default > /etc/containerd/config.toml # (containerd 1.x writes the empty registry path as "", 2.x as '' - the first one is the CRI's) sed -i "s/SystemdCgroup = false/SystemdCgroup = true/; 0,/config_path = \\(\"\"\\|''\\)/s##config_path = \"\/etc\/containerd\/certs.d\"#" /etc/containerd/config.toml grep -q 'SystemdCgroup = true' /etc/containerd/config.toml && grep -q 'config_path = "/etc/containerd/certs.d"' /etc/containerd/config.toml install -m644 /mnt/build/hosts.toml /etc/containerd/certs.d/docker.io/hosts.toml systemctl restart containerd printf 'runtime-endpoint: unix:///run/containerd/containerd.sock\nimage-endpoint: unix:///run/containerd/containerd.sock\n' > /etc/crictl.yaml kubeadm config images pull --kubernetes-version "v$K8S_VERSION" for i in busybox:1.36 nginx:1.27; do crictl pull docker.io/library/$i || true; done # --- Falco, with the modern eBPF probe (no kernel module, no compiler) FALCO_FRONTEND=noninteractive FALCO_DRIVER_CHOICE=modern_ebpf apt-get install -y --no-install-recommends falco systemctl enable falco-modern-bpf.service falco --version | head -1 # --- the candidate, as on an exam host userdel -r ubuntu 2>/dev/null || true useradd -m -s /bin/bash -u 1000 candidate echo 'candidate ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/candidate && chmod 440 /etc/sudoers.d/candidate install -m644 /mnt/build/exam.bashrc /etc/exam.bashrc && echo '. /etc/exam.bashrc' >> /etc/bash.bashrc for h in /etc/skel /root /home/candidate; do install -m644 /mnt/build/vimrc $h/.vimrc; touch $h/.hushlogin; done chown candidate:candidate /home/candidate/.vimrc /home/candidate/.hushlogin printf 'PasswordAuthentication no\nPermitRootLogin prohibit-password\nUseDNS no\nPrintMotd no\n' > /etc/ssh/sshd_config.d/00-exam.conf chmod -x /etc/update-motd.d/* 2>/dev/null || true # --- what every boot does: name, keys, /opt/course, and on the first one the cluster install -m755 /mnt/build/firstboot.sh /usr/local/sbin/ksim-firstboot cat > /etc/systemd/system/ksim-firstboot.service <<'EOF' [Unit] Description=ksim exam host: name, keys, /opt/course and the cluster After=network-online.target containerd.service Wants=network-online.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/sbin/ksim-firstboot TimeoutStartSec=600 [Install] WantedBy=multi-user.target EOF systemctl enable ksim-firstboot.service mkdir -p /opt/course # --- the image is done: cloud-init must not run again, and the disk should be small touch /etc/cloud/cloud-init.disabled apt-get clean umount /mnt/build || true fstrim -av || true echo KSIM-PROVISION-OK sync poweroff