123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- #!/bin/bash -e
- #----------
- # Interactive installation steps for debian bookworm from GRML using debootstrap
- # Design decisions
- # - Fokus on a small and simple setup
- # - Use systemd whereever possible (network, ntp, cron, journald logging)
- # - Minimal number of packages
- # - One disk
- # - Schema VM: single ext4 partition, cloud kernel, grub-pc
- # - Schema Physical Server partitions for efi, os and luks data container
- # - Random root and admin user password generation
- # - Swap-file as safety net
- # - SSH on port 50101 limited to the admin user
- # Usage
- # Boot grml or run from an existing debian/ubuntu installation
- # grml: passwd root; grml-network; Start ssh
- # git clone https://git.in-ulm.de/ulpeters/bootstrap.git
- # cp config.sh.template config.sh # copy template
- # config-get-netconf-eth0.sh # get running grml network config
- # vi config.sh # update installation variables
- # bootstrap-bookworm.sh install # start installation
- # !! Note down the admin passwords and reboot
- # sudo /installer/bootstrap-bookworm.sh postinstall # run postinstall in the new system
- # Default variables
- mnt="/mnt/root" # mountpoint for the new root filesystem
- hostname="somehost.example.com"
- kernelPkg="linux-image-cloud-amd64" # alternative: linux-image-amd64
- partition="mbr-single" # mbr-single or efi-crypt
- disk="/dev/vda" # find with: lsblk --list
- disk0=$disk"p1" # efi partion, only relevant if partion="efi-crypt"
- disk1=$disk"1" # "p1" for nbd or p2 for efi-crypt
- netDev="eth0" # find with: ip link
- netAddress="203.0.113.66/24" # "" blank for dhcp on e*
- netGateway="203.0.113.1" # likely first IP within the network
- netBroadcast="203.0.113.255" # last IP within the network
- netDNS1="192.0.2.10"
- netDNS2="198.51.100.10"
- netNTP="pool.ntp.org"
- pwdAdmin="" # "" blank for auto-generation
- pwdRoot="" # "" blank for auto-generation
- debootstrap="native" # docker, to run in docker container
- extraPackages="qemu-guest-agent" # additional packages to install, e.g. cryptsetup
- # Overwrite default variables from config file
- [ -f ./config.sh ] && source config.sh
- # Setup network in grml based on config.sh
- grmlnetwork(){
- ip link show # list interfaces
- ip addr add $netAddress dev $netDev
- ip link set $netDev up
- ip route add default via $netGateway
- echo nameserver $netDNS1 >> /etc/resolv.conf
- echo nameserver $netDNS2 >> /etc/resolv.conf
- }
- install(){
- #----------
- # Prepare disk
- echo "🗑️ Wipe existing partition table to supress warnings from parted"
- dd if=/dev/zero of=$disk bs=512 count=34
- # Parition disks -- pkg: parted
- # Prepare partition tables and partitions
- # -parted --script does not accept blanks in partition names
- # Prepare disks with a single mbr partition
- if [ "$partition" = "mbr-single" ]
- then
- echo "📐 Prepare mbr partition table with a single partition"
- parted $disk --script \
- mklabel msdos \
- mkpart primary ext4 512M 100% toggle 1 boot \
- print
- fi
- # Prepare disks with gpt/efi
- if [ "$partition" = "efi-crypt" ]
- then
- echo "📐 Prepare gpt partition table with following layout:"
- echo "- 301 MB partition for EFI --> p1"
- echo "- 50 GB root partition for the OS (includes /boot) --> p2"
- echo "- Remaining disk left to create a luks container --> p3"
- parted $disk --script \
- mklabel gpt \
- mkpart EFI_system_partition fat32 1MiB 301MiB \
- set 1 esp on \
- set 1 boot on \
- align-check optimal 1 \
- mkpart Linux_system_parition ext4 301MiB 50GiB \
- align-check optimal 2 \
- mkpart Data_partion 50GiB 100% \
- align-check optimal 3 \
- unit MiB \
- print
-
- # Inform OS about partition table change
- partprobe $disk && sleep 1
-
- # Format EFI partition -- pkg: dosfstools e2fsprogs
- mkfs.fat -v -F 32 -n EFIBOOT $disk0 && fsck $disk0
- fi
- # Format OS disk -- pkg: e2fsprogs
- mkfs.ext4 -v -F $disk1 && fsck $disk1
- # Prepare mount points and mount
- mkdir -p $mnt
- mount $disk1 $mnt
- # Create swapfile
- swapfile=$mnt/swapfile
- dd if=/dev/zero of=$swapfile bs=1M count=1024 status=progress # create 1GB file
- chmod 600 $swapfile # restric permissions
- mkswap $swapfile # format file
- #----------
- # Bootstrap -- pkg: debootstrap
- # Remark: Debootstrap does not install recommands!!
- case "$debootstrap" in
- native) debootstrap --variant=minbase --arch=amd64 bookworm $mnt http://ftp2.de.debian.org/debian/
- ;;
- # Alternatively bootstrap from a docker container, e.g. if no recent debootstrap is available
- docker) docker run -it --rm --cap-add=SYS_CHROOT --name debootstrap \
- -v $mnt:$mnt -e mnt=$mnt debian /bin/sh -c \
- "apt-get update && apt-get install --yes debootstrap \
- && debootstrap --variant=minbase --arch=amd64 bookworm \
- $mnt http://ftp2.de.debian.org/debian/"
- ;;
- esac
- # Configure disk mounts
- # Or get UUID from blkid...
- [ "$partition" = "efi-crypt" ] && \
- echo "$disk0 /boot/efi/ vfat rw 0 0" >> $mnt/etc/fstab
- echo "$disk1 / ext4 rw 0 0" >> $mnt/etc/fstab
- echo "/swapfile none swap defaults 0 0" >> $mnt/etc/fstab
- # Configure sources.list
- cat >$mnt/etc/apt/sources.list <<EOL
- deb http://deb.debian.org/debian bookworm main non-free-firmware
- # deb-src http://deb.debian.org/debian bookworm main non-free-firmware
- deb http://deb.debian.org/debian-security/ bookworm-security main non-free-firmware
- # deb-src http://deb.debian.org/debian-security/ bookworm-security main non-free-firmware
- deb http://deb.debian.org/debian bookworm-updates main non-free-firmware
- # deb-src http://deb.debian.org/debian bookworm-updates main non-free-firmware
- EOL
- # Configure hostname
- echo "127.0.0.1 $hostname" >> $mnt/etc/hosts
- echo "$hostname" > $mnt/etc/hostname
- #----------
- # Prepare chroot
- mount -o bind /dev $mnt/dev
- mount -o bind /dev/pts $mnt/dev/pts
- mount -t sysfs /sys $mnt/sys
- mount -t proc /proc $mnt/proc
- cp /proc/mounts $mnt/etc/mtab
- cp /etc/resolv.conf $mnt/etc/resolv.conf
- mkdir -p $mnt/installer
- cp $(dirname `realpath $0`)/*.sh $mnt/installer
- # Run script in chroot
- chroot $mnt /bin/bash /installer/bootstrap-bookworm.sh install2
- # Install bootloader
- $0 bootloader
- }
- #----------
- # Function executed within chroot
- install2(){
- source /installer/config.sh
- # Select grub version based on partition table
- case "$partition" in
- mbr-single) grubPkg="grub-pc" ;;
- efi-crypt) grubPkg="grub-efi" ;;
- esac
- # Install basic system
- apt-get update
- export DEBIAN_FRONTEND=noninteractive
- apt-get install --yes \
- apt-utils dialog file msmtp-mta \
- systemd-sysv polkitd locales tzdata haveged \
- $kernelPkg $grubPkg \
- iproute2 netbase \
- ssh sudo molly-guard \
- less vim-tiny bash-completion pwgen lsof \
- dnsutils iputils-ping pciutils curl wget bzip2 \
- $extraPackages
- # Upgrade and clean up
- apt-get upgrade --yes
- apt-get autoremove --yes
- apt-get clean --yes
- # Setup users and passwords
- [ -z $pwdAdmin ] && pwdAdmin=`pwgen --capitalize --numerals --ambiguous 12 1`
- useradd admin --create-home --shell /bin/bash
- echo "admin:$pwdAdmin" | chpasswd
- usermod -a -G sudo admin
- echo -e "\e[1;33;4;44m🔐 Password for the user admin: $pwdAdmin\e[0m"
- pass=`pwgen --capitalize --numerals --ambiguous 12 1`
- [ -z $pwdRoot ] && pwdRoot=`pwgen --capitalize --numerals --ambiguous 12 1`
- echo "root:$pwdRoot" | chpasswd
- echo -e "\e[1;33;4;44m🔐 Password for the user root: $pwdRoot\e[0m"
- # Harden SSHD
- sed -i -e 's/#Port 22/Port 50101/g' /etc/ssh/sshd_config
- sed -i -e 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' /etc/ssh/sshd_config
- # https://infosec.mozilla.org/guidelines/openssh.html
- # Allow admin to sudo without password
- echo AllowUsers admin >> /etc/ssh/sshd_config
- echo "admin ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/admin
- ## Configure network using systemd
- if [ -z $netAddress ]
- then
- ## Network OPTION 1 - DHCP
- cat >/etc/systemd/network/20-wired.network <<EOL
- [Match]
- Name=e*
- [Network]
- DHCP=ipv4
- IPv6PrivacyExtensions=false
- IPv6AcceptRA=false
- NTP=$netNTP
- EOL
- else
- ## Network OPTION 2 - static
- cat >/etc/systemd/network/20-wired.network <<EOL
- [Match]
- Name=$netDev
- [Network]
- Address=$netAddress
- Gateway=$netGateway
- Broadcast=$netBroadcast
- DNS=$netDNS1
- DNS=$netDNS2
- NTP=$netNTP
- EOL
- fi
- ## Setup systemd resolver
- apt-get install --yes systemd-resolved
- echo -e "\n# Disable local name resolution" >> /etc/systemd/resolved.conf
- echo "LLMNR=no" >> /etc/systemd/resolved.conf
- echo "MulticastDNS=no" >> /etc/systemd/resolved.conf
- systemctl enable systemd-networkd
- systemctl enable systemd-resolved
- # Limit journald logging to 1 month, 1 GB in total and split files per week
- mkdir -p /etc/systemd/journald.conf.d/
- cat >/etc/systemd/journald.conf.d/retention.conf <<EOL
- [Journal]
- MaxRetentionSec=1month
- SystemMaxUse=1G
- MaxFileSec=1week
- EOL
- # Show errors in motd
- rm /etc/motd
- cat >/etc/update-motd.d/15-boot-errors<<EOL
- #!/bin/sh
- echo
- journalctl --boot --priority=3 --no-pager
- EOL
- chmod 755 /etc/update-motd.d/15-boot-errors
- # Setup keyboard layout
- cat >/etc/default/keyboard <<EOL
- XKBMODEL="pc105"
- XKBLAYOUT="de"
- XKBVARIANT="nodeadkeys"
- XKBOPTIONS=""
- BACKSPACE="guess"
- EOL
- # Leave chroot
- exit
- }
- bootloader(){
- # Install grub
- echo "$partition partition"
- case "$partition" in
- mbr-single)
- chroot $mnt /bin/bash -c "grub-install $disk && update-grub"
- ;;
- efi-crypt)
- chroot $mnt /bin/bash -c \
- "mkdir -p /boot/efi && mount /boot/efi \
- && grub-install --target=x86_64-efi --efi-directory=/boot/efi \
- --bootloader-id=grub && update-grub \
- && umount /boot/efi"
- ;;
- esac
- }
- unmount(){
- # Unmount if mounted
- # Remark: Function called unmount and not umount to avoid collision with the command
- ! mountpoint -q $mnt/proc || umount -v $mnt/proc
- ! mountpoint -q $mnt/sys || umount -v $mnt/sys
- ! mountpoint -q $mnt/dev/pts || umount -v $mnt/dev/pts
- ! mountpoint -q $mnt/dev || umount -v $mnt/dev
- ! mountpoint -q $mnt || umount -v $mnt
- # Delete mount-point if empty and not mounted
- [ -d $mnt ] && [ -z "$(ls -A $mnt)" ] && ! mountpoint -q $mnt \
- && rm -R $mnt && echo "🗑️ mountpoint $mnt deleted"
- }
- postinstall(){
- ####----REBOOT into the new system, so we'll have dbus running
- localectl set-locale LANG=de_DE.UTF-8 # Default for LC_* variables not set.
- localectl set-locale LC_MESSAGES=en_US.UTF-8 # System messages.
- #localectl set-locale LC_RESPONSE=en_US.UTF-8 # How responses (such as Yes and No) appear
- update-locale
- timedatectl set-timezone Europe/Berlin
- }
- # Switch to functions...
- case $1 in
- grmlnetwork)
- echo Setup network in grml
- grmlnetwork
- ;;
- install)
- echo "Stage 1: Start installation"
- install
- ;;
- install2)
- echo "Stage 2: Start installation in chroot"
- install2
- ;;
- bootloader)
- echo "Stage 3: Install bootloader and unmount chroot"
- bootloader
- unmount
- echo "👍 We're done and can reboot now"
- ;;
- postinstall)
- echo "Stage 4: Start post-installation in live system"
- postinstall
- ;;
- unmount)
- echo "Unmount chroot, e.g. in case installation fails"
- unmount
- ;;
- *)
- echo "Valid functions are: grmlnetwork, install, postinstall and unmount" >&2
- ;;
- esac
|