docs(0007): spec encryption-at-rest del control plane (JetStream/SQLite en disco)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
issue: 0007
|
||||
title: Cifrado at-rest del control plane (JetStream KV / SQLite en disco)
|
||||
status: spec
|
||||
created: 2026-06-07
|
||||
domain: security
|
||||
scope: unibus (pkg/embeddednats, cmd/membershipd, deploy/cluster) + procedimiento de migración del store existente
|
||||
---
|
||||
|
||||
# Objetivo
|
||||
|
||||
Cifrar en reposo el almacenamiento del plano de control para que un nodo comprometido
|
||||
(root en el VPS) o un disco robado no exponga los metadatos de control en claro.
|
||||
|
||||
Estado actual (auditado el 07/06/2026, report 0012 y siguientes):
|
||||
|
||||
- **Contenido de los mensajes**: cifrado E2E por room (megolm/olm). El servidor nunca ve el
|
||||
plaintext; no vive en el plano de control. **No es el objeto de este issue.**
|
||||
- **Claves de room** (`UNIBUS_room_keys`): guardadas **selladas** (sealed box X25519, cifradas
|
||||
para cada miembro). El servidor las almacena y reparte pero no puede abrirlas. **Ya protegidas.**
|
||||
- **Metadatos de control** (`UNIBUS_rooms`, `UNIBUS_members`, `UNIBUS_rooms_by_member`,
|
||||
`UNIBUS_users`): se serializan con `json.Marshal` y se escriben **en claro** en el store. En
|
||||
cluster ese store es el directorio `local_files/jetstream/` de cada nodo; en single-node es el
|
||||
archivo SQLite `local_files/unibus.db`. Hoy **no hay cifrado at-rest**: con root en un nodo se
|
||||
pueden leer subjects de salas, la pertenencia (quién está en qué sala con qué rol), los handles
|
||||
y roles de los usuarios, y las claves públicas (signPub/kexPub). No se exponen mensajes (E2E) ni
|
||||
se pueden descifrar salas (claves selladas), pero sí toda la topología.
|
||||
|
||||
Tras este issue, los buckets/archivos del control plane quedan cifrados en disco con una clave por
|
||||
nodo gestionada fuera de git. El modelo de amenaza pasa de "root del nodo ve la topología" a "root
|
||||
del nodo necesita además la clave at-rest (que puede vivir en un secreto separado / TPM / variable
|
||||
de entorno inyectada) para leer cualquier cosa".
|
||||
|
||||
# Contexto técnico
|
||||
|
||||
- NATS Server / JetStream soporta **encryption at-rest** nativo: se configura una cifra
|
||||
(`aes` o `chacha20`) y una clave; JetStream cifra los ficheros de los streams/KV en disco. El
|
||||
bus usa un NATS **embebido** (`pkg/embeddednats`), así que la activación es por opciones del
|
||||
servidor embebido, no por un `nats-server.conf` externo.
|
||||
- Para el backend SQLite (single-node) el equivalente sería SQLCipher o cifrado a nivel de
|
||||
archivo/FS; queda como sub-tarea de menor prioridad porque el despliegue real es cluster (KV).
|
||||
|
||||
# Tareas
|
||||
|
||||
1. Confirmar la API de encryption-at-rest del NATS embebido en la versión usada (opción de
|
||||
servidor para cipher + clave; cómo se pasa la clave de forma que no quede en argv ni en git).
|
||||
2. Activar el cifrado en `pkg/embeddednats` detrás de una opción de configuración. La clave se
|
||||
inyecta por archivo (`--jetstream-encryption-key-file`, 0600, junto a las claves TLS del nodo)
|
||||
o variable de entorno desde el unit systemd; nunca en argv ni commiteada.
|
||||
3. `cmd/membershipd`: flag/env para la clave + reflejar el estado en la posture publicada en
|
||||
`/healthz` (p.ej. `"at_rest":true`) para que el monitor lo verifique.
|
||||
4. `deploy/cluster`: provisionar la clave at-rest por nodo (generación + `pass`/secrets gitignored)
|
||||
y cablearla en `cluster.env` + el unit. Documentar en el runbook.
|
||||
5. **Migración del store existente** (gotcha crítico): JetStream no re-cifra retroactivamente los
|
||||
datos ya escritos en claro. Diseñar y documentar el procedimiento seguro para el cluster en
|
||||
producción (probable: backup → exportar snapshot del control plane → parar nodo → recrear el
|
||||
store con la clave activa → re-importar; o rotación nodo a nodo aprovechando la replicación R3).
|
||||
Respetar la regla de migraciones (aditivo, sin pérdida de datos).
|
||||
6. Tests: arrancar un nodo con clave at-rest, escribir un user/room, y verificar que el fichero en
|
||||
disco **no** contiene en claro un subject/handle conocido (grep negativo), y que el nodo sigue
|
||||
leyéndolos con la clave. Verificar que sin la clave el store no se abre.
|
||||
|
||||
# Definition of Done
|
||||
|
||||
- Cifrado at-rest activo en los 3 nodos del cluster; `/healthz` lo refleja en la posture.
|
||||
- Evidencia ejecutable: un valor conocido (subject de sala / handle de usuario) **no** aparece en
|
||||
claro al hacer `grep` sobre `local_files/jetstream/`; el nodo lo sigue sirviendo con la clave.
|
||||
- Procedimiento de migración probado sobre datos reales sin pérdida (snapshot/restore verificado).
|
||||
- La clave at-rest nunca está en git ni en argv; vive en archivo 0600 / secreto inyectado.
|
||||
- No baja ninguna otra capa de seguridad (enforce + ACL + TLS + E2E + sealed keys intactas).
|
||||
|
||||
# Notas
|
||||
|
||||
Aditivo y ortogonal al resto de la seguridad: TLS protege en tránsito, E2E el contenido, las claves
|
||||
de room van selladas; este issue cierra el último hueco (metadatos de control en claro en disco)
|
||||
para el modelo de amenaza "VPS comprometido / disco robado". Prioridad media: el despliegue ya es
|
||||
seguro frente a ataques de red (enforce+TLS+ACL); esto endurece frente a compromiso físico/root del
|
||||
host. Relacionado con el endurecimiento de los issues 0004/0005/0006.
|
||||
Reference in New Issue
Block a user