diff --git a/services/mam-api/src/auth/rate-limit.js b/services/mam-api/src/auth/rate-limit.js new file mode 100644 index 0000000..5b81f2e --- /dev/null +++ b/services/mam-api/src/auth/rate-limit.js @@ -0,0 +1,18 @@ +// Per-IP exponential backoff for /auth/login. Single-instance — fine for +// Dragonflight's deployment shape (one mam-api per node). Documented limitation. +const failures = new Map(); // ip -> count + +const STEPS = [1000, 2000, 4000, 8000, 16000, 30000]; + +export const ipBackoff = { + delayMs(ip) { + const n = failures.get(ip) || 0; + if (n === 0) return 0; + return STEPS[Math.min(n - 1, STEPS.length - 1)]; + }, + recordFailure(ip) { + failures.set(ip, (failures.get(ip) || 0) + 1); + }, + recordSuccess(ip) { failures.delete(ip); }, + reset(ip) { failures.delete(ip); }, +}; diff --git a/services/mam-api/src/index.js b/services/mam-api/src/index.js index 3ee3ea7..6e2ac99 100644 --- a/services/mam-api/src/index.js +++ b/services/mam-api/src/index.js @@ -8,7 +8,7 @@ import os from 'node:os'; import { exec } from 'node:child_process'; import pool from './db/pool.js'; import { errorHandler } from './middleware/errors.js'; -import { requireAuth } from './middleware/auth.js'; +import { requireAuth, requireUiHeader } from './middleware/auth.js'; import { loadS3ConfigFromDb } from './s3/client.js'; import authRouter from './routes/auth.js'; @@ -100,6 +100,8 @@ const UNAUTH_PATHS = new Set(['/auth/login', '/auth/setup', '/auth/setup-require // to require auth. If node-agent grows another endpoint, add it here. // TODO: long-term, issue node-agent a real bound api_token and drop this carve-out. const SERVICE_PATHS = new Set(['/cluster/heartbeat']); +app.use('/api/v1', requireUiHeader); +// then the existing gate: app.use('/api/v1', (req, res, next) => { if (UNAUTH_PATHS.has(req.path)) return next(); if (SERVICE_PATHS.has(req.path)) return next(); diff --git a/services/mam-api/src/middleware/auth.js b/services/mam-api/src/middleware/auth.js index d0bdec2..1d8cb3c 100644 --- a/services/mam-api/src/middleware/auth.js +++ b/services/mam-api/src/middleware/auth.js @@ -62,3 +62,18 @@ export async function requireAuth(req, res, next) { // 3. Nothing matched return res.status(401).json({ error: 'unauthorized' }); } + +// Belt-and-suspenders CSRF: SameSite=Lax already blocks most cross-site +// cookie sends, but a custom header that no