├── .github └── FUNDING.yml ├── LICENSE ├── LICENSE.spdx ├── README.md ├── headless.apkovl.tar.gz ├── headless.apkovl.tar.gz.sha512 ├── make.sh ├── overlay ├── etc │ ├── .default_boot_services │ ├── init.d │ │ ├── headless_bootstrap │ │ ├── headless_cleanup │ │ └── headless_unattended │ ├── modprobe.d │ │ └── headless_gadget.conf │ └── runlevels │ │ └── default │ │ └── headless_bootstrap ├── tmp │ └── .trash │ │ ├── ssh_host_ed25519_key │ │ ├── ssh_host_ed25519_key.pub │ │ ├── ssh_host_rsa_key │ │ └── ssh_host_rsa_key.pub └── usr │ └── local │ └── bin │ └── headless_bootstrap ├── sample_auto-updt ├── sample_interfaces ├── sample_unattended.sh └── sample_wpa_supplicant.conf /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: macmpi 4 | open_collective: alpinelinux 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 macmpi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.1 2 | DataLicense: CC0-1.0 3 | PackageName: alpine-linux-headless-bootstrap 4 | PackageOriginator: macmpi 5 | PackageHomePage: https://github.com/macmpi/alpine-linux-headless-bootstrap 6 | PackageLicenseDeclared: MIT 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap Alpine Linux on a headless system 2 | 3 | [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.\ 4 | 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). 5 | 6 | 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.\ 7 | An optional script may also be launched during that same initial bootstrap, to perform fully automated setup. 8 | 9 | 10 | ## Setup procedure: 11 | Please follow [Alpine Linux Wiki](https://wiki.alpinelinux.org/wiki/Installation#Installation_Overview) to download & create installation media for the target platform.\ 12 | Tools provided here can be used on any plaform for any install modes (diskless, data disk, system disk). 13 | 14 | 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.\ 15 | 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@`\ 16 | (system IP address may be determined with any IP scanning tools such as `nmap`). 17 | 18 | As with Alpine Linux initial bring-up, `root` account has no password initially (change that during target setup!).\ 19 | 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). 20 | 21 | ## Extra configuration: 22 | Extra files may be added next to `headless.apkovl.tar.gz` to customise boostrapping configuration (check sample files): 23 | - `wpa_supplicant.conf`[^3] (*mandatory for wifi usecase*): define wifi SSID & password. 24 | - `unattended.sh`[^3] (*optional*): provide a deployment script to automate setup & customizations during initial bootstrap. 25 | - `interfaces`[^3] (*optional*): define network interfaces at will, if defaults DCHP-based are not suitable. 26 | - `authorized_keys` (*optional*): provide client's public SSH key to secure `root` ssh login. 27 | - `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). 28 | - `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). 29 | - `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). 30 | 31 | Main execution steps are logged: `cat /var/log/messages | grep headless`. 32 | 33 | ## Goody: 34 | Seamless USB-serial & USB-ethernet gadget mode (*e.g. PiZero*): 35 | - 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.\ 36 | (on supporting Pi devices, just add `dtoverlay=dwc2,dr_mode=peripheral` in `usercfg.txt` (or `config.txt`) to force by software) 37 | - Plug USB cable into host Computer port before boot.\ 38 | Serial terminal can then be connected-to from host Computer (e.g. `cu -l ttyACM0` on Linux. xon/xoff flow control).\ 39 | 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`. 40 | 41 | [^1]: Initial boot fully preserves system's original state (config files & installed packages): a fresh system will therefore come-up as unconfigured. 42 | 43 | [^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). 44 | 45 | [^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). 46 | 47 | 48 | ## Want to tweak more ? 49 | This repository may be forked/cloned/downloaded.\ 50 | Main script file is [`headless_bootstrap`](https://github.com/macmpi/alpine-linux-headless-bootstrap/tree/main/overlay/usr/local/bin/headless_bootstrap).\ 51 | Execute `./make.sh` to rebuild `headless.apkovl.tar.gz` after changes.\ 52 | (requires `busybox`; check `busybox` build options if not running from Alpine or Ubuntu) 53 | 54 | 55 | ## Credits 56 | Thanks for the initial guides & scripts from @sodface and @davidmytton. 57 | 58 | -------------------------------------------------------------------------------- /headless.apkovl.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macmpi/alpine-linux-headless-bootstrap/3666fd7caf82946715c6fd2b555e5db8b94b2e36/headless.apkovl.tar.gz -------------------------------------------------------------------------------- /headless.apkovl.tar.gz.sha512: -------------------------------------------------------------------------------- 1 | fecc6b66f1b08a959c6509334b5c1942b8df76ba6b7d51c54630f0c502706650b9d345bab71cd3d787681113eb5c1a40b2f6bc238ca6e4137fd00de8a6bc8a17 headless.apkovl.tar.gz 2 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/busybox sh 2 | 3 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 4 | # SPDX-License-Identifier: MIT 5 | 6 | # Script meant to be run on Alpine (busybox) or on Ubuntu. 7 | # Check busybox version & options if eventually using other platforms. 8 | 9 | # Ubuntu LTS busybox 1.30.1 tar does NOT support setting owner/group/mtime 10 | # probably available after busybox 1.31.1, following 2019-08-01 change: 11 | # https://git.busybox.net/busybox/commit/?id=e6a87e74837ba5f2f2207a75cd825acf8cf28afb 12 | # This limitation requires copying files and setting owner/group/mtime before archiving. 13 | 14 | command -v doas > /dev/null || alias doas="/usr/bin/sudo" 15 | 16 | build_path="$(mktemp -d)" 17 | if [ -n "$build_path" ]; then 18 | # prefer timestamp option for touch as it works on directories too 19 | t_stamp="$( TZ=UTC date +%Y%m%d0000.00 )" 20 | cp -a overlay "$build_path"/. 21 | find "$build_path"/overlay/ -exec sh -c 'TZ=UTC touch -chm -t "$0" "$1"' "$t_stamp" {} \; 22 | # setting modes and owner/groups for runtime (won't affect mtime) 23 | find "$build_path"/overlay/etc -type d -exec chmod 755 {} \; 24 | chmod 755 "$build_path"/overlay/etc/init.d/* 25 | chmod 755 "$build_path"/overlay/etc/runlevels/default/* 26 | chmod 777 "$build_path"/overlay/tmp 27 | chmod 700 "$build_path"/overlay/tmp/.trash 28 | chmod -R 600 "$build_path"/overlay/tmp/.trash/ssh_host_*_key 29 | find "$build_path"/overlay/usr -type d -exec chmod 755 {} \; 30 | chmod 755 "$build_path"/overlay/usr/local/bin/* 31 | doas chown -Rh 0:0 "$build_path"/overlay/* 32 | 33 | # busybox config on Alpine & Ubuntu has FEATURE_TAR_GNU_EXTENSIONS 34 | # (will preserve user/group/modes & mtime) and FEATURE_TAR_LONG_OPTIONS 35 | # shellcheck disable=SC2046 # we want word splitting as result of find 36 | doas tar cv -C "$build_path"/overlay --no-recursion \ 37 | $(doas find "$build_path"/overlay/ | sed "s|$build_path/overlay/||" | sort | xargs ) | \ 38 | gzip -c9n > headless.apkovl.tar.gz 39 | sha512sum headless.apkovl.tar.gz > headless.apkovl.tar.gz.sha512 40 | TZ=UTC touch -cm -t "$t_stamp" headless.apkovl.tar.gz* 41 | doas rm -rf "$build_path" 42 | fi 43 | 44 | -------------------------------------------------------------------------------- /overlay/etc/.default_boot_services: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macmpi/alpine-linux-headless-bootstrap/3666fd7caf82946715c6fd2b555e5db8b94b2e36/overlay/etc/.default_boot_services -------------------------------------------------------------------------------- /overlay/etc/init.d/headless_bootstrap: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 4 | # SPDX-License-Identifier: MIT 5 | 6 | description="Headless main boostrappring script" 7 | name="Headless bootstrap" 8 | 9 | command="/usr/local/bin/headless_bootstrap" 10 | command_background=true 11 | pidfile="/run/${RC_SVCNAME}.pid" 12 | 13 | -------------------------------------------------------------------------------- /overlay/etc/init.d/headless_cleanup: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 4 | # SPDX-License-Identifier: MIT 5 | 6 | description="Headless cleanup script" 7 | name="Headless cleanup" 8 | 9 | command="/tmp/.trash/headless_cleanup" 10 | command_background=true 11 | pidfile="/run/${RC_SVCNAME}.pid" 12 | 13 | -------------------------------------------------------------------------------- /overlay/etc/init.d/headless_unattended: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 4 | # SPDX-License-Identifier: MIT 5 | 6 | description="Headless unattended setup script (optional)" 7 | name="Headless unattended" 8 | 9 | command="/tmp/headless_unattended" 10 | command_background=true 11 | pidfile="/run/${RC_SVCNAME}.pid" 12 | 13 | -------------------------------------------------------------------------------- /overlay/etc/modprobe.d/headless_gadget.conf: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 2 | # SPDX-License-Identifier: MIT 3 | 4 | # support g_cdc USB-Ethernet gadget mode at boot for Pi devices 5 | 6 | options g_cdc dev_addr=ea:64:2f:e8:19:94 host_addr=f6:67:ce:b3:c0:ea 7 | -------------------------------------------------------------------------------- /overlay/etc/runlevels/default/headless_bootstrap: -------------------------------------------------------------------------------- 1 | ../../init.d/headless_bootstrap -------------------------------------------------------------------------------- /overlay/tmp/.trash/ssh_host_ed25519_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACBQ5DhvGz8A3YPVH218RCYdvsz6YrArDxbRdM+Qog+ceAAAAJiMSpNejEqT 4 | XgAAAAtzc2gtZWQyNTUxOQAAACBQ5DhvGz8A3YPVH218RCYdvsz6YrArDxbRdM+Qog+ceA 5 | AAAECmulBcMfFgxjUrIiuPnOjEkwNcHp9+NOtggzitv4d4F1DkOG8bPwDdg9UfbXxEJh2+ 6 | zPpisCsPFtF0z5CiD5x4AAAAFHJvb3RAYWxwaW5lLWhlYWRsZXNzAQ== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /overlay/tmp/.trash/ssh_host_ed25519_key.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDkOG8bPwDdg9UfbXxEJh2+zPpisCsPFtF0z5CiD5x4 root@alpine-headless 2 | -------------------------------------------------------------------------------- /overlay/tmp/.trash/ssh_host_rsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | -----END OPENSSH PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /overlay/tmp/.trash/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBSPBqlg0Lg43IXL5id3DfyFvwtm3kqSnEYBI0LHhiuTvV0j4R5+QVlYghSgYPkSfTJuSCMZkjc7C9BHV/CdJ9MhbFIOgOJhkRkxnSCiRcu2C2u/M6csD+VH7N9xj7LWtxVvu6UsyV81w4dLRIuKAAJjIcySQHXOCC77dvrrGWCEXKVJPv00jOjO2KOCwK8xuTphn2W4QRz0Fh+GL5jDhReqG2TtInFJ1ih0jmE/JHq5BcdsNvgu/E3guKb1I2SOQbO1KjIOXreKvAKdqn4/eBRJvv6HsCk/hmiodKVqGfpkuBSNNOU9RHHVg8+251iVcNojrayuVlt+Hmp1sDJcEyiEyvdt3z9aETzRbaTV3BTGCenGMwzUW3yelbb2zSKMz7ZeedvdTobIlWsgVfF+fPt0O61BJ0cFT7QBOb95xREvE8JcJm7hoeQK+s5qxF+lCbxegIKiC5D8JDbmSjWkq/f3cvmW80YjZPrxk00xZowlYKkVhu3bEt8HbLLyjsT3s= root@alpine-headless 2 | -------------------------------------------------------------------------------- /overlay/usr/local/bin/headless_bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 4 | # SPDX-License-Identifier: MIT 5 | 6 | HDLSBSTRP_VERSION="1.2.3" 7 | 8 | _apk() { 9 | local cmd="$1" 10 | local pkg="$2" 11 | 12 | case $cmd in 13 | add) # install only if not already present 14 | if ! apk info | grep -wq "${pkg}"; then 15 | apk add "$pkg" && printf '%s ' "${pkg}" >>/tmp/.trash/installed 16 | fi 17 | ;; 18 | del) # delete only if previously installed 19 | if grep -wq "$pkg" /tmp/.trash/installed >/dev/null 2>&1; then 20 | apk del "$pkg" && sed -i 's/\b'"${pkg}"'\b//' /tmp/.trash/installed 21 | fi 22 | ;; 23 | *) 24 | echo "only add/del: wrong usage"; exit 25 | ;; 26 | esac 27 | } 28 | 29 | _preserve() { 30 | # Create a back-up of element (file, folder, symlink). 31 | [ -z "${1}" ] && return 1 32 | [ -e "${1}" ] && cp -a "${1}" "${1}".orig 33 | } 34 | 35 | _restore() { 36 | # Remove element (file, folder, symlink) and replace by 37 | # previous back-up if available. 38 | [ -z "${1}" ] && return 1 39 | rm -rf "${1}" 40 | [ -e "${1}".orig ] && mv -f "${1}".orig "${1}" 41 | } 42 | 43 | # shellcheck disable=SC2142 # known special case 44 | alias _logger='logger -st "${0##*/}"' 45 | 46 | ##### End of part to be duplicated into headless_cleanup (do not alter!) 47 | 48 | _prep_cleanup() { 49 | ## Prep for final headless_cleanup: 50 | # clears any installed packages and settings. 51 | # Copy begininng of this file to keep functions. 52 | sed -n '/^#* End .*alter!)$/q;p' /usr/local/bin/headless_bootstrap >/tmp/.trash/headless_cleanup 53 | cat <<-EOF >>/tmp/.trash/headless_cleanup 54 | # Redirect stdout and errors to console as service won't show messages 55 | exec 1>/dev/console 2>&1 56 | 57 | _logger "Cleaning-up..." 58 | _restore "/etc/ssh/sshd_config" 59 | _restore "/etc/conf.d/sshd" 60 | _apk del openssh-server 61 | _restore "/etc/wpa_supplicant/wpa_supplicant.conf" 62 | _apk del wpa_supplicant 63 | _restore "/etc/network/interfaces" 64 | _restore "/etc/hostname" 65 | rm -f /etc/modprobe.d/headless_gadget.conf 66 | 67 | # Remove from boot service to avoid spurious openrc recalls from unattended script. 68 | rm -f /etc/runlevels/default/headless_bootstrap 69 | rm -f /usr/local/bin/headless_bootstrap 70 | 71 | # Run unattended script if available. 72 | install -m755 ${ovlpath}/unattended.sh /tmp/headless_unattended >/dev/null 2>&1 && \ 73 | _logger "Starting headless_unattended service" && \ 74 | rc-service headless_unattended start 75 | 76 | rm -f /etc/init.d/headless_* 77 | _logger "Clean-up done, enjoy !" 78 | cat /tmp/.trash/banner >/dev/console 79 | if [ -c /dev/ttyGS${gdgt_id} ]; then 80 | # Enabling terminal login into valid serial port: 81 | # no choice than making permanent change to /etc/securetty (Alpine 3.19 already has ttyGS0). 82 | grep -q "ttyGS${gdgt_id}" /etc/securetty || echo "ttyGS${gdgt_id}" >>/etc/securetty 83 | /sbin/getty -L 115200 /dev/ttyGS${gdgt_id} vt100 & 84 | fi 85 | exit 0 86 | EOF 87 | chmod +x /tmp/.trash/headless_cleanup 88 | } 89 | 90 | _setup_sshd() { 91 | ## Setup temporary SSH server (root login, no password): 92 | # we use some bundled (or optionaly provided) keys to avoid generation at startup and save time. 93 | _apk add openssh-server 94 | # Preserve sshd-session & al binaries before uninstall 95 | [ -d /usr/lib/ssh ] && cp -a /usr/lib/ssh /tmp/.trash/. 96 | 97 | _preserve "/etc/ssh/sshd_config" 98 | _preserve "/etc/conf.d/sshd" 99 | 100 | cat <<-EOF >/etc/ssh/sshd_config 101 | PermitRootLogin yes 102 | Banner /tmp/.trash/banner 103 | EOF 104 | 105 | # Client authorized_keys or no authentication. 106 | if install -m600 "${ovlpath}"/authorized_keys /tmp/.trash/authorized_keys >/dev/null 2>&1; then 107 | _logger "Enabling public key SSH authentication..." 108 | cat <<-EOF >>/etc/ssh/sshd_config 109 | AuthenticationMethods publickey 110 | AuthorizedKeysFile /tmp/.trash/authorized_keys 111 | # relax strict mode as authorized_keys are inside /tmp 112 | StrictModes no 113 | EOF 114 | else 115 | _logger "No SSH authentication." 116 | cat <<-EOF >>/etc/ssh/sshd_config 117 | AuthenticationMethods none 118 | PermitEmptyPasswords yes 119 | EOF 120 | fi 121 | 122 | # Define sshd-session & al files new location into sshd_config 123 | for f in /tmp/.trash/ssh/sshd-*; do 124 | [ -e "$f" ] || continue # protect failing glob 125 | name=$(echo $(basename $f) | cut -c6-) 126 | initial=$(echo $name | cut -c1 | tr [a-z] [A-Z]) 127 | final=$(echo $name | cut -c2-) 128 | echo "Sshd${initial}${final}Path $f" >>/etc/ssh/sshd_config 129 | done 130 | 131 | # Server keys: inject optional custom keys, or generate new (might be stored), 132 | # or use bundeled ones (not stored) 133 | local keygen_stance="sshd_disable_keygen=yes" 134 | if install -m600 "${ovlpath}"/ssh_host_*_key* /etc/ssh/ >/dev/null 2>&1; then 135 | # Check for empty key within injected ones: if found, generate new keys. 136 | if find /etc/ssh/ -maxdepth 1 -type f -name 'ssh_host_*_key*' -empty | grep -q .; then 137 | rm /etc/ssh/ssh_host_*_key* 138 | keygen_stance="" 139 | _logger "Will generate new SSH keys..." 140 | else 141 | chmod 644 /etc/ssh/ssh_host_*_key.pub 142 | _logger "Using injected SSH keys..." 143 | fi 144 | else 145 | _logger "Using bundled ssh keys from RAM..." 146 | cat <<-EOF >>/etc/ssh/sshd_config 147 | HostKey /tmp/.trash/ssh_host_ed25519_key 148 | HostKey /tmp/.trash/ssh_host_rsa_key 149 | EOF 150 | fi 151 | 152 | echo "$keygen_stance" >>/etc/conf.d/sshd 153 | 154 | rc-service sshd restart 155 | } 156 | 157 | _updt_apkovl() { 158 | ## Update apkovl overlay file & eventually reboot 159 | # URL redirects to apkovl file on github master: is.gd shortener provides basic analytics. 160 | # Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_master 161 | # Privacy policy: https://is.gd/privacy.php 162 | local file_url="https://is.gd/apkovl_master" 163 | local sha_url="https://github.com/macmpi/alpine-linux-headless-bootstrap/raw/main/headless.apkovl.tar.gz.sha512" 164 | local updt_status="failed, keeping original version" 165 | 166 | # Ensure system date is correct to allow SSL transactions 167 | ntpd -N -p pool.ntp.org -n -q 168 | 169 | _is_ro && mount -o remount,rw "${ovlpath}" 170 | if wget -q -O "${ovl}_new" -T 10 "$file_url" >/dev/null 2>&1 && \ 171 | wget -q -O /tmp/sha -T 10 "$sha_url" >/dev/null 2>&1 && \ 172 | [ "$( sha512sum "${ovl}_new" | awk '{print $1}' )" = "$( awk '{print $1}' /tmp/sha )" ]; then 173 | mv -f "${ovl}_new" "${ovl}" 174 | updt_status="successful" 175 | fi 176 | rm -f "${ovl}_new" /tmp/sha 177 | _is_ro && mount -o remount,ro "${ovlpath}" 178 | _logger "Update $updt_status" 179 | 180 | if [ "$updt_status" = "successful" ]; then 181 | printf '%s\n\n' "Updated (Read release notes!)" >>/tmp/.trash/banner 182 | else 183 | printf '\n' >>/tmp/.trash/banner 184 | return 1 185 | fi 186 | # Reboot if specified in auto-updt file (and no ssh session ongoing nor unattended.sh script available). 187 | ! pgrep -a -P "$( cat /run/sshd.pid 2>/dev/null )" 2>/dev/null | grep -q "sshd: root@pts" && \ 188 | ! [ -f "${ovlpath}"/unattended.sh ] && \ 189 | grep -q "^reboot$" "${ovlpath}"/auto-updt && \ 190 | _logger "Will reboot in 3sec..." && sleep 3 && reboot 191 | exit 0 192 | } 193 | 194 | _tst_version() { 195 | ## Compare current version with latest online, notify & eventally calls for update 196 | # URL redirects to github project page: is.gd shortener provides basic analytics. 197 | # Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_run 198 | # Privacy policy: https://is.gd/privacy.php 199 | local vers="" 200 | local ref="/macmpi/alpine-linux-headless-bootstrap/releases/tag/v" 201 | local url="https://is.gd/apkovl_run" 202 | 203 | if wget -q -O /tmp/homepg -T 10 --no-check-certificate "$url" >/dev/null 2>&1; then 204 | _logger "Internet access: success" 205 | vers="$( grep -o "$ref.*\"" /tmp/homepg | grep -Eo '[0-9]+[\.[0-9]+]*' )" 206 | rm -f /tmp/homepg 207 | if [ -n "$vers" ] && ! [ "$vers" = "$HDLSBSTRP_VERSION" ]; then 208 | vers="!! Version $vers is available on Github project page !!" 209 | _logger "$vers" 210 | printf '%s\n' "$vers" >>/tmp/.trash/banner 211 | # Optionally update apkovl if key-file allows it. 212 | if [ -f "${ovlpath}"/auto-updt ]; then 213 | _logger "Updating overlay file..." 214 | _updt_apkovl & 215 | else 216 | _logger "(check doc to enable auto-update)" 217 | printf '%s\n\n' "(check doc to enable auto-update)" >>/tmp/.trash/banner 218 | fi 219 | fi 220 | else 221 | _logger "Internet access: failed" 222 | fi 223 | } 224 | 225 | _setup_networking() { 226 | ## Setup network interfaces. 227 | local has_wifi wlan_lst 228 | _has_wifi() { return "$has_wifi"; } 229 | 230 | wlan_lst="$( find /sys/class/net/*/phy80211 -exec \ 231 | sh -c 'printf %s\| "$( basename "$( dirname "$0" )" )"' {} \; 2>/dev/null )" 232 | wlan_lst="${wlan_lst%\|}" 233 | [ -n "$wlan_lst" ] && [ -f "${ovlpath}"/wpa_supplicant.conf ] 234 | has_wifi=$? 235 | 236 | _preserve "/etc/network/interfaces" 237 | if ! install -m644 "${ovlpath}"/interfaces /etc/network/interfaces >/dev/null 2>&1; then 238 | _logger "No interfaces file supplied, building defaults..." 239 | cat <<-EOF >/etc/network/interfaces 240 | auto lo 241 | iface lo inet loopback 242 | 243 | EOF 244 | for dev in /sys/class/net/*; do 245 | [ -e "$dev" ] || continue # protect failing glob 246 | # shellcheck disable=SC2034 # Unused IFINDEX while still sourced from uevent. 247 | local DEVTYPE INTERFACE IFINDEX 248 | DEVTYPE="" 249 | # shellcheck source=/dev/null 250 | . "$dev"/uevent 251 | case ${INTERFACE%%[0-9]*} in 252 | lo) 253 | ;; 254 | eth) 255 | cat <<-EOF >>/etc/network/interfaces 256 | auto $INTERFACE 257 | iface $INTERFACE inet dhcp 258 | 259 | EOF 260 | ;; 261 | *) 262 | # According to below we could rely on DEVTYPE for wlan devices 263 | # https://lists.freedesktop.org/archives/systemd-devel/2014-January/015999.html 264 | # but...some wlan might still be ill-behaved: use wlan_lst 265 | # shellcheck disable=SC2169 # ash does support string replacement. 266 | _has_wifi && ! [ "${wlan_lst/$INTERFACE/}" = "$wlan_lst" ] && \ 267 | cat <<-EOF >>/etc/network/interfaces 268 | auto $INTERFACE 269 | iface $INTERFACE inet dhcp 270 | 271 | EOF 272 | # Ensure considered gadget interface is actually the connected one (may have several). 273 | [ "$DEVTYPE" = "gadget" ] && \ 274 | find /sys/class/udc/*/device/gadget."${gdgt_id}"/net/"$INTERFACE" -maxdepth 0 >/dev/null 2>&1 && \ 275 | cat <<-EOF >>/etc/network/interfaces && cat <<-EOF >/etc/resolv.conf 276 | auto $INTERFACE 277 | iface $INTERFACE inet static 278 | address 10.42.0.2/24 279 | gateway 10.42.0.1 280 | 281 | EOF 282 | nameserver 9.9.9.9 283 | nameserver 149.112.112.112 284 | 285 | EOF 286 | ;; 287 | esac 288 | done 289 | fi 290 | 291 | echo "###################################" 292 | echo "Using following network interfaces:" 293 | cat /etc/network/interfaces 294 | echo "###################################" 295 | 296 | if _has_wifi && grep -qE "$wlan_lst" /etc/network/interfaces; then 297 | _logger "Configuring wifi..." 298 | _apk add wpa_supplicant 299 | _preserve "/etc/wpa_supplicant/wpa_supplicant.conf" 300 | install -m600 "${ovlpath}"/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf 301 | rc-service wpa_supplicant restart 302 | else 303 | _logger "No wifi interface or SSID/pass file supplied" 304 | fi 305 | 306 | _preserve "/etc/hostname" 307 | echo "alpine-headless" >/etc/hostname 308 | hostname -F /etc/hostname 309 | 310 | rc-service networking restart 311 | } 312 | 313 | _setup_gadget() { 314 | ## Load composite USB Serial/USB Ethernel driver & setup terminal. 315 | _logger "Enabling USB-gadget Serial and Ethernet ports" 316 | # Remove conflicting modules in case they were initially loaded (cmdline.txt). 317 | modprobe -r g_serial g_ether g_cdc 318 | modprobe g_cdc 319 | # Wait for g_cdc to settle and serial ports become available 320 | timeout 1 sh <<-EOF 321 | while ! grep -q "ttyGS" /proc/devices; do sleep 0.2; done 322 | EOF 323 | 324 | # Determine which gadget ID is connected with USB cable (assume just one max). 325 | # (setting console to unconnected serial port would block boot) 326 | gdgt_id="$( find /sys/class/udc/*/current_speed -exec \ 327 | sh -c 'grep -vq "UNKNOWN" "$0" && find ${0/current_speed/}device/gadget.* -maxdepth 0' {} \; \ 328 | | sed 's/\/.*gadget\.//' )" 329 | if [ -c /dev/ttyGS"${gdgt_id}" ]; then 330 | # Default serial config: xon/xoff flow control. 331 | stty -F /dev/ttyGS"${gdgt_id}" 332 | setconsole /dev/ttyGS"${gdgt_id}" 333 | # Notes to users willing to connect from Linux Ubuntu-based host terminal: 334 | # - user on host needs to be part of dialout group (reboot required), and 335 | # - disable spurious AT commands from ModemManager on host-side Gadget serial port 336 | # one may create a /etc/udev/rules.d/99-ttyacms-gadget.rules as per: 337 | # https://linux-tips.com/t/prevent-modem-manager-to-capture-usb-serial-devices/284/2 338 | # ATTRS{idVendor}=="0525" ATTRS{idProduct}=="a4aa", ENV{ID_MM_DEVICE_IGNORE}="1" 339 | else 340 | _logger "USB-gadget port not connected !" 341 | modprobe -r g_cdc 342 | fi 343 | } 344 | 345 | 346 | ############################################################################# 347 | ## Main 348 | 349 | # Redirect stdout and errors to console as service won't show messages 350 | exec 1>/dev/console 2>&1 351 | _logger "Alpine Linux headless bootstrap v$HDLSBSTRP_VERSION by macmpi" 352 | 353 | # Help randomness for wpa_supplicant and sshd (urandom until 3.16). 354 | rc-service seedrng restart || rc-service urandom restart 355 | 356 | # Setup USB gadget ports if some ports are enabled in peripheral mode. 357 | # Note: we assume dwc2/dwc3 is pre-loaded, we just check mode. 358 | gdgt_id="" 359 | find /sys/class/udc/*/is_a_peripheral -print0 2>/dev/null | \ 360 | xargs -0 cat 2>/dev/null | grep -q "0" && \ 361 | _setup_gadget 362 | 363 | # Determine ovl file location. 364 | # Grab used ovl filename from dmesg. 365 | ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )" 366 | if [ -f "${ovl}" ]; then 367 | ovlpath="$( dirname "$ovl" )" 368 | else 369 | # Search path again as mountpoint have been changed later in the boot process... 370 | ovl="$( basename "${ovl}" )" 371 | ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name "${ovl}" -exec dirname {} \; | head -1 ) 372 | ovl="${ovlpath}/${ovl}" 373 | fi 374 | 375 | # Create banner file. 376 | warn="" 377 | grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$? 378 | _is_ro() { return "$is_ro"; } 379 | 380 | _is_ro && warn="(remount partition rw!)" 381 | cat <<-EOF >/tmp/.trash/banner 382 | 383 | Alpine Linux headless bootstrap v$HDLSBSTRP_VERSION by macmpi 384 | 385 | You may want to delete/rename .apkovl file before reboot ${warn}: 386 | ${ovl} 387 | (can be done automatically with unattended script - see sample snippet) 388 | 389 | 390 | EOF 391 | 392 | _setup_networking 393 | 394 | # Test latest available version online. 395 | # Can be skipped by creating a 'opt-out'-named dummy file aside apkovl file. 396 | [ -f "${ovlpath}"/opt-out ] || _tst_version & 397 | 398 | # Setup sshd unless unattended.sh script prevents it. 399 | grep -q "^#NO_SSH$" "${ovlpath}"/unattended.sh >/dev/null 2>&1 \ 400 | || _setup_sshd 401 | 402 | _prep_cleanup 403 | _logger "Initial setup done, handing-over to clean-up" 404 | rc-service headless_cleanup start 405 | exit 0 406 | 407 | -------------------------------------------------------------------------------- /sample_auto-updt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Automated reboot after update is cancelled if: 5 | # - there is an opened ssh session 6 | # - unattended.sh script is provided 7 | 8 | # Uncomment line below to enable reboot after update 9 | #reboot 10 | 11 | -------------------------------------------------------------------------------- /sample_interfaces: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Sample network interfaces file 5 | 6 | auto lo 7 | iface lo inet loopback 8 | 9 | auto eth0 10 | iface eth0 inet dhcp 11 | 12 | auto wlan0 13 | iface wlan0 inet dhcp 14 | 15 | auto usb0 16 | iface usb0 inet static 17 | address 10.42.0.2/24 18 | gateway 10.42.0.1 19 | 20 | -------------------------------------------------------------------------------- /sample_unattended.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 4 | # SPDX-License-Identifier: MIT 5 | 6 | ## collection of few code snippets as sample unnatteded actions some may find usefull 7 | 8 | ## will run encapusated within headless_unattended OpenRC service 9 | 10 | # To prevent headless bootstrap script from starting sshd 11 | # only keep a single starting # on the line below 12 | ##NO_SSH 13 | 14 | # Uncomment to enable stdout and errors redirection to console (service won't show messages) 15 | # exec 1>/dev/console 2>&1 16 | 17 | # shellcheck disable=SC2142 # known special case 18 | alias _logger='logger -st "${0##*/}"' 19 | 20 | ## Obvious one; reminder: is run as background service 21 | _logger "hello world !!" 22 | sleep 60 23 | _logger "Finished script" 24 | ######################################################## 25 | 26 | 27 | ## This snippet removes apkovl file on volume after initial boot 28 | # grab used ovl filename from dmesg 29 | ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )" 30 | if [ -f "${ovl}" ]; then 31 | ovlpath="$( dirname "$ovl" )" 32 | else 33 | # search path again as mountpoint have been changed later in the boot process... 34 | ovl="$( basename "${ovl}" )" 35 | ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name "${ovl}" -exec dirname {} \; | head -1 ) 36 | ovl="${ovlpath}/${ovl}" 37 | fi 38 | 39 | # also works in case volume is mounted read-only 40 | grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$? 41 | _is_ro() { return "$is_ro"; } 42 | _is_ro && mount -o remount,rw "${ovlpath}" 43 | rm -f "${ovl}" 44 | _is_ro && mount -o remount,ro "${ovlpath}" 45 | 46 | ######################################################## 47 | 48 | 49 | ## This snippet configures Minimal diskless environment 50 | # note: with INTERFACESOPTS=none, no networking will be setup so it won't work after reboot! 51 | # Change it or run setup-interfaces in interractive mode afterwards (and lbu commit -d thenafter) 52 | 53 | _logger "Setting-up minimal environment" 54 | 55 | cat <<-EOF > /tmp/ANSWERFILE 56 | # base answer file for setup-alpine script 57 | 58 | # Do not set keyboard layout 59 | KEYMAPOPTS=none 60 | 61 | # Keep hostname 62 | HOSTNAMEOPTS="$(hostname)" 63 | 64 | # Set device manager to mdev 65 | DEVDOPTS=mdev 66 | 67 | # Contents of /etc/network/interfaces 68 | INTERFACESOPTS=none 69 | 70 | # Set Public nameserver 71 | DNSOPTS="-n 9.9.9.9" 72 | 73 | # Set timezone to UTC 74 | TIMEZONEOPTS="UTC" 75 | 76 | # set http/ftp proxy 77 | PROXYOPTS=none 78 | 79 | # Add first mirror (CDN) 80 | APKREPOSOPTS="-1" 81 | 82 | # Do not create any user 83 | USEROPTS=none 84 | 85 | # No Openssh 86 | SSHDOPTS=none 87 | 88 | # Use openntpd 89 | NTPOPTS="chrony" 90 | 91 | # No disk install (diskless) 92 | DISKOPTS=none 93 | 94 | # Setup storage for diskless (find boot directory in /media/xxxx/apk/.boot_repository) 95 | LBUOPTS="$( find /media -maxdepth 3 -type d -path '*/.*' -prune -o -type f -name '.boot_repository' -exec dirname {} \; | head -1 | xargs dirname )" 96 | APKCACHEOPTS="\$LBUOPTS/cache" 97 | 98 | EOF 99 | 100 | # trick setup-alpine to pretend existing SSH connection 101 | # and therefore keep (do not reset) network interfaces while running in background 102 | # requires alpine-conf 3.15.1 and later, available from Alpine 3.17 103 | SSH_CONNECTION="FAKE" setup-alpine -ef /tmp/ANSWERFILE 104 | lbu commit -d 105 | 106 | ######################################################## 107 | 108 | 109 | _logger "Finished unattended script" 110 | 111 | -------------------------------------------------------------------------------- /sample_wpa_supplicant.conf: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Sample wpa_supplicant.conf 5 | country=FR 6 | 7 | network={ 8 | key_mgmt=WPA-PSK 9 | ssid="mySSID" 10 | psk="myPassPhrase" 11 | } 12 | --------------------------------------------------------------------------------