Files
dtic-DIIAA/tools/ns8-candados/ns8-candados.rb
T
Ricardo Monla 15725e0b3c 🚀 P02: Despliegue exitoso del sistema DASUTEN en pcv-dasu0
Infraestructura y Red:
- Tailscale v1.94.2 instalado en srv-dasu (IP: 100.116.210.36)
- Configurado como subnet router para 10.0.100.0/24
- Fix: Repositorios enterprise de Proxmox deshabilitados (.sources → .bak)

Despliegue del Sistema (pcv-dasu0):
- Transferencia SCP de runSysDasuten.zip via SSH puerto 7022
- Extracción en C:\Dasuten\runSysDasuten- Kermet.ini configurado: SERVER=sql-dasuten
- InstaladorComponentesKermet.exe (silent install)
- Fuentes TrueType registradas en Windows (Code39, I2OF5)
- Firewall TCP 1433 abierto en sql-dasuten
- Test-NetConnection: TcpTestSucceeded=True
- Login exitoso con usuario aalmiron 

ADN y Contexto:
- 05_ia.md: Regla 13 Auto-Contextualización Topológica
- 07_proyectos.md: Sección 'Topología y Red' obligatoria en manifiestos
- P2601_dasuten.md: Contexto de red documentado
- nodos/pcv-dasu0.md: Puerto SSH actualizado a 7022

Dashboard P2601:
- Hitos completados: 8 → 11 (P02.APP, NET01, TEST03)
- Progreso: 67% → 79%
- pcv-dasu0: Testing → Online
- Nuevo hito activo: UAT (verificación presencial)

Bitácoras y Documentación:
- Bitácora 04/03 y 05/03 completas
- Plan de despliegue en docs/plan/260305-1017
- Investigación de ingeniería inversa documentada
- ns8-candados: Instrucciones de uso para IA agregadas
- dtic-BITACORAs: CRUD completo, Dashboard P2601 integrado

Herramientas:
- w-zombi: Mejora anti-loop (max 5 ciclos)
- Payloads de despliegue SQL y sistema generados
2026-03-05 13:15:31 -03:00

