SECURITY: Eliminate complex fallback storage, enforce environment variables

Core changes:
- Remove file/database fallback storage complexity
- Enforce JWT_SECRET and DATABASE_KEY as environment variables only
- Auto-generate keys on first startup with clear user guidance
- Eliminate circular dependencies and storage layer abstractions

Security improvements:
- Single source of truth for secrets (environment variables)
- No persistent storage of secrets in files or database
- Clear deployment guidance for production environments
- Simplified attack surface by removing storage complexity

WebSocket authentication:
- Implement JWT authentication for WebSocket handshake
- Add connection limits and user tracking
- Update frontend to pass JWT tokens in WebSocket URLs
- Configure Nginx for authenticated WebSocket proxy

Additional fixes:
- Replace CORS wildcard with specific origins
- Remove password logging security vulnerability
- Streamline encryption architecture following Linus principles
This commit is contained in:
ZacharyZcR
2025-09-22 08:57:37 +08:00
parent ed11b309f4
commit dfc92428e0
6 changed files with 316 additions and 315 deletions

View File

@@ -72,25 +72,37 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
# WebSocket proxy for authenticated terminal connections
location /ssh/websocket/ { location /ssh/websocket/ {
# Pass to WebSocket server with authentication support
proxy_pass http://127.0.0.1:8082/; proxy_pass http://127.0.0.1:8082/;
proxy_http_version 1.1; proxy_http_version 1.1;
# WebSocket upgrade headers
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s; # Pass client information for authentication logging
proxy_send_timeout 300s;
proxy_connect_timeout 75s;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# Important: Pass query parameters (contains JWT token)
proxy_pass_request_args on;
# WebSocket timeouts (longer for terminal sessions)
proxy_read_timeout 86400s; # 24 hours
proxy_send_timeout 86400s; # 24 hours
proxy_connect_timeout 10s; # Quick auth check
# Disable buffering for real-time terminal
proxy_buffering off;
proxy_request_buffering off;
# Handle connection errors gracefully
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
} }
location /ssh/tunnel/ { location /ssh/tunnel/ {

View File

@@ -20,7 +20,16 @@ import { UserDataImport } from "../utils/user-data-import.js";
const app = express(); const app = express();
app.use( app.use(
cors({ cors({
origin: "*", // SECURITY: Specific origins only - no wildcard for production safety
origin: process.env.ALLOWED_ORIGINS ?
process.env.ALLOWED_ORIGINS.split(',').map(origin => origin.trim()) :
[
"http://localhost:3000", // Development React
"http://localhost:5173", // Development Vite
"http://127.0.0.1:3000", // Local development
"http://127.0.0.1:5173", // Local development
],
credentials: true, // Enable credentials for secure cookies/auth
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: [ allowedHeaders: [
"Content-Type", "Content-Type",

View File

@@ -1,34 +1,195 @@
import { WebSocketServer, WebSocket, type RawData } from "ws"; import { WebSocketServer, WebSocket, type RawData } from "ws";
import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2"; import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2";
import { parse as parseUrl } from "url";
import { db } from "../database/db/index.js"; import { db } from "../database/db/index.js";
import { sshCredentials } from "../database/db/schema.js"; import { sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { sshLogger } from "../utils/logger.js"; import { sshLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js";
import { UserCrypto } from "../utils/user-crypto.js";
const wss = new WebSocketServer({ port: 8082 }); // Get auth instances
const authManager = AuthManager.getInstance();
const userCrypto = UserCrypto.getInstance();
sshLogger.success("SSH Terminal WebSocket server started", { // Track user connections for rate limiting
operation: "server_start", const userConnections = new Map<string, Set<WebSocket>>();
const wss = new WebSocketServer({
port: 8082, port: 8082,
// WebSocket authentication during handshake
verifyClient: async (info) => {
try {
const url = parseUrl(info.req.url!, true);
const token = url.query.token as string;
if (!token) {
sshLogger.warn("WebSocket connection rejected: missing token", {
operation: "websocket_auth_reject",
reason: "missing_token",
origin: info.origin,
ip: info.req.socket.remoteAddress
});
return false;
}
// Verify JWT token
const payload = await authManager.verifyJWTToken(token);
if (!payload) {
sshLogger.warn("WebSocket connection rejected: invalid token", {
operation: "websocket_auth_reject",
reason: "invalid_token",
origin: info.origin,
ip: info.req.socket.remoteAddress
});
return false;
}
// Check for TOTP pending (should not allow terminal access during TOTP)
if (payload.pendingTOTP) {
sshLogger.warn("WebSocket connection rejected: TOTP verification pending", {
operation: "websocket_auth_reject",
reason: "totp_pending",
userId: payload.userId,
ip: info.req.socket.remoteAddress
});
return false;
}
// Check connection limits per user (max 3 concurrent connections)
const existingConnections = userConnections.get(payload.userId);
if (existingConnections && existingConnections.size >= 3) {
sshLogger.warn("WebSocket connection rejected: too many connections", {
operation: "websocket_auth_reject",
reason: "connection_limit",
userId: payload.userId,
currentConnections: existingConnections.size,
ip: info.req.socket.remoteAddress
});
return false;
}
// Attach user info to request object
(info.req as any).userId = payload.userId;
(info.req as any).userPayload = payload;
sshLogger.info("WebSocket connection authenticated", {
operation: "websocket_auth_success",
userId: payload.userId,
ip: info.req.socket.remoteAddress
});
return true;
} catch (error) {
sshLogger.error("WebSocket authentication error", error, {
operation: "websocket_auth_error",
ip: info.req.socket.remoteAddress
});
return false;
}
}
}); });
wss.on("connection", (ws: WebSocket) => { sshLogger.success("SSH Terminal WebSocket server started with authentication", {
operation: "server_start",
port: 8082,
features: ["JWT_auth", "connection_limits", "data_access_control"]
});
wss.on("connection", (ws: WebSocket, req) => {
// Extract authenticated user info from request
const userId = (req as any).userId;
const userPayload = (req as any).userPayload;
if (!userId) {
sshLogger.error("WebSocket connection without authentication - should not happen", {
operation: "websocket_security_violation",
ip: req.socket.remoteAddress
});
ws.close(1008, "Authentication required");
return;
}
// Check data access permissions
const dataKey = userCrypto.getUserDataKey(userId);
if (!dataKey) {
sshLogger.warn("WebSocket connection rejected: data locked", {
operation: "websocket_data_locked",
userId,
ip: req.socket.remoteAddress
});
ws.send(JSON.stringify({
type: "error",
message: "Data locked - re-authenticate with password",
code: "DATA_LOCKED"
}));
ws.close(1008, "Data access required");
return;
}
// Track user connections for limits
if (!userConnections.has(userId)) {
userConnections.set(userId, new Set());
}
const userWs = userConnections.get(userId)!;
userWs.add(ws);
sshLogger.info("WebSocket connection established", {
operation: "websocket_connection_established",
userId,
userConnections: userWs.size,
ip: req.socket.remoteAddress
});
let sshConn: Client | null = null; let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null; let sshStream: ClientChannel | null = null;
let pingInterval: NodeJS.Timeout | null = null; let pingInterval: NodeJS.Timeout | null = null;
ws.on("close", () => { ws.on("close", () => {
// Clean up user connection tracking
const userWs = userConnections.get(userId);
if (userWs) {
userWs.delete(ws);
if (userWs.size === 0) {
userConnections.delete(userId);
}
}
sshLogger.info("WebSocket connection closed", {
operation: "websocket_connection_closed",
userId,
remainingConnections: userWs?.size || 0
});
cleanupSSH(); cleanupSSH();
}); });
ws.on("message", (msg: RawData) => { ws.on("message", (msg: RawData) => {
// Verify user still has data access before processing any messages
const currentDataKey = userCrypto.getUserDataKey(userId);
if (!currentDataKey) {
sshLogger.warn("WebSocket message rejected: data access expired", {
operation: "websocket_message_rejected",
userId,
reason: "data_access_expired"
});
ws.send(JSON.stringify({
type: "error",
message: "Data access expired - please re-authenticate",
code: "DATA_EXPIRED"
}));
ws.close(1008, "Data access expired");
return;
}
let parsed: any; let parsed: any;
try { try {
parsed = JSON.parse(msg.toString()); parsed = JSON.parse(msg.toString());
} catch (e) { } catch (e) {
sshLogger.error("Invalid JSON received", e, { sshLogger.error("Invalid JSON received", e, {
operation: "websocket_message", operation: "websocket_message_invalid_json",
userId,
messageLength: msg.toString().length, messageLength: msg.toString().length,
}); });
ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" })); ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" }));
@@ -39,9 +200,14 @@ wss.on("connection", (ws: WebSocket) => {
switch (type) { switch (type) {
case "connectToHost": case "connectToHost":
// Ensure userId is attached to hostConfig for secure credential resolution
if (data.hostConfig) {
data.hostConfig.userId = userId;
}
handleConnectToHost(data).catch((error) => { handleConnectToHost(data).catch((error) => {
sshLogger.error("Failed to connect to host", error, { sshLogger.error("Failed to connect to host", error, {
operation: "ssh_connect", operation: "ssh_connect",
userId,
hostId: data.hostConfig?.id, hostId: data.hostConfig?.id,
ip: data.hostConfig?.ip, ip: data.hostConfig?.ip,
}); });
@@ -82,7 +248,8 @@ wss.on("connection", (ws: WebSocket) => {
default: default:
sshLogger.warn("Unknown message type received", { sshLogger.warn("Unknown message type received", {
operation: "websocket_message", operation: "websocket_message_unknown_type",
userId,
messageType: type, messageType: type,
}); });
} }
@@ -187,15 +354,15 @@ wss.on("connection", (ws: WebSocket) => {
hasCredentialId: !!credentialId, hasCredentialId: !!credentialId,
}); });
if (password) { // SECURITY: Never log password information - removed password preview logging
sshLogger.debug(`Password preview: "${password.substring(0, 15)}..."`, { sshLogger.debug(`SSH authentication setup`, {
operation: "terminal_ssh_password", operation: "terminal_ssh_auth_setup",
}); userId,
} else { hostId: id,
sshLogger.debug(`No password provided`, { authType,
operation: "terminal_ssh_password", hasPassword: !!password,
}); hasCredentialId: !!credentialId,
} });
let resolvedCredentials = { password, key, keyPassword, keyType, authType }; let resolvedCredentials = { password, key, keyPassword, keyType, authType };
if (credentialId && id && hostConfig.userId) { if (credentialId && id && hostConfig.userId) {

View File

@@ -1,9 +1,4 @@
import crypto from "crypto"; import crypto from "crypto";
import path from "path";
import { promises as fs } from "fs";
import { db } from "../database/db/index.js";
import { settings } from "../database/db/schema.js";
import { eq } from "drizzle-orm";
import { databaseLogger } from "./logger.js"; import { databaseLogger } from "./logger.js";
/** /**
@@ -20,11 +15,6 @@ class SystemCrypto {
private jwtSecret: string | null = null; private jwtSecret: string | null = null;
private databaseKey: Buffer | null = null; private databaseKey: Buffer | null = null;
// Storage path configuration
private static readonly JWT_SECRET_FILE = path.join(process.cwd(), '.termix', 'jwt.key');
private static readonly JWT_SECRET_DB_KEY = 'system_jwt_secret';
private static readonly DATABASE_KEY_FILE = path.join(process.cwd(), '.termix', 'db.key');
private static readonly DATABASE_KEY_DB_KEY = 'system_database_key';
private constructor() {} private constructor() {}
@@ -36,7 +26,7 @@ class SystemCrypto {
} }
/** /**
* Initialize JWT secret - open source friendly way * Initialize JWT secret - environment variable only
*/ */
async initializeJWTSecret(): Promise<void> { async initializeJWTSecret(): Promise<void> {
try { try {
@@ -44,7 +34,7 @@ class SystemCrypto {
operation: "jwt_init", operation: "jwt_init",
}); });
// 1. Environment variable priority (production best practice) // Check environment variable
const envSecret = process.env.JWT_SECRET; const envSecret = process.env.JWT_SECRET;
if (envSecret && envSecret.length >= 64) { if (envSecret && envSecret.length >= 64) {
this.jwtSecret = envSecret; this.jwtSecret = envSecret;
@@ -55,30 +45,8 @@ class SystemCrypto {
return; return;
} }
// 2. Check filesystem storage // No environment variable - generate and guide user
const fileSecret = await this.loadSecretFromFile(); await this.generateAndGuideUser();
if (fileSecret) {
this.jwtSecret = fileSecret;
databaseLogger.info("✅ Loaded JWT secret from file", {
operation: "jwt_file_loaded",
source: "file"
});
return;
}
// 3. Check database storage
const dbSecret = await this.loadSecretFromDB();
if (dbSecret) {
this.jwtSecret = dbSecret;
databaseLogger.info("✅ Loaded JWT secret from database", {
operation: "jwt_db_loaded",
source: "database"
});
return;
}
// 4. Generate new key and persist
await this.generateAndStoreSecret();
} catch (error) { } catch (error) {
databaseLogger.error("Failed to initialize JWT secret", error, { databaseLogger.error("Failed to initialize JWT secret", error, {
@@ -99,7 +67,7 @@ class SystemCrypto {
} }
/** /**
* Initialize database encryption key - same pattern as JWT but for database file encryption * Initialize database encryption key - environment variable only
*/ */
async initializeDatabaseKey(): Promise<void> { async initializeDatabaseKey(): Promise<void> {
try { try {
@@ -107,7 +75,7 @@ class SystemCrypto {
operation: "db_key_init", operation: "db_key_init",
}); });
// 1. Environment variable priority (production best practice) // Check environment variable
const envKey = process.env.DATABASE_KEY; const envKey = process.env.DATABASE_KEY;
if (envKey && envKey.length >= 64) { if (envKey && envKey.length >= 64) {
this.databaseKey = Buffer.from(envKey, 'hex'); this.databaseKey = Buffer.from(envKey, 'hex');
@@ -118,19 +86,8 @@ class SystemCrypto {
return; return;
} }
// 2. Check filesystem storage // No environment variable - generate and guide user
const fileKey = await this.loadDatabaseKeyFromFile(); await this.generateAndGuideDatabaseKey();
if (fileKey) {
this.databaseKey = fileKey;
databaseLogger.info("✅ Loaded database key from file", {
operation: "db_key_file_loaded",
source: "file"
});
return;
}
// 3. Generate new key and persist (NO database storage to avoid circular dependency)
await this.generateAndStoreDatabaseKey();
} catch (error) { } catch (error) {
databaseLogger.error("Failed to initialize database key", error, { databaseLogger.error("Failed to initialize database key", error, {
@@ -151,234 +108,71 @@ class SystemCrypto {
} }
/** /**
* Generate new key and persist storage * Generate and guide user - no fallback storage
*/ */
private async generateAndStoreSecret(): Promise<void> { private async generateAndGuideUser(): Promise<void> {
const newSecret = crypto.randomBytes(32).toString('hex'); const newSecret = crypto.randomBytes(32).toString('hex');
const instanceId = crypto.randomBytes(8).toString('hex'); const instanceId = crypto.randomBytes(8).toString('hex');
databaseLogger.info("🔑 Generating new JWT secret for this Termix instance", { // Set in memory for current session
operation: "jwt_generate",
instanceId
});
// Try file storage (priority, faster and doesn't depend on database)
try {
await this.saveSecretToFile(newSecret);
databaseLogger.info("✅ JWT secret saved to file", {
operation: "jwt_file_saved",
path: SystemCrypto.JWT_SECRET_FILE
});
} catch (fileError) {
databaseLogger.warn("⚠️ Cannot save to file, using database storage", {
operation: "jwt_file_save_failed",
error: fileError instanceof Error ? fileError.message : "Unknown error"
});
// File storage failed, use database
await this.saveSecretToDB(newSecret, instanceId);
databaseLogger.info("✅ JWT secret saved to database", {
operation: "jwt_db_saved"
});
}
this.jwtSecret = newSecret; this.jwtSecret = newSecret;
databaseLogger.success("🔐 This Termix instance now has a unique JWT secret", { // Guide user to set environment variable
operation: "jwt_generated_success", console.log("\n" + "=".repeat(80));
console.log("🔐 TERMIX FIRST STARTUP - JWT SECRET REQUIRED");
console.log("=".repeat(80));
console.log(`Generated JWT Secret: ${newSecret}`);
console.log("");
console.log("⚠️ REQUIRED: Set this environment variable:");
console.log(` export JWT_SECRET=${newSecret}`);
console.log("");
console.log("🔄 Restart Termix after setting the environment variable");
console.log("=".repeat(80) + "\n");
databaseLogger.warn("⚠️ JWT secret generated for current session only", {
operation: "jwt_temp_generated",
instanceId, instanceId,
note: "All tokens from previous sessions are invalidated" envVarName: "JWT_SECRET",
note: "Set environment variable and restart for persistent operation"
}); });
} }
// ===== File storage methods =====
/**
* Save key to file
*/
private async saveSecretToFile(secret: string): Promise<void> {
const dir = path.dirname(SystemCrypto.JWT_SECRET_FILE);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(SystemCrypto.JWT_SECRET_FILE, secret, {
mode: 0o600 // Only owner can read/write
});
}
/**
* Load key from file
*/
private async loadSecretFromFile(): Promise<string | null> {
try {
const secret = await fs.readFile(SystemCrypto.JWT_SECRET_FILE, 'utf8');
if (secret.trim().length >= 64) {
return secret.trim();
}
databaseLogger.warn("JWT secret file exists but too short", {
operation: "jwt_file_invalid",
length: secret.length
});
} catch (error) {
// File doesn't exist or can't be read, this is normal
}
return null;
}
// ===== Database key generation and storage methods ===== // ===== Database key generation and storage methods =====
/** /**
* Generate new database key and persist to file storage only * Generate and guide database key - no fallback storage
* (avoid circular dependency with database)
*/ */
private async generateAndStoreDatabaseKey(): Promise<void> { private async generateAndGuideDatabaseKey(): Promise<void> {
const newKey = crypto.randomBytes(32); // 256-bit key for AES-256 const newKey = crypto.randomBytes(32); // 256-bit key for AES-256
const newKeyHex = newKey.toString('hex');
const instanceId = crypto.randomBytes(8).toString('hex'); const instanceId = crypto.randomBytes(8).toString('hex');
databaseLogger.info("🔑 Generating new database encryption key for this Termix instance", { // Set in memory for current session
operation: "db_key_generate",
instanceId
});
// Only try file storage (no database storage to avoid circular dependency)
try {
await this.saveDatabaseKeyToFile(newKey);
databaseLogger.info("✅ Database key saved to file", {
operation: "db_key_file_saved",
path: SystemCrypto.DATABASE_KEY_FILE
});
} catch (fileError) {
databaseLogger.error("❌ Failed to save database key to file", {
operation: "db_key_file_save_failed",
error: fileError instanceof Error ? fileError.message : "Unknown error",
note: "Database encryption cannot work without persistent key storage"
});
throw new Error("Database key file storage is required for database encryption");
}
this.databaseKey = newKey; this.databaseKey = newKey;
databaseLogger.success("🔐 This Termix instance now has a unique database encryption key", { // Guide user to set environment variable
operation: "db_key_generated_success", console.log("\n" + "=".repeat(80));
console.log("🔒 TERMIX FIRST STARTUP - DATABASE KEY REQUIRED");
console.log("=".repeat(80));
console.log(`Generated Database Key: ${newKeyHex}`);
console.log("");
console.log("⚠️ REQUIRED: Set this environment variable:");
console.log(` export DATABASE_KEY=${newKeyHex}`);
console.log("");
console.log("🔄 Restart Termix after setting the environment variable");
console.log("=".repeat(80) + "\n");
databaseLogger.warn("⚠️ Database key generated for current session only", {
operation: "db_key_temp_generated",
instanceId, instanceId,
note: "Database file is now encrypted at rest" envVarName: "DATABASE_KEY",
note: "Set environment variable and restart for persistent operation"
}); });
} }
/**
* Save database key to file (binary format)
*/
private async saveDatabaseKeyToFile(key: Buffer): Promise<void> {
const dir = path.dirname(SystemCrypto.DATABASE_KEY_FILE);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(SystemCrypto.DATABASE_KEY_FILE, key.toString('hex'), {
mode: 0o600 // Only owner can read/write
});
}
/**
* Load database key from file
*/
private async loadDatabaseKeyFromFile(): Promise<Buffer | null> {
try {
const keyHex = await fs.readFile(SystemCrypto.DATABASE_KEY_FILE, 'utf8');
if (keyHex.trim().length >= 64) { // 32 bytes = 64 hex chars
return Buffer.from(keyHex.trim(), 'hex');
}
databaseLogger.warn("Database key file exists but too short", {
operation: "db_key_file_invalid",
length: keyHex.length
});
} catch (error) {
// File doesn't exist or can't be read, this is normal
}
return null;
}
// ===== JWT Database storage methods =====
/**
* Save key to database (plaintext storage, don't pretend encryption helps)
*/
private async saveSecretToDB(secret: string, instanceId: string): Promise<void> {
const secretData = {
secret,
generatedAt: new Date().toISOString(),
instanceId,
algorithm: "HS256"
};
const existing = await db
.select()
.from(settings)
.where(eq(settings.key, SystemCrypto.JWT_SECRET_DB_KEY));
const encodedData = JSON.stringify(secretData);
if (existing.length > 0) {
await db
.update(settings)
.set({ value: encodedData })
.where(eq(settings.key, SystemCrypto.JWT_SECRET_DB_KEY));
} else {
await db.insert(settings).values({
key: SystemCrypto.JWT_SECRET_DB_KEY,
value: encodedData,
});
}
}
/**
* Load key from database
*/
private async loadSecretFromDB(): Promise<string | null> {
try {
const result = await db
.select()
.from(settings)
.where(eq(settings.key, SystemCrypto.JWT_SECRET_DB_KEY));
if (result.length === 0) {
return null;
}
const secretData = JSON.parse(result[0].value);
// Check key validity
if (!secretData.secret || secretData.secret.length < 64) {
databaseLogger.warn("Invalid JWT secret in database", {
operation: "jwt_db_invalid",
hasSecret: !!secretData.secret,
length: secretData.secret?.length || 0
});
return null;
}
return secretData.secret;
} catch (error) {
databaseLogger.warn("Failed to load JWT secret from database", {
operation: "jwt_db_load_failed",
error: error instanceof Error ? error.message : "Unknown error",
});
return null;
}
}
/**
* Regenerate JWT secret (admin function)
*/
async regenerateJWTSecret(): Promise<string> {
databaseLogger.warn("🔄 Regenerating JWT secret - ALL TOKENS WILL BE INVALIDATED", {
operation: "jwt_regenerate",
});
await this.generateAndStoreSecret();
databaseLogger.success("JWT secret regenerated successfully", {
operation: "jwt_regenerated",
warning: "All existing JWT tokens are now invalid",
});
return this.jwtSecret!;
}
/** /**
* Validate JWT secret system * Validate JWT secret system
@@ -412,36 +206,6 @@ class SystemCrypto {
const isValid = await this.validateJWTSecret(); const isValid = await this.validateJWTSecret();
const hasSecret = this.jwtSecret !== null; const hasSecret = this.jwtSecret !== null;
// Check file storage
let hasFileStorage = false;
try {
await fs.access(SystemCrypto.JWT_SECRET_FILE);
hasFileStorage = true;
} catch {
// File doesn't exist
}
// Check database storage
let hasDBStorage = false;
let dbInfo = null;
try {
const result = await db
.select()
.from(settings)
.where(eq(settings.key, SystemCrypto.JWT_SECRET_DB_KEY));
if (result.length > 0) {
hasDBStorage = true;
const secretData = JSON.parse(result[0].value);
dbInfo = {
generatedAt: secretData.generatedAt,
instanceId: secretData.instanceId,
algorithm: secretData.algorithm
};
}
} catch (error) {
// Database read failed
}
// Check environment variable // Check environment variable
const hasEnvVar = !!(process.env.JWT_SECRET && process.env.JWT_SECRET.length >= 64); const hasEnvVar = !!(process.env.JWT_SECRET && process.env.JWT_SECRET.length >= 64);
@@ -450,11 +214,8 @@ class SystemCrypto {
hasSecret, hasSecret,
isValid, isValid,
storage: { storage: {
environment: hasEnvVar, environment: hasEnvVar
file: hasFileStorage,
database: hasDBStorage
}, },
dbInfo,
algorithm: "HS256", algorithm: "HS256",
note: "Using simplified key management without encryption layers" note: "Using simplified key management without encryption layers"
}; };

View File

@@ -213,7 +213,15 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
window.location.port === "5173" || window.location.port === "5173" ||
window.location.port === ""); window.location.port === "");
const wsUrl = isDev // Get JWT token for WebSocket authentication
const jwtToken = localStorage.getItem("jwt");
if (!jwtToken) {
console.error("No JWT token available for WebSocket connection");
setConnectionStatus("disconnected");
return;
}
const baseWsUrl = isDev
? "ws://localhost:8082" ? "ws://localhost:8082"
: isElectron() : isElectron()
? (() => { ? (() => {
@@ -227,6 +235,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
})() })()
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
// Add JWT token as query parameter for authentication
const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
webSocketRef.current = ws; webSocketRef.current = ws;
wasDisconnectedBySSH.current = false; wasDisconnectedBySSH.current = false;
@@ -351,6 +362,24 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
if (terminal) { if (terminal) {
terminal.clear(); terminal.clear();
} }
// Handle authentication errors (code 1008)
if (event.code === 1008) {
console.error("WebSocket authentication failed:", event.reason);
setConnectionError("Authentication failed - please re-login");
setIsConnecting(false);
shouldNotReconnectRef.current = true;
// Clear invalid JWT token
localStorage.removeItem("jwt");
// Show authentication error message
toast.error("Authentication failed. Please log in again.");
// Don't attempt to reconnect on auth failure
return;
}
setIsConnecting(true); setIsConnecting(true);
if ( if (
!wasDisconnectedBySSH.current && !wasDisconnectedBySSH.current &&

View File

@@ -147,7 +147,19 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
} catch (error) {} } catch (error) {}
}); });
ws.addEventListener("close", () => { ws.addEventListener("close", (event) => {
// Handle authentication errors (code 1008)
if (event.code === 1008) {
console.error("WebSocket authentication failed:", event.reason);
terminal.writeln(`\r\n[Authentication failed - please re-login]`);
// Clear invalid JWT token
localStorage.removeItem("jwt");
// Don't attempt to reconnect on auth failure
return;
}
if (!wasDisconnectedBySSH.current) { if (!wasDisconnectedBySSH.current) {
terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`); terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`);
} }
@@ -240,7 +252,15 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
window.location.port === "5173" || window.location.port === "5173" ||
window.location.port === ""); window.location.port === "");
const wsUrl = isDev // Get JWT token for WebSocket authentication
const jwtToken = localStorage.getItem("jwt");
if (!jwtToken) {
console.error("No JWT token available for WebSocket connection");
setConnectionStatus("disconnected");
return;
}
const baseWsUrl = isDev
? "ws://localhost:8082" ? "ws://localhost:8082"
: isElectron() : isElectron()
? (() => { ? (() => {
@@ -255,6 +275,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
})() })()
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
// Add JWT token as query parameter for authentication
const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
webSocketRef.current = ws; webSocketRef.current = ws;
wasDisconnectedBySSH.current = false; wasDisconnectedBySSH.current = false;