Compare commits

...

20 Commits
v0.8 ... main

Author SHA1 Message Date
macmpi bba42717fa
Merge pull request #24 from macmpi/dev
doc update
2023-12-06 09:09:33 +01:00
macmpi 7445b19996
doc update 2023-12-06 09:08:21 +01:00
macmpi f35a866875
Merge pull request #22 from macmpi/dev
version 1.2
2023-11-24 10:48:50 +01:00
macmpi 2046d877eb version 1.2
New feature: auto-update apkovl file with latest release on master
gadget: improved setup if several ports available and/or some unconnected
wlan: only install & start wpa_supplicant if needed
updated doc
2023-11-24 10:44:12 +01:00
macmpi 0d3694172a
Merge pull request #21 from macmpi/dev
version 1.1
2023-11-17 10:11:29 +01:00
macmpi 4026274721 version 1.1
improved interfaces detection: fixes https://github.com/macmpi/alpine-linux-headless-bootstrap/issues/20
make.sh: tighten-up archive construction (reproducibility)
2023-11-17 10:09:14 +01:00
macmpi 256b90bfbb
Merge pull request #19 from macmpi/dev
version 1.0
2023-11-13 22:01:00 +01:00
macmpi f430fc3ce5 version 1.0
unattended.sh script may disable sshd (#NO_SSH option)
gadget mode: enable both USB serial terminal and USB-ethernet networking
check new version availability online (notified on login banner & logs)
redesigned as OpenRC services
many other minor improvements
2023-11-13 21:34:41 +01:00
macmpi e263862335
Merge pull request #18 from macmpi/dev
Release 0.9
2023-09-20 14:17:33 +02:00
macmpi 03eb12b52f version 0.9
Allow authorized_keys authentication
Fix apkovl file detection (again)
Test internet availability
Misc wording tweaks
2023-09-20 14:10:29 +02:00
macmpi ab94ea6f31 log messages tweaks 2023-09-20 12:03:16 +02:00
macmpi e85ef08be2 fix ovl file detection again 2023-09-20 10:54:42 +02:00
macmpi 33742dec79 Allow authorized_keys 2023-09-20 09:09:30 +02:00
macmpi ebc52d13a8 Add test for internet availability 2023-09-18 19:25:40 +02:00
macmpi f7571ac6e5
Merge pull request #16 from macmpi/dev
Update README.md
2023-08-02 17:05:02 +02:00
macmpi aa87d4b540
Update README.md 2023-08-02 17:04:00 +02:00
macmpi 545118421a
Merge pull request #15 from macmpi/dev
Update README.md
2023-08-02 16:12:26 +02:00
macmpi bfd9056fed
Update README.md 2023-08-02 16:10:52 +02:00
macmpi c8c85d6e23
Merge pull request #14 from macmpi/dev
Update README.md
2023-08-02 16:05:19 +02:00
macmpi 49a9f625ca
Update README.md 2023-08-02 16:01:12 +02:00
23 changed files with 538 additions and 271 deletions

View File

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

View File

@ -3,45 +3,53 @@
[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 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:
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)[^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@<IP>`\
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.\
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>`\
(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 setup!).\
As with Alpine Linux initial bring-up, `root` account has no password initially (change that during target 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).
## Extra configuration:
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.
- `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.
- `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).
- `authorized_keys` (*optional*): provide client's public SSH key to secure `root` ssh login.
- `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).
- `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:** 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`.
## Goody:
Seamless USB-serial & USB-ethernet gadget mode (*e.g. PiZero*):
- 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.\
(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.\
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.
[^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).
[^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).
[^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 ?
## Want to tweak more ?
This repository may be forked/cloned/downloaded.\
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.
Main script file is [`headless_bootstrap`](https://github.com/macmpi/alpine-linux-headless-bootstrap/tree/main/overlay/usr/local/bin/headless_bootstrap).\
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

Binary file not shown.

View File

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

39
make.sh
View File

@ -1,8 +1,37 @@
#!/bin/sh
#!/bin/busybox sh
# Copyright 2022 - 2023, macmpi
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
chmod 600 overlay/etc/ssh/ssh_host_*_key
chmod +x overlay/etc/local.d/headless.start
tar czvf headless.apkovl.tar.gz -C overlay etc --owner=0 --group=0
# script meant to be run on Alpine (busybox) or on Ubuntu
# verify busybox build options if eventually using other platforms
command -v doas > /dev/null || alias doas="/usr/bin/sudo"
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

@ -0,0 +1,12 @@
#!/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

@ -0,0 +1,12 @@
#!/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

@ -0,0 +1,12 @@
#!/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

@ -1,216 +0,0 @@
#!/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

@ -1,6 +0,0 @@
# 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

@ -0,0 +1,6 @@
# 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

@ -1,9 +0,0 @@
# 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

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

View File

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

View File

@ -0,0 +1,390 @@
#!/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

10
sample_auto-updt Normal file
View File

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

View File

@ -1,27 +1,47 @@
#/bin/sh
#!/bin/sh
# Copyright 2022 - 2023, macmpi
# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi
# SPDX-License-Identifier: MIT
## collection of few code snippets as sample unnatteded actions some may find usefull
## will run encapusated within headless_unattended OpenRC service
## Obvious one; reminder: is run in the background
echo hello world !!
# To prevent headless bootstrap script from starting sshd
# only keep a single starting # on the line below
##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
_logger "Finished script"
########################################################
## 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/:.*$//' )"
ovlpath="$( dirname "$ovl" )"
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
# 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}"
grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$?
_is_ro() { return "$is_ro"; }
_is_ro && mount -o remount,rw "${ovlpath}"
rm -f "${ovl}"
[ "$RO" -eq "0" ] && mount -o remount,ro "${ovlpath}"
_is_ro && mount -o remount,ro "${ovlpath}"
########################################################
@ -30,7 +50,7 @@ rm -f "${ovl}"
# 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)
logger -st ${0##*/} "Setting-up minimal environment"
_logger "Setting-up minimal environment"
cat <<-EOF > /tmp/ANSWERFILE
# base answer file for setup-alpine script
@ -79,11 +99,12 @@ cat <<-EOF > /tmp/ANSWERFILE
# trick setup-alpine to pretend existing SSH connection
# 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
lbu commit -d
########################################################
logger -st ${0##*/} "Finished unattended script"
_logger "Finished unattended script"

View File

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