Saltar al contenido

Auditoría

Audit log e integridad criptográfica

Cómo funciona el log inmutable de acciones y por qué importa.

Todo lo que pasa de crítico en tu organización queda registrado para siempre. No es un nice-to-have — es la base legal y operativa de cualquier disputa, refund, o investigación.

Qué se registra

Toda acción que cambia state crítico:

  • Refunds y cancelaciones
  • Cambios de precio
  • Cambios de visibilidad de un evento (draft ↔ public)
  • Transferencias de entradas
  • Cambios de rol o permisos
  • Cancelaciones y reactivaciones de evento
  • Conexiones/desconexiones de providers de pago

NO se registra:

  • Reads (consultar el dashboard)
  • Acciones del usuario sobre sus propios recursos (Ej. cambiar su propio email)
  • Logs de debug — eso es zerolog, no audit

Schema

Cada entrada tiene:

sql
id              -- ULID o UUID, generado por el caller
organization_id -- nullable, contexto
user_id         -- el actor
resource_type   -- "ticket", "order", "organization", ...
resource_id     -- el ID del recurso afectado
action          -- "refund", "cancel", "transfer", ...
old_values      -- snapshot pre-cambio (JSON)
new_values      -- snapshot post-cambio (JSON)
ip_address      -- inet del actor
user_agent      -- agent del browser/cliente
timestamp       -- TIMESTAMPTZ NOT NULL
metadata        -- contexto extra (JSON libre)
seq             -- número monotónico
prev_hash       -- SHA-256 de la fila anterior
row_hash        -- SHA-256 del contenido + prev_hash

Append-only enforcement

A nivel DB hay triggers BEFORE UPDATE y BEFORE DELETE que abortan cualquier intento de modificar una row. Probalo:

sql
UPDATE audit_log SET action='hacked' WHERE id='...';
-- ERROR: audit_log is append-only — UPDATE is not allowed

La única forma de evadir el trigger es ser superuser de Postgres y deshabilitarlo manualmente. Si eso pasara, queda el siguiente nivel:

Hash chain criptográfico

Cada row tiene row_hash = SHA-256(canonical(row, prev_hash)). Modificar retroactivamente UNA fila cambia su hash, pero la siguiente fila apunta al hash original — la cadena se rompe.

Para verificar la cadena:

sql
SELECT * FROM audit_log_verify_chain();
-- 0 filas = chain sano.
-- N filas = N breaks; cada uno reporta seq, id, reason, expected_hex, actual_hex.

Desde Go:

go
import "github.com/Beepers/platform/shared/pkg/auditlog"

breaks, err := auditlog.VerifyChain(ctx, pool)
if len(breaks) > 0 {
    // Page on-call. Audit log was tampered with.
}

Concurrencia

El trigger BEFORE INSERT toma un pg_advisory_xact_lock(420911) que serializa el cómputo del hash. Esto significa que las inserciones concurrentes en audit_log se serializan, pero el resto de la DB no se ve afectado — el lock es scoped a la transaction y libera al commit/rollback.

Costo: una llamada de lock rápida por insert. En carga típica (cientos de inserts por segundo) el overhead es despreciable.

Retention

Política sugerida:

  • Mantener todo en audit_log por 7 años (regla general financiera AR/EU).
  • Comprimir / mover a cold storage a partir del año 2 (S3 Glacier o pg_archive).
  • Nunca borrar registros bajo investigación legal.

Cómo consultarlo

Hoy hay endpoints internos para soporte. Próximamente vamos a tener un panel dedicado en /dashboard/admin/audit con filtros por:

  • resource_type + resource_id (historia completa de un recurso)
  • user_id (todo lo que hizo un actor)
  • action (todos los refunds, todas las cancelaciones)
  • Rango de fechas

Mientras tanto, podés pedir un export por email.

¿Te resultó útil?