233 lines
8.3 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
# ns8-candados — Gestión unificada de secretos y autorización SSH
# ==============================================================================
# 🤖 INSTRUCCIONES PARA LA IA (AGENTES EXTERNOS):
# Esta herramienta permite obtener credenciales seguras sin exponerlas en el chat.
# PARA USARLA CORRECTAMENTE (SIN FILTRAR SECRETOS):
# Ejecuta el comando y asigna su salida a una variable silenciada:
# PASS=$(ruby tools/ns8-candados/ns8-candados.rb get nombre_clave)
# sshpass -p "$PASS" ssh usuario@servidor
# NUNCA ejecutes `ruby ns8-candados.rb get clave` directamente en run_command sin
# capturar o redirigir el output, porque el secreto se filtrará en la bitácora del LLM.
# ==============================================================================
# Uso: ruby tools/ns8-candados/ns8-candados.rb <comando> [args]
#
# Comandos:
# abrir Autorizar SSH (cargar llave con passphrase)
# encrypt <txt> Cifrar texto plano
# decrypt <tok> Descifrar payload
# get <clave> Obtener secreto de la bóveda (salida: solo el valor)
# set <clave> Guardar secreto (pide input interactivo, sin eco)
# rm <clave> Eliminar secreto de la bóveda
# list Listar claves disponibles en la bóveda
require 'openssl'
require 'base64'
require 'json'
require 'io/console'
require 'fileutils'
# ─── Rutas ───────────────────────────────────────────────────────────
TOOL_DIR = File.expand_path(__dir__)
PROJECT_DIR = File.expand_path('../..', TOOL_DIR)
MASTER_KEY = File.join(PROJECT_DIR, '.master.key')
BOVEDA = File.join(TOOL_DIR, '.boveda.json')
# ─── Colores ─────────────────────────────────────────────────────────
module C
RESET = "\e[0m"
GREEN = "\e[32m"
CYAN = "\e[36m"
YELLOW = "\e[33m"
RED = "\e[31m"
DIM = "\e[2m"
end
# ─── Motor Criptográfico (AES-256-GCM) ──────────────────────────────
class Cripto
ALGORITHM = 'aes-256-gcm'
def initialize(key_content)
@key = Digest::SHA256.digest(key_content.strip)
end
def encrypt(plaintext)
cipher = OpenSSL::Cipher.new(ALGORITHM).tap do |c|
c.encrypt
c.key = @key
end
iv = cipher.random_iv
ciphertext = cipher.update(plaintext.to_s) + cipher.final
payload = { iv: b64(iv), tag: b64(cipher.auth_tag), data: b64(ciphertext) }
Base64.strict_encode64(payload.to_json)
end
def decrypt(token)
p = JSON.parse(Base64.strict_decode64(token), symbolize_names: true)
cipher = OpenSSL::Cipher.new(ALGORITHM).tap do |c|
c.decrypt
c.key = @key
c.iv = db64(p[:iv])
c.auth_tag = db64(p[:tag])
end
cipher.update(db64(p[:data])) + cipher.final
rescue => e
abort "#{C::RED}✗ Error al descifrar: #{e.message}#{C::RESET}"
end
private
def b64(d) = Base64.strict_encode64(d)
def db64(d) = Base64.strict_decode64(d)
end
# ─── Bóveda ──────────────────────────────────────────────────────────
class Boveda
def initialize(path)
@path = path
@data = File.exist?(path) ? JSON.parse(File.read(path)) : {}
end
def keys = @data.keys
def get(k) = @data[k]
def set(k, v) = (@data[k] = v) && save!
def rm(k) = @data.delete(k) && save!
def empty? = @data.empty?
private
def save!
File.write(@path, JSON.pretty_generate(@data) + "\n")
FileUtils.chmod(0600, @path)
end
end
# ─── Master Key ──────────────────────────────────────────────────────
def cargar_master_key
unless File.exist?(MASTER_KEY)
require 'securerandom'
File.write(MASTER_KEY, SecureRandom.hex(32))
FileUtils.chmod(0600, MASTER_KEY)
$stderr.puts "#{C::YELLOW}⚠ Nueva clave maestra creada en #{MASTER_KEY}#{C::RESET}"
$stderr.puts "#{C::YELLOW} NO subir a Git (ya cubierta por .gitignore).#{C::RESET}"
end
File.read(MASTER_KEY)
end
# ─── Comandos ────────────────────────────────────────────────────────
def cmd_abrir
puts "#{C::CYAN}🔐 ns8-candados: Autorización SSH#{C::RESET}"
puts "#{C::DIM}Limpiando identidades previas...#{C::RESET}"
system('ssh-add -D > /dev/null 2>&1')
puts "#{C::YELLOW}🔑 Ingresá tu passphrase:#{C::RESET}"
system('ssh-add')
system('ssh-add -l > /dev/null 2>&1')
unless $?.success?
abort "#{C::RED}✗ Sin identidades SSH cargadas. Abortando.#{C::RESET}"
end
puts "#{C::GREEN}🔓 SSH autorizado. Las identidades permanecen activas mientras dure la sesión del agente.#{C::RESET}"
end
def cmd_encrypt(cripto, texto)
abort "#{C::RED}✗ Falta texto a cifrar.#{C::RESET}" if texto.nil? || texto.empty?
puts cripto.encrypt(texto)
end
def cmd_decrypt(cripto, token)
abort "#{C::RED}✗ Falta payload a descifrar.#{C::RESET}" if token.nil? || token.empty?
puts cripto.decrypt(token)
end
def cmd_get(cripto, boveda, clave)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
token = boveda.get(clave)
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token.nil?
# Salida limpia (solo el valor) para uso en scripts:
# PASS=$(ruby tools/ns8-candados/ns8-candados.rb get admindasu)
puts cripto.decrypt(token)
end
def cmd_set(cripto, boveda, clave)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
$stderr.print "#{C::YELLOW}🔑 Valor para '#{clave}': #{C::RESET}"
valor = $stdin.noecho(&:gets)&.chomp
$stderr.puts
abort "#{C::RED}✗ Valor vacío.#{C::RESET}" if valor.nil? || valor.empty?
boveda.set(clave, cripto.encrypt(valor))
$stderr.puts "#{C::GREEN}✓ Clave '#{clave}' guardada en la bóveda.#{C::RESET}"
end
def cmd_rm(boveda, clave)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" unless boveda.get(clave)
boveda.rm(clave)
$stderr.puts "#{C::GREEN}✓ Clave '#{clave}' eliminada.#{C::RESET}"
end
def cmd_list(boveda)
if boveda.empty?
$stderr.puts "#{C::DIM}Bóveda vacía.#{C::RESET}"
return
end
$stderr.puts "#{C::CYAN}🔐 Claves en la bóveda:#{C::RESET}"
boveda.keys.each { |k| $stderr.puts " #{C::GREEN}#{C::RESET} #{k}" }
end
def cmd_help
puts <<~HELP
#{C::CYAN}🔐 ns8-candados — Gestión de secretos#{C::RESET}
#{C::GREEN}Uso:#{C::RESET} ruby ns8-candados.rb <comando> [args]
#{C::YELLOW}Comandos:#{C::RESET}
abrir Autorizar SSH (cargar passphrase)
encrypt <texto> Cifrar texto plano token
decrypt <token> Descifrar token texto plano
get <clave> Obtener secreto de la bóveda
set <clave> Guardar secreto (input seguro)
rm <clave> Eliminar secreto
list Listar claves disponibles
#{C::YELLOW}Ejemplos:#{C::RESET}
#{C::DIM}# Guardar un secreto#{C::RESET}
ruby ns8-candados.rb set mi_password
#{C::DIM}# Usar en scripts (salida limpia)#{C::RESET}
PASS=$(ruby ns8-candados.rb get admindasu)
sshpass -p "$PASS" ssh user@host "cmd"
HELP
end
# ─── Main ────────────────────────────────────────────────────────────
comando = ARGV[0]
arg = ARGV[1]
case comando
when 'abrir'
cmd_abrir
when 'encrypt', 'decrypt', 'get', 'set', 'rm', 'list'
cripto = Cripto.new(cargar_master_key)
boveda = Boveda.new(BOVEDA)
case comando
when 'encrypt' then cmd_encrypt(cripto, arg)
when 'decrypt' then cmd_decrypt(cripto, arg)
when 'get' then cmd_get(cripto, boveda, arg)
when 'set' then cmd_set(cripto, boveda, arg)
when 'rm' then cmd_rm(boveda, arg)
when 'list' then cmd_list(boveda)
end
when 'help', '-h', '--help', nil
cmd_help
else
$stderr.puts "#{C::RED}✗ Comando desconocido: '#{comando}'#{C::RESET}"
cmd_help
exit 1
end