Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ jobs:
"https://downloads.raspberrypi.org/raspios_lite_arm64/images/$BASEDIR/$FILE"
ls -la src/image-raspberrypiarm64/

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable

- name: Build + stage hub-os-config (captive-portal onboarding)
run: |
set -euo pipefail
# Single source of truth for the repo + pinned commit.
source src/modules/albyhub/config
git clone --quiet "$HUBOSCONFIG_REPO" /tmp/hub-os-config
git -C /tmp/hub-os-config checkout --quiet "$HUBOSCONFIG_COMMIT"
( cd /tmp/hub-os-config \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -ldflags "-s -w -X main.version=$HUBOSCONFIG_COMMIT" \
-o /tmp/hub-os-config-bin ./cmd/hub-os-config )
DEST=src/modules/albyhub/filesystem/root/usr/local/bin
mkdir -p "$DEST"
install -m 0755 /tmp/hub-os-config-bin "$DEST/hub-os-config"
ls -l "$DEST/hub-os-config"

- name: Build image
run: sudo bash ./build.sh

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# build artifacts
workspace/
# hub-os-config binary staged into the overlay by CI (built from source, not committed)
src/modules/albyhub/filesystem/root/usr/local/bin/hub-os-config
*.img
*.img.xz
*.img.zip
Expand Down
50 changes: 33 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ Lightning) with **zero manual install** — flash, boot, open
Manually installing Alby Hub on a Pi means SSH + `curl | bash` + a signature
prompt + systemd setup. This image bakes all of that in, so onboarding is just:

1. Flash the image with **Raspberry Pi Imager 2.0+** (“Use custom”), set WiFi + password.
1. Flash the image with **any** flasher — no customization needed.
2. Insert the card, power on, wait ~2 min.
3. Open `http://albyhub.local`, set a password, save your seed, open a channel.
3. On your phone/laptop, join the **`albyhub-setup`** WiFi. A setup page opens
automatically (captive portal) — pick your home WiFi and confirm.
4. The Pi reboots onto your network. Open `http://albyhub.local`, set a
password, save your seed, open a channel.

No terminal. Tested target: Raspberry Pi Zero 2 W (512 MB) and up.
No terminal, no Imager customization. Tested target: Raspberry Pi Zero 2 W
(512 MB) and up.

## What's baked in (`src/modules/albyhub`)

Expand All @@ -25,13 +29,25 @@ No terminal. Tested target: Raspberry Pi Zero 2 W (512 MB) and up.
- Port-80 binding via systemd `AmbientCapabilities`; `ld.so.conf.d` entry for the bundled libraries.
- `gpu_mem=16` (frees ~48 MB) and `vm.swappiness=10` + 1 GB swap.
- `avahi-daemon` for `albyhub.local`; default hostname `albyhub`.

## What Imager handles at flash time (not baked)

