diff --git a/.github/workflows/release-dashboard.yml b/.github/workflows/release-dashboard.yml index 3b42eee..ff1628a 100644 --- a/.github/workflows/release-dashboard.yml +++ b/.github/workflows/release-dashboard.yml @@ -253,6 +253,24 @@ jobs: cp -r .next/static .next/standalone/.next/static cp -r public .next/standalone/public + - name: Patch server.js for portable __dirname + working-directory: lynx/dashboard/ui + run: | + # bun --compile embeds the compile-time __dirname (CI path) into the binary. + # At runtime on the deployment server that path does not exist, causing chdir to fail. + # Fix: shadow __dirname with a version that falls back to the binary's own directory + # when the compile-time path is absent from disk. + node -e " + const fs = require('fs'); + let content = fs.readFileSync('.next/standalone/server.js', 'utf8'); + content = content.replace( + \"const path = require('path')\", + \"const path = require('path')\\nconst __dirname = require('fs').existsSync(__dirname) ? __dirname : path.dirname(process.execPath)\" + ); + fs.writeFileSync('.next/standalone/server.js', content); + process.stdout.write('server.js patched: __dirname resolves to binary directory on deployment\\n'); + " + - name: Compile standalone binary working-directory: lynx/dashboard/ui run: | diff --git a/lynx/dashboard/docker-compose.yml b/lynx/dashboard/docker-compose.yml index a0cc365..5650015 100644 --- a/lynx/dashboard/docker-compose.yml +++ b/lynx/dashboard/docker-compose.yml @@ -1,11 +1,31 @@ services: + nginx: + container_name: lynx-dashboard-nginx + image: docker.io/library/nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10 + ports: + - "19443:19443" + volumes: + - /etc/lynx/tls:/etc/lynx/tls:ro + - /etc/lynx/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - /etc/lynx/nginx/updating.html:/etc/lynx/nginx/updating.html:ro + depends_on: + frontend: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep nginx > /dev/null"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + networks: + - lynx-dashboard-app + frontend: container_name: lynx-dashboard-frontend image: docker.io/library/alpine@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d working_dir: /etc/lynx/frontend - command: ["/etc/lynx/frontend/lynx-dashboard-frontend"] - ports: - - "19443:3000" + command: ["/bin/sh", "-c", "apk add --no-cache libgcc libstdc++ && exec /etc/lynx/frontend/lynx-dashboard-frontend"] environment: - NODE_ENV=production - PORT=3000 @@ -90,7 +110,7 @@ services: - lynx-dashboard-pg-root - lynx-dashboard-pg-pass volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql - ./server/db/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d lynx_dashboard"] diff --git a/lynx/dashboard/nginx/default.conf b/lynx/dashboard/nginx/default.conf new file mode 100644 index 0000000..d86461c --- /dev/null +++ b/lynx/dashboard/nginx/default.conf @@ -0,0 +1,30 @@ +server { + listen 19443 ssl; + + ssl_certificate /etc/lynx/tls/dashboard.crt; + ssl_certificate_key /etc/lynx/tls/dashboard.key; + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + add_header X-Frame-Options DENY always; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy no-referrer always; + + location / { + proxy_pass http://lynx-dashboard-frontend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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 https; + proxy_read_timeout 3600s; + } + + error_page 502 503 /updating.html; + location = /updating.html { + root /etc/lynx/nginx; + internal; + } +} diff --git a/lynx/dashboard/nginx/updating.html b/lynx/dashboard/nginx/updating.html new file mode 100644 index 0000000..cd12551 --- /dev/null +++ b/lynx/dashboard/nginx/updating.html @@ -0,0 +1,45 @@ + + + + + + + Lynx — Updating + + + +
+
+

Updating Lynx Dashboard

+

The system is applying an update. This page will reload automatically.

