CORS: - Restrict CORS to DASHBOARD_URL environment variable - Default to production dashboard domain Authentication: - Add AuthUser requirement to all agent management endpoints - Add AuthUser requirement to all command endpoints - Add AuthUser requirement to all metrics endpoints - Add audit logging for command execution (user_id tracked) Agent Security: - Replace Unicode characters with ASCII markers [OK]/[ERROR]/[WARNING] - Add certificate pinning for update downloads (allowlist domains) - Fix insecure temp file creation (use /var/run/gururmm with 0700 perms) - Fix rollback script backgrounding (use setsid instead of literal &) Dashboard Security: - Move token storage from localStorage to sessionStorage - Add proper TypeScript types (remove 'any' from error handlers) - Centralize token management functions Legacy Agent: - Add -AllowInsecureTLS parameter (opt-in required) - Add Windows Event Log audit trail when insecure mode used - Update documentation with security warnings Closes: Phase 1 items in issue #1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
180 lines
5.2 KiB
Rust
180 lines
5.2 KiB
Rust
//! GuruRMM Server - RMM Management Server
|
|
//!
|
|
//! Provides the backend API and WebSocket endpoint for GuruRMM agents.
|
|
//! Features:
|
|
//! - Agent registration and management
|
|
//! - Real-time WebSocket communication with agents
|
|
//! - Metrics storage and retrieval
|
|
//! - Command execution via agents
|
|
//! - Dashboard authentication
|
|
|
|
mod api;
|
|
mod auth;
|
|
mod config;
|
|
mod db;
|
|
mod updates;
|
|
mod ws;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::Result;
|
|
use axum::{
|
|
routing::{get, post},
|
|
Router,
|
|
};
|
|
use sqlx::postgres::PgPoolOptions;
|
|
use tokio::sync::RwLock;
|
|
use http::HeaderValue;
|
|
use tower_http::cors::{AllowOrigin, CorsLayer};
|
|
use tower_http::trace::TraceLayer;
|
|
use tracing::info;
|
|
|
|
use crate::config::ServerConfig;
|
|
use crate::updates::UpdateManager;
|
|
use crate::ws::AgentConnections;
|
|
|
|
/// Shared application state
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
/// Database connection pool
|
|
pub db: sqlx::PgPool,
|
|
|
|
/// Server configuration
|
|
pub config: Arc<ServerConfig>,
|
|
|
|
/// Connected agents (WebSocket connections)
|
|
pub agents: Arc<RwLock<AgentConnections>>,
|
|
|
|
/// Agent update manager
|
|
pub updates: Arc<UpdateManager>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Load environment variables from .env file
|
|
dotenvy::dotenv().ok();
|
|
|
|
// Initialize logging
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::from_default_env()
|
|
.add_directive("gururmm_server=info".parse()?)
|
|
.add_directive("tower_http=debug".parse()?)
|
|
.add_directive("info".parse()?),
|
|
)
|
|
.init();
|
|
|
|
info!("GuruRMM Server starting...");
|
|
|
|
// Load configuration
|
|
let config = ServerConfig::from_env()?;
|
|
info!("Server configuration loaded");
|
|
|
|
// Connect to database
|
|
info!("Connecting to database...");
|
|
let db = PgPoolOptions::new()
|
|
.max_connections(config.database.max_connections)
|
|
.connect(&config.database.url)
|
|
.await?;
|
|
info!("Database connected");
|
|
|
|
// Run migrations
|
|
info!("Running database migrations...");
|
|
sqlx::migrate!("./migrations").run(&db).await?;
|
|
info!("Migrations complete");
|
|
|
|
// Initialize update manager
|
|
let update_manager = UpdateManager::new(
|
|
config.updates.downloads_dir.clone(),
|
|
config.updates.downloads_base_url.clone(),
|
|
config.updates.auto_update_enabled,
|
|
config.updates.update_timeout_secs,
|
|
);
|
|
|
|
// Initial scan for available versions
|
|
info!("Scanning for available agent versions...");
|
|
if let Err(e) = update_manager.scan_versions().await {
|
|
tracing::warn!("Failed to scan agent versions: {} (continuing without auto-update)", e);
|
|
}
|
|
|
|
let update_manager = Arc::new(update_manager);
|
|
|
|
// Spawn background scanner (handle is intentionally not awaited - runs until server shutdown)
|
|
let _scanner_handle = update_manager.spawn_scanner(config.updates.scan_interval_secs);
|
|
info!(
|
|
"Auto-update: {} (scan interval: {}s)",
|
|
if config.updates.auto_update_enabled { "enabled" } else { "disabled" },
|
|
config.updates.scan_interval_secs
|
|
);
|
|
|
|
// Create shared state
|
|
let state = AppState {
|
|
db,
|
|
config: Arc::new(config.clone()),
|
|
agents: Arc::new(RwLock::new(AgentConnections::new())),
|
|
updates: update_manager,
|
|
};
|
|
|
|
// Build router
|
|
let app = build_router(state);
|
|
|
|
// Start server
|
|
let addr = format!("{}:{}", config.server.host, config.server.port);
|
|
info!("Starting server on {}", addr);
|
|
|
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
|
axum::serve(listener, app).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Build the application router
|
|
fn build_router(state: AppState) -> Router {
|
|
// TODO: Add rate limiting for registration endpoints using tower-governor
|
|
// Currently, registration is protected by AuthUser authentication.
|
|
// For additional protection against brute-force attacks, consider adding:
|
|
// - tower-governor crate for per-IP rate limiting on /api/agents/register
|
|
// - Configurable limits via environment variables
|
|
// Reference: https://docs.rs/tower-governor/latest/tower_governor/
|
|
|
|
// CORS configuration - restrict to specific dashboard origin
|
|
let dashboard_origin = std::env::var("DASHBOARD_URL")
|
|
.unwrap_or_else(|_| "https://rmm.azcomputerguru.com".to_string());
|
|
|
|
let cors = CorsLayer::new()
|
|
.allow_origin(AllowOrigin::exact(
|
|
HeaderValue::from_str(&dashboard_origin).expect("Invalid DASHBOARD_URL"),
|
|
))
|
|
.allow_methods([
|
|
http::Method::GET,
|
|
http::Method::POST,
|
|
http::Method::PUT,
|
|
http::Method::DELETE,
|
|
http::Method::OPTIONS,
|
|
])
|
|
.allow_headers([
|
|
http::header::AUTHORIZATION,
|
|
http::header::CONTENT_TYPE,
|
|
http::header::ACCEPT,
|
|
])
|
|
.allow_credentials(true);
|
|
|
|
Router::new()
|
|
// Health check
|
|
.route("/health", get(health_check))
|
|
// WebSocket endpoint for agents
|
|
.route("/ws", get(ws::ws_handler))
|
|
// API routes
|
|
.nest("/api", api::routes())
|
|
// Middleware
|
|
.layer(TraceLayer::new_for_http())
|
|
.layer(cors)
|
|
// State
|
|
.with_state(state)
|
|
}
|
|
|
|
/// Health check endpoint
|
|
async fn health_check() -> &'static str {
|
|
"OK"
|
|
}
|