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:
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_hashAppend-only enforcement
A nivel DB hay triggers BEFORE UPDATE y BEFORE DELETE que abortan cualquier intento de modificar una row. Probalo:
UPDATE audit_log SET action='hacked' WHERE id='...';
-- ERROR: audit_log is append-only — UPDATE is not allowedLa ú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:
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:
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_logpor 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?