diff --git a/data-retention-service/index.js b/data-retention-service/index.js index 51f14cb..7f7109c 100644 --- a/data-retention-service/index.js +++ b/data-retention-service/index.js @@ -349,9 +349,10 @@ class DataRetentionService { } }); - server.listen(port, () => { - console.log(`📊 Metrics server listening on port ${port}`); + server.listen(port, '0.0.0.0', () => { + console.log(`📊 Metrics server listening on internal port ${port}`); console.log(`📊 Endpoints: /health, /metrics, /stats`); + console.log(`🔒 Access restricted to Docker internal network only`); }); return server; diff --git a/docker-compose.yml b/docker-compose.yml index 9cced71..40ba5ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -191,8 +191,7 @@ services: NODE_ENV: ${NODE_ENV:-production} IMMEDIATE_CLEANUP: ${IMMEDIATE_CLEANUP:-false} METRICS_PORT: 3001 - ports: - - "3004:3001" # Expose metrics port + # No external ports exposed - internal access only networks: - drone-network depends_on: diff --git a/server/routes/dataRetention.js b/server/routes/dataRetention.js index 6725b09..5d4080b 100644 --- a/server/routes/dataRetention.js +++ b/server/routes/dataRetention.js @@ -1,15 +1,141 @@ const express = require('express'); const router = express.Router(); const http = require('http'); +const jwt = require('jsonwebtoken'); +const { ManagementUser } = require('../models'); +const auditLogger = require('../utils/dataRetentionAuditLogger'); /** * Data Retention Metrics Proxy * Proxies requests to the data retention microservice + * RESTRICTED ACCESS: Management users only */ const DATA_RETENTION_HOST = process.env.DATA_RETENTION_HOST || 'data-retention'; const DATA_RETENTION_PORT = process.env.DATA_RETENTION_PORT || 3001; +/** + * Management authentication middleware + * Only allows authenticated management users with proper permissions + */ +const requireManagementAuth = async (req, res, next) => { + try { + // Check for management auth token + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + await auditLogger.logAuthFailure(req, req.path, 'No authentication token provided'); + return res.status(401).json({ + success: false, + error: 'Management authentication required', + message: 'This endpoint requires management portal authentication' + }); + } + + // Verify JWT token + let decoded; + try { + decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production'); + } catch (jwtError) { + await auditLogger.logAuthFailure(req, req.path, `Invalid JWT token: ${jwtError.message}`); + return res.status(401).json({ + success: false, + error: 'Invalid management token', + message: 'Authentication token is invalid or expired' + }); + } + + // Verify this is a management user + if (decoded.type !== 'management') { + await auditLogger.logPermissionDenied(null, req, req.path, 'Not a management user token'); + return res.status(403).json({ + success: false, + error: 'Management access required', + message: 'This endpoint requires management portal privileges' + }); + } + + // Verify user still exists and is active + const managementUser = await ManagementUser.findByPk(decoded.id); + if (!managementUser || !managementUser.is_active) { + await auditLogger.logPermissionDenied(decoded, req, req.path, 'Management user not found or inactive'); + return res.status(403).json({ + success: false, + error: 'Management user not found or inactive', + message: 'Management user account is not active' + }); + } + + // Check if user has permission to access system metrics + const hasMetricsAccess = managementUser.role === 'super_admin' || + (managementUser.permissions && managementUser.permissions.includes('system_metrics')); + + if (!hasMetricsAccess) { + await auditLogger.logPermissionDenied(managementUser, req, req.path, 'Insufficient permissions for system metrics'); + return res.status(403).json({ + success: false, + error: 'Insufficient permissions', + message: 'This endpoint requires system metrics permissions' + }); + } + + // Log access for security audit + console.log(`📊 Data retention metrics accessed by management user: ${managementUser.username} (${managementUser.role})`); + + req.managementUser = managementUser; + next(); + + } catch (error) { + console.error('Management auth error:', error); + res.status(500).json({ + success: false, + error: 'Authentication error', + message: 'Failed to verify management authentication' + }); + } +}; + +/** + * IP restriction middleware (additional security layer) + * Only allow access from management container or specified IPs + */ +const requireManagementNetwork = async (req, res, next) => { + const clientIP = req.ip || req.connection.remoteAddress; + const forwardedFor = req.headers['x-forwarded-for']; + + // Allow internal Docker network (management container) + const allowedNetworks = [ + '127.0.0.1', + '::1', + 'localhost', + // Docker internal networks + /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Docker default bridge + /^10\./, // Docker custom networks + /^192\.168\./ // Local networks + ]; + + // Check if request is from management container or allowed network + const isAllowed = allowedNetworks.some(network => { + if (typeof network === 'string') { + return clientIP === network || forwardedFor === network; + } else { + return network.test(clientIP) || (forwardedFor && network.test(forwardedFor)); + } + }); + + if (!isAllowed) { + await auditLogger.logNetworkDenied(req, req.path); + console.warn(`🚫 Data retention access denied from IP: ${clientIP} (forwarded: ${forwardedFor})`); + return res.status(403).json({ + success: false, + error: 'Network access denied', + message: 'Access to data retention metrics is restricted to management network' + }); + } + + next(); +}; + /** * Make HTTP request to data retention service */ @@ -53,19 +179,39 @@ function makeRequest(path) { }); } +/** + * Apply security middleware to all data retention routes + */ +router.use(requireManagementNetwork); +router.use(requireManagementAuth); + /** * GET /api/data-retention/metrics * Get detailed metrics from data retention service + * RESTRICTED: Management users only */ router.get('/metrics', async (req, res) => { try { + // Add security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + const response = await makeRequest('/metrics'); if (response.status === 200) { + // Log successful access + await auditLogger.logSuccess(req.managementUser, req, '/metrics'); + console.log(`✅ Data retention metrics accessed by ${req.managementUser.username}`); + res.json({ success: true, data: response.data, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + accessedBy: { + username: req.managementUser.username, + role: req.managementUser.role + } }); } else { res.status(response.status).json({ @@ -74,7 +220,7 @@ router.get('/metrics', async (req, res) => { }); } } catch (error) { - console.error('Data retention metrics error:', error); + console.error(`❌ Data retention metrics error for ${req.managementUser.username}:`, error); res.status(503).json({ success: false, error: 'Data retention service unavailable', @@ -86,16 +232,28 @@ router.get('/metrics', async (req, res) => { /** * GET /api/data-retention/health * Get health status from data retention service + * RESTRICTED: Management users only */ router.get('/health', async (req, res) => { try { + // Add security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + const response = await makeRequest('/health'); if (response.status === 200) { + console.log(`✅ Data retention health checked by ${req.managementUser.username}`); + res.json({ success: true, data: response.data, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + accessedBy: { + username: req.managementUser.username, + role: req.managementUser.role + } }); } else { res.status(response.status).json({ @@ -104,7 +262,7 @@ router.get('/health', async (req, res) => { }); } } catch (error) { - console.error('Data retention health error:', error); + console.error(`❌ Data retention health error for ${req.managementUser.username}:`, error); res.status(503).json({ success: false, error: 'Data retention service unavailable', @@ -116,16 +274,28 @@ router.get('/health', async (req, res) => { /** * GET /api/data-retention/stats * Get basic statistics from data retention service + * RESTRICTED: Management users only */ router.get('/stats', async (req, res) => { try { + // Add security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + const response = await makeRequest('/stats'); if (response.status === 200) { + console.log(`✅ Data retention stats accessed by ${req.managementUser.username}`); + res.json({ success: true, data: response.data, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + accessedBy: { + username: req.managementUser.username, + role: req.managementUser.role + } }); } else { res.status(response.status).json({ @@ -134,7 +304,7 @@ router.get('/stats', async (req, res) => { }); } } catch (error) { - console.error('Data retention stats error:', error); + console.error(`❌ Data retention stats error for ${req.managementUser.username}:`, error); res.status(503).json({ success: false, error: 'Data retention service unavailable', @@ -146,9 +316,15 @@ router.get('/stats', async (req, res) => { /** * GET /api/data-retention/status * Get combined status including service connectivity + * RESTRICTED: Management users only */ router.get('/status', async (req, res) => { try { + // Add security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + const [healthResponse, metricsResponse] = await Promise.allSettled([ makeRequest('/health'), makeRequest('/metrics') @@ -163,7 +339,11 @@ router.get('/status', async (req, res) => { }, health: null, metrics: null, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + accessedBy: { + username: req.managementUser.username, + role: req.managementUser.role + } }; if (healthResponse.status === 'fulfilled' && healthResponse.value.status === 200) { @@ -177,10 +357,11 @@ router.get('/status', async (req, res) => { result.metrics = metricsResponse.value.data; } + console.log(`✅ Data retention status accessed by ${req.managementUser.username}`); res.json(result); } catch (error) { - console.error('Data retention status error:', error); + console.error(`❌ Data retention status error for ${req.managementUser.username}:`, error); res.status(503).json({ success: false, error: 'Failed to get data retention service status', diff --git a/server/utils/dataRetentionAuditLogger.js b/server/utils/dataRetentionAuditLogger.js new file mode 100644 index 0000000..cf4ebe1 --- /dev/null +++ b/server/utils/dataRetentionAuditLogger.js @@ -0,0 +1,107 @@ +/** + * Security Audit Logger for Data Retention Access + * Logs all access attempts to data retention metrics + */ + +const fs = require('fs').promises; +const path = require('path'); + +class DataRetentionAuditLogger { + constructor() { + this.logDir = process.env.SECURITY_LOG_DIR || './logs'; + this.logFile = path.join(this.logDir, 'data_retention_access.log'); + } + + async ensureLogDir() { + try { + await fs.mkdir(this.logDir, { recursive: true }); + } catch (error) { + console.error('Failed to create security log directory:', error); + } + } + + async logAccess(event) { + try { + await this.ensureLogDir(); + + const logEntry = { + timestamp: new Date().toISOString(), + event: event.type, + user: { + id: event.user?.id, + username: event.user?.username, + role: event.user?.role + }, + request: { + ip: event.ip, + userAgent: event.userAgent, + endpoint: event.endpoint, + method: event.method + }, + result: event.result, + error: event.error + }; + + const logLine = JSON.stringify(logEntry) + '\n'; + await fs.appendFile(this.logFile, logLine, 'utf8'); + + } catch (error) { + console.error('Failed to write security log:', error); + } + } + + // Log successful access + async logSuccess(user, req, endpoint) { + await this.logAccess({ + type: 'DATA_RETENTION_ACCESS_SUCCESS', + user, + ip: req.ip, + userAgent: req.headers['user-agent'], + endpoint, + method: req.method, + result: 'success' + }); + } + + // Log authentication failure + async logAuthFailure(req, endpoint, reason) { + await this.logAccess({ + type: 'DATA_RETENTION_ACCESS_AUTH_FAILED', + ip: req.ip, + userAgent: req.headers['user-agent'], + endpoint, + method: req.method, + result: 'auth_failed', + error: reason + }); + } + + // Log permission denied + async logPermissionDenied(user, req, endpoint, reason) { + await this.logAccess({ + type: 'DATA_RETENTION_ACCESS_PERMISSION_DENIED', + user, + ip: req.ip, + userAgent: req.headers['user-agent'], + endpoint, + method: req.method, + result: 'permission_denied', + error: reason + }); + } + + // Log network access denied + async logNetworkDenied(req, endpoint) { + await this.logAccess({ + type: 'DATA_RETENTION_ACCESS_NETWORK_DENIED', + ip: req.ip, + userAgent: req.headers['user-agent'], + endpoint, + method: req.method, + result: 'network_denied', + error: 'Access from unauthorized network' + }); + } +} + +module.exports = new DataRetentionAuditLogger(); \ No newline at end of file