@dotrino/vaultd 0.38.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -3
- package/lib/README.md +7 -0
- package/lib/src/admin.js +4 -0
- package/lib/src/atrest.js +0 -0
- package/lib/src/enroll.js +12 -3
- package/lib/src/env.js +2 -2
- package/lib/src/protocol.js +12 -1
- package/lib/src/service.js +355 -54
- package/lib/src/sshAgent.js +100 -0
- package/lib/src/sshKeys.js +76 -0
- package/package.json +6 -6
- package/src/approvals.js +69 -0
- package/src/ctl.js +323 -22
- package/src/daemon.js +174 -15
- package/src/manager.js +6 -0
- package/src/profiles.js +82 -9
- package/src/sealKey.js +80 -0
- package/src/sealer.js +170 -0
- package/src/secretsStore.js +805 -90
- package/src/sshAgent.js +2 -0
- package/src/sshKeys.js +2 -0
- package/src/store.js +3 -1
- package/src/tui/app.js +124 -10
- package/src/tui/i18n.js +33 -6
- package/src/vault.js +839 -35
- package/src/vaultControl.js +50 -14
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ solo de qué cert se emitió un día.
|
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
75
|
dotrino-vault members # el acta: qué llaves son tuyas y qué puede hacer cada una
|
|
76
|
-
dotrino-vault caps <ID> +firma # cambia permisos (+firma -guarda +lee …)
|
|
76
|
+
dotrino-vault caps <ID> +firma # cambia permisos (+firma -guarda +lee +administra +aprueba …)
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
**Los certificados caducan y se renuevan solos.** Un cert dura **30 días**. Mientras
|
|
@@ -235,10 +235,14 @@ dotrino-vault approve <código> # aprueba tecleando los 6 dígitos que MUEST
|
|
|
235
235
|
dotrino-vault reject <deviceId> # rechaza un dispositivo pendiente
|
|
236
236
|
dotrino-vault devices # lista dispositivos enrolados / revocados
|
|
237
237
|
dotrino-vault members # el acta del perfil: qué llaves son tuyas y qué puede cada una
|
|
238
|
-
dotrino-vault caps <ID> ±permiso # cambia lo que puede un dispositivo (+firma -guarda +lee …)
|
|
238
|
+
dotrino-vault caps <ID> ±permiso # cambia lo que puede un dispositivo (+firma -guarda +lee +administra +aprueba …)
|
|
239
239
|
dotrino-vault revoke <nonce> # revoca un dispositivo (le ordena autoborrarse)
|
|
240
240
|
dotrino-vault activity [n] # bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
|
|
241
241
|
dotrino-vault pair --service <ns> # empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
242
|
+
dotrino-vault pair --scope <lista> # los PERMISOS del cert: sign,read,store,secrets:<ns>. Sin esto, sign,read,store.
|
|
243
|
+
dotrino-vault secret policy <ns> approval on|off # el cajón pide tu aprobación en cada lectura (15 min de ventana)
|
|
244
|
+
# Se combina con --service: `--service eco --scope sign` = un bot que firma
|
|
245
|
+
# como aparato del acta y lee SOLO su cajón. `admin` no se empareja (caps).
|
|
242
246
|
dotrino-vault secret set <ns> <CLAVE> <valor> # variable del SCOPE: la comparten todos los
|
|
243
247
|
# aparatos que sirven ese namespace
|
|
244
248
|
dotrino-vault secret set <ns> CLAVE=valor CLAVE2=valor2 … # VARIAS de una vez (un solo aviso)
|
|
@@ -313,7 +317,7 @@ dos.
|
|
|
313
317
|
```sh
|
|
314
318
|
dotrino-vault secret set web PUBLIC_URL https://ejemplo.com --public
|
|
315
319
|
dotrino-vault secret set web API_KEY sk-… # sin bandera: privada
|
|
316
|
-
dotrino-vault secret visibility web PUBLIC_URL private #
|
|
320
|
+
dotrino-vault secret visibility web PUBLIC_URL private # taparla sin tocar el valor (privada → pública no existe)
|
|
317
321
|
```
|
|
318
322
|
|
|
319
323
|
- **Se nace privada.** Y **rotar el valor conserva la visibilidad**: exponer un secreto
|
|
@@ -752,6 +756,47 @@ CLI de apoyo: `dotrino-env status` (qué hay enrolado aquí), `dotrino-env check
|
|
|
752
756
|
los secretos en el entorno de un proceso que no es Node). Primer consumidor:
|
|
753
757
|
`dotrino-proxy` (TURN de Cloudflare).
|
|
754
758
|
|
|
759
|
+
### Aprobación por uso (el teléfono dice que sí) y la llave SSH en el teléfono
|
|
760
|
+
|
|
761
|
+
Un cajón puede exigir el **visto bueno de un aparato** en cada lectura: para lo que corre
|
|
762
|
+
en tu PC (un asistente, un script) y no debería llevarse tus claves sin que lo sepas.
|
|
763
|
+
|
|
764
|
+
```sh
|
|
765
|
+
dotrino-vault caps <ID-del-teléfono> +aprueba # quién aprueba (no viaja en un QR)
|
|
766
|
+
dotrino-vault secret policy claude approval on # el cajón «claude» espera el sí
|
|
767
|
+
dotrino-env run --ns claude -- node mi-script.js # el proceso se queda esperando…
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
…la bóveda apunta el pedido, avisa al teléfono (cola del proxio → aviso nativo), y solo su
|
|
771
|
+
firma entrega las claves — **al proceso que pidió, en memoria**, con una **ventana de
|
|
772
|
+
15 min** por aparato y cajón para no pedir veinte veces. Lo denegado corta sin reintentos;
|
|
773
|
+
lo que nadie atiende vence a los 5 min; todo queda en `dotrino-vault activity`.
|
|
774
|
+
|
|
775
|
+
**La llave SSH vive en el teléfono.** El daemon expone un **agente SSH** (protocolo de
|
|
776
|
+
`ssh-agent`, socket en `$XDG_RUNTIME_DIR/dotrino-vault/ssh-agent.sock`) que **no guarda
|
|
777
|
+
ninguna llave privada**: la llave nace en el teléfono (vault.dotrino.com → *Llave SSH de
|
|
778
|
+
este aparato*; WebCrypto no extraíble, `ecdsa-sha2-nistp256`) y cada firma es un pedido
|
|
779
|
+
que apruebas ahí. En el PC no queda nada que copiar.
|
|
780
|
+
|
|
781
|
+
```sh
|
|
782
|
+
dotrino-vault ssh # imprime el export SSH_AUTH_SOCK=… y la receta
|
|
783
|
+
dotrino-vault ssh keys # las llaves registradas (= ssh-add -L)
|
|
784
|
+
ssh mi-servidor # el teléfono pide tu «sí» y firma
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
**La bóveda puede estar en OTRA máquina** (la de Dotrino en el VPS): en tu PC corre el
|
|
788
|
+
agente **delgado**, que no custodia nada y reenvía cada reto como un pedido a la bóveda
|
|
789
|
+
que enroló ese servicio:
|
|
790
|
+
|
|
791
|
+
```sh
|
|
792
|
+
dotrino-env enroll --ns claude # una vez, contra la bóveda que sea
|
|
793
|
+
dotrino-env ssh-agent --ns claude # imprime export SSH_AUTH_SOCK=…
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Para no aprobar cada comando, reusa la conexión con `ControlMaster auto` +
|
|
797
|
+
`ControlPersist 15m` en `~/.ssh/config`: esa es la ventana de 15 min del SSH.
|
|
798
|
+
`DOTRINO_VAULT_SSH_AGENT=0` apaga el agente.
|
|
799
|
+
|
|
755
800
|
## Alcance
|
|
756
801
|
|
|
757
802
|
- **v1 (este):** daemon headless en Node, **multi-perfil**. En **Linux** queda como
|
package/lib/README.md
CHANGED
|
@@ -279,3 +279,10 @@ cert de una máquina vigente: sin esto toda máquina enrolada caducaba a los 30
|
|
|
279
279
|
| bitácora, cifrado en reposo, candado, multi-perfil | son del daemon; en el navegador dependen de `@dotrino/identity` |
|
|
280
280
|
|
|
281
281
|
MIT · parte de [Dotrino](https://dotrino.com).
|
|
282
|
+
|
|
283
|
+
## Agente SSH delgado (`dotrino-env ssh-agent`)
|
|
284
|
+
|
|
285
|
+
La llave SSH vive en el teléfono del dueño (registrada en su bóveda). En esta máquina solo
|
|
286
|
+
corre un agente `ssh-agent` sin llaves: cada `SIGN_REQUEST` viaja a la bóveda como un
|
|
287
|
+
pedido que el teléfono aprueba firmando. `dotrino-env ssh-agent --ns <ns>` imprime el
|
|
288
|
+
`SSH_AUTH_SOCK`. Como librería: `listSshKeys(args)` y `requestSshSign(args, { keyId, data })`.
|
package/lib/src/admin.js
CHANGED
|
@@ -41,6 +41,10 @@ export const ADMIN_OPS = Object.freeze([
|
|
|
41
41
|
// no debe poder dejar sin configuración a los servicios.
|
|
42
42
|
// `var.setMany` es la MISMA operación con varias variables dentro de un solo sobre: no
|
|
43
43
|
// añade permisos, quita reinicios (ver el enrutado abajo).
|
|
44
|
+
// Ver un valor, su histórico o volver a una versión NO están, y es a propósito: un
|
|
45
|
+
// aparato que administra no tiene sobres de lo privado (solo el servicio dueño y la
|
|
46
|
+
// recuperación). Eso lo hace la bóveda en su máquina (`secret show/history/revert`).
|
|
47
|
+
// No lo cablees.
|
|
44
48
|
'vars', 'var.set', 'var.setMany'
|
|
45
49
|
])
|
|
46
50
|
|
package/lib/src/atrest.js
CHANGED
|
Binary file
|
package/lib/src/enroll.js
CHANGED
|
@@ -261,9 +261,15 @@ export function createEnrollDesk ({
|
|
|
261
261
|
pend.dpub = d.dpub
|
|
262
262
|
pend.deviceId = deviceId
|
|
263
263
|
pend.commit = d.commit
|
|
264
|
-
// Llave de CIFRADO del dispositivo: con ella se le envuelve la clave
|
|
265
|
-
//
|
|
264
|
+
// Llave de CIFRADO del dispositivo: con ella se le envuelve la clave del cajón al
|
|
265
|
+
// admitirlo. Un SERVICIO sin ella entraría al acta y no podría leer NUNCA ninguna
|
|
266
|
+
// variable —las privadas van selladas a esta llave—, así que se corta aquí en vez
|
|
267
|
+
// de admitirlo y dejar que falle más tarde y en otro sitio. Un dispositivo de
|
|
268
|
+
// persona sí puede entrar sin ella: no lee variables de servicio.
|
|
266
269
|
if (typeof d.encPub === 'string') pend.encPub = d.encPub
|
|
270
|
+
if (!pend.encPub && scopeToCn(pend.scope)) {
|
|
271
|
+
return reply(from, { type: MSG_ERROR, error: 'a service must send its encryption key (update @dotrino/vault on the service)' })
|
|
272
|
+
}
|
|
267
273
|
// Certificado de continuidad (opcional): lo firma la identidad que se une, con su
|
|
268
274
|
// propia llave. Se comprueba aquí y se guarda con el miembro al aprobar.
|
|
269
275
|
if (d.continuity) {
|
|
@@ -335,8 +341,11 @@ export function createEnrollDesk ({
|
|
|
335
341
|
let record = null
|
|
336
342
|
try {
|
|
337
343
|
if (typeof identity.admitMember === 'function') {
|
|
344
|
+
// PERMISOS, no tipos (2026-08-22): las capacidades son las del scope ENTERO. Un
|
|
345
|
+
// cajón (`secrets:<ns>`) suma `secrets` y fija el CN; no borra lo demás — un bot
|
|
346
|
+
// con `sign,secrets:eco` firma como aparato del acta Y lee solo su cajón.
|
|
338
347
|
const cn = scopeToCn(pend.scope)
|
|
339
|
-
const caps = cn ? ['secrets'] :
|
|
348
|
+
const caps = [...new Set([...scopeToCaps(pend.scope), ...(cn ? ['secrets'] : [])])]
|
|
340
349
|
if (caps.length) await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', cn, caps, cert, continuity: pend.continuity || null })
|
|
341
350
|
}
|
|
342
351
|
record = (await identity.profileActa?.())?.acta || null
|
package/lib/src/env.js
CHANGED
|
@@ -98,12 +98,12 @@ let lastApplied = null
|
|
|
98
98
|
* `overridden` = las que YA tenían otro valor en el entorno y el vault pisó. Es
|
|
99
99
|
* el dato que delata un `.env` rancio, así que se reporta en vez de callarse.
|
|
100
100
|
*/
|
|
101
|
-
export async function loadEnv ({ ns, dir, override, wait = true, required = [], onRetry } = {}) {
|
|
101
|
+
export async function loadEnv ({ ns, dir, override, wait = true, required = [], onRetry, onPending } = {}) {
|
|
102
102
|
if (override === undefined) override = overrideByDefault()
|
|
103
103
|
ns = resolveNs(ns)
|
|
104
104
|
dir = dir || serviceDir(ns)
|
|
105
105
|
const load = wait ? waitForSecrets : fetchSecrets
|
|
106
|
-
const secrets = await load({ dir, ns, onRetry })
|
|
106
|
+
const secrets = await load({ dir, ns, onRetry, onPending })
|
|
107
107
|
|
|
108
108
|
const missing = required.filter((k) => !(k in secrets))
|
|
109
109
|
if (missing.length) {
|
package/lib/src/protocol.js
CHANGED
|
@@ -66,6 +66,16 @@ export const MSG = Object.freeze({
|
|
|
66
66
|
// Va FIRMADO por la maestra y el agente lo verifica contra su `iss` pineada: un
|
|
67
67
|
// aviso de reinicio sin autenticar ES un ataque de denegación.
|
|
68
68
|
SECRETS_CHANGED: 'vault.secrets.changed', // vault → servicio: { body:{op,ns,ts}, signature }
|
|
69
|
+
// REPARTIR LA LLAVE DE UN CAJÓN A UN MIEMBRO NUEVO. Lo hace el SERVICIO y no la
|
|
70
|
+
// bóveda, porque la bóveda no puede abrir la llave sin la frase y el servicio ya la
|
|
71
|
+
// tiene: re-envolverla no le añade ningún poder. Ver `docs/secretos-sellados.md` §8.11.
|
|
72
|
+
//
|
|
73
|
+
// El servicio NO se fía de lo que le manden: comprueba la firma de la maestra, comprueba
|
|
74
|
+
// el ACTA que viene dentro (también firmada) y saca de ahí la llave pública del
|
|
75
|
+
// destinatario. Así, ni siquiera una bóveda comprometida puede hacerle envolver la
|
|
76
|
+
// llave para alguien que la maestra no haya metido en su propio cajón.
|
|
77
|
+
REWRAP: 'vault.rewrap', // vault → servicio: { body:{op,owner,gen,wrap,target,acta,ts}, signature }
|
|
78
|
+
REWRAP_OK: 'vault.rewrap.ok', // servicio → vault: { data:{op,owner,gen,target,wrap,ts}, signature, cert }
|
|
69
79
|
// --- CONSOLA REMOTA (docs/consola-remota.md) — requiere cert `vault:admin` ---
|
|
70
80
|
// Un solo mensaje con `data.op`: pending · pair · approve · reject · revoke · audit.
|
|
71
81
|
// Admitir y expulsar, nada más: cambiar permisos, traspasar el mando y los secretos
|
|
@@ -87,7 +97,8 @@ export const SCOPE = Object.freeze({
|
|
|
87
97
|
// Consola remota (docs/consola-remota.md): admitir y expulsar miembros a distancia.
|
|
88
98
|
// NO incluye cambiar permisos, traspasar el mando ni conceder `admin`: eso es el rol
|
|
89
99
|
// de master y sigue siendo local. No se empareja — se concede desde el PC.
|
|
90
|
-
ADMIN: 'vault:admin'
|
|
100
|
+
ADMIN: 'vault:admin',
|
|
101
|
+
APPROVE: 'vault:approve' // aprobar pedidos de secretos (cajones con `approval`); se concede a mano, como admin
|
|
91
102
|
})
|
|
92
103
|
|
|
93
104
|
/**
|