Files
claudetools/projects/msp-tools/guru-rmm/server/src/main.rs
azcomputerguru 65086f4407 fix(security): Implement Phase 1 critical security fixes
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>
2026-01-20 21:16:24 -07:00

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"
}