Seguridad · registro de auditoría
Esa frase cierra el manual de Amedi. Es una promesa a los pacientes y una obligación legal. Pero la tabla que debía cumplirla está vacía: audit_logs existe en el esquema desde el primer día y nunca la ha escrito nadie. Si mañana alguien lee una historia que no le toca, no queda rastro de que pasó.
Esta propuesta diseña el registro completo — qué se guarda, por qué se permitió, cómo se prueba que nadie lo tocó y qué ve el paciente cuando pregunta.
No es que el registro esté incompleto. Es que no existe, y una tabla vacía se parece mucho a una tabla que funciona hasta el día que la necesitas.
Declarada en schema.prisma:1661 con oldValues y newValues como JSON libre. Cero llamadas en todo el código. Ninguna mutación de historia, cita, pago o verificación queda registrada con actor, momento y cambio.
El único registro de lectura que existe, y cubre un solo flujo: la historia compartida al aceptar una cita. Leer una ficha, un chat, un documento o un pago no deja huella. «¿Quién vio mi historia?» hoy no tiene respuesta.
El piso provisional ya está puesto. pgaudit quedó activo en producción y registra escrituras, DDL y cambios de rol en la capa de Postgres. Pero vive lo que dure la retención de logs del plan — días. Los incidentes se descubren semanas después. Sirve para no estar a ciegas mañana; no sirve para responderle a un regulador dentro de un año.
La primera decisión es la que condiciona todas las demás: de qué habla un evento. Hay dos escuelas y la diferencia parece cosmética hasta que llega la primera solicitud de acceso.
Responder «¿quién vio la historia de este paciente?» obliga a escarbar dentro de un arreglo JSON en cada fila. Se puede. No se indexa bien.
La spec lo dice sin rodeos: existe para el «seguimiento determinista de actividades donde el paciente es el sujeto del dato». Un índice, no un escaneo.
«WorkOS es más limpio, el equipo sabe REST, y actor → targets → action cubre todo.»
Cubre todo salvo lo único con plazo legal. El Art. 15 del GDPR convierte
«¿quién accedió a mis datos?» en una respuesta obligatoria con fecha límite, no en una
consulta ocasional. Un campo propio para el paciente es la diferencia entre contestar
en milisegundos y contestar a mano.
De WorkOS sí tomamos lo mejor que tiene: la notación de puntos y, sobre todo,
los esquemas registrados por adelantado. Declarar los eventos válidos antes de
emitirlos es lo que impide que el registro se pudra en cadenas inventadas por cada quien.
Un registro de auditoría que solo entiende un ingeniero no sirve para lo que de verdad se necesita: que un paciente entienda quién vio lo suyo, y que el DPO reconstruya un incidente sin abrir una consola. El evento se guarda estructurado y se cuenta en prosa.
La Dra. Aura Amedi abrió la historia de Carlos Carrasquero
el martes 28 de julio a las 3:40 pm,
porque tenía una cita activa con él.
lo que se guarda
La frase no se almacena: se compone al leer, a partir de los campos. Cambiar el idioma o el tono no toca el registro. Y el registro nunca guarda el contenido de la historia — solo que fue leída.
Este es el campo que falta en casi todos los registros de auditoría, incluido el de WorkOS y los dos tickets que Amedi tiene abiertos. Sin él sabes que la Dra. X leyó la historia de Y — pero no puedes distinguir tratamiento de fisgoneo, que es exactamente el caso que te van a preguntar.
La razón ya se calcula: assertCanAccessPatient decide hoy si el acceso se permite, y para decidirlo averigua por qué — médico tratante, cita activa, secretaria de un doctor, historia compartida. Solo hay que anotar esa razón en vez de descartarla.
Con el porqué registrado, la consulta interesante deja de ser «quién leyó qué» y pasa a ser «qué accesos no tienen una razón clínica detrás». Eso es una alerta, no un informe. Y es lo que detecta al insider.
«Estás inventando un campo que ningún ticket pide.»
No lo invento: es authorization en FHIR, presente en dos niveles del estándar, y es el purpose of use que exige la norma clínica. Lo que sí es nuevo es la consecuencia: CB-015 y CB-008 son la misma decisión. No puedes registrar por qué se permitió un acceso hasta haber decidido qué lo permite. CB-015 está en «refining» y en ningún sitio dice que es el prerequisito real de CB-008.
«Si la autorización tiene un fallo, el registro dirá que el acceso fue por tratamiento. El log firma el ataque como legítimo.»
Cierto, y es la objeción más incómoda de toda la propuesta. Un porqué derivado registra
lo que el sistema creyó, no lo que pasó. Quien explote un fallo de autorización
se lleva un asiento que lo declara legítimo.
Por eso la etiqueta sola es un pasivo. Lo que se guarda no es
treatment: es treatment más la
evidencia que lo sostuvo — el identificador de la cita, la fila de la membresía,
el registro de la historia compartida. Así, cuando la autorización resulte estar mal,
la evidencia se puede re-verificar contra el estado real y el acceso se cae solo.
Un registro que solo guarde la conclusión no admite ser refutado, y lo que no se puede
refutar no es forense.
escritura · quién cambió qué
«Un interceptor que escribe después es más simple y no bloquea la petición.»
Y te deja mutaciones sin registro cuando el proceso muere entre las dos escrituras,
y registros sin mutación cuando hay rollback. Un registro de auditoría que a veces
miente es peor que no tenerlo, porque lo vas a citar creyéndole. Es el detalle más
importante de toda la implementación y no está en el plan de CB-007.
Pero la transacción única tiene un precio real y hay que decirlo: si la escritura
del registro falla, el doctor no puede guardar la consulta. Eso es elegir consistencia
sobre disponibilidad, y en una clínica no es gratis. Por eso lo que va dentro de la
transacción es solo la bandeja de salida — una fila, misma base, sin red ni
criptografía. Firmar y anclar viene después. Se conserva la atomicidad y se deja de
apostar la consulta del paciente a que el firmante responda.
lectura · quién vio qué
«El registro semántico solo ve lo que alguien se acordó de instrumentar. Un endpoint con un IDOR no aparece.»
Exacto, y Amedi acaba de cerrar un clúster entero de IDOR. La matriz de cobertura
te protege de lo que recordaste, no de lo que olvidaste — y un registro incompleto
tampoco es forense.
Por eso el registro de la aplicación no puede ser la única fuente. pgaudit ve
toda escritura llegue por donde llegue: interfaz, script, editor SQL o migración. La
aplicación aporta el significado — quién, por qué, sobre qué paciente — y la base
aporta la completitud. Ninguna de las dos basta sola, y el desajuste entre ambas
es en sí mismo una señal: una escritura que la base vio y la aplicación no explica
es exactamente lo que hay que investigar.
Lo que hace falta para que eso funcione es que el rastro de la base dure, y hoy
no dura. Ver la fase 4.
El registro guarda qué campos cambiaron y un hash de cada valor. Nunca el valor.
Guardar los valores convierte el registro en una segunda copia de historias clínicas, con seis años de retención y otro control de acceso. Y haría imposible el derecho de supresión: borras la historia y el rastro sigue teniéndola.
«Con hashes no puedes reconstruir qué decía antes. Eso no es forense.»
No hace falta reconstruirlo: hace falta probarlo. El registro demuestra qué campo cambió, cuándo y por quién, y permite verificar un valor que alguien afirme — lo hasheas y comparas. La historia es la fuente de la verdad; el registro prueba que cambió. Y cuando un dato se borra por ley, su hash queda sin significado, que es justo lo correcto.
Cada entrada encadena el hash de la anterior. Alterar una fila rompe todo lo que viene después, y romperlo se nota. Pero eso solo vale si la cabeza de la cadena vive donde el atacante no llega.
«Con el ancla diaria basta: si alguien altera una fila, la cadena no cuadra.»
No basta, y esta es la objeción que hundía la primera versión de esta propuesta.
Si la exportación la hace la aplicación, quien controle la aplicación puede alterar
filas y recalcular la cadena antes de que corra la exportación. El ancla solo prueba
el estado del momento en que se exportó: deja una ventana de un día entera en la que
el rastro es reescribible. Y borrarse el rastro propio lleva minutos, no semanas.
Por eso la firma va primero y el ancla después. Con una clave que la app no tiene,
recalcular la cadena deja de ser posible: hace falta una firma nueva por cada entrada
alterada, y esas firmas no las puede producir quien tomó el servidor. El ancla pasa
a ser la segunda línea, no la única.
«El estado del arte es Merkle: Certificate Transparency, Trillian. Una cadena de hashes se quedó en 1998.»
Certificate Transparency existe porque no puedes confiar en el operador del log —
miles de autoridades certificadoras, adversarias entre sí, y terceros que verifican
entradas sueltas. Esa no es tu amenaza. La tuya es alguien de dentro con acceso a la base.
Y contra esa amenaza, una raíz de Merkle guardada en el mismo Postgres que controla
el atacante es teatro. Lo que importa no es la estructura del árbol: es dónde vive
la raíz de confianza. Merkle aporta pruebas de inclusión en O(log n), que sirven cuando
un tercero verifica entradas una a una. Aquí nadie lo hace todavía —
la cadena se diseña para que Merkle se pueda añadir encima sin migrar nada.
quién audita al auditor
Si el rol de la aplicación puede hacer UPDATE o DELETE sobre la tabla, el registro no vale nada contra un administrador. Append-only se impone en Postgres, revocando esos permisos — no por convención ni por revisión de código. Y leer el registro es un rol aparte del de escribirlo.
Todo lo anterior construye evidencia. La evidencia no es una defensa: un registro que nadie consulta es un artefacto de cumplimiento. Sin un bucle que lo lea, te enteras de la brecha porque te la cuenta el paciente — y para entonces ya hay que notificarla.
tres bucles, de más rápido a más fiable
Dispara sobre lo que no debería pasar nunca: acceso sin razón clínica detrás, un acceso denegado seguido del mismo acceso concedido, volumen anómalo de fichas abiertas por una misma persona, lectura de un paciente sin cita ni relación. Barato y ruidoso: sirve para lo obvio.
Alguien con nombre revisa dos listas cortas: los accesos por break-glass y los que la regla marcó. Que sea corta es parte del diseño — una lista que nadie termina de leer no se lee. Y queda registro de que se revisó.
el tercer bucle
El paciente es el mejor detector que vas a tener. Ninguna regla sabe que la doctora que abrió su historia el jueves es su cuñada. Él sí. Darle la pantalla de «quién vio lo mío» no es solo cumplir el Art. 15 del GDPR: es poner un revisor motivado por cada historia clínica, que además conoce el contexto que ningún sistema tiene.
Es también la razón de que el evento se lea en español. Un registro que el sujeto no entiende no lo puede auditar nadie más que tú — y tú eres justo la parte interesada.
«Enseñarle al paciente quién vio su historia va a generar sustos y trabajo de soporte.»
Va a generar preguntas, sí. Y cada pregunta que llegue es un acceso que alguien no supo explicar — que es exactamente lo que quieres descubrir, y barato comparado con descubrirlo en una denuncia. Si el volumen asusta, el problema no es la pantalla: es la cantidad de accesos sin razón.
«Todo cubierto» no es una promesa: es una matriz que o está completa o no lo está. Cada modelo con datos de salud, por cada acción, con su prueba.
| modelo | leer | crear | editar | borrar | hoy |
|---|---|---|---|---|---|
| MedicalRecord | sí | sí | sí | sí | nada |
| ConsultationRecord | sí | sí | sí | sí | solo compartida |
| Patient | sí | sí | sí | sí | nada |
| Appointment | sí | sí | sí | sí | nada |
| ChatMessage | sí | sí | — | sí | nada |
| DoctorDocument | sí | sí | sí | sí | nada |
| ConsultationPayment | sí | sí | sí | — | nada |
| S3File · adjuntos | sí | sí | — | sí | nada |
La matriz es el criterio de terminado, no una aspiración: una prueba por celda, y la suite falla si aparece un modelo de salud sin instrumentar. Lo que no está en la matriz no se registra — y eso también se decide a propósito, no por olvido.
cómo se construye
Sin el modelo de acceso no hay porqué que registrar. Es el prerequisito real, y hoy figura como una tarea suelta en refinamiento.
CB-015 · assertCanAccessPatient
Rehacer la tabla con el modelo FHIR, el diff redactado y la cadena. Instrumentar las mutaciones en la misma transacción. Append-only en Postgres.
CB-007 · sustrato de escritura
Registrar los accesos con significado y la razón que los permitió. Incluidos los denegados.
CB-008 · sustrato de lectura
Firma en KMS por entrada, exportación diaria de la cabeza a object-lock, y el rastro de pgaudit enviado fuera de la plataforma para que dure más que la retención del plan.
IN-007 · log drains · object-lock
Las alertas, la revisión semanal con dueño, y la pantalla donde el paciente pregunta quién vio lo suyo — el revisor mejor informado que vas a tener.
GDPR Art. 15 · retención 6 años
el criterio
Cuando un paciente pregunte quién vio su historia,
la respuesta tiene que caber en una frase
y ser verdad.