@dotrino/vaultd 0.94.1 → 0.96.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 +0 -0
- package/bin/dotrino-vault-tui.js +0 -0
- package/bin/dotrino-vault.js +0 -0
- package/bin/dotrino-vaultd.js +0 -0
- package/bin/sea-entry.js +0 -0
- package/lib/src/admin.js +0 -0
- package/lib/src/atrest.js +0 -0
- package/lib/src/config.js +0 -0
- package/lib/src/enroll.js +0 -0
- package/lib/src/env.js +0 -0
- package/lib/src/envtext.js +0 -0
- package/lib/src/index.js +0 -0
- package/lib/src/invite.js +0 -0
- package/lib/src/kek.js +0 -0
- package/lib/src/lock.js +0 -0
- package/lib/src/protocol.js +0 -0
- package/lib/src/revocation.js +0 -0
- package/lib/src/sealed.js +0 -0
- package/lib/src/service.js +45 -8
- package/lib/src/sshAgent.js +0 -0
- package/lib/src/sshKeys.js +0 -0
- package/package.json +2 -2
- package/src/approvals.js +0 -0
- package/src/atrest.js +0 -0
- package/src/client.js +0 -0
- package/src/commKey.js +0 -0
- package/src/ctl.js +0 -0
- package/src/daemon.js +0 -0
- package/src/ipc.js +0 -0
- package/src/keyowner.js +0 -0
- package/src/manager.js +0 -0
- package/src/node-globals.js +0 -0
- package/src/passwords.js +0 -0
- package/src/paths.js +0 -0
- package/src/profiles.js +0 -0
- package/src/protocol.js +0 -0
- package/src/qr.js +0 -0
- package/src/sealKey.js +0 -0
- package/src/sealer.js +0 -0
- package/src/sealers.js +0 -0
- package/src/secretsStore.js +0 -0
- package/src/selfupdate.js +0 -0
- package/src/sshAgent.js +0 -0
- package/src/sshKeys.js +0 -0
- package/src/store.js +0 -0
- package/src/threadStore.js +0 -0
- package/src/transport.js +0 -0
- package/src/tui/app.js +0 -0
- package/src/tui/i18n.js +0 -0
- package/src/tui/term.js +0 -0
- package/src/vault.js +14 -20
- package/src/vaultControl.js +0 -0
- package/src/version.js +0 -0
- package/vendor/qrcode-generator.cjs +0 -0
- package/lib/README.md +0 -289
package/README.md
CHANGED
|
File without changes
|
package/bin/dotrino-vault-tui.js
CHANGED
|
File without changes
|
package/bin/dotrino-vault.js
CHANGED
|
File without changes
|
package/bin/dotrino-vaultd.js
CHANGED
|
File without changes
|
package/bin/sea-entry.js
CHANGED
|
File without changes
|
package/lib/src/admin.js
CHANGED
|
File without changes
|
package/lib/src/atrest.js
CHANGED
|
File without changes
|
package/lib/src/config.js
CHANGED
|
File without changes
|
package/lib/src/enroll.js
CHANGED
|
File without changes
|
package/lib/src/env.js
CHANGED
|
File without changes
|
package/lib/src/envtext.js
CHANGED
|
File without changes
|
package/lib/src/index.js
CHANGED
|
File without changes
|
package/lib/src/invite.js
CHANGED
|
File without changes
|
package/lib/src/kek.js
CHANGED
|
File without changes
|
package/lib/src/lock.js
CHANGED
|
File without changes
|
package/lib/src/protocol.js
CHANGED
|
File without changes
|
package/lib/src/revocation.js
CHANGED
|
File without changes
|
package/lib/src/sealed.js
CHANGED
|
File without changes
|
package/lib/src/service.js
CHANGED
|
@@ -668,7 +668,13 @@ async function openSealedBundle (sealed, ident, acta = null, masterPubkey = null
|
|
|
668
668
|
// sería aceptar un valor en claro dentro de un paquete que se supone todo sellado, o
|
|
669
669
|
// sea la puerta para colar uno.
|
|
670
670
|
if (typeof e.v === 'string' && !e.e) {
|
|
671
|
-
|
|
671
|
+
// TAMPOCO SE ARREGLA ESPERANDO, igual que `no-wrapping`. El valor está guardado en
|
|
672
|
+
// claro en la bóveda, de antes de que todo viajara sellado, y solo vuelve a
|
|
673
|
+
// escribirlo quien puede abrirla. Reintentar no lo sella: solo vuelve a timbrar.
|
|
674
|
+
throw Object.assign(
|
|
675
|
+
new Error(`${key} arrived in the clear: since 2026-09-02 every variable travels sealed` +
|
|
676
|
+
` — rewrite it in the vault (dotrino-vault unlock, then secret set ${ident?.ns || '<ns>'} ${key} …) or remove it`),
|
|
677
|
+
{ code: 'plaintext-var' })
|
|
672
678
|
}
|
|
673
679
|
// `owner` dice de qué cajón salió, y `gen` con qué llave de ese cajón se abre.
|
|
674
680
|
const cual = String(e.owner || '').startsWith('dev:') ? 'dev' : 'ns'
|
|
@@ -1026,21 +1032,52 @@ function fingerprintOf (secrets) {
|
|
|
1026
1032
|
* Se decide por `e.code` donde lo hay: emparejar un mensaje por su texto se rompe en
|
|
1027
1033
|
* silencio en cuanto alguien lo reescribe (ver `dotrino-error-strings`).
|
|
1028
1034
|
*/
|
|
1035
|
+
const FINAL_CODES = new Set([
|
|
1036
|
+
'no-wrapping', // falta la envoltura, y solo la fabrica el dueño abriendo la bóveda
|
|
1037
|
+
'plaintext-var' // el valor está en claro en la bóveda: hay que volver a escribirlo
|
|
1038
|
+
])
|
|
1039
|
+
|
|
1029
1040
|
export function isFinal (e) {
|
|
1030
|
-
if (e?.code
|
|
1041
|
+
if (FINAL_CODES.has(e?.code)) return true
|
|
1031
1042
|
return /not enrolled|invalid ns|unauthorized: (revoked|expired|scope|cn|untrusted-issuer|cert-device-mismatch|denied)/.test(e?.message || '')
|
|
1032
1043
|
}
|
|
1033
1044
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1045
|
+
/**
|
|
1046
|
+
* CUÁNTO ESPERAR ANTES DE VOLVER A PEDIR, y por qué no siempre es el backoff.
|
|
1047
|
+
*
|
|
1048
|
+
* Un cajón con aprobación hace sonar el teléfono en CADA petición. Con el reintento a
|
|
1049
|
+
* cinco segundos, un fallo posterior a la aprobación volvía a pedir enseguida y el dueño
|
|
1050
|
+
* veía dos avisos del mismo cajón: aprobaba el primero para nada. La regla es que un
|
|
1051
|
+
* reintento no llegue nunca antes de que venza el pedido que ya está sonando.
|
|
1052
|
+
*
|
|
1053
|
+
* La otra mitad importa igual: si el intento se pasó la ventana entera esperando a que
|
|
1054
|
+
* alguien aprobara, no se le suma otra ventana — ahí ya no hay nada sonando, y hacerle
|
|
1055
|
+
* esperar cinco minutos más sería un cuelgue.
|
|
1056
|
+
*
|
|
1057
|
+
* @param {number} backoffMs lo que tocaría por el backoff normal
|
|
1058
|
+
* @param {number} rangAt cuándo timbró este intento (0 si no timbró)
|
|
1059
|
+
* @param {number} approvalTimeoutMs lo que dura un pedido de aprobación
|
|
1060
|
+
* @param {number} now
|
|
1061
|
+
*/
|
|
1062
|
+
export function retryDelay (backoffMs, rangAt, approvalTimeoutMs = APPROVAL_TIMEOUT_MS, now = Date.now()) {
|
|
1063
|
+
if (!rangAt) return backoffMs
|
|
1064
|
+
return Math.max(backoffMs, approvalTimeoutMs - (now - rangAt))
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, retryMs = 5000, maxRetryMs = 60000, approvalTimeoutMs = APPROVAL_TIMEOUT_MS, onRetry, onPending, onCert, publicOnly = false } = {}) {
|
|
1068
|
+
let backoff = retryMs
|
|
1069
|
+
let rangAt = 0 // cuándo timbró ESTE intento; 0 = no hubo aprobación de por medio
|
|
1070
|
+
const notePending = (info) => { rangAt = Date.now(); onPending?.(info) }
|
|
1036
1071
|
for (;;) {
|
|
1037
1072
|
try {
|
|
1038
|
-
return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, onPending, onCert, publicOnly })
|
|
1073
|
+
return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, approvalTimeoutMs, onPending: notePending, onCert, publicOnly })
|
|
1039
1074
|
} catch (e) {
|
|
1040
1075
|
if (isFinal(e)) throw e
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1076
|
+
const wait = retryDelay(backoff, rangAt, approvalTimeoutMs)
|
|
1077
|
+
rangAt = 0
|
|
1078
|
+
try { onRetry?.(e, wait) } catch (_) {}
|
|
1079
|
+
await new Promise((r) => setTimeout(r, wait))
|
|
1080
|
+
backoff = Math.min(maxRetryMs, Math.round(backoff * 1.6))
|
|
1044
1081
|
}
|
|
1045
1082
|
}
|
|
1046
1083
|
}
|
package/lib/src/sshAgent.js
CHANGED
|
File without changes
|
package/lib/src/sshKeys.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@dotrino/identity": "0.76.0",
|
|
24
|
-
"@dotrino/passmanager": "^0.
|
|
24
|
+
"@dotrino/passmanager": "^0.10.1",
|
|
25
25
|
"@dotrino/proxy-client": "^0.13.1",
|
|
26
26
|
"ws": "^8.18.0"
|
|
27
27
|
},
|
package/src/approvals.js
CHANGED
|
File without changes
|
package/src/atrest.js
CHANGED
|
File without changes
|
package/src/client.js
CHANGED
|
File without changes
|
package/src/commKey.js
CHANGED
|
File without changes
|
package/src/ctl.js
CHANGED
|
File without changes
|
package/src/daemon.js
CHANGED
|
File without changes
|
package/src/ipc.js
CHANGED
|
File without changes
|
package/src/keyowner.js
CHANGED
|
File without changes
|
package/src/manager.js
CHANGED
|
File without changes
|
package/src/node-globals.js
CHANGED
|
File without changes
|
package/src/passwords.js
CHANGED
|
File without changes
|
package/src/paths.js
CHANGED
|
File without changes
|
package/src/profiles.js
CHANGED
|
File without changes
|
package/src/protocol.js
CHANGED
|
File without changes
|
package/src/qr.js
CHANGED
|
File without changes
|
package/src/sealKey.js
CHANGED
|
File without changes
|
package/src/sealer.js
CHANGED
|
File without changes
|
package/src/sealers.js
CHANGED
|
File without changes
|
package/src/secretsStore.js
CHANGED
|
File without changes
|
package/src/selfupdate.js
CHANGED
|
File without changes
|
package/src/sshAgent.js
CHANGED
|
File without changes
|
package/src/sshKeys.js
CHANGED
|
File without changes
|
package/src/store.js
CHANGED
|
File without changes
|
package/src/threadStore.js
CHANGED
|
File without changes
|
package/src/transport.js
CHANGED
|
File without changes
|
package/src/tui/app.js
CHANGED
|
File without changes
|
package/src/tui/i18n.js
CHANGED
|
File without changes
|
package/src/tui/term.js
CHANGED
|
File without changes
|
package/src/vault.js
CHANGED
|
@@ -19,6 +19,8 @@ import * as Acta from '@dotrino/identity/acta'
|
|
|
19
19
|
import { createEnrollDesk, deviceIdOf, DEVICE_TTL_MS } from '../lib/src/enroll.js'
|
|
20
20
|
import { createAdminDesk, authorBody } from '../lib/src/admin.js'
|
|
21
21
|
import { shouldNotifyRevoked } from '../lib/src/revocation.js'
|
|
22
|
+
// El sobre del gestor, el mismo que usan la bóveda-en-pestaña y la extensión.
|
|
23
|
+
import { identitySealing } from '@dotrino/passmanager'
|
|
22
24
|
import { createTransport, masterPubkeyOf } from './transport.js'
|
|
23
25
|
import { startSealersPublisher } from './sealers.js'
|
|
24
26
|
import { assertKeyOwnsDir } from './keyowner.js'
|
|
@@ -1200,30 +1202,22 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1200
1202
|
* EL SELLADO del gestor: la misma cripto de los sobres del ecosistema
|
|
1201
1203
|
* (`@dotrino/identity/content`), atada a la llave de cifrado de esta bóveda.
|
|
1202
1204
|
*
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1205
|
+
* Lo arma el PILAR (`identitySealing`), que es la MISMA pieza que usan las otras dos
|
|
1206
|
+
* bóvedas —la de la pestaña y la de dentro de la extensión—. Estaba escrito a mano aquí,
|
|
1207
|
+
* y son las tres puntas del mismo sobre: tres copias son dos que se pueden quedar atrás,
|
|
1208
|
+
* y ese fallo no hace ruido — la petición sale, al otro lado «no es para mí», y desde
|
|
1209
|
+
* fuera se ve como que nadie respondió.
|
|
1210
|
+
*
|
|
1211
|
+
* NO se pone `requireSealed` en el cliente, y no es un olvido: el cliente del vault es
|
|
1212
|
+
* UNO y lo comparte todo, y el protocolo de la CA viaja en claro a propósito (un
|
|
1213
|
+
* enrolamiento es público hasta que hay cert). Lo que corta aquí es `VaultResponder`,
|
|
1214
|
+
* que tira lo que llegue sin la marca `sealed`. Por eso `isSealed` mira la marca del
|
|
1215
|
+
* gestor y no otra.
|
|
1207
1216
|
*
|
|
1208
1217
|
* Sin esto la bóveda RECIBÍA los sobres y los tiraba —el cliente no tenía con qué
|
|
1209
1218
|
* abrirlos— y el aparato se quedaba esperando sin que nada lo dijera.
|
|
1210
1219
|
*/
|
|
1211
|
-
const passwordSealing =
|
|
1212
|
-
async seal (msg, peerEncPub) {
|
|
1213
|
-
if (!peerEncPub) throw Object.assign(new Error('no encryption key'), { code: 'unsealed' })
|
|
1214
|
-
return {
|
|
1215
|
-
app: 'passmanager',
|
|
1216
|
-
// Destinatarios como OBJETOS y no como llaves sueltas: `encrypt` expande cada
|
|
1217
|
-
// uno a todos los aparatos de esa persona, y una cadena a secas se le cae por
|
|
1218
|
-
// el `continue` sin envolver nada — el sobre salía vacío y sin error.
|
|
1219
|
-
sealed: await identity.encrypt([{ encryptionPubkey: peerEncPub }], JSON.stringify(msg)),
|
|
1220
|
-
from: await identity.getEncryptionPubkey()
|
|
1221
|
-
}
|
|
1222
|
-
},
|
|
1223
|
-
// `decrypt` devuelve `{ plaintext }`, no la cadena.
|
|
1224
|
-
async open (env) { return JSON.parse((await identity.decrypt(env.from, null, env.sealed)).plaintext) },
|
|
1225
|
-
isSealed: (m) => !!m && m.app === 'passmanager' && !!m.sealed
|
|
1226
|
-
}
|
|
1220
|
+
const passwordSealing = identitySealing(identity)
|
|
1227
1221
|
const startPasswordDesk = async () => {
|
|
1228
1222
|
if (passwords || !passwordDevices().length) return
|
|
1229
1223
|
const { createPasswordDesk } = await import('./passwords.js')
|
package/src/vaultControl.js
CHANGED
|
File without changes
|
package/src/version.js
CHANGED
|
File without changes
|
|
File without changes
|
package/lib/README.md
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
# @dotrino/vault
|
|
2
|
-
|
|
3
|
-
Usa **este dispositivo (navegador) como bóveda/CA** del ecosistema Dotrino, sin un PC
|
|
4
|
-
con el daemon. Es la contraparte browser del daemon `dotrino-vault`: atiende el mismo
|
|
5
|
-
protocolo de enrolamiento endurecido por el proxy y firma certificados de delegación
|
|
6
|
-
`D ← P` (donde `P` es la identidad de este dispositivo, `@dotrino/identity`).
|
|
7
|
-
|
|
8
|
-
Pensado para que **cualquier app** del ecosistema (no solo la terminal) pueda ofrecer
|
|
9
|
-
"usar este dispositivo como bóveda".
|
|
10
|
-
|
|
11
|
-
## Uso
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
import { Identity } from '@dotrino/identity'
|
|
15
|
-
import { startDeviceVault } from '@dotrino/vault'
|
|
16
|
-
|
|
17
|
-
const identity = await Identity.connect()
|
|
18
|
-
const vault = await startDeviceVault(identity) // se conecta al proxy como P
|
|
19
|
-
|
|
20
|
-
// 1) Abrir un emparejamiento y mostrar el QR/JSON al dispositivo a enrolar:
|
|
21
|
-
const { qr } = vault.startPairing({ label: 'mi-agente' })
|
|
22
|
-
// El dispositivo (p. ej. @dotrino/identity#enrollDevice) consume `qr`, GENERA un
|
|
23
|
-
// código aleatorio y lo MUESTRA (no lo envía).
|
|
24
|
-
|
|
25
|
-
// 2) Cuando el dispositivo pide acceso, aparece en la lista de pendientes:
|
|
26
|
-
vault.onPendingChange(() => {
|
|
27
|
-
for (const { deviceId } of vault.listPending()) {
|
|
28
|
-
// Un humano LEE el código del dispositivo y lo TIPEA aquí:
|
|
29
|
-
// await vault.approve(deviceId, codigoTipeado)
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// 3) Máquinas ya enroladas / revocar:
|
|
34
|
-
const machines = await vault.listMachines() // [{ sub, deviceId, label, exp, nonce, scope }]
|
|
35
|
-
// await vault.revoke(nonce)
|
|
36
|
-
|
|
37
|
-
vault.close()
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Credenciales del vault en vez del `.env` (Node)
|
|
41
|
-
|
|
42
|
-
La cara "dotenv" del paquete: **cualquier proyecto Node** jala sus credenciales del
|
|
43
|
-
vault del dueño y las deja en `process.env`. En el disco del servicio **no queda
|
|
44
|
-
ningún secreto**: solo la llave del dispositivo (generada ahí, nunca sale) y un
|
|
45
|
-
certificado con scope `vault:secrets:<ns>`. Los valores viven **solo en memoria**;
|
|
46
|
-
si la máquina se compromete, revocas el cert y no había nada que robar.
|
|
47
|
-
|
|
48
|
-
### 1) Registro del cliente (una sola vez)
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
# en el VAULT (tu PC): abres el emparejamiento del servicio y cargas sus secretos
|
|
52
|
-
dotrino-vault pair --service miapp # invitación con scope SOLO vault:secrets:miapp
|
|
53
|
-
dotrino-vault secret set miapp API_KEY sk-… # la comparten TODAS las máquinas del ns
|
|
54
|
-
|
|
55
|
-
# en el PROYECTO/servidor: enrola esta máquina (pega la invitación)
|
|
56
|
-
npx dotrino-env enroll --ns miapp
|
|
57
|
-
# → muestra un código: dotrino-vault approve 7K3F-92Q1
|
|
58
|
-
|
|
59
|
-
# de vuelta en el VAULT: lo tipeas leyéndolo de esa pantalla
|
|
60
|
-
dotrino-vault approve 7K3F-92Q1
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Si la misma app corre en **varias máquinas**, lo que cambia de una a otra (el puerto,
|
|
64
|
-
la URL pública) va en el cajón **por aparato**, sin partir el `ns`:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
dotrino-vault devices # el ID del aparato: AB12-CD34
|
|
68
|
-
dotrino-vault secret device set AB12-CD34 PORT 8443 # solo la lee ESA máquina
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Llegan **mezcladas en el mismo bundle** —y por lo tanto en el mismo `process.env`—:
|
|
72
|
-
las del scope, con las del aparato **encima** si se llaman igual.
|
|
73
|
-
|
|
74
|
-
El código lo **genera el servicio** y **no viaja** por la red: el vault solo puede
|
|
75
|
-
echarlo de vuelta si un humano lo tipeó. Así, un vault falso no puede enrolarte y
|
|
76
|
-
aprobar a ciegas no enrola a nadie. Queda `~/.dotrino/service/<ns>/service-identity.json`
|
|
77
|
-
(0600) con `{ device, cert, iss, proxy, ns }`.
|
|
78
|
-
|
|
79
|
-
Es un **comando previo**, no el primer arranque de la app: el enrolamiento necesita a
|
|
80
|
-
un humano leyendo el código en esta pantalla (bajo systemd/PM2 no hay TTY y el código
|
|
81
|
-
acabaría en un log), bloquea esperando la aprobación y **escribe** en disco consumiendo
|
|
82
|
-
una invitación de un solo uso. El arranque, en cambio, solo **lee** la identidad ya
|
|
83
|
-
guardada: es idempotente y no interactúa con nadie. Corre el `enroll` donde corres el
|
|
84
|
-
`npm ci` al aprovisionar la máquina.
|
|
85
|
-
|
|
86
|
-
### 2) En el código
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault (ns = DOTRINO_NS)
|
|
90
|
-
console.log(process.env.API_KEY)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
o explícito:
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
import { loadEnv } from '@dotrino/vault/env'
|
|
97
|
-
const { secrets } = await loadEnv({ ns: 'miapp', required: ['API_KEY'] })
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Es **asíncrono a propósito**: el `import` bloquea el arranque (top-level await) hasta
|
|
101
|
-
que los secretos estén. Si el vault no está disponible, **espera** (reintento con
|
|
102
|
-
backoff) — un servicio sin vault no arranca, no opera con secretos viejos ni vacíos.
|
|
103
|
-
Un fallo NO transitorio (sin enrolar, cert revocado, scope equivocado) sí aborta.
|
|
104
|
-
|
|
105
|
-
#### Esperar al vault es la REGLA. La excepción es una sola
|
|
106
|
-
|
|
107
|
-
Un agente enrolado **espera**. No es una preferencia: arrancar igual significaría
|
|
108
|
-
operar con la configuración vieja del `.env`, que es justo lo que el vault vino a
|
|
109
|
-
dejar de ser. Y la espera casi nunca duele, porque estos agentes no son críticos:
|
|
110
|
-
que un bot o un firmador tarden en levantar no rompe a nadie.
|
|
111
|
-
|
|
112
|
-
La **única** excepción conocida es **el proxio**, y no por importancia sino por una
|
|
113
|
-
razón estructural: el vault habla con sus servicios **por el proxio**. Un proxio que
|
|
114
|
-
espera al vault espera a alguien que necesita que el proxio ya esté escuchando —
|
|
115
|
-
abrazo mortal, y con él se cae el vault de todo el mundo. Por eso el proxio arranca
|
|
116
|
-
con lo que tenga y aplica la configuración cuando llega, con `applyEnv`.
|
|
117
|
-
|
|
118
|
-
> `applyEnv` existe **para ese caso**, no como alternativa cómoda al bloqueo. Si tu
|
|
119
|
-
> agente no está en el camino por el que viaja el propio vault, usa
|
|
120
|
-
> `import '@dotrino/vault/config'` y deja que espere. El precio de la excepción es
|
|
121
|
-
> real: lo que sólo se lee al arrancar llega tarde y no toma efecto hasta reiniciar,
|
|
122
|
-
> así que hay que avisarlo en el log — el proxio lo hace.
|
|
123
|
-
|
|
124
|
-
Para procesos que no son Node, el CLI los inyecta en el entorno de un hijo:
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
dotrino-env run --ns miapp -- ./mi-binario
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Un agente tiene UNA identidad, y se la da el vault
|
|
131
|
-
|
|
132
|
-
Un **aparato** puede llevar varios perfiles, y hasta meter su propia cuenta al vault
|
|
133
|
-
por adopción: llega con una historia que conservar. Un **agente** no es eso. Es un
|
|
134
|
-
servicio: su identidad se la cede el vault y no hay caso en que quiera empujar la
|
|
135
|
-
suya hacia arriba. De ahí tres reglas, que el paquete aplica solas:
|
|
136
|
-
|
|
137
|
-
- **No adopta, nunca.** La invitación declara su modo (`join` / `adopt`); si viene
|
|
138
|
-
abierta para adoptar, `enrollService` la **rechaza al pegarla**, sin salir a la red.
|
|
139
|
-
La intención `join` va además firmada dentro de la petición, para que nadie en el
|
|
140
|
-
medio la convierta en otra cosa.
|
|
141
|
-
- **No acumula.** Enrolar de nuevo **reemplaza** la identidad anterior, que deja de
|
|
142
|
-
existir en ese agente. No es un error a desbloquear con `--force`: es la forma de
|
|
143
|
-
**rotar** la identidad de un agente comprometido. Se avisa por `onReplace` qué se
|
|
144
|
-
descarta.
|
|
145
|
-
- **Un agente, un `ns`.** Varios agentes pueden convivir en una máquina (un
|
|
146
|
-
directorio por namespace); lo que no existe es un agente que sea varios.
|
|
147
|
-
|
|
148
|
-
> ⚠ Si esa llave es además la **identidad de red** del servicio —el caso del proxio,
|
|
149
|
-
> cuyo id de nodo se deriva de ella— reemplazarla le cambia el nombre en la red: las
|
|
150
|
-
> instancias y citas vivas dejan de resolver y los peers que lo tenían pineado lo
|
|
151
|
-
> rechazan hasta re-pinearlo. Es a propósito: así se echa a un nodo comprometido.
|
|
152
|
-
|
|
153
|
-
### Rotar: la bóveda avisa y el agente SE REINICIA
|
|
154
|
-
|
|
155
|
-
Cambiar un secreto en la bóveda no sirve de nada si quien lo usa no se entera. Al
|
|
156
|
-
guardar, la bóveda manda un **aviso firmado** a los agentes de ese `ns` (sin
|
|
157
|
-
valores: sólo dice que cambió), **agrupando** las escrituras seguidas para que
|
|
158
|
-
cargar cinco valores no provoque cinco reinicios.
|
|
159
|
-
|
|
160
|
-
El agente **no recarga en caliente: termina**, y lo levanta su supervisor (pm2,
|
|
161
|
-
systemd `Restart=always`). Con `import '@dotrino/vault/config'` ya viene puesto.
|
|
162
|
-
|
|
163
|
-
Salir en vez de recargar, por tres razones — y la primera es la de peso:
|
|
164
|
-
|
|
165
|
-
1. **Borra de memoria el valor viejo.** En JavaScript un secreto no se puede
|
|
166
|
-
borrar: los strings son inmutables, no hay `zeroize`, y el valor sigue en el
|
|
167
|
-
heap hasta que al recolector le apetezca, más lo que capturó cada *closure* y
|
|
168
|
-
cada caché derivada. Una llave se rota casi siempre **porque se filtró**, así
|
|
169
|
-
que dejarla viva en el proceso anula el motivo de rotarla. Un proceso nuevo
|
|
170
|
-
empieza con el heap limpio.
|
|
171
|
-
2. **Lee todo fresco.** Recargar en caliente exige que cada sitio que leyó una
|
|
172
|
-
variable sepa releerla; esa lista hay que mantenerla para siempre y, cuando se
|
|
173
|
-
queda corta, falla en silencio.
|
|
174
|
-
3. **Es un interruptor de emergencia.** Revocar el cert de un agente ya no espera a
|
|
175
|
-
que alguien se acuerde de reiniciarlo: recibe el `REVOKED` firmado, se apaga, y
|
|
176
|
-
al arrancar `fetchSecrets` recibe «unauthorized: revoked», que no se arregla
|
|
177
|
-
reintentando. Antes, revocar no le quitaba nada a un proceso ya corriendo.
|
|
178
|
-
|
|
179
|
-
Defensas, porque una señal que provoca reinicios es un arma si se descuida: firma
|
|
180
|
-
de la maestra pineada, `ns` que coincida, frescura y anti-replay, **gracia de
|
|
181
|
-
arranque** y **piso entre avisos** (si la configuración nueva rompe el arranque, sin
|
|
182
|
-
eso el servicio entra en ciclo) y **jitter** (diez agentes del mismo `ns` no salen
|
|
183
|
-
todos en el mismo segundo).
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
import { watchEnv } from '@dotrino/vault/env'
|
|
187
|
-
await watchEnv({ ns: 'miapp' }) // termina el proceso al cambiar
|
|
188
|
-
await watchEnv({ ns: 'proxy', onUpdate: (i) => … }) // o decide tú (ver abajo)
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
`onUpdate` es para cuando terminar **no es una opción**. El caso real es el proxio:
|
|
192
|
-
reiniciarlo corta el transporte de todo el ecosistema, así que anota el aviso, lo
|
|
193
|
-
publica en su `GET /peers` y deja el momento a un humano.
|
|
194
|
-
|
|
195
|
-
### Precedencia: el vault MANDA
|
|
196
|
-
|
|
197
|
-
Los valores del vault **pisan** los del `.env` y los del entorno. El vault no
|
|
198
|
-
reemplaza al `.env` —que sigue siendo lo que arranca una máquina sin enrolar— pero
|
|
199
|
-
sí tiene la última palabra sobre las claves que administra.
|
|
200
|
-
|
|
201
|
-
Esa es la pieza que hace barata la **rotación**: cambias el valor en un solo lugar y
|
|
202
|
-
ningún `.env` viejo olvidado en un VPS puede seguir ganando. Con la precedencia al
|
|
203
|
-
revés (como estaba hasta la 0.14.0) rotar exigía además ir a limpiar cada copia
|
|
204
|
-
rancia —el trabajo que se quería evitar— y, peor, el servicio arrancaba con la llave
|
|
205
|
-
vieja **sin decir nada**.
|
|
206
|
-
|
|
207
|
-
Lo que sí se dice en voz alta: al arrancar se listan las claves que el vault tuvo que
|
|
208
|
-
pisar. Es la señal de que en esa máquina quedó un `.env` por limpiar.
|
|
209
|
-
|
|
210
|
-
```bash
|
|
211
|
-
DOTRINO_ENV_OVERRIDE=0 node server.js # escotilla: por esta corrida, gana el entorno
|
|
212
|
-
dotrino-env check # dice qué claves pisaría en esta máquina
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### API `@dotrino/vault/env`
|
|
216
|
-
|
|
217
|
-
- `loadEnv({ ns?, dir?, override?, wait?, required?, onRetry? }) → { ns, secrets, injected, overridden, skipped }`
|
|
218
|
-
(por defecto **pisa** lo que ya esté en el entorno; `override: false` invierte la regla).
|
|
219
|
-
`overridden` son las claves que tenían otro valor y el vault reemplazó.
|
|
220
|
-
- `applyEnv(secrets, override?) → { injected, overridden, skipped }` — vuelca un bundle
|
|
221
|
-
ya obtenido, sin pedirlo. Para servicios que **no pueden bloquear su arranque**
|
|
222
|
-
esperando al vault y lo aplican cuando llega (el caso del proxy: el vault le habla
|
|
223
|
-
*por* el proxy, así que esperarlo sería un abrazo mortal).
|
|
224
|
-
- `serviceDir(ns)`, `serviceRoot()`, `listEnrolled()`, `resolveNs(ns?)`
|
|
225
|
-
- Entorno: `DOTRINO_NS` · `DOTRINO_ENV_DIR` · `DOTRINO_ENV_HOME` · `DOTRINO_ENV_QUIET` ·
|
|
226
|
-
`DOTRINO_ENV_OVERRIDE`
|
|
227
|
-
- CLI: `dotrino-env enroll|status|check|run` (`check` lista **nombres** de secretos, nunca valores)
|
|
228
|
-
|
|
229
|
-
Bajo el capó es `@dotrino/vault/service` (`enrollService` / `waitForSecrets`): petición
|
|
230
|
-
firmada por la llave del servicio + cert, respuesta **sellada** (ECDH efímero + AES-GCM,
|
|
231
|
-
el proxy no ve los valores) y **firmada por la maestra**, verificada contra la `iss`
|
|
232
|
-
pineada en el enrolamiento.
|
|
233
|
-
|
|
234
|
-
## Modelo de aprobación (seguro por diseño)
|
|
235
|
-
|
|
236
|
-
- El **dispositivo** que se enrola genera un **código aleatorio** (`makePairingCode`) y
|
|
237
|
-
lo **muestra**; el código **no viaja** por la red.
|
|
238
|
-
- Esta bóveda **no conoce** el código: un humano lo **lee del dispositivo** y lo **tipea**
|
|
239
|
-
aquí. Al aprobar, la bóveda firma el cert y **echa** el código tipeado de vuelta.
|
|
240
|
-
- El dispositivo acepta el cert **solo si el código echado coincide** con el que generó.
|
|
241
|
-
Así, una bóveda falsa (que nunca vio el código) no puede enrolarlo, y **aprobar a ciegas**
|
|
242
|
-
(sin ir a leer el código del dispositivo) no enrola a nadie.
|
|
243
|
-
|
|
244
|
-
## API
|
|
245
|
-
|
|
246
|
-
`startDeviceVault(identity, { proxyUrl? }) → Promise<handle>`
|
|
247
|
-
|
|
248
|
-
- `startPairing({ scope?, ttlMs?, label?, mode?, account? }) → { qr, expiresInMs }`
|
|
249
|
-
- `stopPairing(token)`
|
|
250
|
-
- `listPending() → [{ deviceId, label }]`
|
|
251
|
-
- `approve(deviceId, code) → Promise<{ ok, deviceId }>` (code = lo que muestra el dispositivo)
|
|
252
|
-
- `reject(deviceId)`
|
|
253
|
-
- `listMachines() → Promise<[{ sub, deviceId, label, scope, exp, nonce }]>`
|
|
254
|
-
- `revoke(nonce) → Promise`
|
|
255
|
-
- `getSelfCert() → Promise<cert>` (self-cert `P ← P`, para actuar además de cliente)
|
|
256
|
-
- `onPendingChange(fn)`, `onAdopted(fn)`, `close()`
|
|
257
|
-
|
|
258
|
-
Cripto y firma: `@dotrino/identity`. Transporte: `@dotrino/proxy-client`. No reimplementa
|
|
259
|
-
nada del ecosistema.
|
|
260
|
-
|
|
261
|
-
### Qué atiende, y qué NO
|
|
262
|
-
|
|
263
|
-
Esta bóveda **no es** el daemon del PC: comparte el núcleo de enrolamiento
|
|
264
|
-
(`lib/src/enroll.js`, el mismo archivo), pero atiende menos mensajes del protocolo.
|
|
265
|
-
|
|
266
|
-
Atiende: `vault.hello` (la llave que pide el QR corto), `vault.enroll` +
|
|
267
|
-
`vault.acta.sealed` (enrolar y adoptar), `vault.renew` (**renovación automática** del
|
|
268
|
-
cert de una máquina vigente: sin esto toda máquina enrolada caducaba a los 30 días) y
|
|
269
|
-
`vault.devices` (lista + revocados, con re-emisión del `REVOKED` firmado).
|
|
270
|
-
|
|
271
|
-
**No** atiende, y hoy solo existen contra el daemon `dotrino-vault`:
|
|
272
|
-
|
|
273
|
-
| Falta | Qué implica |
|
|
274
|
-
|---|---|
|
|
275
|
-
| `vault.sign` | una máquina no puede pedirle a la maestra que firme por ella |
|
|
276
|
-
| `vault.store` / `vault.get` | no hay store centralizado, ni edición de perfil, ni clave de contenido |
|
|
277
|
-
| `vault.secrets` | `@dotrino/vault/config` (el reemplazo del `.env`) **no funciona** contra un dispositivo |
|
|
278
|
-
| `vault.admin` | sin consola remota |
|
|
279
|
-
| bitácora, cifrado en reposo, candado, multi-perfil | son del daemon; en el navegador dependen de `@dotrino/identity` |
|
|
280
|
-
|
|
281
|
-
MIT · parte de [Dotrino](https://dotrino.com).
|
|
282
|
-
|
|
283
|
-
## Agente SSH con llaves en memoria (`dotrino-env ssh-agent`)
|
|
284
|
-
|
|
285
|
-
La llave SSH es un secreto más del cajón (`SSH_KEY_*`, el archivo en base64). Al arrancar,
|
|
286
|
-
el agente pide el cajón a la bóveda (con la aprobación del teléfono si el aparato la pide),
|
|
287
|
-
carga las llaves en memoria y sirve el protocolo de `ssh-agent`; en el disco no queda nada.
|
|
288
|
-
`dotrino-env ssh-agent --ns ssh` imprime el `SSH_AUTH_SOCK`. Como librería:
|
|
289
|
-
`loadPrivateKey`, `publicLine`, `signSsh` (`src/sshKeys.js`) y `startSshAgent` (`src/sshAgent.js`).
|