docs: Actualización de contexto IA, Drones, Candados y nuevos nodos

This commit is contained in:
Ricardo Monla
2026-04-08 18:09:04 -03:00
parent cad987ce7f
commit fb8c8eefe5
139 changed files with 900 additions and 36345 deletions
+12 -13
View File
@@ -1,16 +1,15 @@
{
"rsa": "eyJpdiI6IkpoTmpZRWZMeE12eWhXSVMiLCJ0YWciOiJUUnJGY05qRlVoOTl2REplTms0ZU5RPT0iLCJkYXRhIjoiNkFIWCsyYm9ndzZKbGM3c0hRPT0ifQ==",
"domain_admin": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
"admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
"win_domain": "eyJpdiI6IitrY2V0eEQxektEMFk2cGgiLCJ0YWciOiJtQmdIYU90ZTFFWUlGZElsMXBIOTJBPT0iLCJkYXRhIjoiNVVFanhaN2tIdHViZDRDRnlnPT0ifQ==",
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=",
"srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==",
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==",
"ns8:rmonla:sudo": "eyJpdiI6IkRqbTBhVlZSYUpVdno3TFkiLCJ0YWciOiJJSnE2ei8zZHIrV2hFQks1VjNpVGN3PT0iLCJkYXRhIjoiT3JxTjRhTmlQK2R3TlJZZk9BPT0ifQ==",
"srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0=",
"srv-ns8:rmonla:sudo": "eyJpdiI6InJDOUp2STdYR1crb2NkMTgiLCJ0YWciOiJHSUZVbXM5VnVmZ3d1NXQ3SFVwOUdRPT0iLCJkYXRhIjoib1ZVTzdaLzk1WGdwZDhKREM3aWpnV3c9In0=",
"dasu-sql2:Administrator": "eyJpdiI6IlJ0VWJzblVGM2NhWEtvNlgiLCJ0YWciOiJtM0llb2luUmRUZng0NnBsL1JtKzl3PT0iLCJkYXRhIjoiSHVhaklOTFlsSm1PbDJkTjNoYlF0UT09In0=",
"srvv-nginx-rm:root": "eyJpdiI6Ijc0STVkTk92Tk5LZEZZWWciLCJ0YWciOiJlOTAwcGpTa21PZFVhN2FJTzIxU1VBPT0iLCJkYXRhIjoiYUJRR3hRakFub1F1OCtyTVdWcW8ifQ==",
"srv-ns8:root": "eyJpdiI6Ik9UbHBieDJhU0JZbFFEM0wiLCJ0YWciOiJZeWJ5K0lUUFdOcEQwTmJ5UzhwcVFRPT0iLCJkYXRhIjoiNUE1QW5yenZ0QldjNEdVNXVBPT0ifQ==",
"srv-ns8:rsa:passphrase": "eyJpdiI6ImpwRnFPYTZJdEtnUXdxT00iLCJ0YWciOiJMcXRBQ0lOT0paUE50Qjgva3lHSElRPT0iLCJkYXRhIjoiRDUzOFNuY3V1RUZXVHV4U3NnPT0ifQ==",
"srv-ns8:rmonla:sudo": "eyJpdiI6Ilg2VzhIMWN6Y2xYVzR3ZzgiLCJ0YWciOiJuWDk0SDcraVhJQTQxd3I3bEpLRFBnPT0iLCJkYXRhIjoiSllyRy9ScXFBRFBFc0l5emRRPT0ifQ==",
"srv-ns8:rmonla:rsa": "eyJpdiI6IkpoTmpZRWZMeE12eWhXSVMiLCJ0YWciOiJUUnJGY05qRlVoOTl2REplTms0ZU5RPT0iLCJkYXRhIjoiNkFIWCsyYm9ndzZKbGM3c0hRPT0ifQ==",
"srvv-dns:rmonla:sudo": "eyJpdiI6InkyY01pSHlNeGFzZ2lDcnkiLCJ0YWciOiJqYTdkaFM0bTBNMWZhTWxJRUtGTWtBPT0iLCJkYXRhIjoiWmVYbzRWRjNNS244UktuaFVnPT0ifQ==",
"srvv-DNS:rmonla:sudo": "eyJpdiI6InkyY01pSHlNeGFzZ2lDcnkiLCJ0YWciOiJqYTdkaFM0bTBNMWZhTWxJRUtGTWtBPT0iLCJkYXRhIjoiWmVYbzRWRjNNS244UktuaFVnPT0ifQ=="
"srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==",
"srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0=",
"dasu-dc:admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
"srvv-nginx-rm:root": "eyJpdiI6Ijc0STVkTk92Tk5LZEZZWWciLCJ0YWciOiJlOTAwcGpTa21PZFVhN2FJTzIxU1VBPT0iLCJkYXRhIjoiYUJRR3hRakFub1F1OCtyTVdWcW8ifQ==",
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==",
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=",
"srv-ns8:rmonla": "eyJpdiI6IlJ4cHZkbFpJQWVsTTl3T1YiLCJ0YWciOiJ3WTZGMDEvNHNsZm1wZm5jMEF4Q1lBPT0iLCJkYXRhIjoibFljUHRYTm44bUxvQjhCa2tBPT0ifQ==",
"dasu-sql3:Administrador": "eyJpdiI6IlRiMDh3U1N2aVhTcDYzbzciLCJ0YWciOiI5NVNyUDFlMXlrM0dGRlNqUXhLbEt3PT0iLCJkYXRhIjoiVkp2V09zSjczcS96UktTSlZTOFdBdz09In0="
}
+1 -1
View File
@@ -1 +1 @@
1775576757
1775682230
+65 -39
View File
@@ -1,31 +1,43 @@
# 🔐 candados: Gestión Segura de Secretos
# 🔐 candados: Acceso Seguro a Contraseñas (Sin Exponer)
## Resumen Ejecutivo
`candados` es la herramienta central de seguridad del ecosistema ADN. Cifra y gestiona secretos con **AES-256-GCM**, implementando un sistema de **Candado Temporal** que requiere autorización explícita (5 min) antes de liberar credenciales.
`candados` es la herramienta central de seguridad del ecosistema ADN. Cifra y gestiona secretos con **AES-256-GCM**, implementando un sistema de **Candado Temporal** que requiere autorización explícita (30 min) antes de liberar credenciales.
**Principio fundamental:** Las contraseñas **NUNCA** deben aparecer en pantalla, logs ni archivos temporales.
**Principio fundamental:** Las contraseñas **NUNCA** deben aparecer en pantalla, logs ni archivos temporales visibles.
---
## Arquitectura
- **Cripto**: Motor AES-256-GCM con IV aleatorio y autenticación.
- **Bóveda**: Persistencia cifrada en `.boveda.json` (permisos 600).
- **MFA Temporal**: Sesiones de 5 min via `.session` (permisos 600).
- **Master Key**: `.master.key` local (permisos 600, nunca se comparte).
## REGLA DE ORO — NUNCA MOSTRAR CONTRASEÑAS
## 🚀 Comando Primario: `run` ★
Esta herramienta está diseñada para que las contraseñas **NUNCA** se muestren en pantalla ni queden registradas en logs, históricos de shell o archivos temporales visibles.
El comando `run` es la forma **recomendada** de usar credenciales. Inyecta `USR` y `PASS` directamente en el entorno del subproceso sin imprimir nada:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 🛡️ NUNCA uses `get` en comandos automatizados o logs │
│ │
│ ✅ CORRECTO: ruby candados.rb run <clave> 'ssh $USR@host' │
│ ❌ INCORRECTO: ruby candados.rb get <clave> | algo │
│ │
│ El comando `get` imprime la contraseña a stdout → queda en logs │
│ El comando `run` inyecta USR/PASS en el entorno → no deja rastro │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 📥 MÉTODOS PARA CARGAR CONTRASEÑAS EN VARIABLES
### ★ MÉTODO 1: `run` (PRIMARIO - Recomendado para comandos)
Ejecuta un comando con `USR` y `PASS` inyectadas en su entorno. La contraseña **NUNCA** se imprime ni queda en logs.
```bash
# 1. Autorizar (dura 5 min)
ruby candados.rb authorize
# Sintaxis
ruby candados.rb run <clave> 'comando que usa $USR y $PASS'
# 2. Ejecutar comando con credenciales inyectadas
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host "comando"'
# Retrocompatible con variable legacy (SSHPASS, etc.)
ruby candados.rb run admindasu SSHPASS 'sshpass -e ssh root@host'
# Ejemplos
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host'
ruby candados.rb run srv-ns8:root 'ssh $USR@$HOSTNAME "comando"'
```
**¿Qué pasa internamente?**
@@ -35,6 +47,30 @@ ruby candados.rb run admindasu SSHPASS 'sshpass -e ssh root@host'
4. Limpia las variables al terminar
5. **Nada se imprime en pantalla ni queda en logs**
### ★ MÉTODO 2: `load` (Para shell interactivo)
Carga `USR` y `PASS` en variables de entorno del shell actual. Usa un archivo temporal (600) que se auto-elimina tras el source.
```bash
# Sintaxis
eval $(ruby candados.rb load <clave>)
# Ejemplo
eval $(ruby candados.rb load srv-dasu:rmonla)
# → Carga $USR y $PASS en tu shell actual
sshpass -p $PASS ssh $USR@servidor
```
**Seguridad:** El archivo temporal tiene permisos `600` y se auto-borra inmediatamente después del `source`.
---
## ⚙️ Arquitectura
- **Cripto**: Motor AES-256-GCM con IV aleatorio y autenticación.
- **Bóveda**: Persistencia cifrada en `.boveda.json` (permisos 600).
- **MFA Temporal**: Sesiones de 30 min via `.session` (permisos 600).
- **Master Key**: `.master.key` local (permisos 600, nunca se comparte).
## 🔒 Comando: `sudo`
Ejecuta comandos con privilegios, resolviendo la contraseña automáticamente:
@@ -49,20 +85,7 @@ La clave se busca automáticamente:
2. Variantes normalizadas del hostname
3. Fallback genérico: `sudo`
## 📦 Comando Secundario: `load`
Para uso interactivo cuando se necesitan las variables en el shell actual. **Nunca imprime la contraseña:**
```bash
eval $(ruby candados.rb load srv-dasu:rmonla)
# USR y PASS están disponibles en el shell
```
Internamente escribe los exports a un archivo temporal (`tmp/.candados_env_<hex>`) con permisos `600`, y a stdout solo emite el comando `source` + autoborrado.
**Nota:** Los temporales se guardan en `tmp/` del repositorio (no en `/tmp/`) para cumplir con el principio 11 de `05_ia.md` (Workspace Bounds).
## 🔑 Resolución Automática de Pares
## 🔑 Resolución Automática de Claves
Los comandos `run` y `load` detectan automáticamente el par usuario/contraseña:
@@ -77,12 +100,13 @@ Los comandos `run` y `load` detectan automáticamente el par usuario/contraseña
| Comando | Seguridad | Descripción |
|---|---|---|
| `run <clave> <cmd>` | ✅ Nada expuesto | **★ PRIMARIO** - Ejecuta con USR/PASS inyectados |
| `sudo <cmd>` | ✅ Nada expuesto | Ejecuta con privilegios desde bóveda |
| `load <clave>` | ✅ Archivo temporal 600 | Carga en shell (usar con `eval`) |
| `get <clave>` | ⚠ Imprime a stdout | Obtener valor directo (usar con cuidado) |
| `authorize` | ✅ | Abrir candado (5 min) |
| `sudo <cmd>` | ✅ Nada expuesto | Ejecuta con privilegios desde bóveda |
| `authorize` | ✅ | Abrir candado (30 min) |
| `cerrar` | ✅ | Cerrar sesión manualmente |
| `set <clave>` | ✅ Input sin eco | Guardar secreto |
| `list` | ✅ | Listar claves |
| `list` | ✅ | Listar claves (no muestra valores) |
| `get <clave>` | ⚠ Imprime a stdout | Obtener valor directo (⚠ solo uso manual) |
## 🛡️ Auditoría
@@ -93,12 +117,14 @@ Archivo `access.log` (permisos 600) registra:
## 🧠 Contexto para IA
1. **Siempre** ejecutar `authorize` antes de operar
2. **Usar `run`** como método primario (inyecta, no imprime)
3. **Evitar `get`** en comandos automatizados (filtra en logs)
4. Cerrar sesión con `cerrar` cuando ya no se necesite
1. **Siempre** ejecutar `authorize` antes de operar (sesión de 30 min)
2. **Usar `run`** como método primario (inyecta USR/PASS, no imprime)
3. **Usar `load`** para shell interactivo (eval + source)
4. **Evitar `get`** en automatizaciones (imprime la contraseña)
5. **Cerrar sesión** con `cerrar` cuando ya no se necesite
```bash
# Flujo típico
ruby candados.rb authorize
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host "ls"'
ruby candados.rb cerrar
+185 -41
View File
@@ -258,7 +258,14 @@ def cmd_run(cripto, boveda, clave, *args)
token_user = boveda.get("#{prefijo}_user")
user_value = cripto.decrypt(token_user) if token_user
elsif clave.include?(':')
user_value = clave.split(':').last
# Formato: <nodo>:<usuario>[:<tipo>]
# Ej: "srv-ns8:rmonla" → usuario="rmonla"
# "srv-ns8:rmonla:sudo" → usuario="rmonla" (no "sudo")
partes = clave.split(':')
if partes.size >= 2
# El usuario es siempre la segunda parte (índice 1)
user_value = partes[1]
end
else
token_user = boveda.get("#{clave}_user")
user_value = cripto.decrypt(token_user) if token_user
@@ -366,6 +373,64 @@ def cmd_load(cripto, boveda, clave)
$stderr.puts "#{C::GREEN}✓ Credenciales cargadas en variables USR y PASS (archivo temporal seguro).#{C::RESET}"
end
# ─── Comando ssh (ejecutar comando remoto con password de bóveda) ─────
# Uso: candados ssh <clave> <usuario@host> '<comando>'
# - clave: <nodo>:<usuario> para SSH (busca automáticamente <nodo>:<usuario>:sudo para sudo)
# - Si existe clave :sudo separada, la usa para comandos con sudo
def cmd_ssh(cripto, boveda, clave, usuario_host, *cmd_args)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
abort "#{C::RED}✗ Falta usuario@host.#{C::RESET}" if usuario_host.nil? || usuario_host.empty?
abort "#{C::RED}✗ Falta comando a ejecutar.#{C::RESET}" if cmd_args.nil? || cmd_args.empty?
verificar_sesion!
# Obtener password SSH
token_ssh = boveda.get(clave)
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token_ssh.nil?
ssh_pass = cripto.decrypt(token_ssh)
safe_ssh_pass = ssh_pass.gsub("'", "'\\''")
# Buscar automáticamente clave :sudo si existe
comando = cmd_args.join(' ')
sudo_pass = ssh_pass
# Intentar buscar clave :sudo basada en la clave SSH
if clave.include?(':') && !clave.end_with?(':sudo')
# Construir clave sudo: <nodo>:<usuario>:sudo
partes = clave.split(':')
if partes.size >= 2
clave_sudo = "#{partes[0]}:#{partes[1]}:sudo"
token_sudo = boveda.get(clave_sudo)
if token_sudo
sudo_pass = cripto.decrypt(token_sudo)
$stderr.puts "#{C::DIM} Usando clave sudo: #{clave_sudo}#{C::RESET}"
end
end
end
safe_sudo_pass = sudo_pass.gsub("'", "'\\''")
registrar_acceso('SSH', "Clave: #{clave}, Host: #{usuario_host}, Cmd: #{comando}")
# Escapar comando para bash remoto: reemplazar ' con '\''
comando_escaped = comando.gsub("'", "'\\''")
# Crear script temporal con el comando y password para sudo
require 'securerandom'
require 'fileutils'
FileUtils.mkdir_p(TMP_DIR) unless File.exist?(TMP_DIR)
script_path = File.join(TMP_DIR, ".ssh_sudo_#{SecureRandom.hex(4)}")
File.write(script_path, "#!/bin/bash\nexport DEBIAN_FRONTEND=noninteractive\necho '#{safe_sudo_pass}' | sudo -S -p '' bash -c '#{comando_escaped}'\n")
File.chmod(0700, script_path)
# Ejecutar: sshpass para SSH, luego script remoto que hace sudo
result = system("sshpass -p '#{safe_ssh_pass}' ssh -o StrictHostKeyChecking=no -tt #{usuario_host} 'bash -s' < '#{script_path}'")
FileUtils.rm_f(script_path)
exit result ? 0 : 1
end
# ─── Ayuda mejorada para cmd_help_resumida ─────────────────────────────
# ─── Comando sudo (primitiva reutilizable) ─────────────────────
# Resolución inteligente de la clave sudo:
# 1. <hostname>:<usuario>:sudo (convención específica)
@@ -426,58 +491,130 @@ def cmd_sudo(cripto, boveda, comando_args)
exit $?.exitstatus || 0
end
def cmd_help
def cmd_help_resumida
puts <<~HELP
#{C::CYAN}🔐 candados — Gestión segura de secretos#{C::RESET}
#{C::CYAN}╔═══════════════════════════════════════════════════════════════════╗#{C::RESET}
#{C::CYAN}║#{C::RESET} 🔐 #{C::BOLD}candados — Acceso Seguro a Contraseñas (Sin Exponer)#{C::RESET} #{C::CYAN}║#{C::RESET}
#{C::CYAN}╚═══════════════════════════════════════════════════════════════════╝#{C::RESET}
#{C::GREEN}Uso:#{C::RESET} ruby adn/tools/seguridad/candados.rb <comando> [args]
#{C::RED}⚠️ REGLA DE ORO: NUNCA MOSTRAR CONTRASEÑAS#{C::RESET}
#{C::YELLOW}━━━ Comandos principales ━━━#{C::RESET}
#{C::BOLD}run <clave> <cmd>#{C::RESET} #{C::GREEN}★ PRIMARIO#{C::RESET} Ejecutar comando con USR y PASS inyectados
#{C::BOLD}sudo <comando>#{C::RESET} Ejecutar con privilegios (resuelve clave automáticamente)
#{C::BOLD}load <clave>#{C::RESET} Cargar USR/PASS en shell actual (usar con eval)
#{C::GREEN}┌─────────────────────────────────────────────────────────────────┐#{C::RESET}
#{C::GREEN}│#{C::RESET} #{C::BOLD}MÉTODOS PARA CARGAR CONTRASEÑAS EN VARIABLES#{C::RESET} #{C::GREEN}│#{C::RESET}
#{C::GREEN}└─────────────────────────────────────────────────────────────────┘#{C::RESET}
#{C::YELLOW}━━━ Gestión de bóveda ━━━#{C::RESET}
authorize Abrir candado (autorización temporal 30 min)
cerrar Cerrar candado manualmente
set <clave> Guardar secreto (input seguro, sin eco)
get <clave> Obtener secreto ( imprime a stdout)
rm <clave> Eliminar secreto
list Listar claves disponibles
#{C::BOLD}★ run <clave> 'comando'#{C::RESET} → Ejecuta con $USR y $PASS inyectadas (PRIMARIO)
#{C::BOLD}★ load <clave>#{C::RESET} → Carga $USR y $PASS en shell (usar con eval)
#{C::DIM}Ejemplo: ruby candados.rb run srv-ns8:rmonla 'sshpass -p $PASS ssh $USR@host'#{C::RESET}
#{C::YELLOW}━━━ Comandos ━━━#{C::RESET}
#{C::BOLD}run#{C::RESET} <clave> <cmd> #{C::DIM}Ejecutar con credenciales inyectadas#{C::RESET}
#{C::BOLD}load#{C::RESET} <clave> #{C::DIM}Cargar en shell actual (eval)#{C::RESET}
#{C::BOLD}sudo#{C::RESET} <comando> #{C::DIM}Ejecutar con privilegios#{C::RESET}
#{C::BOLD}ssh#{C::RESET} <clave> <u@h> <cmd> #{C::DIM}SSH remoto con password de bóveda#{C::RESET}
#{C::BOLD}authorize#{C::RESET} #{C::DIM}Abrir candado (30 min)#{C::RESET}
#{C::BOLD}list#{C::RESET} #{C::DIM}Listar claves disponibles#{C::RESET}
#{C::BOLD}set/rm#{C::RESET} <clave> #{C::DIM}Guardar/Eliminar secreto#{C::RESET}
#{C::DIM}Usá#{C::RESET} ruby candados.rb --help #{C::DIM}para ayuda completa y ejemplos detallados.#{C::RESET}
HELP
end
def cmd_help_completo
puts <<~HELP
#{C::CYAN}╔═══════════════════════════════════════════════════════════════════╗#{C::RESET}
#{C::CYAN}║#{C::RESET} 🔐 #{C::BOLD}candados — Acceso Seguro a Contraseñas (Sin Exponer)#{C::RESET} #{C::CYAN}║#{C::RESET}
#{C::CYAN}╚═══════════════════════════════════════════════════════════════════╝#{C::RESET}
#{C::RED}⚠️ REGLA DE ORO: NUNCA MOSTRAR CONTRASEÑAS#{C::RESET}
Esta herramienta está diseñada para que las contraseñas #{C::BOLD}NUNCA#{C::RESET} se muestren en pantalla
ni queden registradas en logs, históricos de shell o archivos temporales visibles.
#{C::GREEN}┌─────────────────────────────────────────────────────────────────┐#{C::RESET}
#{C::GREEN}│#{C::RESET} #{C::BOLD}MÉTODOS PARA CARGAR CONTRASEÑAS EN VARIABLES#{C::RESET} #{C::GREEN}│#{C::RESET}
#{C::GREEN}└─────────────────────────────────────────────────────────────────┘#{C::RESET}
#{C::BOLD}★ MÉTODO 1: run (PRIMARIO - Recomendado para comandos)#{C::RESET}
Ejecuta un comando con USR y PASS inyectadas en su entorno.
La contraseña #{C::BOLD}NUNCA#{C::RESET} se imprime ni queda en logs.
#{C::DIM}ruby candados.rb run <clave> 'comando que usa $USR y $PASS'#{C::RESET}
#{C::CYAN}Ejemplos:#{C::RESET}
#{C::GREEN}✓#{C::RESET} ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host'
#{C::GREEN}✓#{C::RESET} ruby candados.rb run srv-ns8:root 'ssh $USR@$HOSTNAME "comando"'
#{C::BOLD}★ MÉTODO 2: load (Para shell interactivo)#{C::RESET}
Carga USR y PASS en variables de entorno del shell actual.
Usa un archivo temporal (600) que se auto-elimina tras el source.
#{C::DIM}eval $(ruby candados.rb load <clave>)#{C::RESET}
#{C::CYAN}Ejemplo:#{C::RESET}
#{C::GREEN}✓#{C::RESET} eval $(ruby candados.rb load srv-dasu:rmonla)
#{C::DIM} → Carga $USR y $PASS en tu shell actual#{C::RESET}
#{C::GREEN}✓#{C::RESET} sshpass -p $PASS ssh $USR@servidor
#{C::YELLOW}━━━ Otros Comandos ━━━#{C::RESET}
#{C::BOLD}sudo <comando>#{C::RESET} Ejecutar con sudo (resuelve contraseña automáticamente)
#{C::BOLD}authorize#{C::RESET} Abrir candado (sesión de 30 min, requerida para operar)
#{C::BOLD}cerrar#{C::RESET} Cerrar sesión manualmente antes de los 30 min
#{C::BOLD}abrir#{C::RESET} Cargar clave SSH en ssh-add (usa passphrase de bóveda)
#{C::YELLOW}━━━ Gestión de Bóveda ━━━#{C::RESET}
#{C::BOLD}set <clave>#{C::RESET} Guardar secreto (input seguro sin eco)
#{C::BOLD}list#{C::RESET} Listar claves disponibles (no muestra valores)
#{C::BOLD}rm <clave>#{C::RESET} Eliminar una clave de la bóveda
#{C::YELLOW}━━━ Utilidades ━━━#{C::RESET}
abrir Autorizar SSH (cargar passphrase)
encrypt <texto> Cifrar texto plano token
decrypt <token> Descifrar token texto plano
#{C::BOLD}encrypt <texto>#{C::RESET} Cifrar texto plano → token (para bóveda)
#{C::BOLD}decrypt <token>#{C::RESET} Descifrar token → texto plano
#{C::YELLOW}Resolución automática de claves:#{C::RESET}
#{C::DIM}• run/load: Detecta pares USR/PASS por convención:#{C::RESET}
srv-dasu:rmonla USR=rmonla, PASS=(valor cifrado)
tailscale_dasuten busca _user/_pass automáticamente
#{C::DIM}• sudo: Resuelve la clave de sudo automáticamente:#{C::RESET}
<hostname>:<usuario>:sudo (ej: ns8:rmonla:sudo)
#{C::YELLOW}━━━ Resolución Automática de Claves ━━━#{C::RESET}
#{C::DIM}• run/load con formato `servidor:usuario`:#{C::RESET}
srv-dasu:rmonla USR=rmonla, PASS=(descifrado de bóveda)
#{C::DIM}• run/load con formato `prefijo_pass`:#{C::RESET}
tailscale_dasuten_pass busca automáticamente tailscale_dasuten_user
#{C::DIM}• sudo resuelve la clave automáticamente:#{C::RESET}
Busca: <hostname>:<usuario>:sudo fallback: `sudo`
#{C::YELLOW}Ejemplos:#{C::RESET}
#{C::DIM}# 1. Autorizar (dura 30 min)#{C::RESET}
ruby candados.rb authorize
#{C::YELLOW}━━━ Flujo de Trabajo Típico ━━━#{C::RESET}
#{C::DIM}1. Autorizar (una vez cada 30 min):#{C::RESET}
ruby candados.rb authorize
#{C::GREEN}# 2. Ejecutar comando con credenciales (RECOMENDADO)#{C::RESET}
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host'
ruby candados.rb run admindasu SSHPASS 'sshpass -e ssh root@host'
#{C::DIM}2. Ejecutar comandos (Método recomendado):#{C::RESET}
ruby candados.rb run srv-ns8:rmonla 'sshpass -p $PASS ssh $USR@host "comando"'
#{C::DIM}# 3. Ejecutar con sudo#{C::RESET}
ruby candados.rb sudo tailscale set --operator=$USER
#{C::DIM}3. O cargar en shell interactivo:#{C::RESET}
eval $(ruby candados.rb load srv-ns8:rmonla)
sshpass -p $PASS ssh $USR@host
#{C::DIM}# 4. Cargar en shell interactivo (secundario)#{C::RESET}
eval $(ruby candados.rb load srv-dasu:rmonla)
#{C::RED}⚠️ ADVERTENCIAS DE SEGURIDAD:#{C::RESET}
#{C::BOLD}• NUNCA usar `get`#{C::RESET} en automatizaciones → imprime la contraseña a stdout
#{C::BOLD}• `run` y `sudo`#{C::RESET} → las credenciales no dejan rastro en logs
#{C::BOLD}• `load`#{C::RESET} → archivo temporal con permisos 600, auto-borrado
#{C::BOLD}• Siempre ejecutar `authorize`#{C::RESET} antes de operar (sesión de 30 min)
#{C::RED}⚠ Seguridad:#{C::RESET}
#{C::DIM}• run/sudo: Las credenciales NUNCA se imprimen en pantalla ni logs.#{C::RESET}
#{C::DIM}• load: Usa archivo temporal (600) con autoborrado, nada visible.#{C::RESET}
#{C::DIM}• get: ⚠ Imprime el valor a stdout. Usar solo si es necesario.#{C::RESET}
#{C::DIM}Documentación completa: adn/tools/candados/README.md#{C::RESET}
HELP
end
alias cmd_help cmd_help_completo
# ─── Recordatorio de Seguridad (solo en ayuda) ──────────────────
def mostrar_recordatorio_seguridad
puts <<~RECORDATORIO
#{C::CYAN}╔════════════════════════════════════════════════════════════════╗#{C::RESET}
#{C::CYAN}║#{C::RESET} 🔐 #{C::BOLD}REGLA DE ORO: NUNCA MOSTRAR CONTRASEÑAS#{C::RESET} #{C::CYAN}║#{C::RESET}
#{C::CYAN}║#{C::RESET} #{C::GREEN}✓ CORRECTO:#{C::RESET} ruby candados.rb run <clave> '<cmd>' #{C::CYAN}║#{C::RESET}
#{C::CYAN}║#{C::RESET} #{C::RED}✗ INCORRECTO:#{C::RESET} ruby candados.rb get <clave> #{C::CYAN}║#{C::RESET}
#{C::CYAN}║#{C::RESET} → `run` inyecta USR/PASS sin mostrar. `get` imprime y deja rastro. #{C::CYAN}║#{C::RESET}
#{C::CYAN}╚════════════════════════════════════════════════════════════════╝#{C::RESET}
RECORDATORIO
puts
end
# ─── Main ────────────────────────────────────────────────────────────
if __FILE__ == $0
comando = ARGV[0]
@@ -496,6 +633,10 @@ if __FILE__ == $0
cripto = Cripto.new(cargar_master_key)
boveda = Boveda.new(BOVEDA)
cmd_sudo(cripto, boveda, ARGV[1..])
when 'ssh'
cripto = Cripto.new(cargar_master_key)
boveda = Boveda.new(BOVEDA)
cmd_ssh(cripto, boveda, ARGV[1], ARGV[2], *ARGV[3..])
when 'encrypt', 'decrypt', 'get', 'load', 'run', 'set', 'rm', 'list'
cripto = Cripto.new(cargar_master_key)
boveda = Boveda.new(BOVEDA)
@@ -509,11 +650,14 @@ if __FILE__ == $0
when 'rm' then cmd_rm(boveda, arg)
when 'list' then cmd_list(boveda)
end
when 'help', 'ayuda', '-h', '--help', nil
cmd_help
when '--help', '-h'
cmd_help_completo
when 'help', 'ayuda', nil
cmd_help_resumida
else
$stderr.puts "#{C::RED}✗ Comando desconocido: '#{comando}'#{C::RESET}"
cmd_help
$stderr.puts ""
cmd_help_resumida
exit 1
end
end
+6 -1
View File
@@ -66,6 +66,7 @@ module Dron
lines << "\n#{Color::BOLD}Opciones:#{Color::RESET}"
lines << " #{Color::GREEN}--nota \"texto\"#{Color::RESET} Nota descriptiva para la bitácora"
lines << " #{Color::GREEN}--evento ID#{Color::RESET} Vincular a evento existente"
lines << " #{Color::GREEN}--nodo NOMBRE#{Color::RESET} Nodo para bitácora (auto-detect si no se especifica)"
lines << " #{Color::GREEN}--timeout N#{Color::RESET} Timeout en segundos (default: 3600)"
lines << " #{Color::GREEN}--dry-run#{Color::RESET} Simulación sin ejecutar"
lines << " #{Color::GREEN}--help#{Color::RESET} Mostrar esta ayuda"
@@ -107,7 +108,8 @@ module Dron
nota: opciones[:nota],
evento_id: opciones[:evento_id],
timeout: opciones[:timeout],
flujo_id: opciones[:flujo_id]
flujo_id: opciones[:flujo_id],
nodo: opciones[:nodo]
)
# Mostrar resultado
@@ -128,6 +130,7 @@ module Dron
evento_id: nil,
timeout: 3600,
flujo_id: nil,
nodo: nil,
cmd: nil
}
@@ -143,6 +146,8 @@ module Dron
opciones[:timeout] = args.shift.to_i
when '--flujo'
opciones[:flujo_id] = args.shift
when '--nodo'
opciones[:nodo] = args.shift
when '--help', '-h'
Dispatcher.mostrar_ayuda
exit 0
+43 -6
View File
@@ -13,15 +13,15 @@ module Dron
HEARTBEAT_INTERVAL = 30
class << self
def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil)
def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil, nodo: nil)
Base.logger.info("🛠️ Dron Ejecutor iniciado: #{Process.pid}")
dron_id = ADN::DB::DronDB.generar_dron_id
ADN::DB::DronDB.registrar_inicio(tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, metadata: { nota: nota, evento_id: evento_id, timeout: timeout })
evento_id = registrar_bitacora(nota, evento_id, 'inicio') if nota
ADN::DB::DronDB.registrar_inicio(tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, metadata: { nota: nota, evento_id: evento_id, timeout: timeout, nodo: nodo })
evento_id = registrar_bitacora(nota, evento_id, 'inicio', nodo) if nota
resultado = ejecutar_comando(cmd, dron_id, timeout)
ADN::DB::DronDB.registrar_fin(dron_id, resultado[:exit_code], resultado[:output])
registrar_bitacora(nota, evento_id, 'fin', resultado) if evento_id
registrar_bitacora(nota, evento_id, 'fin', resultado, nodo) if evento_id
Base.logger.info("🛠️ Dron Ejecutor completado: #{dron_id} - #{resultado[:exit_code] == 0 ? '✅' : '❌'}")
{ dron_id: dron_id, exit_code: resultado[:exit_code], duration: resultado[:duration], evento_id: evento_id }
@@ -67,14 +67,18 @@ module Dron
{ exit_code: exit_code || 1, output: output.join("\n").strip, duration: (Time.now - start_time).round(2) }
end
def registrar_bitacora(nota, evento_id, tipo, resultado = nil)
def registrar_bitacora(nota, evento_id, tipo, resultado = nil, nodo = nil)
return nil unless nota
require_relative '../../db/core/bitacora_db'
# Detectar nodo automáticamente si no se especifica
nodo_id = detectar_nodo_id(nodo)
if tipo == 'inicio'
descripcion = "🛸 dron: #{nota}"
BitacorasDB::BitacoraDB.with_connection do |db|
db.crear_evento(nodo_id: 1, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳')
evento = db.crear_evento(nodo_id: nodo_id, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳')
evento['id'] # Retornar ID para actualizar después
end
else
estado = resultado[:exit_code] == 0 ? '✅' : '❌'
@@ -82,11 +86,44 @@ module Dron
BitacorasDB::BitacoraDB.with_connection do |db|
db.actualizar_evento(evento_id, descripcion: descripcion, fin: Time.now.strftime('%H:%M'))
end
evento_id # Retornar mismo ID
end
rescue StandardError => e
Base.logger.warn("⚠️ Error en bitácora: #{e.message}")
nil
end
def detectar_nodo_id(nodo)
return nodo if nodo.is_a?(Integer)
# Si no se especifica nodo, usar el hostname para detectar
require 'socket'
hostname = Socket.gethostname.downcase
# Buscar nodo por nombre en la tabla de nodos
require_relative '../../db/core/bitacora_db'
sql = 'SELECT id FROM nodos WHERE LOWER(nombre) = $1 LIMIT 1'
resultado = nil
BitacorasDB::BitacoraDB.with_connection do |db|
resultado = db.execute(sql, [hostname])
end
if resultado&.any?
resultado.first['id']
else
# Fallback: buscar por prefijo (ej: srv-ns8 coincide con srv-ns8.frlr.utn.edu.ar)
sql = 'SELECT id FROM nodos WHERE LOWER(nombre) LIKE $1 LIMIT 1'
resultado = nil
BitacorasDB::BitacoraDB.with_connection do |db|
resultado = db.execute(sql, ["#{hostname}%"])
end
resultado&.any? ? resultado.first['id'] : 1
end
rescue StandardError => e
Base.logger.warn("⚠️ Error detectando nodo: #{e.message}, usando default (1)")
1
end
end
end
end
+30 -4
View File
@@ -666,20 +666,45 @@ module BitacorasDB
where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}"
# Para events, adaptar condiciones (solo las que aplican)
where_clause_events = if conditions.empty?
''
else
# Reemplazar referencias a e. por ev. y eliminar condiciones que no aplican a events
conditions_events = conditions.reject { |c| c.include?('bitacora_id') || c.include?('tema_id') || c.include?('ambito_id') }
"WHERE #{conditions_events.join(' AND ').gsub('e.', 'ev.')}"
end
# Unificar eventos de tabla events y entradas para mostrar todo junto
query = <<~SQL
SELECT e.id, e.inicio, e.fin, e.descripcion, e.estado, e.modo, e.es_ia,
e.tema_id, e.bitacora_id, e.nodo_id, e.ambito_id, e.created_at, e.updated_at,
SELECT ev.id, ev.inicio::text as inicio, ev.fin::text as fin, ev.descripcion, ev.estado,
'A' as modo, false as es_ia,
NULL as tema_id, NULL as bitacora_id, ev.nodo_id, NULL as ambito_id,
ev.created_at, ev.updated_at,
CURRENT_DATE as bitacora_fecha,
n.nombre as nodo_nombre,
NULL as tema_titulo,
na.nombre as ambito_nombre
FROM bitacoras.events ev
LEFT JOIN bitacoras.nodos n ON ev.nodo_id = n.id
LEFT JOIN bitacoras.ambitos na ON n.ambito_id = na.id
#{where_clause_events}
UNION ALL
SELECT e.id, e.inicio::text, e.fin::text, e.descripcion, e.estado, e.modo, e.es_ia,
e.tema_id, e.bitacora_id, e.nodo_id, e.ambito_id,
e.created_at, e.updated_at,
b.fecha as bitacora_fecha,
n.nombre as nodo_nombre,
t.titulo as tema_titulo,
a.nombre as ambito_nombre
COALESCE(a.nombre, na.nombre) as ambito_nombre
FROM #{SCHEMA}.#{TABLES[:entradas]} e
LEFT JOIN #{SCHEMA}.#{TABLES[:bitacoras]} b ON e.bitacora_id = b.id
LEFT JOIN #{SCHEMA}.#{TABLES[:nodos]} n ON e.nodo_id = n.id
LEFT JOIN #{SCHEMA}.#{TABLES[:temas]} t ON e.tema_id = t.id
LEFT JOIN #{SCHEMA}.ambitos a ON e.ambito_id = a.id
LEFT JOIN #{SCHEMA}.ambitos na ON n.ambito_id = na.id
#{where_clause}
ORDER BY COALESCE(b.fecha, CURRENT_DATE) DESC, e.inicio DESC
ORDER BY bitacora_fecha DESC, inicio DESC
LIMIT $#{param_index} OFFSET $#{param_index + 1}
SQL
@@ -1007,6 +1032,7 @@ module BitacorasDB
public
# Crear un evento en la tabla events (visible en Bitácora Web)
# El ámbito se obtiene automáticamente del nodo mediante JOIN en las consultas
def crear_evento(nodo_id:, descripcion:, estado: '⏳', inicio: nil, fin: nil, modo: 'A', metadata: nil)
inicio ||= Time.now.strftime('%H:%M')
query = <<~SQL