diff --git a/README.md b/README.md index 26f278a..1e1d737 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Home IoT SCADA Stack for openSUSE MicroOS +# Home IoT SCADA Stack for openSUSE Leap Micro -A comprehensive, containerized Home IoT SCADA (Supervisory Control and Data Acquisition) Stack built with Podman for resiliency and security on an openSUSE MicroOS host. +A comprehensive, containerized Home IoT SCADA (Supervisory Control and Data Acquisition) Stack built with Podman for resiliency and security on an openSUSE Leap Micro host. ## Credits @@ -8,13 +8,13 @@ This project was 99% developed by AI assistants (Gemini and GitHub Copilot). The ## Features -* **Host OS:** Optimized for **openSUSE MicroOS** (or other transactional OS) for enhanced stability and rollback capability. +* **Host OS:** Optimized for **openSUSE Leap Micro** (or other transactional OS) for enhanced stability and rollback capability. * **Container Runtime:** Uses **Podman** for managing containers, networks, and persistent volumes. * **Core Components:** Integrates **MQTT Broker** (Mosquitto), **Time Series Database** (InfluxDB), **Visualization** (Grafana), **Automation** (Node-RED), **NVR** (Frigate with Double-Take facial recognition), and **Zigbee Gateway** (Zigbee2MQTT). -* **Reverse Proxy:** Nginx-based reverse proxy with hostname-based routing for all services, including openSUSE Cockpit web console. +* **Reverse Proxy:** Nginx-based reverse proxy with hostname-based routing for all services, including openSUSE Cockpit web console. Nginx configuration is dynamically generated based on running services to prevent startup failures. * **Security:** Uses `create_secrets.sh` to generate unique, random, 64-character passwords/tokens for sensitive environment variables. * **External Storage:** Includes logic to mount an **SMB/CIFS** share for Frigate recordings on the host machine. -* **Resilience:** The `startup.sh` script continues running even if individual service starts fail, providing a complete status report. +* **Resilience:** The `startup.sh` script continues running even if individual service starts fail, providing a complete status report. Nginx automatically adapts to only proxy running services. ## System Requirements @@ -34,8 +34,8 @@ This project was 99% developed by AI assistants (Gemini and GitHub Copilot). The ### Software Requirements -* **Operating System:** openSUSE MicroOS (or compatible transactional Linux distribution) -* **Container Runtime:** Podman (installed by default on MicroOS) +* **Operating System:** openSUSE Leap Micro (or compatible transactional Linux distribution) +* **Container Runtime:** Podman (installed by default on Leap Micro) * **Package Dependencies:** * `cifs-utils` - Required for SMB/CIFS share mounting (only if using NVR/Frigate) * `sudo` - Required for mounting shares and system operations @@ -61,11 +61,11 @@ This project was 99% developed by AI assistants (Gemini and GitHub Copilot). The Follow these steps to set up and run the entire stack. -### 1. Prerequisites (openSUSE MicroOS) +### 1. Prerequisites (openSUSE Leap Micro) You must have the following installed on your host machine: -* **Podman:** Installed by default on MicroOS. +* **Podman:** Installed by default on Leap Micro. * **cifs-utils:** Required only if you plan to use the NVR (Frigate) with SMB share mounting. Use `transactional-update` to install this package permanently: ```bash @@ -105,7 +105,7 @@ The script will ask you to choose between: After the automatic setup, you must manually edit the `secrets.env` file to configure: * `ZIGBEE_DEVICE_PATH` - Update with the path to your Zigbee adapter (e.g., `/dev/ttyACM0` or `/dev/serial/by-id/...`) -* `PODMAN_SOCKET_PATH` - Update for Node-RED integration. On modern Podman/MicroOS systems, this is typically: +* `PODMAN_SOCKET_PATH` - Update for Node-RED integration. On modern Podman/Leap Micro systems, this is typically: ```bash PODMAN_SOCKET_PATH=/run/user/$(id -u)/podman/podman.sock @@ -278,7 +278,7 @@ podman logs codesysgateway | **Mosquitto** | MQTT Broker | Port 1883 (Internal/External) | IoT/SCADA modes only | | **InfluxDB** | Time-Series Database | Port 8086 (Internal/External) | IoT/SCADA modes only | -**Note:** Cockpit is installed by default on openSUSE MicroOS. If for any reason it is not installed or running, you can install and enable it with: +**Note:** Cockpit is installed by default on openSUSE Leap Micro. If for any reason it is not installed or running, you can install and enable it with: ```bash sudo transactional-update pkg install cockpit sudo reboot @@ -301,7 +301,7 @@ sudo systemctl enable --now cockpit.socket ## Troubleshooting -* **openSUSE MicroOS Updates:** Use `sudo transactional-update` for package management and system upgrades, followed by a reboot. +* **openSUSE Leap Micro Updates:** Use `sudo transactional-update` for package management and system upgrades, followed by a reboot. * **Container Logs:** If a container starts with a FAILURE status, check the logs for detailed errors: @@ -311,6 +311,24 @@ podman logs * **Zigbee Adapter:** If zigbee2mqtt fails, ensure the `ZIGBEE_DEVICE_PATH` is correct and that the host user has the necessary permissions. +* **Nginx Dynamic Configuration:** The nginx reverse proxy is configured dynamically based on which services are actually running. This prevents nginx startup failures when some services are not configured or fail to start. During startup: + 1. All backend services are started first + 2. The system waits 3 seconds for services to stabilize + 3. Running services are detected using `podman ps` + 4. Nginx configuration is generated with only the running services + 5. Nginx starts last as the reverse proxy + +This means if a service like zigbee2mqtt is not configured or fails to start, nginx will automatically exclude it from the configuration and start successfully. You'll see output like: + +``` +Waiting 3 seconds for services to stabilize... +Checking which services are running and generating nginx configuration... + [ok] Grafana is running - adding to nginx config + [ok] Node-RED is running - adding to nginx config + [INFO] Zigbee2MQTT is not running - skipping from nginx config +Starting nginx (reverse proxy) after all upstream services... +``` + * **Port 80 Permission Error (Rootless Podman):** If you encounter a permission error when starting the nginx container (attempting to bind to port 80), this is because ports below 1024 are considered "privileged ports" and normally require root access. When running Podman in rootless mode (as a non-root user), you may see an error like: ``` @@ -344,10 +362,10 @@ Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_u Allowing unprivileged users to bind to privileged ports (ports < 1024) reduces a traditional security boundary in Unix-like systems. Historically, only root could bind to these ports, which prevented non-root processes from impersonating system services. **When to use this workaround:** -* ✅ **Recommended for:** Single-user systems, home lab environments, personal IoT setups -* ✅ **Safe when:** You trust all users on the system and understand the security tradeoff -* ⚠️ **Use with caution in:** Multi-user environments or systems where additional security isolation is needed -* ❌ **Not recommended for:** Production servers with untrusted users or strict security requirements +* [OK] **Recommended for:** Single-user systems, home lab environments, personal IoT setups +* [OK] **Safe when:** You trust all users on the system and understand the security tradeoff +* [WARNING] **Use with caution in:** Multi-user environments or systems where additional security isolation is needed +* [X] **Not recommended for:** Production servers with untrusted users or strict security requirements **Alternative approaches:** * Run nginx on a higher port (e.g., 8080) and use port forwarding at the router/firewall level diff --git a/startup.sh b/startup.sh index 9f47b05..c278136 100755 --- a/startup.sh +++ b/startup.sh @@ -244,6 +244,257 @@ NGINX_EOF echo "Nginx configuration generated at ${nginx_conf_file}" } +# --- Function to generate nginx config based on actually running services --- +generate_nginx_config_from_running_services() { + local nginx_conf_file="./nginx/nginx.conf" + + echo "Checking which services are running and generating nginx configuration..." + + # Check which services are actually running + local running_services=$(podman ps --format '{{.Names}}' 2>/dev/null || echo "") + + # Start building the nginx config + cat > "${nginx_conf_file}" << 'NGINX_EOF' +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Default server - redirect to available services + server { + listen 80 default_server; + server_name _; + + location / { + return 200 'Home IoT/SCADA Stack

