wandres.dev
CUÁNDO NO HACERLO · el criterio inverso

Cuando los permisos cambian

Revocar el acceso a quien ya tiene una copia local es imposible por construcción: el cifrado convierte el problema en gestión de claves, pero nunca deshace lo que ya se entregó.

⏱ 21 min

Hay una frase que resume esta lección entera y que conviene tener grabada antes de diseñar cualquier sistema replicado: revocar es una promesa sobre el futuro, jamás sobre el pasado. En una arquitectura centralizada esa distinción no se nota, porque el control de acceso se aplica en el momento de la lectura y quitarle el permiso a alguien tiene un efecto que se percibe como retroactivo: deja de poder pedir lo que antes pedía, y el efecto es inmediato y verificable. En una arquitectura donde el dato viaja al dispositivo, el control de acceso se aplica en el momento de la replicación, y ese momento ya pasó. Los bytes están ahí. No hay comando, protocolo ni actualización que los borre de un disco que no controlas, y toda la industria del cifrado y la rotación de claves no existe para negar esa realidad sino para acotar su alcance.

🎯 Al terminar esta lección sabrás
  • Entender por qué el control de acceso replicado se aplica en la entrega y no en la lectura.
  • Reconocer qué convierte el cifrado en una defensa útil y qué sigue sin resolver.
  • Enumerar con precisión lo que es técnicamente imposible por mucho que se prometa.
  • Usar la estabilidad de la audiencia como criterio para decidir qué se replica.

Revocar no es borrar: el modelo mental correcto

La asimetría es estructural. Un servidor comprueba el permiso cada vez que alguien pide un dato, así que el permiso es una condición evaluada en el presente y modificable en cualquier instante. Una réplica comprueba el permiso una sola vez, cuando decide enviar el dato, y a partir de ahí la comprobación ya no existe: el consumidor lee de su propio disco sin preguntar a nadie, que es precisamente la propiedad que hacía atractivo el enfoque. La misma característica que elimina la latencia elimina el punto de aplicación de la política.

Merece la pena insistir en que esto no es un fallo del enfoque sino su definición. Un sistema donde el cliente tuviera que consultar al servidor para saber si aún puede leer su propia copia no sería local-first: sería una arquitectura centralizada con una caché delante y con las desventajas de ambas. La autonomía de la réplica y la aplicabilidad continua de la política son propiedades incompatibles, y quien promete las dos a la vez está describiendo un sistema que no ha construido.

flowchart LR
A[peticion de lectura] --> B[servidor evalua permiso ahora]
B --> C[respuesta o denegacion]
C --> D[revocar surte efecto en la siguiente lectura]
E[replicacion] --> F[permiso evaluado en la entrega]
F --> G[bytes en el dispositivo del par]
G --> H[revocar solo afecta a entregas futuras]
style D fill:#a6e3a1,color:#11111b
style H fill:#f38ba8,color:#11111b

De ahí sale la reformulación que hay que interiorizar. En un sistema replicado, quitarle el acceso a alguien no significa que deje de tener el dato: significa que deja de recibir actualizaciones. La versión que conserva queda congelada en el instante de la revocación, y esa versión puede ser perfectamente suficiente para hacer daño. Un contrato, una lista de clientes, un histórico de conversaciones o un diseño no caducan por dejar de actualizarse. La pregunta de negocio no es cuándo se corta el flujo, sino cuánto vale la fotografía que la otra parte se queda para siempre.

Hay además un caso límite que conviene separar del resto porque cambia el análisis por completo: el dispositivo comprometido. Cuando revocas a una persona, al menos sabes a quién revocas y puedes razonar sobre su comportamiento probable. Cuando el que tiene la copia es un portátil robado o un equipo con software malicioso, no hay revocación posible ni conversación posible, y la única defensa que queda es el cifrado en reposo con una clave que no viva en el mismo dispositivo. Ese es el motivo por el que el bloqueo del sistema operativo y el cifrado del disco no son detalles ajenos a tu arquitectura: en un sistema replicado son parte de ella.

Este es un buen momento para nombrar el sesgo que produce el error. Los ejemplos canónicos del enfoque local-first son documentos personales y equipos pequeños y estables, donde la audiencia de cada dato se decide al crearlo y no cambia nunca. Sobre ese caso, la revocación es un problema teórico. En cuanto el producto entra en una organización real —con rotación de personal, contratistas temporales, clientes que cambian de plan, colaboraciones que terminan mal— la audiencia de cada dato cambia varias veces al año, y el problema teórico se convierte en el problema principal.

⚠️
Compartir por enlace es una revocación imposible disfrazada