+
+ + diff --git a/lynx/dashboard/server/db/init/01-init.sql b/lynx/dashboard/server/db/init/01-init.sql index 64db0e7..ebe88ec 100644 --- a/lynx/dashboard/server/db/init/01-init.sql +++ b/lynx/dashboard/server/db/init/01-init.sql @@ -10,7 +10,7 @@ GRANT CONNECT ON DATABASE lynx_dashboard TO lynx_dashboard_app; \connect lynx_dashboard -GRANT USAGE ON SCHEMA public TO lynx_dashboard_app; +GRANT USAGE, CREATE ON SCHEMA public TO lynx_dashboard_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO lynx_dashboard_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public diff --git a/lynx/dashboard/server/src/scheduler.rs b/lynx/dashboard/server/src/scheduler.rs index 9ef133d..f5b2b51 100644 --- a/lynx/dashboard/server/src/scheduler.rs +++ b/lynx/dashboard/server/src/scheduler.rs @@ -85,7 +85,9 @@ async fn check_releases(state: &AppState) { } if let Some(ref ver) = latest_dashboard { - let current = env!("CARGO_PKG_VERSION"); + let current = std::fs::read_to_string(crate::update::VERSION_FILE) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_string()); if ver.as_str() != current { tracing::info!(latest = %ver, current, "scheduler: dashboard update available"); trigger_dashboard_update(state, ver).await; diff --git a/lynx/dashboard/server/src/update.rs b/lynx/dashboard/server/src/update.rs index ec4911d..27f80f3 100644 --- a/lynx/dashboard/server/src/update.rs +++ b/lynx/dashboard/server/src/update.rs @@ -9,6 +9,7 @@ const FRONTEND_BINARY: &str = "/etc/lynx/frontend/lynx-dashboard-frontend"; const FRONTEND_DIR: &str = "/etc/lynx/frontend"; const FRONTEND_CONTAINER: &str = "lynx-dashboard-frontend"; const PODMAN_SOCKET: &str = "/run/podman/podman.sock"; +pub const VERSION_FILE: &str = "/etc/lynx/bin/dashboard-version"; const MAX_DOWNLOAD_BYTES: usize = 200 * 1024 * 1024; pub struct DashboardUpdateParams { @@ -52,6 +53,9 @@ pub async fn perform_dashboard_update(p: DashboardUpdateParams) { .await; if result.is_ok() { + if let Err(e) = write_version_file(&p.version) { + tracing::warn!("could not write version file: {e}"); + } tracing::info!( version = p.version, "dashboard update complete — exiting for Podman restart" @@ -166,6 +170,12 @@ fn swap_backend_binary(binary: &[u8]) -> Result<()> { Ok(()) } +/// Write `version` to the on-disk version file so the scheduler can read it +/// on the next startup and avoid re-triggering the same update. +pub(crate) fn write_version_file(version: &str) -> Result<()> { + std::fs::write(VERSION_FILE, version).context("write version file") +} + fn extract_assets(data: &[u8], dest: &str) -> Result<()> { use std::io::Write; use std::process::{Command, Stdio}; diff --git a/scripts/dashboard/install-dashboard.sh b/scripts/dashboard/install-dashboard.sh index 4eef81b..5a781d3 100644 --- a/scripts/dashboard/install-dashboard.sh +++ b/scripts/dashboard/install-dashboard.sh @@ -2,30 +2,394 @@ # ============================================================================= # install-dashboard.sh # ============================================================================= -# Description: Installs the Lynx Dashboard on this VPS. -# Handles fresh installation and version updates. -# Installs to /opt/lynx/dashboard. +# Description: Installs the Lynx Dashboard (backend + frontend + local agent) +# on this VPS. Downloads binaries from the latest dashboard@* +# GitHub release and verifies Ed25519 signatures before installing. # # Dependencies: -# - detect-os.sh must be sourced first (provides PKG_MANAGER, PKG_INSTALL, etc.) -# - install-podman.sh must run first (Podman required) -# - install-nftables.sh must run first (nftables required) +# - detect-os.sh must be sourced first +# - install-podman.sh must run first +# - install-nftables.sh must run first # - Colors must be exported from install.sh # ============================================================================= set -euo pipefail -DASHBOARD_DIR="/opt/lynx/dashboard" +# --- Constants ---------------------------------------------------------------- + +readonly RELEASE_VERIFY_KEY="OsBV4t+vQSn10FAI8UzAJEBS0IUqp8D2bZtlQYD8j+Q=" +readonly GITHUB_REPO="Jaro-c/Lynx" +readonly BIN_DIR="/etc/lynx/bin" +readonly FRONTEND_DIR="/etc/lynx/frontend" +readonly SECRETS_DIR="/etc/lynx/secrets" +readonly TLS_DIR="/etc/lynx/tls" +readonly NGINX_DIR="/etc/lynx/nginx" +readonly DEPLOY_DIR="/opt/lynx/dashboard" + +# --- Helpers ------------------------------------------------------------------ + +_compose() { + if podman compose version &>/dev/null 2>&1; then + podman compose "$@" + elif command -v podman-compose &>/dev/null; then + podman-compose "$@" + else + echo -e "${RED}Error: podman compose not available. Install podman-compose.${RESET}" >&2 + exit 1 + fi +} + +_wait_healthy() { + local container="$1" + local max_secs="${2:-90}" + local elapsed=0 + echo -e "${CYAN}Waiting for ${container} to be healthy...${RESET}" + while [[ $elapsed -lt $max_secs ]]; do + local status + status=$(podman inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null || true) + if [[ "$status" == "healthy" ]]; then + echo -e "${GREEN}${container} is healthy.${RESET}" + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + done + echo -e "${RED}Timeout waiting for ${container} (last status: ${status:-unknown})${RESET}" >&2 + exit 1 +} + +_create_secret() { + local name="$1" + local value="$2" + podman secret rm "$name" &>/dev/null || true + printf '%s' "$value" | podman secret create "$name" - +} + +_ensure_python_crypto() { + if python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey" 2>/dev/null; then + return 0 + fi + echo -e "${CYAN}Installing Python cryptography...${RESET}" + case "$PKG_MANAGER" in + apt-get) apt-get install -y python3-cryptography ;; + dnf) dnf install -y python3-cryptography ;; + pacman) pacman -S --noconfirm python-cryptography ;; + esac +} + +_ensure_podman_compose() { + if podman compose version &>/dev/null 2>&1 || command -v podman-compose &>/dev/null; then + return 0 + fi + echo -e "${CYAN}Installing podman-compose...${RESET}" + case "$PKG_MANAGER" in + apt-get) apt-get install -y podman-compose ;; + dnf) dnf install -y podman-compose ;; + pacman) pacman -S --noconfirm python-podman-compose ;; + esac +} + +_ensure_uuid_gen() { + command -v uuidgen &>/dev/null && return 0 + case "$PKG_MANAGER" in + apt-get) apt-get install -y uuid-runtime ;; + dnf) dnf install -y util-linux ;; + pacman) pacman -S --noconfirm util-linux ;; + esac +} + +# --- TLS certificate ---------------------------------------------------------- + +_gen_self_signed_cert() { + local ip="$1" + mkdir -p "$TLS_DIR" + chmod 700 "$TLS_DIR" + + openssl req -x509 -nodes \ + -newkey ec -pkeyopt ec_paramgen_curve:P-256 \ + -days 90 \ + -subj "/CN=${ip}" \ + -addext "subjectAltName=IP:${ip},IP:127.0.0.1,DNS:localhost" \ + -keyout "${TLS_DIR}/dashboard.key" \ + -out "${TLS_DIR}/dashboard.crt" \ + 2>/dev/null + + chmod 600 "${TLS_DIR}/dashboard.key" + chmod 644 "${TLS_DIR}/dashboard.crt" + echo -e "${GREEN}Self-signed TLS certificate generated (90 days, SAN IP:${ip}).${RESET}" +} + +# --- Signature verification --------------------------------------------------- + +_verify_sig() { + local file="$1" + local sig_file="$2" + python3 - "$file" "$sig_file" "$RELEASE_VERIFY_KEY" <<'PYEOF' +import base64, sys +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from cryptography.exceptions import InvalidSignature + +VERIFY_KEY_B64 = sys.argv[3] +pub_bytes = base64.b64decode(VERIFY_KEY_B64) +pub_key = Ed25519PublicKey.from_public_bytes(pub_bytes) + +with open(sys.argv[1], 'rb') as f: + data = f.read() +with open(sys.argv[2], 'rb') as f: + sig = f.read() + +try: + pub_key.verify(sig, data) +except InvalidSignature: + print(f"FAIL: invalid signature for {sys.argv[1]}", file=sys.stderr) + sys.exit(1) +PYEOF +} + +# --- Download + verify -------------------------------------------------------- + +_download_verify() { + local base_url="$1" + local artifact="$2" + local dest_dir="$3" + + echo -e "${CYAN} Downloading ${artifact}...${RESET}" + curl -fsSL --max-time 300 \ + -o "${dest_dir}/${artifact}" \ + "${base_url}/${artifact}" + curl -fsSL --max-time 30 \ + -o "${dest_dir}/${artifact}.sig" \ + "${base_url}/${artifact}.sig" + + echo -e "${CYAN} Verifying ${artifact}...${RESET}" + _verify_sig "${dest_dir}/${artifact}" "${dest_dir}/${artifact}.sig" + echo -e "${GREEN} ✔ ${artifact}${RESET}" +} + +# --- Secret generation -------------------------------------------------------- + +_gen_secrets() { + local pg_root pg_pass redis_pass api_token setup_token pepper kek + + pg_root=$(openssl rand -hex 32) + pg_pass=$(openssl rand -hex 32) + redis_pass=$(openssl rand -hex 32) + api_token=$(openssl rand -hex 32) + setup_token=$(openssl rand -hex 32) + pepper=$(openssl rand -base64 32 | tr -d '\n') + kek=$(openssl rand -base64 32 | tr -d '\n') + + # Ed25519 + X25519 key pairs via Python + local jwt_sign_priv jwt_sign_pub jwt_enc_priv jwt_enc_pub ca_priv ca_pub + read -r jwt_sign_priv jwt_sign_pub < <(python3 - <<'PYEOF' +import base64, secrets +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +seed = secrets.token_bytes(32) +priv = Ed25519PrivateKey.from_private_bytes(seed) +pub_bytes = priv.public_key().public_bytes_raw() +print(base64.b64encode(seed).decode(), base64.b64encode(pub_bytes).decode()) +PYEOF +) + + read -r jwt_enc_priv jwt_enc_pub < <(python3 - <<'PYEOF' +import base64 +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey +priv = X25519PrivateKey.generate() +priv_bytes = priv.private_bytes_raw() +pub_bytes = priv.public_key().public_bytes_raw() +print(base64.b64encode(priv_bytes).decode(), base64.b64encode(pub_bytes).decode()) +PYEOF +) + + read -r ca_priv ca_pub < <(python3 - <<'PYEOF' +import base64, secrets +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +seed = secrets.token_bytes(32) +priv = Ed25519PrivateKey.from_private_bytes(seed) +pub_bytes = priv.public_key().public_bytes_raw() +print(base64.b64encode(seed).decode(), base64.b64encode(pub_bytes).decode()) +PYEOF +) + + local database_url="postgresql://lynx_dashboard_app:${pg_pass}@lynx-dashboard-postgres:5432/lynx_dashboard" + local redis_url="redis://:${redis_pass}@lynx-dashboard-redis:6379" + + # Persist secrets to Podman + _create_secret lynx-dashboard-pg-root "$pg_root" + _create_secret lynx-dashboard-pg-pass "$pg_pass" + _create_secret lynx-dashboard-redis-pass "$redis_pass" + _create_secret lynx-dashboard-api-token "$api_token" + _create_secret lynx-dashboard-setup-token "$setup_token" + _create_secret lynx-dashboard-pepper "$pepper" + _create_secret lynx-dashboard-kek "$kek" + _create_secret lynx-dashboard-jwt-sign-private "$jwt_sign_priv" + _create_secret lynx-dashboard-jwt-sign-public "$jwt_sign_pub" + _create_secret lynx-dashboard-jwt-enc-private "$jwt_enc_priv" + _create_secret lynx-dashboard-jwt-enc-public "$jwt_enc_pub" + _create_secret lynx-dashboard-ca-private "$ca_priv" + _create_secret lynx-dashboard-ca-public "$ca_pub" + _create_secret lynx-dashboard-database-url "$database_url" + _create_secret lynx-dashboard-redis-url "$redis_url" + + # Save critical secrets to /etc/lynx/secrets (owner root, mode 600) + # These MUST be backed up — without them, data is irrecoverable. + mkdir -p "$SECRETS_DIR" + chmod 700 "$SECRETS_DIR" + printf '%s' "$kek" > "${SECRETS_DIR}/lynx-dashboard-kek" + printf '%s' "$pg_root" > "${SECRETS_DIR}/lynx-dashboard-pg-root" + printf '%s' "$pg_pass" > "${SECRETS_DIR}/lynx-dashboard-pg-pass" + printf '%s' "$ca_priv" > "${SECRETS_DIR}/lynx-dashboard-ca-private" + printf '%s' "$ca_pub" > "${SECRETS_DIR}/lynx-dashboard-ca-public" + printf '%s' "$jwt_sign_priv" > "${SECRETS_DIR}/lynx-dashboard-jwt-sign-private" + printf '%s' "$jwt_sign_pub" > "${SECRETS_DIR}/lynx-dashboard-jwt-sign-public" + printf '%s' "$setup_token" > "${SECRETS_DIR}/lynx-dashboard-setup-token" + chmod 600 "${SECRETS_DIR}"/* + + # Return setup token for display + SETUP_TOKEN="$setup_token" +} + +# --- Main install function ---------------------------------------------------- install_dashboard() { echo -e "${CYAN}Installing Lynx Dashboard...${RESET}" - if [[ -d "$DASHBOARD_DIR" ]]; then - echo -e "${YELLOW}Existing installation detected at ${BOLD}${DASHBOARD_DIR}${RESET}" - echo -e "${CYAN}Checking version...${RESET}" - # TODO: version check and update logic - else - echo -e "${CYAN}No existing installation found. Proceeding with fresh install...${RESET}" - mkdir -p "$DASHBOARD_DIR" - # TODO: fresh install logic + # Detect arch + case "$(uname -m)" in + x86_64) ARCH="x86_64" ;; + aarch64) ARCH="arm64" ;; + *) + echo -e "${RED}Unsupported architecture: $(uname -m)${RESET}" >&2 + exit 1 + ;; + esac + + # Ensure dependencies + _ensure_python_crypto + _ensure_podman_compose + _ensure_uuid_gen + + # Check existing installation + if [[ -d "$DEPLOY_DIR" ]] || podman container exists lynx-dashboard-backend 2>/dev/null; then + echo -e "${YELLOW}Existing installation detected.${RESET}" + echo -e " ${BOLD}1)${RESET} Abort (default)" + echo -e " ${BOLD}2)${RESET} Update → use auto-update instead" + echo -e " ${BOLD}3)${RESET} Reinstall clean" + read -rp "Option [1/2/3]: " OPT + case "${OPT:-1}" in + 3) + echo -e "${YELLOW}Stopping existing containers...${RESET}" + (cd "$DEPLOY_DIR" && _compose -f compose.yml down --volumes 2>/dev/null) || true + rm -rf "$DEPLOY_DIR" + ;; + *) + echo -e "${CYAN}Aborting. Use the dashboard auto-update for upgrades.${RESET}" + exit 0 + ;; + esac fi + + # Fetch latest release tag + echo -e "${CYAN}Fetching latest dashboard release...${RESET}" + LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases" \ + | python3 -c " +import sys, json +releases = json.load(sys.stdin) +for r in releases: + if r['tag_name'].startswith('dashboard@') and not r.get('prerelease', False): + print(r['tag_name']); break +") + if [[ -z "${LATEST_TAG:-}" ]]; then + echo -e "${RED}Failed to find a dashboard release.${RESET}" >&2 + exit 1 + fi + VERSION="${LATEST_TAG#dashboard@}" + echo -e "${GREEN}Latest: ${BOLD}${LATEST_TAG}${RESET}" + + # Download and verify binaries + echo -e "${CYAN}Downloading binaries...${RESET}" + TMPDIR_DL=$(mktemp -d) + trap 'rm -rf "$TMPDIR_DL"' EXIT + + RELEASE_BASE="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_TAG}" + _download_verify "$RELEASE_BASE" "lynx-dashboard-backend-linux-${ARCH}" "$TMPDIR_DL" + _download_verify "$RELEASE_BASE" "lynx-dashboard-frontend-linux-${ARCH}" "$TMPDIR_DL" + _download_verify "$RELEASE_BASE" "lynx-dashboard-frontend-assets-linux-${ARCH}.tar.gz" "$TMPDIR_DL" + + # Install binaries + echo -e "${CYAN}Installing binaries...${RESET}" + mkdir -p "$BIN_DIR" "$FRONTEND_DIR" "$DEPLOY_DIR" + + install -m 755 \ + "${TMPDIR_DL}/lynx-dashboard-backend-linux-${ARCH}" \ + "${BIN_DIR}/lynx-dashboard-backend" + install -m 755 \ + "${TMPDIR_DL}/lynx-dashboard-frontend-linux-${ARCH}" \ + "${FRONTEND_DIR}/lynx-dashboard-frontend" + tar -xzf "${TMPDIR_DL}/lynx-dashboard-frontend-assets-linux-${ARCH}.tar.gz" \ + -C "$FRONTEND_DIR" + + # Write version file so the backend scheduler knows the current version on startup. + printf '%s' "$VERSION" > "${BIN_DIR}/dashboard-version" + echo -e "${GREEN}Binaries installed.${RESET}" + + # Generate TLS certificate and deploy nginx config + echo -e "${CYAN}Generating TLS certificate...${RESET}" + CERT_IP=$(hostname -I 2>/dev/null | awk '{print $1}') + [[ -z "$CERT_IP" ]] && CERT_IP=$(curl -fsSL --max-time 5 https://ifconfig.me 2>/dev/null || echo "127.0.0.1") + _gen_self_signed_cert "$CERT_IP" + mkdir -p "$NGINX_DIR" + cp "${SCRIPT_DIR}/lynx/dashboard/nginx/default.conf" "${NGINX_DIR}/default.conf" + cp "${SCRIPT_DIR}/lynx/dashboard/nginx/updating.html" "${NGINX_DIR}/updating.html" + echo -e "${GREEN}nginx config deployed.${RESET}" + + # Generate and create all secrets + echo -e "${CYAN}Generating secrets...${RESET}" + _gen_secrets + echo -e "${GREEN}Secrets created.${RESET}" + + # Podman networks + echo -e "${CYAN}Creating Podman networks...${RESET}" + for net in lynx-dashboard-db lynx-dashboard-cache lynx-dashboard-app; do + podman network exists "$net" 2>/dev/null || podman network create "$net" + done + + # Deploy compose file and init SQL + cp "${SCRIPT_DIR}/lynx/dashboard/docker-compose.yml" "${DEPLOY_DIR}/compose.yml" + mkdir -p "${DEPLOY_DIR}/server/db/init" + cp -r "${SCRIPT_DIR}/lynx/dashboard/server/db/init/." "${DEPLOY_DIR}/server/db/init/" + + # Fix relative path in compose to absolute + sed -i "s|./server/db/init|${DEPLOY_DIR}/server/db/init|g" "${DEPLOY_DIR}/compose.yml" + + # Start containers + echo -e "${CYAN}Starting containers...${RESET}" + cd "$DEPLOY_DIR" + _compose -f compose.yml up -d + + # Wait for services + _wait_healthy lynx-dashboard-postgres 60 + _wait_healthy lynx-dashboard-redis 30 + _wait_healthy lynx-dashboard-backend 90 + _wait_healthy lynx-dashboard-frontend 60 + _wait_healthy lynx-dashboard-nginx 30 + + # Use the same IP that's in the TLS cert SAN + VPS_IP="$CERT_IP" + + # Done + echo + echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo -e "${GREEN}${BOLD} Lynx Dashboard ${VERSION} installed successfully!${RESET}" + echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo + echo -e "${BOLD}Create your admin account:${RESET}" + echo -e " ${CYAN}https://${VPS_IP}:19443/register?setup_token=${SETUP_TOKEN}${RESET}" + echo + echo -e "${YELLOW}${BOLD}IMPORTANT — Back up these files now:${RESET}" + echo -e " ${YELLOW}${SECRETS_DIR}/${RESET}" + echo -e " Without KEK and pg-root, data is irrecoverable." + echo + echo -e "${BOLD}If something fails:${RESET}" + echo -e " ${CYAN}lynx-dashboard-backend logs --errors${RESET}" + echo } diff --git a/scripts/install-nftables.sh b/scripts/install-nftables.sh index c53f441..75a1ccc 100644 --- a/scripts/install-nftables.sh +++ b/scripts/install-nftables.sh @@ -67,9 +67,14 @@ table inet filter { ct state established,related accept iif lo accept tcp dport $SSH_PORT accept + # Allow DNS from Podman bridge subnets (aardvark-dns) + ip saddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } udp dport 53 accept + ip saddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } tcp dport 53 accept + ip6 saddr fc00::/7 udp dport 53 accept + ip6 saddr fc00::/7 tcp dport 53 accept } chain forward { - type filter hook forward priority 0; policy drop; + type filter hook forward priority 0; policy accept; } chain output { type filter hook output priority 0; policy accept;