53  39. Endurecimiento: de funciona a publicable

La checklist final que separa portafolio de producción

NotaEn una frase

Nexus funciona en tu máquina: login, documentos, realtime, historial. “Funciona” no es “publicable” — entre ambos hay una auditoría: seguridad por endpoint, configuración sin secretos quemados, errores que no filtran, logs que reconstruyen, índices que aguantan datos reales, tests que cubren lo que importa y docs que permiten a otro levantarlo. Este capítulo es esa pasada — aplicada, no teórica.

53.1 El problema

Todo está “listo”… hasta que se lo enseñas a alguien. El README no levanta en una máquina limpia; hay un endpoint que olvidó requireRole; GET /documents devuelve el content entero de 2MB por documento; un EXPLAIN muestra que la lista escanea la tabla completa; y el log de errores está vacío porque “nunca falla”. La distancia entre

Un array es una colección ordenada de elementos accedidos por posición: ["a","b","c"][0] es "a" (se cuenta desde 0). Python las llama listas, Go slices — misma idea, distinto acento.

return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.

Un log es el diario del programa: qué pasó, cuándo, con qué request. Estructurado = en JSON con campos (request_id, user_id), para filtrar por máquina y no con los ojos.

funciona y publicable se mide en findings — y se cierra con una auditoría sistemática, no con suerte.

53.2 Cómo lo resuelve un equipo

Un equipo serio audita por categorías con evidencia, no por vibes: cada hallazgo es fila de una tabla — área, endpoint/archivo, riesgo, fix, test de regresión. Las categorías son exactamente los capítulos de robustez del libro: seguridad (26), configuración (24), errores (25), observabilidad (23), performance (12), tests (27–29), y docs/operación. Nada nuevo que aprender — todo por aplicar.

53.3 Conceptos nuevos

  • U Auditoría integral: todo lo de los caps. 23–26 aplicado sobre código real
  • D Performance con datos reales: índices faltantes, N+1 escondidos, endpoints que escanean tablas
  • U La suite de tests como contrato: cobertura donde importa, no donde impresiona
  • Ops Documentación usable: README de setup, openapi.json publicado, runbook básico

53.4 La auditoría de Nexus — la tabla viva

Área Hallazgo típico en Nexus Control que debe existir Cap.
Auth GET /documents/{id} sin verificar membresía isMember en la query → 404 a lo ajeno 17
Auth /auth/login sin rate limit 5/min por IP+email → 429 18
Datos GET /me filtra campos internos Contrato whitelist por salida 5, 16
Roles viewer puede POST /revisions requireRole('editor') por ruta 17
Webhooks/WS Socket sin auth ni isMember Misma auth que REST en el upgrade 38
Config JWT_SECRET con default Fail-fast al boot, sin defaults 24
Errores Un endpoint devuelve stack trace Handler global único → {error:{code}} 25
Logs Requests sin request_id Middleware + JSON + nunca secretos 23
Perf GET /documents hace N+1 por revisiones Una query con agregación 12
Perf WHERE project_id sin índice Índice por patrón de acceso 12
Tests Solo camino feliz 401/403/404/422 + fuga de campos 28
Docs README dice “instala y corre” Setup reproducible + openapi + runbook —

53.5 Implementación

Capítulo agnóstico — los dos hallazgos que aparecen siempre en la pasada de performance con datos reales:

-- Finding 1: the documents list scans + N+1 per document
-- Before: one query per document to count its revisions
SELECT d.public_id, d.title, d.current_revision, d.updated_at,
       COUNT(r.rev) AS revision_count
FROM documents d
LEFT JOIN revisions r ON r.document_id = d.id
WHERE d.project_id = $1
GROUP BY d.id
ORDER BY d.updated_at DESC
LIMIT 21;  -- one query, no N+1

-- Finding 2: the access pattern without an index
CREATE INDEX CONCURRENTLY idx_documents_project_updated
  ON documents (project_id, updated_at DESC);
CREATE INDEX CONCURRENTLY idx_revisions_doc_rev
  ON revisions (document_id, rev DESC);
-- EXPLAIN ANALYZE before and after — the audit is measured, not felt

Y el seed para que “con datos reales” no sea una metáfora:

-- 100k rows in 10 seconds — the audit needs volume or it lies
INSERT INTO revisions (document_id, rev, snapshot, author_id)
SELECT 1, g, jsonb_build_object('elements', '[]'::jsonb), 1
FROM generate_series(1, 100000) g;
Audita mi API Nexus [stack] antes de publicarla. Recorre cada endpoint
y para cada uno verifica: auth (¿middleware?), autorización (¿rol/
ownership en la query?), exposición (¿solo campos del contrato?),
errores (¿formato {error:{code}} sin stack?), rate limit si aplica,
queries parametrizadas, índices para su patrón de acceso (EXPLAIN con
100k filas seedeadas), y si su comportamiento tiene test de regresión
(401/403/404/422 + fuga de campos). Salida: tabla de hallazgos
endpoint × riesgo × control × estado, ordenada por severidad, con el
fix y el test de cada uno.

53.6 ¿Por qué una auditoría y no “otra feature más”?

Lo único que necesitas llevarte: endurecer no agrega funciones — agrega confianza verificable: cada riesgo convertido en control testeado es una 3am que no pasa. La auditoría es la diferencia entre “creo que está bien” y “puedo demostrar que está bien”.

README de setup honesto (la prueba: alguien lo levanta sin ti):

# Nexus API
## Setup
1. `docker compose up` — postgres + api + migraciones automáticas
2. `npm test` — suite completa contra taskflow_test
3. API en :3000 — OpenAPI en /docs
## Entorno
Ver `.env.example` — todas las llaves requeridas documentadas.