El enlace público con permiso de lectura es, en un sistema replicado, la peor combinación posible: una entrega a un destinatario desconocido, sin identidad, sin registro y sin ninguna forma de saber cuántas copias existen. Revocar el enlace impide nuevas entregas y no hace absolutamente nada respecto a las ya realizadas. Si tu producto ofrece esta función sobre datos sensibles, la política de acceso que estás aplicando de hecho es que cualquiera que haya tenido el enlace alguna vez conserva el contenido de forma permanente.

Lo que sí se puede hacer: cifrado, rotación y ámbito

El cifrado extremo a extremo no resuelve la revocación, pero la transforma en un problema mejor: en lugar de controlar bytes esparcidos por dispositivos ajenos, se controla el material que los hace legibles. Si cada documento se cifra con una clave y cada miembro recibe esa clave envuelta con la suya, expulsar a alguien consiste en generar una clave nueva, distribuirla a los que quedan y cifrar con ella todo lo que se escriba a partir de ese momento. El expulsado conserva la clave antigua, y con ella todo el contenido anterior a la rotación. Nada más.

El estado del arte de esta idea está en los protocolos de mensajería de grupo modernos, que mantienen un árbol de claves donde cada cambio de pertenencia produce una renovación eficiente del secreto de grupo. Sus dos garantías tienen nombres precisos y merece la pena entenderlos porque delimitan justo lo que se puede prometer: el secreto hacia el futuro protege los mensajes pasados si una clave actual se ve comprometida, y la seguridad tras compromiso restaura la confidencialidad de los mensajes futuros después de expulsar al intruso. Ninguna de las dos hace ilegible lo que un participante legítimo ya descifró mientras era miembro, y eso no es un defecto de implementación sino una imposibilidad lógica.

// Rotacion perezosa frente a rotacion ansiosa.
// Perezosa: barata, deja legible todo lo anterior con la clave vieja.
await grupo.expulsar(usuario);
await grupo.rotarClave();          // lo nuevo se cifra con la clave nueva

// Ansiosa: recifra el historico completo con la clave nueva.
for (const doc of await grupo.documentos()) {
  await doc.recifrarCon(grupo.claveActual); // costoso, y no recupera nada
}
// El expulsado ya descifro y pudo copiar lo que leyo mientras era miembro.
// Recifrar protege frente a copias del texto cifrado, no frente a su memoria.

Ese matiz final es el que separa una defensa real de un ritual. Recifrar el histórico sirve contra un adversario que se llevó los archivos cifrados y espera obtener la clave más adelante; no sirve absolutamente de nada contra alguien que fue miembro legítimo, descifró en su momento y guardó una copia en claro. Como el segundo escenario es el habitual en una revocación —se expulsa a quien tenía acceso, no a un intruso— la rotación ansiosa consume mucho cómputo para cubrir el caso menos frecuente. Conviene saberlo antes de construirla.

Hay un tercer coste de la rotación que casi nunca se presupuesta y que decide la viabilidad del diseño: la disponibilidad durante el cambio. Rotar una clave de grupo exige que todos los miembros reciban el material nuevo, y en un sistema donde los dispositivos pueden estar semanas sin conectarse, eso significa que durante un tiempo indeterminado coexisten miembros con la clave antigua y miembros con la nueva. Si el contenido nuevo solo se cifra con la nueva, los rezagados dejan de ver actualizaciones aunque sigan siendo miembros legítimos; si se cifra con ambas por compatibilidad, la expulsión no surte efecto hasta que todos hayan migrado. Se elige entre corrección inmediata y disponibilidad, y no hay una tercera opción escondida.

📝
El ámbito de replicación es la palanca más efectiva y la más ignorada

Antes de invertir en criptografía sofisticada conviene agotar una medida mucho más simple: reducir lo que se entrega. Replicar por proyecto en lugar de por organización, por año en lugar de por histórico completo, o solo lo abierto en las últimas semanas, recorta el material irrecuperable en una revocación en uno o dos órdenes de magnitud sin añadir una sola línea de protocolo. La mayoría de los sistemas replican de más porque es cómodo, no porque haga falta, y ese exceso es exactamente lo que se pierde el día que alguien se marcha.

🚫

Deshacer una lectura

Nadie puede hacer que otro deje de saber lo que ya leyó. Toda promesa de revocación retroactiva sobre un dispositivo ajeno es, en el mejor caso, una cortesía que el software del otro puede ignorar.

🚫

Permisos por campo

Ver la ficha pero no la columna de salarios exige filtrar antes de entregar. Si la réplica lleva el registro entero, la interfaz que oculta el campo es maquillaje: el dato está en el disco del usuario.

🚫

Auditar quién leyó

Una lectura local no produce tráfico ni registro. Donde la norma exige la traza de acceso, la propiedad que da valor al enfoque es la que impide cumplirla.

🚫

Probar el borrado

