From b008b486ae0e58edeb238621ea36dbcfe98c4619 Mon Sep 17 00:00:00 2001 From: PaulVua Date: Tue, 19 May 2026 11:58:19 +0200 Subject: [PATCH] v1.9.0: Enrolement automatique des capteurs sur le tailnet AirCarto (Tailscale/Headscale) - installation_part1.sh: install paquet Tailscale + sudoers /usr/bin/tailscale - services/tailscale_bootstrap.sh (nouveau): script idempotent d'enrolement au boot - services/setup_services.sh: service systemd nebuleair-tailscale-bootstrap (one-shot) - update_firmware.sh: nouvelle etape 3d 'Bootstrap Tailscale' (self-heal install + fetch authkey depuis data.nebuleair.fr/pro_4G/get_tailscale_key.php + enrolement). Fallback HTTPS->HTTP en attendant le cert TLS cote serveur. Permet l'acces SSH distant aux 200 capteurs deployes via le tailnet une fois que leur client a clique sur 'Update' dans l'admin web. Necessite l'endpoint serveur get_tailscale_key.php en place sur data.nebuleair.fr (a deployer en parallele cote AirCarto, auth par deviceID + rate limit + audit log). Co-Authored-By: Claude Opus 4.7 (1M context) --- VERSION | 2 +- changelog.json | 25 ++++++++++++ installation_part1.sh | 9 +++++ services/setup_services.sh | 30 ++++++++++++++ services/tailscale_bootstrap.sh | 57 ++++++++++++++++++++++++++ update_firmware.sh | 72 +++++++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 services/tailscale_bootstrap.sh diff --git a/VERSION b/VERSION index a7ee35a..f8e233b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.3 +1.9.0 diff --git a/changelog.json b/changelog.json index 66bdab7..0542337 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,30 @@ { "versions": [ + { + "version": "1.9.0", + "date": "2026-05-19", + "changes": { + "features": [ + "Enrôlement automatique des capteurs sur le tailnet AirCarto (Headscale) pour accès SSH distant via Tailscale", + "installation_part1.sh: installation du paquet Tailscale (curl|sh) + règle sudoers /usr/bin/tailscale *", + "services/tailscale_bootstrap.sh: script idempotent d'enrôlement au boot (hostname dérivé du deviceID: nebuleair-pro-)", + "services/setup_services.sh: nouveau service systemd nebuleair-tailscale-bootstrap (one-shot at boot, After=network-online+tailscaled)", + "update_firmware.sh: nouvelle étape 3d 'Bootstrap Tailscale' — self-heal install + fetch authkey depuis data.nebuleair.fr/pro_4G/get_tailscale_key.php + enrôlement" + ], + "improvements": [ + "update_firmware.sh: chmod 755 désormais appliqué aussi aux services/*.sh (cohérence avec le pattern existant des dossiers Python)", + "Auth de l'endpoint serveur basée sur le deviceID (déjà connu côté AirCarto, pas de nouveau secret à provisionner sur les capteurs)", + "Fallback HTTPS → HTTP pour le fetch authkey tant que data.nebuleair.fr n'a pas de certificat TLS (à retirer une fois le cert en place)" + ], + "fixes": [], + "compatibility": [ + "Capteurs déployés pré-v1.9.0: le bootstrap Tailscale s'auto-installe au prochain update OTA (self-heal binary + sudoers via /etc/sudoers.d/nebuleair-tailscale)", + "Capteurs sans connectivité au moment de l'update: l'enrôlement échoue silencieusement et sera retenté au prochain boot via le service systemd", + "Aucun secret committé dans le repo: la preauth key est fetchée à la volée depuis le serveur AirCarto" + ] + }, + "notes": "Permet l'accès SSH distant aux 200 capteurs déployés une fois leur client a cliqué sur 'Update' dans l'admin web — utile pour le support et le debug à distance sans avoir à demander au client d'intervenir. Côté serveur AirCarto, un nouvel endpoint data.nebuleair.fr/pro_4G/get_tailscale_key.php doit être déployé en parallèle (retourne la preauth key Headscale pour les deviceID valides, avec rate-limiting et audit log recommandés)." + }, { "version": "1.8.3", "date": "2026-05-13", diff --git a/installation_part1.sh b/installation_part1.sh index a78938d..db26b1b 100644 --- a/installation_part1.sh +++ b/installation_part1.sh @@ -29,6 +29,14 @@ sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 py info "Installing Python libraries..." sudo pip3 install pyserial requests adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib adafruit-circuitpython-ads1x15 nsrt-mk3-dev pytz --break-system-packages || error "Failed to install Python libraries." +# Install Tailscale (for remote SSH access via Headscale tailnet) +info "Installing Tailscale..." +if ! command -v tailscale >/dev/null 2>&1; then + curl -fsSL https://tailscale.com/install.sh | sh || warning "Tailscale install failed. Remote access via tailnet will be unavailable." +else + warning "Tailscale already installed. Skipping." +fi + # Clone the repository (check if it exists first) REPO_DIR="/var/www/nebuleair_pro_4g" if [[ -d "$REPO_DIR" ]]; then @@ -140,6 +148,7 @@ www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * www-data ALL=(ALL) NOPASSWD: /bin/systemctl * www-data ALL=(ALL) NOPASSWD: /usr/bin/pkill * www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/* +www-data ALL=(ALL) NOPASSWD: /usr/bin/tailscale * EOF # Validate the temporary file diff --git a/services/setup_services.sh b/services/setup_services.sh index 078de20..6ef1bb4 100644 --- a/services/setup_services.sh +++ b/services/setup_services.sh @@ -321,6 +321,28 @@ RemainAfterExit=yes WantedBy=multi-user.target EOL +# Create service file for Tailscale Bootstrap (one-shot at boot) +# Enrolls the device in the AirCarto Headscale tailnet if an authkey is +# present at /etc/tailscale/authkey and the device is not already enrolled. +# Safe to run repeatedly: idempotent and no-op when already enrolled. +cat > /etc/systemd/system/nebuleair-tailscale-bootstrap.service << 'EOL' +[Unit] +Description=NebuleAir Tailscale Bootstrap (enroll on tailnet at boot) +After=network-online.target tailscaled.service +Wants=network-online.target tailscaled.service + +[Service] +Type=oneshot +ExecStart=/var/www/nebuleair_pro_4g/services/tailscale_bootstrap.sh +User=root +StandardOutput=append:/var/www/nebuleair_pro_4g/logs/tailscale_bootstrap.log +StandardError=append:/var/www/nebuleair_pro_4g/logs/tailscale_bootstrap_errors.log +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOL + # Create service file for RTC Save to DB (long-running service, no timer) # Unmask first in case it was previously masked (symlink to /dev/null) # so the cat below writes a real file, not into /dev/null. @@ -364,6 +386,14 @@ systemctl enable nebuleair-cpu-power.service systemctl start nebuleair-cpu-power.service echo "Started nebuleair-cpu-power service" +# Enable Tailscale bootstrap service (runs once at boot to enroll on tailnet) +# Ensure the bootstrap script is executable — git config core.fileMode=false +# strips the executable bit on pull, so chmod +x here is required. +chmod +x /var/www/nebuleair_pro_4g/services/tailscale_bootstrap.sh +systemctl enable nebuleair-tailscale-bootstrap.service +systemctl start nebuleair-tailscale-bootstrap.service +echo "Started nebuleair-tailscale-bootstrap service" + # Enable and start RTC Save to DB service (long-running, no timer) # Use restart instead of start to pick up new content if the service was # already running with an older version of the file. diff --git a/services/tailscale_bootstrap.sh b/services/tailscale_bootstrap.sh new file mode 100644 index 0000000..ebe3903 --- /dev/null +++ b/services/tailscale_bootstrap.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# File: /var/www/nebuleair_pro_4g/services/tailscale_bootstrap.sh +# Purpose: Enroll the device into the AirCarto Headscale tailnet at boot. +# Idempotent: exits cleanly if already enrolled or if authkey is missing. +# +# Invoked by nebuleair-tailscale-bootstrap.service (one-shot at boot). +# Also safe to call manually from update_firmware.sh. + +set -u + +LOGIN_SERVER="https://headscale.aircarto.fr" +AUTHKEY_FILE="/etc/tailscale/authkey" +DB_FILE="/var/www/nebuleair_pro_4g/sqlite/sensors.db" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] tailscale-bootstrap: $1"; } + +if ! command -v tailscale >/dev/null 2>&1; then + log "tailscale binary not found, skipping (install via update_firmware.sh)" + exit 0 +fi + +if tailscale ip -4 >/dev/null 2>&1; then + log "already enrolled (IP $(tailscale ip -4)), nothing to do" + exit 0 +fi + +if [[ ! -s "$AUTHKEY_FILE" ]]; then + log "no authkey at $AUTHKEY_FILE, skipping (will be fetched by update_firmware.sh)" + exit 0 +fi + +if [[ ! -f "$DB_FILE" ]]; then + log "database $DB_FILE not found, cannot derive hostname" + exit 1 +fi + +deviceID=$(sqlite3 "$DB_FILE" "SELECT value FROM config_table WHERE key='deviceID'" 2>/dev/null) +if [[ -z "$deviceID" ]]; then + log "deviceID empty in config_table, cannot enroll" + exit 1 +fi + +# Tailscale hostnames must be RFC 1123 (lowercase letters, digits, hyphens). +deviceID_clean=$(echo "$deviceID" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/-\+/-/g; s/^-//; s/-$//') +hostname="nebuleair-pro-${deviceID_clean}" +authkey=$(cat "$AUTHKEY_FILE") + +log "enrolling as $hostname..." +if tailscale up \ + --login-server="$LOGIN_SERVER" \ + --authkey="$authkey" \ + --hostname="$hostname"; then + log "enrollment OK, IP $(tailscale ip -4)" +else + log "enrollment FAILED (will retry at next boot or update)" + exit 1 +fi diff --git a/update_firmware.sh b/update_firmware.sh index edae6b5..dd8f127 100755 --- a/update_firmware.sh +++ b/update_firmware.sh @@ -79,6 +79,7 @@ sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py sudo chmod 755 /var/www/nebuleair_pro_4g/MPPT/*.py 2>/dev/null sudo chmod 755 /var/www/nebuleair_pro_4g/wifi/*.py 2>/dev/null +sudo chmod 755 /var/www/nebuleair_pro_4g/services/*.sh 2>/dev/null check_status "File permissions update" # Step 3b: Ensure Apache/PHP config allows file uploads @@ -125,6 +126,77 @@ else print_status "⚠ setup_services.sh not found, skipping" fi +# Step 3d: Bootstrap Tailscale (install + fetch authkey + enroll on tailnet) +# Self-heal for sensors deployed before v1.9.0 that never had Tailscale. +# - Installs the tailscale binary if missing. +# - Drops a sudoers rule in /etc/sudoers.d/ (safer than touching /etc/sudoers). +# - Fetches the preauth key from data.nebuleair.fr if not already on disk. +# HTTPS → HTTP fallback because data.nebuleair.fr may not have TLS yet. +# TODO: drop the HTTP fallback once the cert is in place. +# - Triggers the bootstrap script to enroll on the tailnet. +print_status "" +print_status "Step 3d: Bootstrap Tailscale..." + +# 3d.1: Install tailscale binary if absent +if ! command -v tailscale >/dev/null 2>&1; then + print_status "Installing Tailscale..." + curl -fsSL https://tailscale.com/install.sh | sh + check_status "Tailscale install" +else + print_status "ℹ Tailscale already installed ($(tailscale version | head -1))" +fi + +# 3d.2: Self-heal sudoers rule (for sensors installed pre-v1.9.0) +TAILSCALE_SUDOERS=/etc/sudoers.d/nebuleair-tailscale +if [ ! -f "$TAILSCALE_SUDOERS" ]; then + echo "www-data ALL=(ALL) NOPASSWD: /usr/bin/tailscale *" | sudo tee "$TAILSCALE_SUDOERS" > /dev/null + sudo chmod 0440 "$TAILSCALE_SUDOERS" + if sudo visudo -c -f "$TAILSCALE_SUDOERS" >/dev/null 2>&1; then + print_status "✓ Sudoers rule for tailscale added" + else + sudo rm -f "$TAILSCALE_SUDOERS" + print_status "⚠ Tailscale sudoers rule failed validation, removed" + fi +fi + +# 3d.3: Fetch preauth key from data.nebuleair.fr if not already present +AUTHKEY_FILE=/etc/tailscale/authkey +if [ ! -s "$AUTHKEY_FILE" ]; then + deviceID=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db \ + "SELECT value FROM config_table WHERE key='deviceID'" 2>/dev/null) + if [ -z "$deviceID" ]; then + print_status "⚠ deviceID empty, cannot fetch tailscale authkey" + else + print_status "Fetching tailscale authkey for deviceID=$deviceID..." + URL_HTTPS="https://data.nebuleair.fr/pro_4G/get_tailscale_key.php?deviceID=${deviceID}" + URL_HTTP="http://data.nebuleair.fr/pro_4G/get_tailscale_key.php?deviceID=${deviceID}" + authkey=$(curl -fsS --max-time 30 "$URL_HTTPS" 2>/dev/null || \ + curl -fsS --max-time 30 "$URL_HTTP" 2>/dev/null || true) + if [[ "$authkey" == hskey-auth-* ]]; then + sudo mkdir -p /etc/tailscale + echo "$authkey" | sudo tee "$AUTHKEY_FILE" > /dev/null + sudo chmod 600 "$AUTHKEY_FILE" + sudo chown root:root "$AUTHKEY_FILE" + print_status "✓ Tailscale authkey stored at $AUTHKEY_FILE" + else + print_status "⚠ Could not fetch tailscale authkey (server unreachable or invalid response)" + fi + fi +else + print_status "ℹ Tailscale authkey already present" +fi + +# 3d.4: Trigger the bootstrap (idempotent, no-op if already enrolled) +BOOTSTRAP=/var/www/nebuleair_pro_4g/services/tailscale_bootstrap.sh +if [ -x "$BOOTSTRAP" ]; then + sudo "$BOOTSTRAP" || print_status "⚠ Tailscale bootstrap returned non-zero (see logs/tailscale_bootstrap.log)" +elif [ -f "$BOOTSTRAP" ]; then + sudo chmod +x "$BOOTSTRAP" + sudo "$BOOTSTRAP" || print_status "⚠ Tailscale bootstrap returned non-zero (see logs/tailscale_bootstrap.log)" +else + print_status "⚠ Bootstrap script not found at $BOOTSTRAP" +fi + # Step 4: Restart critical services if they exist print_status "" print_status "Step 4: Managing system services..."