hardcap: auth middleware en /admin — token obligatorio, comparación en tiempo constante
ci / lint-test-leaks (push) Canceled after 0s
ci / lint-test-leaks (pull_request) Canceled after 0s

- adminAuth: exige Authorization: Bearer <HARDCAP_ADMIN_TOKEN> (o
  X-Admin-Token). Comparación timingSafeEqual sobre sha256 de ambos
  valores (sin leak de longitud ni timing). Fail-closed: sin token
  configurado, TODAS las rutas admin responden 503.
- 401s ahora se loguean con método, ruta e IP (primera trazabilidad de
  acceso que tiene el admin API desde que existe).
- token generado (32 bytes hex) y almacenado SOLO en
  /srv/hardcap-backend/.env — nunca impreso ni versionado.
- test-admin-auth.mjs: 6 tests (sin token, incorrecto, vacío, correcto,
  header alternativo, misma-longitud-distinto) — todos pasan en la VPS.
- desplegado en src + dist; servicio reiniciado y activo.

Nota de contexto: el admin API carecía de auth desde su creación
(index.ts: \x27TODO: add auth middleware before production\x27) y estuvo
alcanzable sin credenciales en :3001 hasta la contención de hoy.
This commit is contained in:
2026-10-31 17:39:38 +00:00
parent 34ee25b32f
commit 88b2ad2980
3 changed files with 57 additions and 0 deletions
+23
View File
@@ -4,6 +4,29 @@ import { ingestAllSources } from '../services/ingestion'
const router = Router()
// ── auth middleware (2026-10-31): token obligatorio, comparación en tiempo constante.
// Fail-closed: sin HARDCAP_ADMIN_TOKEN configurado, TODAS las rutas admin responden 503.
import { createHash, timingSafeEqual } from 'crypto'
const ADMIN_TOKEN = process.env.HARDCAP_ADMIN_TOKEN || ''
function _safeEqual(a: string, b: string): boolean {
// hash de ambos a longitud fija: sin leak de longitud ni de timing por contenido
return timingSafeEqual(createHash('sha256').update(String(a)).digest(), createHash('sha256').update(String(b)).digest())
}
function adminAuth(req: any, res: any, next: any) {
if (!ADMIN_TOKEN) {
console.error('[admin-auth] HARDCAP_ADMIN_TOKEN not set — admin request REFUSED (fail-closed)')
return res.status(503).json({ error: 'admin disabled: token not configured' })
}
const provided = String(req.headers['authorization'] || '').replace(/^Bearer\s+/i, '') || String(req.headers['x-admin-token'] || '')
if (provided && _safeEqual(provided, ADMIN_TOKEN)) return next()
console.error(`[admin-auth] 401 ${req.method} ${req.originalUrl} from ${req.socket.remoteAddress}`)
return res.status(401).json({ error: 'unauthorized' })
}
router.use(adminAuth)
// regla permanente de secretos: jamás loguear el token ni volcar headers.
// ── Articles ──────────────────────────────────────────────
// GET /admin/articles?status=QUEUED&topic=lightning