Puedes emitir la orden y esperar. No puedes verificar que se ejecutó en un dispositivo apagado, reinstalado o simplemente modificado por su dueño.

Lo que no se puede hacer, digan lo que digan

Las cuatro tarjetas anteriores no son limitaciones de las herramientas actuales que una versión futura vaya a resolver: son consecuencias de haber entregado el dato. Merece la pena añadir dos efectos secundarios menos conocidos que suelen sorprender incluso a equipos experimentados.

El primero es la filtración por metadatos. Aunque el contenido viaje cifrado, la estructura del historial de operaciones no suele estarlo del todo, y revela mucho: quién participa, con qué frecuencia edita cada uno, cuándo hubo actividad intensa, qué documentos se tocan juntos. En un contexto corporativo o periodístico ese patrón puede ser más sensible que el texto. El segundo es la persistencia de las lápidas: muchos tipos replicados conservan marcas de lo eliminado para poder fusionar correctamente, de modo que el tamaño y la posición de lo borrado siguen ahí, y en algunas implementaciones también su contenido. Borrar en una réplica es, con frecuencia, marcar como borrado.

Aspiración Situación real en un sistema replicado
Revocar acceso Solo detiene entregas futuras, nunca recupera lo entregado
Borrar en remoto Es una petición cortés al software del otro extremo
Permisos por campo Requiere filtrar en el servidor, es decir, no replicar el campo
Traza de lecturas Imposible, la lectura local no genera ningún evento
Caducidad del acceso Aplicable a la sincronización, no a la copia ya descargada
Confidencialidad futura Alcanzable con rotación de claves, y solo hacia adelante

La lectura correcta de esta tabla no es que el enfoque sea inseguro, sino que su modelo de seguridad es distinto y hay que enunciarlo distinto. Un sistema centralizado ofrece control continuo sobre un dato que no posees; uno replicado ofrece posesión real de un dato sobre el que se pierde control. Son dos productos diferentes y la mayoría de las decepciones nacen de vender el segundo con el vocabulario del primero.

Esa confusión de vocabularios tiene además una versión interna, dentro del propio equipo, que hace más daño que la externa. Cuando el modelo de permisos se hereda mentalmente de la arquitectura anterior, se escriben funciones que se llaman revocar, se prueban comprobando que el servidor deja de enviar datos, y todo el mundo queda convencido de que el acceso se retira. Nadie miente y sin embargo el sistema promete algo que no hace, porque la prueba verifica el comportamiento del canal y la promesa hablaba del dato. Renombrar esas funciones —dejar de sincronizar, retirar de futuras entregas— parece un detalle cosmético y en la práctica es lo que impide que alguien construya encima una funcionalidad que dependa de una garantía inexistente.

💡
La regla de la copia ya filtrada

Diseña asumiendo que todo lo que replicas a un dispositivo ajeno ya está fuera de tu control, hoy, aunque el destinatario sea de confianza. Esa suposición no es paranoia: es la única que produce decisiones correctas. Bajo esa lente, la pregunta deja de ser cómo revoco y pasa a ser qué estoy dispuesto a entregar de forma permanente a esta persona, que es la pregunta que un responsable de seguridad haría desde el principio.

Diseñar para permisos estables

De todo lo anterior sale un criterio de clasificación sorprendentemente operativo: replica los datos cuya audiencia se decide al crearlos y cambia poco; deja en el servidor los datos cuya audiencia es una función del estado actual de la organización. Las notas personales, los borradores, un cuaderno compartido con dos colegas de años o los documentos de un proyecto cerrado tienen audiencia estable. El expediente de un cliente en una empresa con rotación, los datos de un equipo con contratistas rotativos o cualquier cosa gobernada por roles y jerarquías tiene audiencia inestable por definición, y ahí la réplica convierte cada cambio organizativo en una fuga potencial.

Ese criterio tiene una propiedad muy útil: es observable antes de escribir código. No hace falta prototipar nada para saber cuántas veces al año cambia quién puede ver el expediente de un cliente, y esa cifra sola separa las colecciones replicables de las que no lo son con mucha más fiabilidad que cualquier debate sobre motores. Un dato cuya audiencia cambia una vez cada cinco años es un candidato excelente; uno cuya audiencia cambia cada vez que alguien entra o sale de un proyecto es un pasivo esperando su momento.

// El criterio hecho explicito en la definicion de cada coleccion.
type PoliticaDeReplica =
  | { modo: 'replica'; motivo: 'audiencia estable' }
  | { modo: 'cache'; caducidadHoras: number }   // audiencia inestable
  | { modo: 'remoto'; motivo: 'auditoria de lectura obligatoria' };

