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) <noreply@anthropic.com>
This commit is contained in:
@@ -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-<id>)",
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
57
services/tailscale_bootstrap.sh
Normal file
57
services/tailscale_bootstrap.sh
Normal file
@@ -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
|
||||
@@ -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..."
|
||||
|
||||
Reference in New Issue
Block a user