mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
fix SSH login alert delivery (#1100)
This commit is contained in:
@@ -857,6 +857,36 @@ app.use((_req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Internal endpoint — only accepts calls from localhost.
|
||||||
|
// Used by the main backend to notify the metrics service of SSH login events.
|
||||||
|
app.post("/internal/login-alert", async (req, res) => {
|
||||||
|
const remoteIp = req.socket.remoteAddress;
|
||||||
|
if (
|
||||||
|
remoteIp !== "127.0.0.1" &&
|
||||||
|
remoteIp !== "::1" &&
|
||||||
|
remoteIp !== "::ffff:127.0.0.1"
|
||||||
|
) {
|
||||||
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
}
|
||||||
|
const systemCrypto = (await import("../../utils/system-crypto.js"))
|
||||||
|
.SystemCrypto;
|
||||||
|
const expectedToken = await systemCrypto.getInstance().getInternalAuthToken();
|
||||||
|
const token = req.headers["x-internal-auth"];
|
||||||
|
if (!token || token !== expectedToken) {
|
||||||
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
}
|
||||||
|
const { hostId, userId, sshUser, fromIp } = req.body as {
|
||||||
|
hostId: number;
|
||||||
|
userId: string;
|
||||||
|
sshUser: string;
|
||||||
|
fromIp: string;
|
||||||
|
};
|
||||||
|
AlertEngine.getInstance()
|
||||||
|
.evaluateUserLogin(hostId, userId, sshUser, fromIp)
|
||||||
|
.catch(() => {});
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
app.use(authManager.createAuthMiddleware());
|
app.use(authManager.createAuthMiddleware());
|
||||||
const requireAdmin = authManager.createAdminMiddleware();
|
const requireAdmin = authManager.createAdminMiddleware();
|
||||||
|
|
||||||
@@ -2803,36 +2833,6 @@ registerManagerRoutes(app, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Internal endpoint — only accepts calls from localhost.
|
|
||||||
// Used by the main backend to notify the metrics service of SSH login events.
|
|
||||||
app.post("/internal/login-alert", async (req, res) => {
|
|
||||||
const remoteIp = req.socket.remoteAddress;
|
|
||||||
if (
|
|
||||||
remoteIp !== "127.0.0.1" &&
|
|
||||||
remoteIp !== "::1" &&
|
|
||||||
remoteIp !== "::ffff:127.0.0.1"
|
|
||||||
) {
|
|
||||||
return res.status(403).json({ error: "Forbidden" });
|
|
||||||
}
|
|
||||||
const systemCrypto = (await import("../../utils/system-crypto.js"))
|
|
||||||
.SystemCrypto;
|
|
||||||
const expectedToken = await systemCrypto.getInstance().getInternalAuthToken();
|
|
||||||
const token = req.headers["x-internal-auth"];
|
|
||||||
if (!token || token !== expectedToken) {
|
|
||||||
return res.status(403).json({ error: "Forbidden" });
|
|
||||||
}
|
|
||||||
const { hostId, userId, sshUser, fromIp } = req.body as {
|
|
||||||
hostId: number;
|
|
||||||
userId: string;
|
|
||||||
sshUser: string;
|
|
||||||
fromIp: string;
|
|
||||||
};
|
|
||||||
AlertEngine.getInstance()
|
|
||||||
.evaluateUserLogin(hostId, userId, sshUser, fromIp)
|
|
||||||
.catch(() => {});
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
pollingManager.destroy();
|
pollingManager.destroy();
|
||||||
connectionPool.destroy();
|
connectionPool.destroy();
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { triggerLoginAlert } from "../../utils/alert-trigger.js";
|
||||||
|
import { SystemCrypto } from "../../utils/system-crypto.js";
|
||||||
|
import { sshLogger } from "../../utils/logger.js";
|
||||||
|
|
||||||
|
describe("triggerLoginAlert", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a rejected metrics-service request", async () => {
|
||||||
|
vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({
|
||||||
|
getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"),
|
||||||
|
} as never);
|
||||||
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||||
|
new Response('{"error":"Missing authentication token"}', {
|
||||||
|
status: 401,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {});
|
||||||
|
|
||||||
|
await triggerLoginAlert(7, "user-1", "root", "192.0.2.1");
|
||||||
|
|
||||||
|
expect(warn).toHaveBeenCalledWith(
|
||||||
|
"Failed to trigger login alert",
|
||||||
|
expect.objectContaining({
|
||||||
|
operation: "login_alert_trigger_error",
|
||||||
|
hostId: 7,
|
||||||
|
error:
|
||||||
|
'Metrics service returned 401: {"error":"Missing authentication token"}',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not log a warning when the metrics service accepts the event", async () => {
|
||||||
|
vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({
|
||||||
|
getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"),
|
||||||
|
} as never);
|
||||||
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||||
|
new Response('{"ok":true}', { status: 200 }),
|
||||||
|
);
|
||||||
|
const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {});
|
||||||
|
|
||||||
|
await triggerLoginAlert(7, "user-1", "root", "192.0.2.1");
|
||||||
|
|
||||||
|
expect(warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,14 +11,23 @@ export async function triggerLoginAlert(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const token = await SystemCrypto.getInstance().getInternalAuthToken();
|
const token = await SystemCrypto.getInstance().getInternalAuthToken();
|
||||||
await fetch(`${METRICS_SERVICE_URL}/internal/login-alert`, {
|
const response = await fetch(
|
||||||
|
`${METRICS_SERVICE_URL}/internal/login-alert`,
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-internal-auth": token,
|
"x-internal-auth": token,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ hostId, userId, sshUser, fromIp }),
|
body: JSON.stringify({ hostId, userId, sshUser, fromIp }),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const details = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`Metrics service returned ${response.status}${details ? `: ${details}` : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.warn("Failed to trigger login alert", {
|
sshLogger.warn("Failed to trigger login alert", {
|
||||||
operation: "login_alert_trigger_error",
|
operation: "login_alert_trigger_error",
|
||||||
|
|||||||
Reference in New Issue
Block a user