const politicas = {
  notasPersonales: { modo: 'replica', motivo: 'audiencia estable' },
  expedienteCliente: { modo: 'cache', caducidadHoras: 8 },
  historialClinico: { modo: 'remoto', motivo: 'auditoria de lectura obligatoria' },
} satisfies Record<string, PoliticaDeReplica>;
// Declararlo asi obliga a justificar cada replica en el momento de crearla.

La virtud de escribirlo en el propio modelo, y no en un documento aparte, es que la justificación viaja con el código y se revisa cuando alguien lo toca. Una política de acceso que vive en una wiki se desactualiza en semanas; una que forma parte de la definición de la colección obliga a quien añade una tabla nueva a tomar una decisión consciente. No es una garantía técnica de nada, pero cambia el comportamiento por defecto del equipo, y en seguridad el comportamiento por defecto es casi todo.

Cuando la audiencia es inestable pero el usuario necesita velocidad, la respuesta correcta no es replicar con más cuidado sino cambiar la naturaleza de la copia local: una caché con caducidad corta, ligada a la sesión, que se purga al cerrar y que exige revalidar el permiso con regularidad. Deja de ser local-first —no hay longevidad ni offline real— y a cambio recupera un punto de aplicación de la política. Es una degradación consciente y explicable, que es exactamente lo contrario de descubrir el problema cuando alguien abandona la empresa con una copia del directorio comercial.

Queda una capa que la ingeniería no cubre y que conviene no despreciar por ser aburrida: los controles operativos y legales. Gestión de dispositivos con borrado remoto en flotas corporativas, cláusulas de confidencialidad y devolución en los contratos, ámbitos de replicación por proyecto que caducan al cerrarlo, y procesos de baja que revoquen antes de comunicar. Ninguno es una garantía criptográfica y todos reducen el riesgo real. Un sistema serio combina las tres capas —criptográfica, operativa y contractual— sabiendo que solo la primera es demostrable y que las otras dos son las que se usan cuando algo sale mal.

Cerrar bien esta lección exige además una decisión de producto que suele posponerse hasta que es tarde: qué dice tu interfaz cuando alguien pulsa revocar. Si el texto es acceso retirado, el sistema está afirmando algo que no puede cumplir y el usuario tomará decisiones con esa creencia. Si el texto dice que se han detenido las actualizaciones y que las copias descargadas permanecen en los dispositivos de esa persona, el usuario entiende la situación real y puede actuar en consecuencia, incluida la opción de recurrir a las capas operativa o legal. Esa frase no es un detalle de redacción: es donde el modelo de seguridad del sistema se hace visible o se oculta, y ocultarlo es la decisión que después se convierte en una queja fundada.

El control de acceso es una propiedad del canal, no del dato

El error conceptual que esta lección desmonta es tratar el permiso como si fuera un atributo que viaja pegado al dato, cuando en realidad es una propiedad del canal por el que ese dato se entrega. Mientras existe un punto donde alguien decide si responder o negarse, el permiso es una condición evaluada en el presente, revisable, auditable y revocable de verdad. En cuanto los bytes cruzan al dispositivo del otro, ese punto desaparece y con él desaparece la política: lo único que queda es la esperanza de que el software del destinatario siga respetando unas reglas que ya no puedes imponer. Por eso replicar es siempre un acto de delegación de confianza, y la única frontera de seguridad realmente sólida de cualquier sistema es la que los bytes nunca cruzaron. El cifrado no cambia esta verdad, la desplaza: convierte un problema de control de datos en uno de control de claves, y ahí sí puedes actuar, pero solo hacia adelante, porque una clave entregada tampoco vuelve. La consecuencia madura no es renunciar al enfoque, sino elegir con intención qué datos merecen ser entregados de forma irreversible a cada audiencia, aceptando que esa entrega es para siempre. Quien diseña así deja de escribir funciones de revocar que no revocan nada y empieza a escribir decisiones de compartir que significan lo que dicen, que es la única forma de que la promesa de privacidad de este movimiento sea algo más que un eslogan.

⚔️ Audita la estabilidad de tus audiencias
  1. Lista las colecciones replicadas de un sistema y anota, para cada una, cuántas veces cambia su audiencia al año. Ordena por esa cifra.
  2. Toma la colección más inestable y describe qué se queda exactamente la persona revocada. Ponle valor en términos de negocio, no de bytes.
  3. Diseña la rotación de claves para un cambio de pertenencia y estima el coste de la variante ansiosa. Argumenta si el caso que cubre justifica el gasto.
  4. Comprueba si tu implementación conserva lápidas con contenido. Si lo hace, decide si eso es compatible con lo que tu política de privacidad promete.
  5. Escribe la frase exacta que aparecerá en tu interfaz al revocar un acceso. Si no menciona que las copias existentes permanecen, reescríbela.