Home IoT/SCADA Stack

'; + add_header Content-Type text/html; + } + } +NGINX_EOF + + local services_html="" + + # Check and add Grafana if running + if echo "$running_services" | grep -q "^grafana$"; then + echo " [ok] Grafana is running - adding to nginx config" + cat >> "${nginx_conf_file}" << NGINX_EOF + + # Grafana + server { + listen 80; + server_name ${GRAFANA_HOSTNAME}.${BASE_DOMAIN}; + + location / { + proxy_pass http://grafana:3000; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + } +NGINX_EOF + services_html+="
  • Grafana
  • " + else + echo " [INFO] Grafana is not running - skipping from nginx config" + fi + + # Check and add Node-RED if running + if echo "$running_services" | grep -q "^nodered$"; then + echo " [ok] Node-RED is running - adding to nginx config" + cat >> "${nginx_conf_file}" << NGINX_EOF + + # Node-RED + server { + listen 80; + server_name ${NODERED_HOSTNAME}.${BASE_DOMAIN}; + + location / { + proxy_pass http://nodered:1880; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +NGINX_EOF + services_html+="
  • Node-RED
  • " + else + echo " [INFO] Node-RED is not running - skipping from nginx config" + fi + + # Check and add Zigbee2MQTT if running + if echo "$running_services" | grep -q "^zigbee2mqtt$"; then + echo " [ok] Zigbee2MQTT is running - adding to nginx config" + cat >> "${nginx_conf_file}" << NGINX_EOF + + # Zigbee2MQTT + server { + listen 80; + server_name ${ZIGBEE2MQTT_HOSTNAME}.${BASE_DOMAIN}; + + location / { + proxy_pass http://zigbee2mqtt:8080; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +NGINX_EOF + services_html+="
  • Zigbee2MQTT
  • " + else + echo " [INFO] Zigbee2MQTT is not running - skipping from nginx config" + fi + + # Check and add Frigate if running + if echo "$running_services" | grep -q "^frigate$"; then + echo " [ok] Frigate is running - adding to nginx config" + cat >> "${nginx_conf_file}" << NGINX_EOF + + # Frigate NVR + server { + listen 80; + server_name ${FRIGATE_HOSTNAME}.${BASE_DOMAIN}; + + location / { + proxy_pass http://frigate:5000; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + } +NGINX_EOF + services_html+="
  • Frigate NVR
  • " + else + echo " [INFO] Frigate is not running - skipping from nginx config" + fi + + # Check and add Double-Take if running + if echo "$running_services" | grep -q "^doubletake$"; then + echo " [ok] Double-Take is running - adding to nginx config" + cat >> "${nginx_conf_file}" << NGINX_EOF + + # Double-Take (Facial Recognition for Frigate) + server { + listen 80; + server_name ${DOUBLETAKE_HOSTNAME}.${BASE_DOMAIN}; + + location / { + proxy_pass http://doubletake:3000; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + } +NGINX_EOF + services_html+="
  • Double-Take
  • " + else + echo " [INFO] Double-Take is not running - skipping from nginx config" + fi + + # Check and add InfluxDB if running + if echo "$running_services" | grep -q "^influxdb$"; then + echo " [ok] InfluxDB is running (available on port 8086)" + else + echo " [INFO] InfluxDB is not running" + fi + + # Check and add Mosquitto if running + if echo "$running_services" | grep -q "^mosquitto$"; then + echo " [ok] Mosquitto is running (available on port 1883)" + else + echo " [INFO] Mosquitto is not running" + fi + + # Always add Cockpit (openSUSE web console) proxy - assuming it runs on host + cat >> "${nginx_conf_file}" << NGINX_EOF + + # openSUSE Cockpit Web Console + server { + listen 80; + server_name ${COCKPIT_HOSTNAME}.${BASE_DOMAIN}; + + location / { + proxy_pass https://host.containers.internal:9090; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_ssl_verify off; + } + } +} +NGINX_EOF + + services_html+="
  • openSUSE Cockpit
  • " + + # Update the services list in the default page + sed -i "s|SERVICES_LIST|${services_html}|g" "${nginx_conf_file}" + + echo "Nginx configuration generated at ${nginx_conf_file} based on running services" +} + +# --- Function to check nginx.conf permissions --- +check_and_fix_nginx_permissions() { + local nginx_conf_file="./nginx/nginx.conf" + + if [ ! -f "${nginx_conf_file}" ]; then + echo "WARNING: nginx.conf not found at ${nginx_conf_file}. It will be generated." + return 0 + fi + + echo "" + echo "Checking nginx.conf permissions..." + + # Check file permissions + local file_perms=$(stat -c "%a" "${nginx_conf_file}" 2>/dev/null || stat -f "%OLp" "${nginx_conf_file}" 2>/dev/null) + local file_owner=$(stat -c "%u" "${nginx_conf_file}" 2>/dev/null || stat -f "%u" "${nginx_conf_file}" 2>/dev/null) + local current_uid=$(id -u) + + echo " File permissions: ${file_perms}" + echo " File owner UID: ${file_owner}" + echo " Current user UID: ${current_uid}" + + # Warn if permissions are not 644 or more restrictive + if [ "${file_perms}" != "644" ] && [ "${file_perms}" != "444" ] && [ "${file_perms}" != "400" ] && [ "${file_perms}" != "600" ]; then + echo " [WARNING] File permissions are ${file_perms}. Recommended: 644" + echo " To fix: chmod 644 ${nginx_conf_file}" + else + echo " [ok] File permissions are acceptable" + fi + + # Warn if file is not owned by current user + if [ "${file_owner}" != "${current_uid}" ]; then + echo " [WARNING] File is owned by UID ${file_owner}, but current user is UID ${current_uid}" + echo " To fix: chown ${current_uid} ${nginx_conf_file}" + else + echo " [ok] File ownership is correct" + fi + + echo "" +} + # ---------------------------------------------------------------------- # --- FIRST-RUN CONFIGURATION --- @@ -495,7 +746,7 @@ SERVICE_CMDS[zigbee2mqtt]="podman run -d --name zigbee2mqtt --restart unless-sto SERVICE_CMDS[frigate]="podman run -d --name frigate --restart unless-stopped --network ${NETWORK_NAME} --privileged -e TZ=${TZ} -p ${FRIGATE_PORT}:5000/tcp -p 1935:1935 -v ${FRIGATE_RECORDINGS_HOST_PATH}:/media/frigate:rw -v ./frigate_config.yml:/config/config.yml:ro -v /etc/localtime:/etc/localtime:ro --shm-size 256m ghcr.io/blakeblackshear/frigate:stable" SERVICE_CMDS[grafana]="podman run -d --name grafana --restart unless-stopped --network ${NETWORK_NAME} -p 3000:3000 -v grafana_data:/var/lib/grafana -e GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER} -e GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} -e GF_SECURITY_SECRET_KEY=${GRAFANA_SECRET_KEY} docker.io/grafana/grafana:latest" SERVICE_CMDS[nodered]="podman run -d --name nodered --restart unless-stopped --network ${NETWORK_NAME} -p ${NODERED_PORT}:1880 -e TZ=${TZ} -e DOCKER_HOST=unix:///var/run/docker.sock -v nodered_data:/data -v ${PODMAN_SOCKET_PATH}:/var/run/docker.sock:ro --security-opt label=disable --user root docker.io/nodered/node-red:latest" -SERVICE_CMDS[nginx]="podman run -d --name nginx --restart unless-stopped --network ${NETWORK_NAME} --add-host=host.containers.internal:host-gateway -p 80:80 -v ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro -v nginx_cache:/var/cache/nginx docker.io/library/nginx:alpine" +SERVICE_CMDS[nginx]="podman run -d --name nginx --restart unless-stopped --network ${NETWORK_NAME} --add-host=host.containers.internal:host-gateway -p 80:80 --security-opt label=disable -v ${PWD}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro -v nginx_cache:/var/cache/nginx docker.io/library/nginx:alpine" SERVICE_CMDS[doubletake]="podman run -d --name doubletake --restart unless-stopped --network ${NETWORK_NAME} -p 3001:3000 -v doubletake_data:/.storage -e TZ=${TZ} docker.io/jakowenko/double-take:latest" SERVICE_NAMES=(mosquitto influxdb zigbee2mqtt frigate grafana nodered nginx doubletake) @@ -564,9 +815,6 @@ setup_system() { mount_smb_share fi - # 3. Generate nginx configuration based on stack type - generate_nginx_config "$stack_type" - echo "" echo "[1/3] Setting up Podman Network and Volumes..." @@ -588,9 +836,8 @@ setup_system() { # --- Start Services (Using run_service function) --- # -------------------------------------------------- for SERVICE in "${SERVICE_NAMES[@]}"; do - # Nginx always starts (it's the entry point for all services) + # Skip nginx for now - it will be started last after all upstream services if [ "$SERVICE" == "nginx" ]; then - run_service "$SERVICE" "${SERVICE_CMDS[$SERVICE]}" continue fi @@ -614,6 +861,24 @@ setup_system() { fi run_service "$SERVICE" "${SERVICE_CMDS[$SERVICE]}" done + + # Wait for services to stabilize before generating nginx config + echo "" + echo "Waiting 3 seconds for services to stabilize..." + sleep 3 + + # Generate nginx config based on actually running services + # This prevents "host not found in upstream" errors for services that didn't start + echo "" + generate_nginx_config_from_running_services + + # Check and fix nginx.conf permissions + check_and_fix_nginx_permissions + + # Start nginx last, after all upstream services are running + echo "" + echo "Starting nginx (reverse proxy) after all upstream services..." + run_service "nginx" "${SERVICE_CMDS[nginx]}" echo "" echo "[3/3] Finalizing Setup..."