WiFi credentials, SSH enable/key, user/password, locale/timezone, hostname —
applied on first boot. **Use Raspberry Pi Imager 2.0 or newer** — older versions
don't reliably apply customization to current Raspberry Pi OS (cloud-init), which
silently drops the SSH/WiFi settings.
- **Captive-portal onboarding** ([`bumi/hub-os-config`](https://github.com/bumi/hub-os-config),
pinned by commit): on first boot with no internet it raises an open
`albyhub-setup` AP serving a setup page where the user picks their WiFi and a
few hub options, then reboots online. The same UI stays reachable afterwards
on `:8090`. Built from source for arm64 and staged into the image by CI.

## First-boot onboarding (captive portal)

No Imager customization is required. On first boot the device checks for
internet; finding none, it broadcasts the open **`albyhub-setup`** WiFi and
serves a captive portal on `192.168.4.1`. The user joins from a phone, the setup
page opens automatically, they choose their home network, and the Pi reboots
onto it. WiFi credentials are stored by NetworkManager and reconnect on boot.

The hub stays off port 80 until the device is online (`albyhub-wait-online`, an
`ExecStartPre` gate) so it never fights the setup portal for `:80`. A WiFi
regulatory country (`ALBYHUB_WIFI_COUNTRY`, default `US`) is baked in — AP mode
needs one set. SSH-based / Imager WiFi setup still works too: if WiFi is already
configured, the portal never appears and the device boots straight online.

## Build

Expand All @@ -46,10 +62,10 @@ CI (`.github/workflows/build.yml`) builds on tag push and publishes
## Distribution

Download the latest `HubOS-<ver>-arm64.img.xz` from the
[latest GitHub release](https://github.com/getAlby/hub-os/releases/latest), then flash it with
**Raspberry Pi Imager** ("Use custom") — or any flasher (Etcher, `dd`). Set your
WiFi + hostname in Imager's customisation, write, boot, and open
`http://albyhub.local`.
[latest GitHub release](https://github.com/getAlby/hub-os/releases/latest), then
flash it with **any** tool (Raspberry Pi Imager, Etcher, `dd`) — no
customization step. Boot, join the `albyhub-setup` WiFi to configure your
network, then open `http://albyhub.local`.

## Configuration

Expand All @@ -75,6 +91,6 @@ app auto-updater off entirely.

## Status

Builds green in CI and boots on a Raspberry Pi Zero 2 W (setup screen serves on
`:80`). Flash with **Raspberry Pi Imager 2.0+** so first-boot customization
applies. Not yet tagged as a release.
Builds green in CI and boots on a Raspberry Pi Zero 2 W (hub UI serves on `:80`).
Captive-portal onboarding (`bumi/hub-os-config`) is newly integrated and still
being validated on hardware. Not yet tagged as a release.
5 changes: 5 additions & 0 deletions src/config
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export DIST_NAME=HubOS
export DIST_VERSION=0.1.0

# Hostname the device advertises (avahi -> albyhub.local). Without this, the base
# module defaults the hostname to the lowercased DIST_NAME ("hubos"), which would
# break the albyhub.local onboarding story.
export BASE_OVERRIDE_HOSTNAME=albyhub

# rpi-imager metadata
export RPI_IMAGER_NAME="Alby Hub"
export RPI_IMAGER_DESCRIPTION="Self-custodial Lightning wallet on your Pi. No full node, no manual install."
Expand Down
17 changes: 17 additions & 0 deletions src/modules/albyhub/config
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,20 @@ export ALBYHUB_HOME="/opt/albyhub"
export ALBYHUB_USER="albyhub"
export ALBYHUB_SWAP_MB="1024"
export ALBYHUB_HOSTNAME="albyhub"

# --- captive-portal onboarding (bumi/hub-os-config) ---
# A small Go service that, on first boot with no internet, raises an open WiFi
# AP ("albyhub-setup") hosting a captive portal where the user picks their WiFi
# and sets a few hub options, then reboots online. Pinned to a commit because
# the upstream repo has no tagged releases yet. The arm64 binary is built on the
# CI host and staged into the overlay before the image build (see
# .github/workflows/build.yml); a local build without that step just ships
# without onboarding (the chroot script skips it gracefully).
export HUBOSCONFIG_REPO="https://github.com/bumi/hub-os-config"
export HUBOSCONFIG_COMMIT="1c7203303cf01ee1e5af309ba4861db5bf151332"

# WiFi regulatory domain (ISO 3166-1 alpha-2). REQUIRED for the setup AP to come
# up — a fresh image leaves the radio in the restrictive "world" domain, which
# blocks AP mode. Channel 6 @ 2.4GHz is permitted in essentially every domain,
# so this just unblocks the radio; installers elsewhere can change it.
export ALBYHUB_WIFI_COUNTRY="US"
18 changes: 18 additions & 0 deletions src/modules/albyhub/filesystem/root/etc/hub-os-config/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# hub-os-config settings for the Alby Hub image.
# Most values are upstream defaults (see bumi/hub-os-config config.toml.example);
# only the entries that matter for this image are pinned here explicitly.

[access_point]
# Open setup network the user joins to reach the captive portal.
ssid = "albyhub-setup"

[web]
# Port the config UI listens on once online (Normal Mode). Alby Hub itself runs
# on :80 in this image, so the config UI sits alongside it on :8090
# (http://albyhub.local:8090). It does NOT collide with the hub.
normal_port = 8090

[paths]
# The Advanced section of the portal edits these keys in the hub's env file,
# preserving everything else.
albyhub_env = "/opt/albyhub/.env"
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ RestartSec=1
User=albyhub
# Grant port-80 binding at runtime (file setcap caps don't survive the image build)
AmbientCapabilities=CAP_NET_BIND_SERVICE
# Don't claim :80 until the device is actually online. On a fresh image with no
# WiFi, hub-os-config serves its setup captive portal on :80 (via the AP gateway)
# — the hub must stay off that port until the user has connected. Once online the
# probe returns immediately and the hub starts normally. wait-online is a no-op
# if hub-os-config isn't installed (local builds without the onboarding binary).
ExecStartPre=/usr/local/bin/albyhub-wait-online
# wait-online may block indefinitely on a never-configured device; that's fine —
# the captive portal owns :80 meanwhile. Don't let the start timeout kill it.
TimeoutStartSec=infinity
ExecStart=/opt/albyhub/bin/albyhub
# Cap CPU so the wallet never starves the rest of a small Pi
CPUQuota=90%
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[Unit]
Description=Alby Hub OS configuration service (captive-portal onboarding)
Documentation=https://github.com/bumi/hub-os-config
After=NetworkManager.service
Wants=NetworkManager.service

[Service]
Type=simple
# Wait for NetworkManager to finish startup (devices managed), and make sure the
# WiFi radio is usable: a fresh Pi image often has wlan0 rfkill-soft-blocked, and
# AP mode needs the radio on. The leading "-" makes each step non-fatal.
# (The WiFi regulatory country that AP mode also requires is baked into the image
# by the albyhub module's start_chroot_script — see ALBYHUB_WIFI_COUNTRY.)
ExecStartPre=-/usr/sbin/rfkill unblock wifi
ExecStartPre=-/usr/bin/nm-online -s -q -t 30
ExecStartPre=-/usr/bin/nmcli radio wifi on
ExecStart=/usr/local/bin/hub-os-config run
Restart=on-failure
RestartSec=5
# Runs as root: needs nmcli, write access to /opt/albyhub/.env, and reboot.
User=root
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Block until the device has working internet, so Alby Hub doesn't claim port 80
# while hub-os-config is serving its setup captive portal there (also :80, via
# the AP gateway). Used as ExecStartPre of albyhub.service.
#
# - Fresh image, no WiFi: blocks here indefinitely. The hub never binds :80, so
# the captive portal owns it. Once the user connects (and the device reboots
# online), this returns immediately and the hub starts.
# - No onboarding installed (local build without the hub-os-config binary):
# nothing else wants :80, so skip the wait entirely and let the hub start now.
set -u

# Only gate when the captive-portal service is actually present.
if [ ! -x /usr/local/bin/hub-os-config ]; then
exit 0
fi

URL="https://getalby.com/api/internal/info"
until curl -fsS --max-time 5 -o /dev/null "$URL"; do
sleep 5
done
29 changes: 29 additions & 0 deletions src/modules/albyhub/start_chroot_script
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ unpack /filesystem/root /
# --- dependencies ---
apt-get update
apt-get install -y --no-install-recommends bzip2 ca-certificates gnupg avahi-daemon libcap2-bin unattended-upgrades
# Captive-portal onboarding deps: NetworkManager (already default on RPi OS, but
# pin it), its dnsmasq backend for AP-mode DHCP/DNS, and the radio tools the
# hub-os-config systemd unit calls (rfkill / iw / nmcli).
apt-get install -y --no-install-recommends network-manager dnsmasq-base rfkill iw

# --- dedicated, unprivileged service user ---
if ! id -u "$ALBYHUB_USER" >/dev/null 2>&1; then
Expand Down Expand Up @@ -83,6 +87,31 @@ sed -i "s/127.0.1.1.*/127.0.1.1\t${ALBYHUB_HOSTNAME}/" /etc/hosts || true
# --- auto-update: make the weekly updater executable ---
chmod +x "$ALBYHUB_HOME/autoupdate.sh"

# --- captive-portal onboarding (bumi/hub-os-config) ---
# The :80 gate helper always ships; it self-disables when the onboarding binary
# is absent (see the script), so local builds without staging still work.
chmod +x /usr/local/bin/albyhub-wait-online

# WiFi regulatory country — REQUIRED for the setup AP. Use the RPi-blessed path
# when available (handles rfkill + persistence), else write the legacy regdb.
if command -v raspi-config >/dev/null 2>&1; then
raspi-config nonint do_wifi_country "$ALBYHUB_WIFI_COUNTRY" || true
else
echo "REGDOMAIN=$ALBYHUB_WIFI_COUNTRY" > /etc/default/crda
fi

# The arm64 binary is staged into the overlay by CI (see build.yml). If present,
# finish wiring it up; otherwise skip onboarding so the build still succeeds.
if [ -x /usr/local/bin/hub-os-config ]; then
install -d -m 0755 /var/lib/hub-os-config # state.json lives here
systemctl_if_exists enable NetworkManager.service
systemctl_if_exists enable hub-os-config.service
else
echo "WARN: /usr/local/bin/hub-os-config not staged — image ships WITHOUT captive-portal onboarding" >&2
rm -f /etc/systemd/system/hub-os-config.service
rm -rf /etc/hub-os-config
fi

# --- enable services (start happens on first boot, not in chroot) ---
systemctl_if_exists enable albyhub.service
systemctl_if_exists enable avahi-daemon.service
Expand Down