diff --git a/docs/superpowers/specs/2026-03-21-netdata-design.md b/docs/superpowers/specs/2026-03-21-netdata-design.md new file mode 100644 index 0000000..488f448 --- /dev/null +++ b/docs/superpowers/specs/2026-03-21-netdata-design.md @@ -0,0 +1,86 @@ +# Netdata Module Design + +**Date:** 2026-03-21 +**Status:** Draft + +## Overview + +Add Netdata real-time monitoring to the NixOS VPS, accessible only from the Headscale/Tailscale network. + +## Requirements + +- Netdata monitoring service running on the VPS +- Accessible via nginx reverse proxy with automatic HTTPS +- Restricted to Tailscale network only (100.64.0.0/10) and localhost +- Direct access on Tailscale IP (port 19999) also available + +## Implementation + +### Module: `modules/netdata.nix` + +Create a new module following the existing pattern. + +**Header comment block:** +```nix +# Netdata Module +# Provides: Real-time system monitoring dashboard +# +# Usage: +# myModules.netdata = { +# enable = true; +# domain = "netdata.example.com"; +# }; +# +# Access is restricted to Tailscale network only via nginx internalOnly. +``` + +**Options:** +- `enable` - Enable Netdata monitoring +- `domain` - Domain for nginx reverse proxy (e.g., `netdata.ashisgreat.xyz`) +- `port` - Internal port (default: 19999), description: "Internal port for Netdata to listen on" + +**Configuration:** +- Enable `services.netdata` with default settings +- Bind Netdata to `0.0.0.0` to allow direct Tailscale access (not just localhost) +- Register domain with `myModules.nginx.domains` using `internalOnly = true` +- Set `contentSecurityPolicy = null` - Netdata dashboard has its own CSP requirements +- No firewall changes needed (nginx handles external access, direct Tailscale access works via mesh network) + +### Usage in configuration.nix + +```nix +myModules.netdata = { + enable = true; + domain = "netdata.ashisgreat.xyz"; +}; +``` + +### Access Control + +- **Via domain:** Only accessible from IPs in `100.64.0.0/10` (Tailscale) or `127.0.0.0/8` (localhost) +- **Direct Tailscale:** `http://:19999` (Tailscale mesh handles access control) + +### Backup Decision + +Netdata metrics data is **not backed up**. Rationale: +- Metrics are ephemeral and regeneratable +- Historical data is downsampled over time (not critical) +- `/var/lib/netdata` excluded from backup paths + +### Secrets + +No SOPS secrets required. Netdata operates without authentication at the service level - access control is enforced via nginx/Tailscale network restrictions. + +## Files Changed + +| File | Action | +|------|--------| +| `modules/netdata.nix` | Create | +| `modules/default.nix` | Add import | +| `configuration.nix` | Enable module | + +## Security + +- No public internet access - blocked at nginx level +- No authentication required at Netdata level (network-level access control) +- Automatic HTTPS via Let's Encrypt diff --git a/modules/open-webui-podman.nix b/modules/open-webui-podman.nix new file mode 100644 index 0000000..cb3389e --- /dev/null +++ b/modules/open-webui-podman.nix @@ -0,0 +1,95 @@ +# OpenWebUI Podman Module +# Provides: Web interface for LLMs using official Docker image +# +# Usage: +# myModules.open-webui-podman = { +# enable = true; +# port = 9000; +# domain = "ai.example.com"; +# ollamaUrl = "http://100.64.0.1:11434"; +# }; + +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.myModules.open-webui-podman; +in +{ + options.myModules.open-webui-podman = { + enable = lib.mkEnableOption "OpenWebUI for LLMs via Podman"; + + port = lib.mkOption { + type = lib.types.port; + default = 9000; + description = "Port to expose OpenWebUI on localhost"; + }; + + domain = lib.mkOption { + type = lib.types.str; + example = "ai.example.com"; + description = "Public domain name for OpenWebUI"; + }; + + ollamaUrl = lib.mkOption { + type = lib.types.str; + default = "http://127.0.0.1:11434"; + example = "http://100.64.0.1:11434"; + description = "URL of the Ollama API endpoint"; + }; + }; + + config = lib.mkIf cfg.enable { + # Ensure podman is enabled + myModules.podman.enable = true; + + # Podman container for OpenWebUI + virtualisation.oci-containers.containers.open-webui = { + image = "ghcr.io/open-webui/open-webui:main"; + ports = ["127.0.0.1:${toString cfg.port}:8080"]; + environment = { + OLLAMA_API_BASE_URL = cfg.ollamaUrl; + WEBUI_URL = "https://${cfg.domain}"; + }; + environmentFiles = [config.sops.templates."openwebui-podman.env".path]; + volumes = [ + "open-webui-data:/app/backend/data" + ]; + }; + + # SOPS template for secrets + sops.templates."openwebui-podman.env" = { + content = '' + WEBUI_SECRET_KEY=${config.sops.placeholder.openwebui_secret_key} + ''; + }; + + sops.secrets.openwebui_secret_key = { }; + + # Nginx configuration + myModules.nginx.domains.${cfg.domain} = { + port = cfg.port; + extraConfig = '' + client_max_body_size 100M; + ''; + # WebSocket support for /ws/ + extraLocations."/ws/" = { + proxyPass = "http://127.0.0.1:${toString cfg.port}"; + extraConfig = '' + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; + proxy_send_timeout 86400; + ''; + }; + # Relaxed CSP for OpenWeb UI + contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:; frame-ancestors 'self'"; + }; + }; +}