diff --git a/README.md b/README.md index ff87050..58023a6 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# Deploy Alpine Linux on a headless system +# Bootstrap Alpine Linux on a headless system [Alpine Linux documentation](https://docs.alpinelinux.org/user-handbook/0.1a/Installing/setup_alpine.html) assumes **initial setup** is carried-out on a system with a keyboard & display to interract with.\ However, in many cases one might want to deploy a headless system that is only available through a network connection (ethernet, wifi or as USB ethernet gadget). -This repo provides an **overlay file** to initially boot such headless system (leveraging Alpine distro's `initramfs` feature): it starts a basic ssh server to log-into from another Computer, in order to then perform actual system setup. +This repo provides an **overlay file** to initially bootstrap[^1] a headless system (leveraging Alpine distro's `initramfs` feature): it starts a ssh server to log-into from another Computer, so that actual install on fresh system (or rescue on existing disk-based system) can then be performed remotely. -## Install procedure: +## Setup procedure: Please follow [Alpine Linux Wiki](https://wiki.alpinelinux.org/wiki/Installation#Installation_Overview) to download & create installation media for the target platform.\ Tools provided here can be used on any plaform for any install modes (diskless, data disk, system disk). -Just add [**headless.apkovl.tar.gz**](https://github.com/macmpi/alpine-linux-headless-bootstrap/raw/main/headless.apkovl.tar.gz)[^1] overlay file at the root of Alpine Linux boot media (or onto any custom side-media) and boot-up the system.\ +Just add [**headless.apkovl.tar.gz**](https://github.com/macmpi/alpine-linux-headless-bootstrap/raw/main/headless.apkovl.tar.gz)[^2] overlay file at the root of Alpine Linux boot media (or onto any custom side-media) and boot-up the system.\ With default network interface definitions (and SSID/pass file if using wifi), system can then be remotely accessed with: `ssh root@`\ (system IP address may be determined with any IP scanning tools such as `nmap`). @@ -19,21 +19,23 @@ From there, actual system install can be performed as usual with `setup-alpine` Extra files may be added next to `headless.apkovl.tar.gz` to customise boostrapping configuration (check sample files): -- `wpa_supplicant.conf`[^2] (*mandatory for wifi usecase*): define wifi SSID & password. -- `interfaces`[^2] (*optional*): define network interfaces at will, if defaults DCHP-based are not suitable. -- `ssh_host_*_key*` (*optional*): provide custom ssh keys to be injected (may be stored), instead of using bundled ones[^1] (not stored). Providing an empty key file will trigger new keys generation (ssh server may take longer to start). -- `unattended.sh`[^2] (*optional*): create custom automated deployment script to further tune & extend actual setup (backgrounded). +- `wpa_supplicant.conf`[^3] (*mandatory for wifi usecase*): define wifi SSID & password. +- `interfaces`[^3] (*optional*): define network interfaces at will, if defaults DCHP-based are not suitable. +- `ssh_host_*_key*` (*optional*): provide custom ssh keys to be injected (may be stored), instead of using bundled ones[^2] (not stored). Providing an empty key file will trigger new keys generation (ssh server may take longer to start). +- `unattended.sh`[^3] (*optional*): create custom automated deployment script to further tune & extend actual setup (backgrounded). -**Goody:** seamless USB bootstrapping for PiZero devices (or similar supporting USB ethernet gadget networking):\ -Just add `dtoverlay=dwc2` in `usercfg.txt` (or `config.txt`), and plug USB cable into Computer port.\ +**Goody:** seamless USB-ethernet gadget boostrapping (PiZero for instance):\ +On supporting Pi devices, just add `dtoverlay=dwc2` in `usercfg.txt` (or `config.txt`), and plug USB cable into Computer port.\ With Computer set-up to share networking with USB interface as 10.42.0.1 gateway, one can log into device from Computer with: `ssh root@10.42.0.2` Main execution steps are logged in `/var/log/messages`. -[^1]: About bundled ssh keys: as this package is essentially intended to **quickly bootstrap** system in order to configure it, it purposely embeds [some ssh keys](https://github.com/macmpi/alpine-linux-headless-bootstrap/tree/main/overlay/etc/ssh) so that bootstrapping is as fast as possible. Those (temporary) keys are moved in RAM /tmp, so they will **not be saved/reused** once permanent configuration is set (with or without ssh server voluntarily installed in permanent setup). +[^1]: Initial boot fully preserves system's original state (config files & installed packages): a fresh system will therefore come-up as unconfigured. -[^2]: These files are linux text files: Windows/macOS users need to use text editors supporting linux text line-ending (such as [notepad++](https://notepad-plus-plus.org/), BBEdit or any similar). +[^2]: About bundled ssh keys: this overlay is meant to **quickly bootstrap** system in order to then proceed with proper install; therefore it purposely embeds [some ssh keys](https://github.com/macmpi/alpine-linux-headless-bootstrap/tree/main/overlay/etc/ssh) so that bootstrapping is as fast as possible. Those temporary keys are moved in RAM /tmp: they will **not be stored/reused** once actual system install is performed (whether or not ssh server is installed in final setup). + +[^3]: These files are linux text files: Windows/macOS users need to use text editors supporting linux text line-ending (such as [notepad++](https://notepad-plus-plus.org/), BBEdit or any similar). ## How to customize ? diff --git a/headless.apkovl.tar.gz b/headless.apkovl.tar.gz index 97fe05d..aaac53c 100644 Binary files a/headless.apkovl.tar.gz and b/headless.apkovl.tar.gz differ diff --git a/overlay/etc/local.d/headless.start b/overlay/etc/local.d/headless.start index 8eec0c6..0ad1ec6 100755 --- a/overlay/etc/local.d/headless.start +++ b/overlay/etc/local.d/headless.start @@ -3,7 +3,44 @@ # Copyright 2022 - 2023, macmpi # SPDX-License-Identifier: MIT -VERSION="0.7" +VERSION="0.8" + + +_apk() { + local cmd="$1" + local pkg="$2" + + case $cmd in + add) # install only if not already present + if ! apk info | grep -wq "${pkg}"; then + apk add "$pkg" && printf "${pkg} " >> /tmp/.trash/installed + fi + ;; + del) # delete only if previously installed + if grep -wq "$pkg" /tmp/.trash/installed; then + apk del "$pkg" && sed -i 's/\b'"${pkg}"'\b//' /tmp/.trash/installed + fi + ;; + *) + echo "only add/del: wrong usage"; exit + ;; + esac +} + +_preserve() { + [ -f "$1" ] && cp "$1" "${1}.orig" +} + +_restore() { + if [ -f "${1}.orig" ]; then + mv -- "${1}.orig" "${1}" + else + rm -rf "${1}" + fi +} + +##### End of part to be dupplicated into post-cleanup (do not alter!) + # Redirect stdout and errors to console as rc.local does not log anything exec 1>/dev/console 2>&1 @@ -11,20 +48,25 @@ exec 1>/dev/console 2>&1 logger -st ${0##*/} "Alpine Linux headless bootstrap v$VERSION by macmpi" mkdir /tmp/.trash -ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name *.apkovl.tar.gz -exec dirname {} \; | head -1 ) -# Help randomess for wpa_supplicant and ssh server +# grab used ovl filename from dmesg +ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )" +ovlpath="$( dirname "$ovl" )" + +# Help randomness for wpa_supplicant and ssh server rc-service seedrng start ## Setup Network interfaces if [ -f "${ovlpath}/wpa_supplicant.conf" ]; then logger -st ${0##*/} "Wifi setup found !" - apk add wpa_supplicant + _apk add wpa_supplicant + _preserve "/etc/wpa_supplicant/wpa_supplicant.conf" install -m600 "${ovlpath}/wpa_supplicant.conf" /etc/wpa_supplicant/wpa_supplicant.conf else logger -st ${0##*/} "Wifi setup not found !" fi +_preserve "/etc/network/interfaces" if ! install -m644 "${ovlpath}/interfaces" /etc/network/interfaces; then # set default interfaces if not specified by interface file on boot storage logger -st ${0##*/} "No interfaces file supplied, building default interfaces..." @@ -74,18 +116,21 @@ fi echo "Using following network interfaces:" cat /etc/network/interfaces +_preserve "/etc/hostname" echo "alpine-headless" > /etc/hostname hostname -F /etc/hostname -grep -q "wlan" /etc/network/interfaces && [ -f /etc/wpa_supplicant/wpa_supplicant.conf ] && rc-service wpa_supplicant start +grep -q "wlan" /etc/network/interfaces && \ + [ -f /etc/wpa_supplicant/wpa_supplicant.conf ] && \ + rc-service wpa_supplicant start rc-service networking start ## Setup temporary SSH server (root login, no password) -## we use some bundled or optionaly provided keys to avoid generation at startup and save time -apk add openssh -cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig -cp /etc/conf.d/sshd /etc/conf.d/sshd.orig +## We use some bundled (or optionaly provided) keys to avoid generation at startup and save time +_apk add openssh +_preserve "/etc/ssh/sshd_config" +_preserve "/etc/conf.d/sshd" cat <<-EOF >> /etc/ssh/sshd_config AuthenticationMethods none @@ -95,20 +140,29 @@ cat <<-EOF >> /etc/ssh/sshd_config EOF # Banner file +warn="" +grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; RO=$? +[ "$RO" -eq "0" ] && warn="(remount partition rw!)" + cat <<-EOF > /tmp/.trash/banner Alpine Linux headless bootstrap v$VERSION by macmpi +You may want to delete/rename .apkovl file before reboot ${warn}: +$ovl +(can be done automatically with unattended script - see sample snippet) + + EOF # Bundled temporary keys are moved in RAM /tmp so they won't be stored -# within permanent config later (new ones will then be generated) +# within permanent config later (new ones will then be generated at reboot) KEYGEN_STANCE="sshd_disable_keygen=yes" mv /etc/ssh/ssh_host_*_key* /tmp/.trash/. # Inject optional custom keys (those might be stored) if install -m600 "${ovlpath}"/ssh_host_*_key* /etc/ssh/; then - # check for empty key within injected ones: generate new keys if found + # check for empty key within injected ones: if found, generate new keys if find /etc/ssh/ -maxdepth 1 -type f -name 'ssh_host_*_key*' -empty | grep -q .; then rm /etc/ssh/ssh_host_*_key* KEYGEN_STANCE="" @@ -130,16 +184,19 @@ rc-service sshd start ## Prep for final post-cleanup ## clears any installed packages and settings -cat <<-EOF > /tmp/.trash/post-cleanup - #!/bin/sh +# copy begininng of this file to keep functions +sed -n '/^#* End .*alter!)$/q;p' /etc/local.d/headless.start > /tmp/.trash/post-cleanup + +cat <<-EOF >> /tmp/.trash/post-cleanup + logger -st ${0##*/} "Cleaning-up..." - mv /etc/ssh/sshd_config.orig /etc/ssh/sshd_config - mv /etc/conf.d/sshd.orig /etc/conf.d/sshd - apk del openssh - apk del wpa_supplicant - rm -rf /etc/wpa_supplicant - rm /etc/network/interfaces - rm /etc/hostname + _restore "/etc/ssh/sshd_config" + _restore "/etc/conf.d/sshd" + _apk del openssh + _restore "/etc/wpa_supplicant/wpa_supplicant.conf" + _apk del wpa_supplicant + _restore "/etc/network/interfaces" + _restore "/etc/hostname" rm /etc/modules-load.d/g_ether.conf rm /etc/modprobe.d/g_ether.conf rc-update del local default diff --git a/sample_unattended.sh b/sample_unattended.sh index 58fab1b..0ab512e 100644 --- a/sample_unattended.sh +++ b/sample_unattended.sh @@ -14,12 +14,13 @@ sleep 60 ## This snippet removes apkovl file on volume after initial boot -ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name *.apkovl.tar.gz -exec dirname {} \; | head -1 ) +ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )" +ovlpath="$( dirname "$ovl" )" # also works in case volume is mounted read-only grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; RO=$? [ "$RO" -eq "0" ] && mount -o remount,rw "${ovlpath}" -rm "${ovlpath}"/*.apkovl.tar.gz +rm -f "${ovl}" [ "$RO" -eq "0" ] && mount -o remount,ro "${ovlpath}" ########################################################