Compare commits

..

No commits in common. "main" and "v0.8" have entirely different histories.
main ... v0.8

23 changed files with 271 additions and 538 deletions

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2022-2023 macmpi Copyright (c) 2022 macmpi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -3,53 +3,45 @@
[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.\ [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). 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 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.\ 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.
An optional script may also be launched during that same initial bootstrap, to perform fully automated setup.
## Setup 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.\ 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). 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://is.gd/apkovl_master)[^2] overlay file *as-is* 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 DCHP-based network interface definitions (and [SSID/pass](#extra-configuration) file if using wifi), system can then be remotely accessed with: `ssh root@<IP>`\ With default network interface definitions (and SSID/pass file if using wifi), system can then be remotely accessed with: `ssh root@<IP>`\
(system IP address may be determined with any IP scanning tools such as `nmap`). (system IP address may be determined with any IP scanning tools such as `nmap`).
As with Alpine Linux initial bring-up, `root` account has no password initially (change that during target setup!).\ As with Alpine Linux initial bring-up, `root` account has no password initially (change that during setup!).\
From there, actual system install can be performed as usual with `setup-alpine` for instance (check [wiki](https://wiki.alpinelinux.org/wiki/Alpine_setup_scripts#setup-alpine) for details). From there, actual system install can be performed as usual with `setup-alpine` for instance (check [wiki](https://wiki.alpinelinux.org/wiki/Alpine_setup_scripts#setup-alpine) for details).
## Extra configuration:
Extra files may be added next to `headless.apkovl.tar.gz` to customise boostrapping configuration (check sample files): Extra files may be added next to `headless.apkovl.tar.gz` to customise boostrapping configuration (check sample files):
- `wpa_supplicant.conf`[^3] (*mandatory for wifi usecase*): define wifi SSID & password. - `wpa_supplicant.conf`[^3] (*mandatory for wifi usecase*): define wifi SSID & password.
- `unattended.sh`[^3] (*optional*): provide a deployment script to automate setup & customizations during initial bootstrap.
- `interfaces`[^3] (*optional*): define network interfaces at will, if defaults DCHP-based are not suitable. - `interfaces`[^3] (*optional*): define network interfaces at will, if defaults DCHP-based are not suitable.
- `authorized_keys` (*optional*): provide client's public SSH key to secure `root` ssh login. - `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).
- `ssh_host_*_key*` (*optional*): provide server's 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).
- `opt-out` (*optional*): dummy file to opt-out internet features (connection status, version check, auto-update) and related links usage anonymous [telemetry](https://is.gd/privacy.php).
- `auto-updt` (*optional*): allow apkovl file automatic update with latest from master branch. If it contains *reboot* keyword all in one line, system will reboot after succesful update (unless ssh session is active or `unattended.sh` script is available).
Main execution steps are logged: `cat /var/log/messages | grep headless`.
## Goody: **Goody:** seamless USB-ethernet gadget boostrapping (PiZero for instance):\
Seamless USB-serial & USB-ethernet gadget mode (*e.g. PiZero*): On supporting Pi devices, just add `dtoverlay=dwc2` in `usercfg.txt` (or `config.txt`), and plug USB cable into Computer port.\
- Make sure dwc2/dwc3 driver is loaded accordingly, and device configuration is set to `peripheral` mode: this may be hardware (including cable) and/or software driven.\ 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`
(on supporting Pi devices, just add `dtoverlay=dwc2,dr_mode=peripheral` in `usercfg.txt` (or `config.txt`) to force by software)
- Plug USB cable into host Computer port before boot.\ Main execution steps are logged in `/var/log/messages`.
Serial terminal can then be connected-to from host Computer (e.g. `cu -l ttyACM0` on Linux. xon/xoff flow control).\
Alternatively, with host Computer set-up to share networking with USB interface as 10.42.0.1 gateway, one can log into device from host with: `ssh root@10.42.0.2`.
[^1]: Initial boot fully preserves system's original state (config files & installed packages): a fresh system will therefore come-up as unconfigured. [^1]: Initial boot fully preserves system's original state (config files & installed packages): a fresh system will therefore come-up as unconfigured.
[^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/tmp/.trash) 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). [^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). [^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).
## Want to tweak more ? ## How to customize ?
This repository may be forked/cloned/downloaded.\ This repository may be forked/cloned/downloaded.\
Main script file is [`headless_bootstrap`](https://github.com/macmpi/alpine-linux-headless-bootstrap/tree/main/overlay/usr/local/bin/headless_bootstrap).\ Main script file is [`headless.start`](https://github.com/macmpi/alpine-linux-headless-bootstrap/blob/main/overlay/etc/local.d/headless.start).\
Execute `./make.sh` to rebuild `headless.apkovl.tar.gz` after changes.\ Execute `./make.sh` to rebuild `headless.apkovl.tar.gz` after changes.
(requires `busybox`; check `busybox` build options if not running from Alpine or Ubuntu)
## Credits ## Credits

Binary file not shown.

View File

@ -1 +0,0 @@
36892e9aa76807941602160ff62948c8953eefa36f9b386a0c7302fc46710b473a3e14b7eaf223a1ae6ac6454a839c78049c311fdbd50a204b2a011cb8faa474 headless.apkovl.tar.gz

39
make.sh
View File

@ -1,37 +1,8 @@
#!/bin/busybox sh #!/bin/sh
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi # Copyright 2022 - 2023, macmpi
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# script meant to be run on Alpine (busybox) or on Ubuntu chmod 600 overlay/etc/ssh/ssh_host_*_key
# verify busybox build options if eventually using other platforms chmod +x overlay/etc/local.d/headless.start
command -v doas > /dev/null || alias doas="/usr/bin/sudo" tar czvf headless.apkovl.tar.gz -C overlay etc --owner=0 --group=0
build_path="$(mktemp -d)"
if [ -n "$build_path" ]; then
# prefer timestamp option for touch as it works on directories too
t_stamp="$( TZ=UTC date +%Y%m%d0000.00 )"
cp -a overlay "$build_path"/.
find "$build_path"/overlay/ -exec sh -c 'TZ=UTC touch -chm -t "$0" "$1"' "$t_stamp" {} \;
# setting modes and owner/groups for runtime (won't affect mtime)
find "$build_path"/overlay/etc -type d -exec chmod 755 {} \;
chmod 755 "$build_path"/overlay/etc/init.d/*
chmod 755 "$build_path"/overlay/etc/runlevels/default/*
chmod 777 "$build_path"/overlay/tmp
chmod 700 "$build_path"/overlay/tmp/.trash
chmod -R 600 "$build_path"/overlay/tmp/.trash/ssh_host_*_key
find "$build_path"/overlay/usr -type d -exec chmod 755 {} \;
chmod 755 "$build_path"/overlay/usr/local/bin/*
doas chown -Rh 0:0 "$build_path"/overlay/*
# busybox config on Alpine & Ubuntu has FEATURE_TAR_GNU_EXTENSIONS
# (will preserve user/group/modes & mtime) and FEATURE_TAR_LONG_OPTIONS
# shellcheck disable=SC2046 # we want word splitting as result of find
doas tar cv -C "$build_path"/overlay --no-recursion \
$(doas find "$build_path"/overlay/ | sed "s|$build_path/overlay/||" | sort | xargs ) | \
gzip -c9n > headless.apkovl.tar.gz
sha512sum headless.apkovl.tar.gz > headless.apkovl.tar.gz.sha512
TZ=UTC touch -cm -t "$t_stamp" headless.apkovl.tar.gz*
doas rm -rf "$build_path"
fi

View File

@ -1,12 +0,0 @@
#!/sbin/openrc-run
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
description="Headless main boostrappring script"
name="Headless bootstrap"
command="/usr/local/bin/headless_bootstrap"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"

View File

@ -1,12 +0,0 @@
#!/sbin/openrc-run
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
description="Headless cleanup script"
name="Headless cleanup"
command="/tmp/.trash/headless_cleanup"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"

View File

@ -1,12 +0,0 @@
#!/sbin/openrc-run
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
description="Headless unattended setup script (optional)"
name="Headless unattended"
command="/tmp/headless_unattended"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"

View File

@ -0,0 +1,216 @@
#!/bin/sh
# Copyright 2022 - 2023, macmpi
# SPDX-License-Identifier: MIT
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
logger -st ${0##*/} "Alpine Linux headless bootstrap v$VERSION by macmpi"
mkdir /tmp/.trash
# 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
_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..."
for dev in $(ls /sys/class/net)
do
case ${dev%%[0-9]*} in
lo)
cat <<-EOF >> /etc/network/interfaces
auto $dev
iface $dev inet loopback
EOF
;;
eth)
cat <<-EOF >> /etc/network/interfaces
auto $dev
iface $dev inet dhcp
EOF
;;
wlan)
[ -f /etc/wpa_supplicant/wpa_supplicant.conf ] && cat <<-EOF >> /etc/network/interfaces
auto $dev
iface $dev inet dhcp
EOF
;;
usb)
cat <<-EOF >> /etc/network/interfaces
auto $dev
iface $dev inet static
address 10.42.0.2/24
gateway 10.42.0.1
EOF
cat <<-EOF > /etc/resolv.conf
nameserver 208.67.222.222
nameserver 208.67.220.220
EOF
;;
esac
done
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
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
_preserve "/etc/ssh/sshd_config"
_preserve "/etc/conf.d/sshd"
cat <<-EOF >> /etc/ssh/sshd_config
AuthenticationMethods none
PermitEmptyPasswords yes
PermitRootLogin yes
Banner /tmp/.trash/banner
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 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: 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=""
logger -st ${0##*/} "Will generate new SSH keys..."
else
chmod 644 /etc/ssh/ssh_host_*_key.pub
logger -st ${0##*/} "Using injected SSH keys..."
fi
else
logger -st ${0##*/} "Using bundled ssh keys from RAM..."
cat <<-EOF >> /etc/ssh/sshd_config
HostKey /tmp/.trash/ssh_host_ed25519_key
HostKey /tmp/.trash/ssh_host_rsa_key
EOF
fi
echo "$KEYGEN_STANCE" >> /etc/conf.d/sshd
rc-service sshd start
## Prep for final post-cleanup
## clears any installed packages and settings
# 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..."
_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
rm /etc/local.d/headless.start
if [ -f "${ovlpath}/unattended.sh" ]; then
install -m755 "${ovlpath}/unattended.sh" /tmp/unattended.sh
/tmp/unattended.sh >/dev/console 2>&1 &
logger -st ${0##*/} "/tmp/unattended.sh script launched in the background with PID \$!"
fi
logger -st ${0##*/} "Done !!"
EOF
chmod +x /tmp/.trash/post-cleanup
exec /tmp/.trash/post-cleanup

View File

@ -0,0 +1,6 @@
# Copyright 2022 - 2023, macmpi
# SPDX-License-Identifier: MIT
# added to support USB-Ethernet gadget mode at boot for Pi devices
options g_ether dev_addr=ea:64:2f:e8:19:94 host_addr=f6:67:ce:b3:c0:ea

View File

@ -1,6 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
# support g_cdc USB-Ethernet gadget mode at boot for Pi devices
options g_cdc dev_addr=ea:64:2f:e8:19:94 host_addr=f6:67:ce:b3:c0:ea

View File

@ -0,0 +1,9 @@
# Copyright 2022 - 2023, macmpi
# SPDX-License-Identifier: MIT
# added to support USB-Ethernet gadget mode at boot for Pi devices
# also requires dtoverlay=dwc2 is added to usercfg.txt or config.txt
dwc2
g_ether

View File

@ -1 +0,0 @@
../../init.d/headless_bootstrap

View File

@ -0,0 +1 @@
/etc/init.d/local

View File

@ -1,390 +0,0 @@
#!/bin/sh
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
HDLSBSTRP_VERSION="1.2"
_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 '%s ' "${pkg}" >>/tmp/.trash/installed
fi
;;
del) # delete only if previously installed
if grep -wq "$pkg" /tmp/.trash/installed >/dev/null 2>&1; then
apk del "$pkg" && sed -i 's/\b'"${pkg}"'\b//' /tmp/.trash/installed
fi
;;
*)
echo "only add/del: wrong usage"; exit
;;
esac
}
_preserve() {
# Create a back-up of element (file, folder, symlink).
[ -z "${1}" ] && return 1
[ -e "${1}" ] && cp -a "${1}" "${1}".orig
}
_restore() {
# Remove element (file, folder, symlink) and replace by
# previous back-up if available.
[ -z "${1}" ] && return 1
rm -rf "${1}"
[ -e "${1}".orig ] && mv -f "${1}".orig "${1}"
}
# shellcheck disable=SC2142 # known special case
alias _logger='logger -st "${0##*/}"'
##### End of part to be duplicated into headless_cleanup (do not alter!)
_prep_cleanup() {
## Prep for final headless_cleanup:
# clears any installed packages and settings.
# Copy begininng of this file to keep functions.
sed -n '/^#* End .*alter!)$/q;p' /usr/local/bin/headless_bootstrap >/tmp/.trash/headless_cleanup
cat <<-EOF >>/tmp/.trash/headless_cleanup
# Redirect stdout and errors to console as service won't show messages
exec 1>/dev/console 2>&1
_logger "Cleaning-up..."
_restore "/etc/ssh/sshd_config"
_restore "/etc/conf.d/sshd"
_apk del openssh-server
_restore "/etc/wpa_supplicant/wpa_supplicant.conf"
_apk del wpa_supplicant
_restore "/etc/network/interfaces"
_restore "/etc/hostname"
rm -f /etc/modprobe.d/headless_gadget.conf
# Remove from boot service to avoid spurious openrc recalls from unattended script.
rm -f /etc/runlevels/default/headless_bootstrap
rm -f /usr/local/bin/headless_bootstrap
# Run unattended script if available.
install -m755 ${ovlpath}/unattended.sh /tmp/headless_unattended >/dev/null 2>&1 && \
_logger "Starting headless_unattended service" && \
rc-service headless_unattended start
rm -f /etc/init.d/headless_*
_logger "Clean-up done, enjoy !"
cat /tmp/.trash/banner >/dev/console
if [ -c /dev/ttyGS${gdgt_id} ]; then
# Enabling terminal login into valid serial port:
# no choice than making permanent change to /etc/securetty (Alpine 3.19 already has ttyGS0).
grep -q "ttyGS${gdgt_id}" /etc/securetty || echo "ttyGS${gdgt_id}" >>/etc/securetty
/sbin/getty -L 115200 /dev/ttyGS${gdgt_id} vt100 &
fi
exit 0
EOF
chmod +x /tmp/.trash/headless_cleanup
}
_setup_sshd() {
## 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-server
_preserve "/etc/ssh/sshd_config"
_preserve "/etc/conf.d/sshd"
cat <<-EOF >/etc/ssh/sshd_config
PermitRootLogin yes
Banner /tmp/.trash/banner
EOF
# Client authorized_keys or no authentication.
if install -m600 "${ovlpath}"/authorized_keys /tmp/.trash/authorized_keys >/dev/null 2>&1; then
_logger "Enabling public key SSH authentication..."
cat <<-EOF >>/etc/ssh/sshd_config
AuthenticationMethods publickey
AuthorizedKeysFile /tmp/.trash/authorized_keys
# relax strict mode as authorized_keys are inside /tmp
StrictModes no
EOF
else
_logger "No SSH authentication."
cat <<-EOF >>/etc/ssh/sshd_config
AuthenticationMethods none
PermitEmptyPasswords yes
EOF
fi
# Server keys: inject optional custom keys, or generate new (might be stored),
# or use bundeled ones (not stored)
local keygen_stance="sshd_disable_keygen=yes"
if install -m600 "${ovlpath}"/ssh_host_*_key* /etc/ssh/ >/dev/null 2>&1; then
# 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=""
_logger "Will generate new SSH keys..."
else
chmod 644 /etc/ssh/ssh_host_*_key.pub
_logger "Using injected SSH keys..."
fi
else
_logger "Using bundled ssh keys from RAM..."
cat <<-EOF >>/etc/ssh/sshd_config
HostKey /tmp/.trash/ssh_host_ed25519_key
HostKey /tmp/.trash/ssh_host_rsa_key
EOF
fi
echo "$keygen_stance" >>/etc/conf.d/sshd
rc-service sshd restart
}
_updt_apkovl() {
## Update apkovl overlay file & eventually reboot
# URL redirects to apkovl file on github master: is.gd shortener provides basic analytics.
# Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_master
# Privacy policy: https://is.gd/privacy.php
local file_url="https://is.gd/apkovl_master"
local sha_url="https://github.com/macmpi/alpine-linux-headless-bootstrap/raw/main/headless.apkovl.tar.gz.sha512"
local updt_status="failed, keeping original version"
if wget -q -O /tmp/apkovl -T 10 "$file_url" >/dev/null 2>&1 && \
wget -q -O /tmp/sha -T 10 "$sha_url" >/dev/null 2>&1 && \
[ "$( sha512sum /tmp/apkovl | awk '{print $1}' )" = "$( awk '{print $1}' /tmp/sha )" ]; then
_is_ro && mount -o remount,rw "${ovlpath}"
cp /tmp/apkovl "${ovl}"
_is_ro && mount -o remount,ro "${ovlpath}"
! [ "$( sha512sum "${ovl}" | awk '{print $1}' )" = "$( awk '{print $1}' /tmp/sha )" ] && \
_logger "Bad update: original apkovl file may be altered, please check!..." && return 1
updt_status="successful"
fi
rm -f /tmp/apkovl /tmp/sha
_logger "Update $updt_status"
if [ "$updt_status" = "successful" ]; then
printf '%s\n\n' "Updated" >>/tmp/.trash/banner
else
printf '\n' >>/tmp/.trash/banner
return 1
fi
# Reboot if specified in auto-updt file (and no ssh session ongoing nor unattended.sh script available).
! pgrep -a -P "$( cat /run/sshd.pid 2>/dev/null )" 2>/dev/null | grep -q "sshd: root@pts" && \
! [ -f "${ovlpath}"/unattended.sh ] && \
grep -q "^reboot$" "${ovlpath}"/auto-updt && \
_logger "Will reboot in 3sec..." && sleep 3 && reboot
exit 0
}
_tst_version() {
## Compare current version with latest online, notify & eventally calls for update
# URL redirects to github project page: is.gd shortener provides basic analytics.
# Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_run
# Privacy policy: https://is.gd/privacy.php
local vers=""
local ref="/macmpi/alpine-linux-headless-bootstrap/releases/tag/v"
local url="https://is.gd/apkovl_run"
if wget -q -O /tmp/homepg -T 10 "$url" >/dev/null 2>&1; then
_logger "Internet access: success"
vers="$( grep -o "$ref.*\"" /tmp/homepg | grep -Eo '[0-9]+[\.[0-9]+]*' )"
rm -f /tmp/homepg
if [ -n "$vers" ] && ! [ "$vers" = "$HDLSBSTRP_VERSION" ]; then
vers="!! Version $vers is available on Github project page !!"
_logger "$vers"
printf '%s\n' "$vers" >>/tmp/.trash/banner
# Optionally update apkovl if key-file allows it.
if [ -f "${ovlpath}"/auto-updt ]; then
_logger "Updating overlay file..."
_updt_apkovl &
else
_logger "(check doc to enable auto-update)"
printf '%s\n\n' "(check doc to enable auto-update)" >>/tmp/.trash/banner
fi
fi
else
_logger "Internet access: failed"
fi
}
_setup_networking() {
## Setup network interfaces.
local has_wifi wlan_lst
_has_wifi() { return "$has_wifi"; }
wlan_lst="$( find /sys/class/net/*/phy80211 -exec \
sh -c 'printf %s\| "$( basename "$( dirname "$0" )" )"' {} \; 2>/dev/null )"
wlan_lst="${wlan_lst%\|}"
[ -n "$wlan_lst" ] && [ -f "${ovlpath}"/wpa_supplicant.conf ]
has_wifi=$?
_preserve "/etc/network/interfaces"
if ! install -m644 "${ovlpath}"/interfaces /etc/network/interfaces >/dev/null 2>&1; then
_logger "No interfaces file supplied, building defaults..."
cat <<-EOF >/etc/network/interfaces
auto lo
iface lo inet loopback
EOF
for dev in /sys/class/net/*; do
# shellcheck disable=SC2034 # Unused IFINDEX while still sourced from uevent.
local DEVTYPE INTERFACE IFINDEX
DEVTYPE=""
# shellcheck source=/dev/null
. "$dev"/uevent
case ${INTERFACE%%[0-9]*} in
lo)
;;
eth)
cat <<-EOF >>/etc/network/interfaces
auto $INTERFACE
iface $INTERFACE inet dhcp
EOF
;;
*)
# According to below we could rely on DEVTYPE for wlan devices
# https://lists.freedesktop.org/archives/systemd-devel/2014-January/015999.html
# but...some wlan might still be ill-behaved: use wlan_lst
# shellcheck disable=SC2169 # ash does support string replacement.
_has_wifi && ! [ "${wlan_lst/$INTERFACE/}" = "$wlan_lst" ] && \
cat <<-EOF >>/etc/network/interfaces
auto $INTERFACE
iface $INTERFACE inet dhcp
EOF
# Ensure considered gadget interface is actually the connected one (may have several).
[ "$DEVTYPE" = "gadget" ] && \
find /sys/class/udc/*/device/gadget."${gdgt_id}"/net/"$INTERFACE" -maxdepth 0 >/dev/null 2>&1 && \
cat <<-EOF >>/etc/network/interfaces && cat <<-EOF >/etc/resolv.conf
auto $INTERFACE
iface $INTERFACE inet static
address 10.42.0.2/24
gateway 10.42.0.1
EOF
nameserver 208.67.222.222
nameserver 208.67.220.220
EOF
;;
esac
done
fi
echo "###################################"
echo "Using following network interfaces:"
cat /etc/network/interfaces
echo "###################################"
if _has_wifi && grep -qE "$wlan_lst" /etc/network/interfaces; then
_logger "Configuring wifi..."
_apk add wpa_supplicant
_preserve "/etc/wpa_supplicant/wpa_supplicant.conf"
install -m600 "${ovlpath}"/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf
rc-service wpa_supplicant restart
else
_logger "No wifi interface or SSID/pass file supplied"
fi
_preserve "/etc/hostname"
echo "alpine-headless" >/etc/hostname
hostname -F /etc/hostname
rc-service networking restart
}
_setup_gadget() {
## Load composite USB Serial/USB Ethernel driver & setup terminal.
_logger "Enabling USB-gadget Serial and Ethernet ports"
# Remove conflicting modules in case they were initially loaded (cmdline.txt).
modprobe -r g_serial g_ether g_cdc
modprobe g_cdc
# Wait for g_cdc to settle and serial ports become available
timeout 1 sh <<-EOF
while ! grep -q "ttyGS" /proc/devices; do sleep 0.2; done
EOF
# Determine which gadget ID is connected with USB cable (assume just one max).
# (setting console to unconnected serial port would block boot)
gdgt_id="$( find /sys/class/udc/*/current_speed -exec \
sh -c 'grep -vq "UNKNOWN" "$0" && find ${0/current_speed/}device/gadget.* -maxdepth 0' {} \; \
| sed 's/\/.*gadget\.//' )"
if [ -c /dev/ttyGS"${gdgt_id}" ]; then
# Default serial config: xon/xoff flow control.
stty -F /dev/ttyGS"${gdgt_id}"
setconsole /dev/ttyGS"${gdgt_id}"
# Notes to users willing to connect from Linux Ubuntu-based host terminal:
# - user on host needs to be part of dialout group (reboot required), and
# - disable spurious AT commands from ModemManager on host-side Gadget serial port
# one may create a /etc/udev/rules.d/99-ttyacms-gadget.rules as per:
# https://linux-tips.com/t/prevent-modem-manager-to-capture-usb-serial-devices/284/2
# ATTRS{idVendor}=="0525" ATTRS{idProduct}=="a4aa", ENV{ID_MM_DEVICE_IGNORE}="1"
else
_logger "USB-gadget port not connected !"
modprobe -r g_cdc
fi
}
#############################################################################
## Main
# Redirect stdout and errors to console as service won't show messages
exec 1>/dev/console 2>&1
_logger "Alpine Linux headless bootstrap v$HDLSBSTRP_VERSION by macmpi"
# Help randomness for wpa_supplicant and sshd (urandom until 3.16).
rc-service seedrng restart || rc-service urandom restart
# Setup USB gadget ports if some ports are enabled in peripheral mode.
# Note: we assume dwc2/dwc3 is pre-loaded, we just check mode.
gdgt_id=""
find /sys/class/udc/*/is_a_peripheral -print0 2>/dev/null | \
xargs -0 cat 2>/dev/null | grep -q "0" && \
_setup_gadget
# Determine ovl file location.
# Grab used ovl filename from dmesg.
ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )"
if [ -f "${ovl}" ]; then
ovlpath="$( dirname "$ovl" )"
else
# Search path again as mountpoint have been changed later in the boot process...
ovl="$( basename "${ovl}" )"
ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name "${ovl}" -exec dirname {} \; | head -1 )
ovl="${ovlpath}/${ovl}"
fi
# Create banner file.
warn=""
grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$?
_is_ro() { return "$is_ro"; }
_is_ro && warn="(remount partition rw!)"
cat <<-EOF >/tmp/.trash/banner
Alpine Linux headless bootstrap v$HDLSBSTRP_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
_setup_networking
# Test latest available version online.
# Can be skipped by creating a 'opt-out'-named dummy file aside apkovl file.
[ -f "${ovlpath}"/opt-out ] || _tst_version &
# Setup sshd unless unattended.sh script prevents it.
grep -q "^#NO_SSH$" "${ovlpath}"/unattended.sh >/dev/null 2>&1 \
|| _setup_sshd
_prep_cleanup
_logger "Initial setup done, handing-over to clean-up"
rc-service headless_cleanup start
exit 0

View File

@ -1,10 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
# Automated reboot after update is cancelled if:
# - there is an opened ssh session
# - unattended.sh script is provided
# Uncomment line below to enable reboot after update
#reboot

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi # Copyright 2022 - 2023, macmpi
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# Sample network interfaces file # Sample network interfaces file
@ -8,12 +8,15 @@ iface lo inet loopback
auto eth0 auto eth0
iface eth0 inet dhcp iface eth0 inet dhcp
hostname localhost
auto wlan0 auto wlan0
iface wlan0 inet dhcp iface wlan0 inet dhcp
hostname localhost
auto usb0 auto usb0
iface usb0 inet static iface usb0 inet static
address 10.42.0.2/24 address 10.42.0.2/24
gateway 10.42.0.1 gateway 10.42.0.1
hostname localhost

View File

@ -1,47 +1,27 @@
#!/bin/sh #/bin/sh
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi # Copyright 2022 - 2023, macmpi
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
## collection of few code snippets as sample unnatteded actions some may find usefull ## collection of few code snippets as sample unnatteded actions some may find usefull
## will run encapusated within headless_unattended OpenRC service
# To prevent headless bootstrap script from starting sshd ## Obvious one; reminder: is run in the background
# only keep a single starting # on the line below echo hello world !!
##NO_SSH
# Uncomment to enable stdout and errors redirection to console (service won't show messages)
# exec 1>/dev/console 2>&1
# shellcheck disable=SC2142 # known special case
alias _logger='logger -st "${0##*/}"'
## Obvious one; reminder: is run as background service
_logger "hello world !!"
sleep 60 sleep 60
_logger "Finished script"
######################################################## ########################################################
## This snippet removes apkovl file on volume after initial boot ## This snippet removes apkovl file on volume after initial boot
# grab used ovl filename from dmesg
ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )" ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )"
if [ -f "${ovl}" ]; then
ovlpath="$( dirname "$ovl" )" ovlpath="$( dirname "$ovl" )"
else
# search path again as mountpoint have been changed later in the boot process...
ovl="$( basename "${ovl}" )"
ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name "${ovl}" -exec dirname {} \; | head -1 )
ovl="${ovlpath}/${ovl}"
fi
# also works in case volume is mounted read-only # also works in case volume is mounted read-only
grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$? grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; RO=$?
_is_ro() { return "$is_ro"; } [ "$RO" -eq "0" ] && mount -o remount,rw "${ovlpath}"
_is_ro && mount -o remount,rw "${ovlpath}"
rm -f "${ovl}" rm -f "${ovl}"
_is_ro && mount -o remount,ro "${ovlpath}" [ "$RO" -eq "0" ] && mount -o remount,ro "${ovlpath}"
######################################################## ########################################################
@ -50,7 +30,7 @@ _is_ro && mount -o remount,ro "${ovlpath}"
# note: with INTERFACESOPTS=none, no networking will be setup so it won't work after reboot! # note: with INTERFACESOPTS=none, no networking will be setup so it won't work after reboot!
# Change it or run setup-interfaces in interractive mode afterwards (and lbu commit -d thenafter) # Change it or run setup-interfaces in interractive mode afterwards (and lbu commit -d thenafter)
_logger "Setting-up minimal environment" logger -st ${0##*/} "Setting-up minimal environment"
cat <<-EOF > /tmp/ANSWERFILE cat <<-EOF > /tmp/ANSWERFILE
# base answer file for setup-alpine script # base answer file for setup-alpine script
@ -99,12 +79,11 @@ cat <<-EOF > /tmp/ANSWERFILE
# trick setup-alpine to pretend existing SSH connection # trick setup-alpine to pretend existing SSH connection
# and therefore keep (do not reset) network interfaces while running in background # and therefore keep (do not reset) network interfaces while running in background
# requires alpine-conf 3.15.1 and later, available from Alpine 3.17
SSH_CONNECTION="FAKE" setup-alpine -ef /tmp/ANSWERFILE SSH_CONNECTION="FAKE" setup-alpine -ef /tmp/ANSWERFILE
lbu commit -d lbu commit -d
######################################################## ########################################################
_logger "Finished unattended script" logger -st ${0##*/} "Finished unattended script"

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi # Copyright 2022 - 2023, macmpi
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# Sample wpa_supplicant.conf # Sample wpa_supplicant.conf