Runbook básico (la 3am tiene guion): “si la API no responde → /health; si responde pero falla → /health/deep (¿BD?); errores → filtra logs por request_id; lento → EXPLAIN de la query del endpoint”. Cuatro líneas que convierten pánico en procedimiento.

openapi.json publicado: el contrato del cap. 36 generado y servido — el frontend y los testers consultan la fuente de verdad, no el código.

53.7 Errores comunes

Error Por qué pasa Fix
Auditar “de memoria” El control existe en 7 de 9 rutas Tabla por endpoint — lo que no se lista no se audita
Perf con la BD vacía de dev “Va rápido en mi máquina” Seed de 100k filas — el EXPLAIN sin datos miente
Coverage como selfie “92%!” Cobertura donde importa: auth, permisos, errores — Ap. K lo repite
Docs escritos para quien ya sabe “Obvio, npm install” README probado por alguien ajeno — si no levanta, el README falla
Findings sin fix asignado Auditoría como lista de lamentos Cada hallazgo = fila con fix + test de regresión + dueño

53.8 Buenas prácticas

  • La tabla de auditoría vive en el repo (docs/AUDIT.md) — cada PR que toca un endpoint actualiza su fila; seguridad mergeable.
  • Seed generoso y repetible — generate_series/script: el volumen que revela N+1 y seq scans se crea en un comando.
  • Hallazgo → test: cada fix sin test de regresión es un fix que vuelve — la auditoría deja tests, no solo notas.
  • Runbook de 5 líneas mínimo: qué mirar, en qué orden — la observabilidad (cap. 23) convertida en procedimiento.

53.9 Ejercicio

  1. De la tabla: ¿por qué “viewer puede POST /revisions” es BOLA-adjacente y no simplemente “un endpoint sin auth”?
  2. El EXPLAIN de GET /documents muestra Seq Scan sobre 100k filas. ¿Qué índice agregas según el patrón de acceso de la query?
  3. Ordena por severidad estos hallazgos y defiende el orden: endpoint sin auth / stack trace en 500 / README que no levanta / lista sin paginar.

Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.

Un índice es la tabla de contenido de una tabla: sin él, buscar un usuario es leer las 10 millones de filas (seq scan); con él, es ir directo a la página. Se crea según cómo se consulta, y cada uno cuesta escrituras.

La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.

53.10 Mini reto

Pasar la checklist del apéndice F a Nexus y documentar cada hallazgo.

Ejercicio.

  1. No es “sin auth” — el viewer está autenticado y es miembro; el problema es que su rol no permite mutar. Es function-level authorization (OWASP #5): la pregunta correcta no es “¿quién eres?” sino “¿qué puedes hacer aquí?” — requireRole('editor') en la ruta, y el test viewer → 403 permanente.
  2. La query filtra project_id y ordena por updated_at DESC → índice compuesto (project_id, updated_at DESC) — igualdad primero, orden después (cap. 12). El EXPLAIN posterior debe mostrar Index Scan/Bitmap — y si no lo muestra, el índice no cuenta.
  3. Orden defendible: (a) endpoint sin auth — exposición de datos a cualquiera, el peor; (b) stack trace en 500 — mapa del código al atacante; (c) lista sin paginar — se degrada con datos, DoS accidental; (d) README roto — duele pero no expone. El criterio del orden: ¿qué daño hace al mundo exterior primero?

Mini reto. El resultado real: la tabla de hallazgos llena — cada fila endpoint | riesgo | control | estado | test. Nexus maduro tiene verde en casi todo porque los caps. 14–28 ya construyeron los controles; el ejercicio demuestra que están activos en cada ruta — que es lo que “auditoría” significa de verdad.

Repaso de entrevista: las preguntas de este tema viven en el Apéndice I (seguridad y operación).

53.11 Vocabulario técnico del capítulo

Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.

Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.

Un middleware es un filtro en la cadena del request: pasa por él antes de llegar a tu handler. Auth es middleware — “verifica el token” vive una vez y protege todas las rutas, no se copia en cada una.

Una variable de entorno es configuración que vive fuera del código: DATABASE_URL, JWT_SECRET. El mismo binario corre en dev y prod con distintas vars — los secretos nunca se escriben en el código.

Un secreto es un dato que no puede publicarse: contraseñas de BD, llaves de API, el secreto que firma los JWT. Viven en .env (fuera de git) o en un secret manager — nunca en el código ni en el repo.

Una IP es la dirección numérica de una máquina en la red: 203.0.113.10. Los servidores tienen IP pública; dentro de una VPC tienen IPs privadas que internet no ve.

Un volumen es almacenamiento que sobrevive al contenedor: el contenedor muere y renace, el volumen (los datos de Postgres) sigue ahí. Sin volumen, docker compose down borra tu base de datos.

Un repositorio (repo) es la carpeta del proyecto con todo su historial Git adentro. GitHub/GitLab son servicios que hospedan repos para compartirlos y respaldarlos.

Un puerto es una puerta numerada dentro de una computadora: la IP llega al edificio, el puerto al departamento. :3000 = “la app que escucha en el departamento 3000”. Postgres suele usar 5432, HTTP el 80, HTTPS el 443.

La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.

Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.

Una migración es un cambio versionado del esquema: un archivo que dice “crea esta columna” (up) y “bórrala” (down). Son el historial Git de la estructura de la BD — se aplican en orden y no se editan una vez aplicadas.

Un JOIN combina tablas por su relación: “cada tarea con el nombre de su proyecto”. Es la superpotencia relacional — en una query traes lo que en NoSQL serían varios viajes.

La autorización responde “¿qué puedes hacer?”: eres usuario válido (autenticado), pero ¿puedes borrar esta tarea? Se decide por rol o por ownership — y se verifica en cada request, no se recuerda.

53.12 Lo que deberías saber hacer ahora