[Core/Infra] Provisionamiento SSH en pc-dasu0 y unificación Nginx tailscale

This commit is contained in:
Ricardo Monla
2026-03-14 10:29:54 -03:00
parent e554bb5d0d
commit b13a389568
50 changed files with 2702 additions and 561 deletions
+8 -8
View File
@@ -16,9 +16,9 @@ Esta hebra gobierna los protocolos de seguridad de red, gestión de secretos y a
3. **Si no hay confirmación**: Revertir cambios inmediatamente y restaurar estado anterior.
4. **Si hay confirmación**: Hacer cambios persistentes.
## 2. Gestión de Secretos — `ns8-candados`
## 2. Gestión de Secretos — `candados`
Herramienta unificada de seguridad: [`adn/tools/seguridad/ns8-candados.rb`](../adn/tools/seguridad/ns8-candados.rb)
Herramienta unificada de seguridad: [`adn/tools/seguridad/candados.rb`](../adn/tools/seguridad/candados.rb)
### 2.1 Prohibición de Texto Plano
La Inteligencia Artificial operante tiene **expresamente prohibido** solicitar contraseñas administrativas, tokens o claves privadas al operador, así como también plasmarlas en logs, comandos literales temporales o en la bitácora.
@@ -27,14 +27,14 @@ La Inteligencia Artificial operante tiene **expresamente prohibido** solicitar c
| Archivo | Ubicación | Git | Descripción |
| :--- | :--- | :---: | :--- |
| `ns8-candados.rb` | `adn/tools/seguridad/` | ✅ | Script Ruby unificado: cifrado, bóveda y autorización SSH. |
| `candados.rb` | `adn/tools/seguridad/` | ✅ | Script Ruby unificado: cifrado, bóveda y autorización SSH. |
| `.boveda.json` | `adn/tools/seguridad/` | ❌ | Payloads AES-256-GCM cifrados. |
| `.master.key` | Raíz del proyecto | ❌ | Clave maestra de la bóveda (nunca en Git). |
### 2.3 Comandos Disponibles
```bash
ruby adn/tools/seguridad/ns8-candados.rb <comando> [args]
ruby adn/tools/seguridad/candados.rb <comando> [args]
```
| Comando | Descripción |
@@ -51,7 +51,7 @@ ruby adn/tools/seguridad/ns8-candados.rb <comando> [args]
```bash
# Obtener password para uso en comando remoto
PASS=$(ruby adn/tools/seguridad/ns8-candados.rb get admindasu)
PASS=$(ruby adn/tools/seguridad/candados.rb get admindasu)
sshpass -p "$PASS" ssh -J root@100.116.210.36 admindasu@10.0.100.12 "cmd"
```
@@ -71,11 +71,11 @@ sshpass -p "$PASS" ssh -J root@100.116.210.36 admindasu@10.0.100.12 "cmd"
### 3.1 Problema
La llave SSH del operador está protegida con passphrase. El agente SSH no retiene las identidades entre sesiones.
### 3.2 Solución: `ns8-candados.rb abrir`
### 3.2 Solución: `candados.rb abrir`
El operador ejecuta el comando **interactivamente** para cargar su identidad SSH:
```bash
ruby adn/tools/seguridad/ns8-candados.rb abrir
ruby adn/tools/seguridad/candados.rb abrir
```
Las identidades permanecen activas mientras dure la sesión del agente SSH (sin timeout artificial).
@@ -94,7 +94,7 @@ Las identidades permanecen activas mientras dure la sesión del agente SSH (sin
**Uso programático** (ej: desde ns8-bkps):
```bash
# 1. Cargar llave SSH
ruby adn/tools/seguridad/ns8-candados.rb abrir
ruby adn/tools/seguridad/candados.rb abrir
# 2. Acceder a hipervisor
ssh root@10.0.10.201 "qm list"
+1 -1
View File
@@ -53,6 +53,6 @@ Antes de iniciar escaneos, pruebas de red, o conexiones SSH a un nodo de un proy
Para tareas operativas complejas (ej. backups), la IA **DEBE** utilizar las herramientas unificadas (`adn/tools/run`) que ya integran la lógica de seguridad y registro:
1. **Ejecución**: Usar `./adn/tools/run backup [nodo]`.
2. **Seguridad**: La herramienta utilizará internamente `ns8-candados run` para no exponer secretos.
2. **Seguridad**: La herramienta utilizará internamente `candados run` para no exponer secretos.
3. **Bitácora**: El registro en `docs/bitacoras/YYYY-MM-DD.md` es automático mediante la herramienta.
4. **Validación**: La herramienta valida el contexto técnico (VMID, Host IP) consultando las fichas del ADN.
+3 -2
View File
@@ -13,6 +13,7 @@
| **Lenguaje** | Ruby (lenguaje de desarrollo para componentes y herramientas) |
| **Filosofía** | DB-First (Base de Datos PostgreSQL → Web) |
| **Zona Horaria** | `America/Argentina/Buenos_Aires` (UTC-3) |
| **Red remota** | **Tailscale** (Cuenta: `pcdasu0@frlr.utn.edu.ar`) |
---
@@ -224,7 +225,7 @@ referencia canónica y exclusiva de las directivas que un agente IA debe seguir.
3. **Auto-Registro en DB**: Toda acción autónoma de la IA DEBE registrarse como entrada en la bitácora del día usando `db evento:crear`.
4. **Ruby como lenguaje de desarrollo**: Todos los componentes nuevos deben escribirse en Ruby, siguiendo principios "Menos es Más" (atómicos, reutilizables).
5. **Validador obsoleto**: NO usar `validador` (orientado a MD-First). En su lugar, validar coherencia mediante consultas a la base de datos.
6. **Secretos en bóveda**: NUNCA exponer contraseñas en texto plano. Usar `ns8-candados`.
6. **Secretos en bóveda**: NUNCA exponer contraseñas en texto plano. Usar `candados` (`eval $(ruby adn/tools/seguridad/candados.rb load <clave>)`).
7. **Topología primero**: Antes de conectarte a un nodo, consultar `./adn/tools/run contexto <nodo>` para entender su ubicación en la red.
8. **Exportación opcional**: Los archivos `.md` son solo para archivo histórico. La operación real ocurre en PostgreSQL → Web.
9. **Verificar en Web**: Tras modificar la DB, acceder a `http://localhost:5174` para verificar que los cambios se reflejan correctamente.
@@ -270,7 +271,7 @@ adn/tools/
│ └── config.yml ← Configuración general
├── hooks/ ← Git hooks (pre-commit)
├── parsers/ ← Parsers especializados (plan.rb)
├── seguridad/ ← ns8-candados (gestión de secretos)
├── seguridad/ ← candados (gestión de secretos)
└── sondas/ ← Sondas remotas (w-zombi)
```
+12
View File
@@ -34,6 +34,7 @@ class SubcomandoAyuda
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
generar nodo <nombre> Generar ficha de nodo
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
#{Color::YELLOW}tailscale <acción>#{Color::RESET} Gestión de identidades Tailscale
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
#{Color::YELLOW}db <subcomando>#{Color::RESET} Operaciones de base de datos
#{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers
@@ -149,6 +150,17 @@ class SubcomandoAyuda
puts ""
puts "Consulta la bitácora web para listar todos los items con estado ⏳ (En proceso)."
puts "Ideal para monitorear procesos automatizados o jornadas abiertas."
when 'tailscale', 'ts'
puts "Uso: ./adn/tools/run tailscale <acción> [perfil]"
puts ""
puts "Gestión de identidades y perfiles de Tailscale:"
puts " • switch <perfil> Alternar entre cuentas (dasuten, personal)"
puts " • status Ver estado y cuenta activa"
puts " • list Listar perfiles en caché local"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run tailscale switch dasuten"
puts " ./adn/tools/run tailscale status"
else
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
+6 -7
View File
@@ -89,17 +89,16 @@ module ADN
# 2. Registrar inicio en bitácora (Bitácora-First)
registrar_inicio_bitacora(nodo, datos)
# 3. Ejecución segura vía ns8-candados
# 3. Ejecución segura vía candados
@logger.info("Solicitando autorización y ejecutando backup remoto...")
comando_vzdump = "vzdump #{datos[:vmid]} --mode #{@opciones[:modo]} --storage #{@opciones[:storage]} --compress #{@opciones[:compress]}"
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
# Usamos el binario de ruby y la ruta absoluta para ns8-candados
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
# Ruta absoluta a candados
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
# Ejecutar con ns8-candados run
# Ejecutar con ns8-candados autorizando primero
# Ejecutar con candados autorizando primero
system("ruby #{candados_path} authorize > /dev/null 2>&1")
task_id = nil
@@ -285,7 +284,7 @@ module ADN
puts "\n#{Color::BOLD}🔍 Estado de Backups en Tiempo Real (Proxmox)#{Color::RESET}"
puts "" * 60
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
# Asegurar autorización general para el lote
system("ruby #{candados_path} authorize > /dev/null 2>&1")
@@ -372,7 +371,7 @@ module ADN
# 1. Obtener lista de VMs directamente de Proxmox para nombres precisos
host = { nombre: 'srv-pmox1', ip: '10.0.10.201' }
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
system("ruby #{candados_path} authorize > /dev/null 2>&1")
@logger.info("Consultando inventario de VMs en Proxmox...")
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ------------------------------------------------------------------------------
# CLI: commit.rb - Herramienta para gestión de commits según principios ADN
# ------------------------------------------------------------------------------
# Principios aplicados:
# 1. Menos es Más: Componente atómico y reutilizable para gestión de commits
# 2. Armonía Integral: Val
+74 -2
View File
@@ -107,6 +107,8 @@ module ADN
puts " #{Color::BOLD}Modo:#{Color::RESET} #{entrada['modo'] == 'P' ? 'Presencial' : 'Remoto'}"
puts " #{Color::BOLD}IA:#{Color::RESET} #{entrada['es_ia'] ? 'Sí' : 'No'}"
puts " #{Color::BOLD}Bitácora:#{Color::RESET} #{fecha_hoy} (ID: #{bitacora_id})"
mostrar_pendientes(db)
end
end
@@ -114,7 +116,7 @@ module ADN
def comando_evento_actualizar(args)
if args.length < 1 || args[0].start_with?('-')
puts "#{Color::YELLOW}Uso: adn db evento:actualizar <ID> [OPCIONES]#{Color::RESET}"
puts "Opciones soportadas: --fin \"HH:MM\", --inicio \"HH:MM\", --estado \"\", --descripcion \"Texto\""
puts "Opciones soportadas: --fin \"HH:MM\", --inicio \"HH:MM\", --estado \"\", --descripcion \"Texto\", --nodo \"Nombre\""
return
end
@@ -127,6 +129,7 @@ module ADN
opt.on("--fin HH:MM", "Nueva hora de fin") { |f| opts[:fin] = f }
opt.on("--descripcion TEXTO", "Nueva descripción") { |d| opts[:descripcion] = d }
opt.on("--estado ESTADO", "Nuevo estado (⏳, ✅, ❌, ⚠️, ⏸️)") { |e| opts[:estado] = e }
opt.on("--nodo NOMBRE", "Nuevo nodo") { |n| opts[:nodo_raw] = n }
opt.on("-h", "--help", "Mostrar ayuda") do
puts opt
exit
@@ -141,13 +144,27 @@ module ADN
end
if opts.empty?
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --fin, --inicio, --descripcion o --estado#{Color::RESET}"
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --fin, --inicio, --descripcion, --estado o --nodo#{Color::RESET}"
return
end
puts "#{Color::CYAN}🔄 Actualizando evento #{id_evento}...#{Color::RESET}"
BitacorasDB::BitacoraDB.with_connection do |db|
if opts.key?(:nodo_raw)
if opts[:nodo_raw].nil? || opts[:nodo_raw].empty?
opts[:nodo_id] = nil
else
nodo = db.find_nodo_by_nombre(opts[:nodo_raw])
if nodo
opts[:nodo_id] = nodo['id']
else
puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo_raw]}' no encontrado#{Color::RESET}"
end
end
opts.delete(:nodo_raw)
end
begin
res = db.update_entrada(id_evento, opts)
if res
@@ -160,6 +177,8 @@ module ADN
rescue => e
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
end
mostrar_pendientes(db)
end
end
@@ -277,6 +296,59 @@ module ADN
rescue => e
puts "#{Color::RED}✗ Error al eliminar: #{e.message}#{Color::RESET}"
end
mostrar_pendientes(db)
end
end
private
def mostrar_pendientes(db)
puts ""
puts "#{Color::CYAN}⏳ Eventos pendientes de cierre:#{Color::RESET}"
# Filtrar por bitácora de hoy y estado '⏳' o '⏸️'
fecha_hoy = Date.today.to_s
bitacora = db.find_bitacora_by_fecha(fecha_hoy)
unless bitacora
puts " #{Color::DIM}No hay bitácora para hoy.#{Color::RESET}"
return
end
# Buscar todos los pendientes de hoy
entradas_pendientes = db.list_entradas(
bitacora_id: bitacora['id'],
estado: '⏳',
limit: 20
)
# Sumamos pausados si los hay
entradas_pausadas = db.list_entradas(
bitacora_id: bitacora['id'],
estado: '⏸️',
limit: 20
)
entradas = entradas_pendientes + entradas_pausadas
if entradas.empty?
puts " #{Color::GREEN}¡Excelente! No hay eventos pendientes de cierre.#{Color::RESET}"
return
end
# Ordenar por hora de inicio (más antiguos primero)
entradas = entradas.sort_by { |e| e['inicio'] }
entradas.each do |e|
id = e['id'].to_s
inicio = e['inicio'][0..4] # HH:MM:SS -> HH:MM
nodo = (e['nodo_nombre'] || '(sin nodo)')[0..14]
desc = e['descripcion']
desc = desc[0..47] + '...' if desc.length > 50
estado_icon = e['estado']
puts " #{estado_icon} #{Color::YELLOW}ID #{id.ljust(5)}#{Color::RESET} #{inicio.ljust(5)} #{nodo.ljust(15)} #{desc}"
end
end
+2 -2
View File
@@ -104,7 +104,7 @@ module ADN
# Obtener passphrase de la bóveda
@passphrase = nil
begin
candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb')
candados_path = File.join(File.dirname(__FILE__), '../seguridad/candados.rb')
if File.exist?(File.join(File.dirname(__FILE__), '../seguridad/.session'))
output = `ruby #{candados_path} get rsa 2>/dev/null`.strip
@passphrase = output unless output.empty? || output.include?('Error')
@@ -212,7 +212,7 @@ module ADN
end
def system_ssh_status_win(ip, remote_cmd)
candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb')
candados_path = File.join(File.dirname(__FILE__), '../seguridad/candados.rb')
if File.exist?(File.join(File.dirname(__FILE__), '../seguridad/.session'))
shell_eval = "sshpass -e ssh -n -o ConnectTimeout=5 -o StrictHostKeyChecking=no #{ip} '#{remote_cmd}' > /dev/null 2>&1"
+202
View File
@@ -0,0 +1,202 @@
# frozen_string_literal: true
# adn/tools/cli/tailscale.rb — Subcomando para gestión de identidades Tailscale
# ==============================================================================
# Este subcomando permite alternar entre diferentes perfiles de Tailscale
# (ej. DASUTEN vs Personal) de forma segura utilizando candados.
#
# Usa `candados.rb sudo` como primitiva reutilizable para ejecutar
# comandos con privilegios (no implementa sudo propio).
#
# Perfiles:
# - dasuten: pcdasu0@frlr.utn.edu.ar (ecosistema DASUTEN)
# - personal: rmonla@gmail.com (otros ecosistemas/topologías)
require_relative '../core/colores'
module ADN
class SubcomandoTailscale
CANDADOS_PATH = File.expand_path('../seguridad/candados.rb', __dir__)
def initialize(args, logger)
@args = args
@logger = logger
end
def ejecutar
if @args.empty? || @args[0] == 'ayuda'
mostrar_ayuda
return
end
accion = @args.shift
case accion
when 'switch'
ejecutar_switch
when 'status', 'estado'
ejecutar_status
when 'list', 'listar'
ejecutar_list
when 'setup'
ejecutar_setup
else
puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
def mostrar_ayuda
puts <<~AYUDA
#{Color::CYAN}🌐 Gestión de Identidades Tailscale#{Color::RESET}
#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run tailscale <acción> [perfil]
#{Color::YELLOW}Acciones:#{Color::RESET}
switch <perfil> Alternar a un perfil (dasuten, personal)
status Ver estado actual de Tailscale y cuenta activa
list Listar perfiles disponibles en Tailscale
setup Configurar operador (una vez, evita pedir sudo)
#{Color::YELLOW}Perfiles:#{Color::RESET}
dasuten Ecosistema DASUTEN (pcdasu0@frlr.utn.edu.ar)
personal Otros ecosistemas (rmonla@gmail.com)
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run tailscale setup
./adn/tools/run tailscale switch dasuten
./adn/tools/run tailscale switch personal
./adn/tools/run tailscale status
AYUDA
end
# ─── Status ───────────────────────────────────────────────────────
def ejecutar_status
puts "#{Color::CYAN}🔍 Estado actual de Tailscale:#{Color::RESET}"
puts "#{Color::DIM}#{'─' * 60}#{Color::RESET}"
system('tailscale status')
puts ""
cuenta = `tailscale status --json 2>/dev/null | jq -r '.User[].LoginName' 2>/dev/null`.strip
if cuenta.empty? || cuenta == 'null'
puts "#{Color::YELLOW}⚠ No se pudo determinar la cuenta activa.#{Color::RESET}"
else
puts "#{Color::BOLD}Cuenta activa:#{Color::RESET} #{Color::GREEN}#{cuenta}#{Color::RESET}"
end
operador_ok = system('tailscale switch --list > /dev/null 2>&1')
if operador_ok
puts "#{Color::GREEN}✓ Operador configurado (switch sin sudo)#{Color::RESET}"
else
puts "#{Color::YELLOW}⚠ Operador NO configurado. Ejecutá: ./adn/tools/run tailscale setup#{Color::RESET}"
end
end
# ─── List ─────────────────────────────────────────────────────────
def ejecutar_list
puts "#{Color::CYAN}📋 Perfiles disponibles en Tailscale:#{Color::RESET}"
# Intentar sin sudo (operador configurado)
output = `tailscale switch --list 2>&1`
if $?.success?
puts output
else
# Fallback: usar candados sudo (reutilizable)
candados_sudo('tailscale', 'switch', '--list')
end
end
# ─── Setup ────────────────────────────────────────────────────────
def ejecutar_setup
@logger.info("Configurando operador Tailscale para #{ENV['USER']}...")
puts "#{Color::CYAN}⚙️ Configurando operador Tailscale...#{Color::RESET}"
puts "#{Color::DIM}Esto permite gestionar Tailscale sin sudo en el futuro.#{Color::RESET}"
success = candados_sudo('tailscale', 'set', "--operator=#{ENV['USER']}")
if success
puts "#{Color::GREEN}✅ Operador configurado exitosamente.#{Color::RESET}"
@logger.exito("Operador Tailscale configurado para #{ENV['USER']}")
else
puts "#{Color::RED}✗ No se pudo configurar el operador.#{Color::RESET}"
puts " Verificá la clave sudo en la bóveda: ruby candados.rb ayuda"
end
end
# ─── Switch ───────────────────────────────────────────────────────
def ejecutar_switch
perfil = @args.shift
if perfil.nil?
puts "#{Color::RED}✗ Falta nombre del perfil (dasuten|personal)#{Color::RESET}"
return
end
case perfil.downcase
when 'dasuten', 'd'
switch_to('dasuten')
when 'personal', 'p'
switch_to('personal')
else
puts "#{Color::YELLOW}⚠ Perfil no reconocido. Perfiles válidos: dasuten, personal#{Color::RESET}"
end
end
def switch_to(tipo)
@logger.info("Cambiando perfil Tailscale a: #{tipo}")
# 1. Obtener usuario de la bóveda
clave_user = tipo == 'dasuten' ? 'tailscale_dasuten_user' : 'tailscale_personal_user'
user = obtener_secreto(clave_user)
if user.nil? || user.empty?
@logger.error("No se encontró '#{clave_user}' en la bóveda.")
puts "#{Color::RED}✗ Credencial '#{clave_user}' no encontrada en candados.#{Color::RESET}"
return
end
puts "#{Color::CYAN}🔄 Cambiando a perfil: #{Color::BOLD}#{user}#{Color::RESET}..."
# 2. Intentar sin sudo (operador configurado)
if system("tailscale switch '#{user}' > /dev/null 2>&1")
exito_switch(tipo, user)
return
end
# 3. Fallback: usar candados sudo (primitiva reutilizable)
puts "#{Color::DIM}Usando candados sudo...#{Color::RESET}"
if candados_sudo('tailscale', 'switch', user)
exito_switch(tipo, user)
return
end
# 4. Perfil sin sesión → login manual
@logger.advertencia("El perfil #{tipo} no tiene sesión activa.")
puts "#{Color::YELLOW}⚠ El perfil '#{user}' no tiene sesión en este equipo.#{Color::RESET}"
puts " Necesitás iniciar sesión manualmente:"
puts " #{Color::BOLD}ruby candados.rb sudo tailscale login#{Color::RESET}"
end
def exito_switch(tipo, user)
puts "#{Color::GREEN}✅ Perfil Tailscale cambiado a '#{tipo}' (#{user}).#{Color::RESET}"
@logger.exito("Perfil Tailscale cambiado a #{tipo} (#{user})")
puts ""
ejecutar_status
end
# ─── Utilidades ───────────────────────────────────────────────────
# Obtiene un secreto de la bóveda (requiere sesión abierta)
def obtener_secreto(clave)
output = `ruby #{CANDADOS_PATH} get #{clave} 2>/dev/null`.strip
return nil if output.empty? || output.include?('Error') || output.include?('✗')
output
end
# Ejecuta un comando con sudo usando candados.rb sudo (primitiva reutilizable)
def candados_sudo(*cmd_parts)
system("ruby #{CANDADOS_PATH} sudo #{cmd_parts.join(' ')}")
end
end
end
+1 -1
View File
@@ -9,7 +9,7 @@ module ADN
class Conciliador
def initialize(logger)
@logger = logger
@candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb')
@candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
end
def ejecutar(silencioso: false)
+1 -1
View File
@@ -431,7 +431,7 @@ module BitacorasDB
i = 1
params.each do |k, v|
if [:inicio, :fin, :descripcion, :estado, :modo, :es_ia].include?(k)
if [:inicio, :fin, :descripcion, :estado, :modo, :es_ia, :nodo_id].include?(k)
val = ([:inicio, :fin].include?(k)) ? parse_time(v) : v
set_clauses << "#{k} = $#{i}"
values << val
+4
View File
@@ -19,6 +19,7 @@ ENV['TZ'] = 'America/Argentina/Buenos_Aires'
# generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
# generar nodo <nombre> Generar ficha de nodo
# generar proyecto <código> Generar manifiesto de proyecto
# tailscale <acción> [opciones] Gestión de identidades Tailscale
# conocimiento asimilar <archivo> Asimilar documentación al ADN
# evolucion <propuesta.md> Gestionar propuesta de evolución del ADN
# ayuda [subcomando] Mostrar ayuda detallada
@@ -51,6 +52,7 @@ require_relative 'cli/conocimiento'
require_relative 'cli/jornada'
require_relative 'cli/proceso'
require_relative 'cli/estados'
require_relative 'cli/tailscale'
require_relative 'core/conciliador'
require_relative 'core/colores'
@@ -110,6 +112,8 @@ class ADNCLI
ADN::SubcomandoProceso.new(args, @logger).ejecutar
when 'estados'
ADN::SubcomandoEstados.new(args, @logger).ejecutar
when 'tailscale', 'ts'
ADN::SubcomandoTailscale.new(args, @logger).ejecutar
when 'inicio'
# Compatibilidad: redirigir a jornada iniciar
ADN::SubcomandoJornada.new(['iniciar'] + args, @logger).ejecutar
+3 -2
View File
@@ -1,10 +1,11 @@
{
"sudo": "eyJpdiI6IkgzUnpoNHVGeWVqQUVGSDQiLCJ0YWciOiI3TmhPcjRGVFo3TFZHTUpNREVncXlBPT0iLCJkYXRhIjoiQlptVmtYbGdEcjFYWkxpZ21nPT0ifQ==",
"rsa": "eyJpdiI6IkpoTmpZRWZMeE12eWhXSVMiLCJ0YWciOiJUUnJGY05qRlVoOTl2REplTms0ZU5RPT0iLCJkYXRhIjoiNkFIWCsyYm9ndzZKbGM3c0hRPT0ifQ==",
"domain_admin": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
"admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
"win_domain": "eyJpdiI6IitrY2V0eEQxektEMFk2cGgiLCJ0YWciOiJtQmdIYU90ZTFFWUlGZElsMXBIOTJBPT0iLCJkYXRhIjoiNVVFanhaN2tIdHViZDRDRnlnPT0ifQ==",
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=",
"srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==",
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ=="
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==",
"ns8:rmonla:sudo": "eyJpdiI6IkRqbTBhVlZSYUpVdno3TFkiLCJ0YWciOiJJSnE2ei8zZHIrV2hFQks1VjNpVGN3PT0iLCJkYXRhIjoiT3JxTjRhTmlQK2R3TlJZZk9BPT0ifQ==",
"srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0="
}
+1 -1
View File
@@ -1 +1 @@
1773303097
1773452033
+91 -40
View File
@@ -1,55 +1,106 @@
# 🔐 ns8-candados: Gestión de Secretos y Autorización SSH
# 🔐 candados: Gestión Segura de Secretos
## Resumen Ejecutivo
`ns8-candados` es una herramienta de seguridad robusta diseñada para centralizar, cifrar y gestionar secretos operativos del ecosistema `srv-ns8`. Utiliza el algoritmo industrial **AES-256-GCM** para garantizar la integridad y confidencialidad de las credenciales. Su arquitectura implementa un sistema de **Candado Temporal (MFA)** que requiere una autorización explícita para liberar secretos, protegiendo la información incluso si el servidor se ve comprometido.
`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.
**Principio fundamental:** Las contraseñas **NUNCA** deben aparecer en pantalla, logs ni archivos temporales.
---
## 🔍 Análisis Estructural
- **Tipo de Proyecto**: Utilidad de seguridad CLI escrita en Ruby.
- **Organización**: Estructura modular centrada en la protección del dato:
- `Cripto`: Motor criptográfico AES-256-GCM con IV aleatorio.
- `Boveda`: Persistencia cifrada en `.boveda.json`.
- **MFA / Autorización**: Sistema de sesiones temporales basadas en archivos con permisos restringidos.
- **Archivos Fundamentales**:
- `ns8-candados.rb`: Lógica central.
- `.master.key` (Local): La semilla de cifrado (permisos 600).
- `.boveda.json`: Almacén de secretos cifrados (permisos 600).
- `.session`: Token de autorización temporal (vida: 5 min, permisos 600).
## ⚙️ 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).
## ⚙️ Configuración y Dependencias
- **Tecnologías**: Ruby, OpenSSL.
- **Seguridad de Archivos**: Todos los archivos sensibles se manejan con `chmod 600`, asegurando que **solo el propietario** de la herramienta pueda operar.
- **Comandos de Gestión**:
- `ruby ns8-candados.rb authorize`: **Abre el candado**. Habilita el acceso a secretos durante 5 minutos.
- `ruby ns8-candados.rb cerrar`: **Cierra el candado** manualmente eliminando la sesión activa.
- `ruby ns8-candados.rb list`: Lista las claves guardadas (requiere autorización).
- `ruby ns8-candados.rb get <clave>`: Recupera el valor descifrado (requiere autorización).
## 🚀 Comando Primario: `run` ★
## 🛡️ Auditoría (logging)
La herramienta mantiene un archivo `access.log` en su directorio local que registra de forma inmutable:
- **Timestamp**: Fecha y hora exacta de la acción.
- **Usuario**: El usuario del sistema operativo que ejecutó el comando.
- **Acción**: `AUTHORIZE`, `GET`, `LOCK`, `DENIED` o `EXPIRED`.
- **Detalle**: Nombre de la clave solicitada (solo en comandos `get`).
El comando `run` es la forma **recomendada** de usar credenciales. Inyecta `USR` y `PASS` directamente en el entorno del subproceso sin imprimir nada:
El archivo tiene permisos `600` para asegurar que solo el propietario pueda auditar los accesos.
```bash
# 1. Autorizar (dura 5 min)
ruby candados.rb authorize
# 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'
```
**¿Qué pasa internamente?**
1. Descifra las credenciales de la bóveda
2. Las inyecta como `USR` y `PASS` en el entorno del subproceso
3. Ejecuta el comando
4. Limpia las variables al terminar
5. **Nada se imprime en pantalla ni queda en logs**
## 🔒 Comando: `sudo`
Ejecuta comandos con privilegios, resolviendo la contraseña automáticamente:
```bash
ruby candados.rb sudo tailscale set --operator=$USER
ruby candados.rb sudo apt update
```
La clave se busca automáticamente:
1. `<hostname>:<usuario>:sudo` (ej: `ns8:rmonla:sudo`)
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.
## 🔑 Resolución Automática de Pares
Los comandos `run` y `load` detectan automáticamente el par usuario/contraseña:
| Formato de clave | USR | PASS |
|---|---|---|
| `srv-dasu:rmonla` | `rmonla` (parte después de `:`) | Valor cifrado |
| `tailscale_dasuten_pass` | Busca `tailscale_dasuten_user` | Valor cifrado |
| `admindasu` | Busca `admindasu_user` (si existe) | Valor cifrado |
## 📋 Referencia Rápida
| 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) |
| `set <clave>` | ✅ Input sin eco | Guardar secreto |
| `list` | ✅ | Listar claves |
## 🛡️ Auditoría
Archivo `access.log` (permisos 600) registra:
- **Timestamp** y **Usuario** del SO
- **Acción**: `AUTHORIZE`, `GET`, `LOAD`, `RUN`, `SUDO`, `LOCK`, `DENIED`, `EXPIRED`
- **Detalle**: Clave solicitada y comando ejecutado
## 🧠 Contexto para IA
- **Flujo Obligatorio**: Los agentes de IA DEBEN ejecutar `authorize` antes de intentar recuperar cualquier clave.
- **Captura Segura**: Capturar siempre la salida en variables silenciadas:
```bash
ruby tools/ns8-candados/ns8-candados.rb authorize
PASS=$(ruby tools/ns8-candados/ns8-candados.rb get clave)
# Operar y luego cerrar:
ruby tools/ns8-candados/ns8-candados.rb cerrar
```
## 🚀 Próximos Pasos
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
### Propuestas de Mejora
- **Auditoría**: Implementar un `access.log` para trazar quién y cuándo abrió el candado.
- **Notificación**: Enviar un mensaje (ej. Telegram/Mail) cada vez que se ejecute `authorize`.
```bash
ruby candados.rb authorize
ruby candados.rb run srv-dasu:rmonla 'sshpass -p $PASS ssh $USR@host "ls"'
ruby candados.rb cerrar
```
---
*Generado siguiendo docs/prompt/documentacion.md*
+490
View File
@@ -0,0 +1,490 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# candados — Gestión unificada de secretos y autorización SSH
# ==============================================================================
# 🤖 INSTRUCCIONES PARA LA IA (AGENTES EXTERNOS):
# Esta herramienta permite obtener credenciales seguras sin exponerlas.
#
# COMANDO PRIMARIO (RECOMENDADO):
# ruby candados.rb run <clave> '<comando con $USR y $PASS>'
# → Inyecta USR y PASS en el entorno del subproceso. Nada se imprime.
#
# COMANDO SECUNDARIO (USO INTERACTIVO):
# eval $(ruby candados.rb load <clave>)
# → Carga USR y PASS en el shell actual via archivo temporal seguro (600).
#
# ⚠ NUNCA usar `get` directamente en run_command: el secreto se filtra en logs.
# ==============================================================================
# Uso: ruby adn/tools/seguridad/candados.rb <comando> [args]
#
# Comandos principales:
# run <clave> <cmd> Ejecutar comando con USR y PASS inyectados (PRIMARIO)
# sudo <cmd> Ejecutar comando con sudo desde la bóveda
# load <clave> Cargar USR/PASS en shell (eval, archivo temporal seguro)
#
# Comandos de gestión:
# authorize Abrir candado (sesión temporal 5 min)
# set <clave> Guardar secreto (input seguro, sin eco)
# get <clave> Obtener secreto (⚠ imprime en stdout)
# rm <clave> Eliminar secreto
# list Listar claves disponibles
require 'openssl'
require 'base64'
require 'json'
require 'io/console'
require 'fileutils'
# ─── Rutas ───────────────────────────────────────────────────────────
TOOL_DIR = File.expand_path(__dir__)
MASTER_KEY = File.join(TOOL_DIR, '.master.key')
OLD_KEY = File.expand_path('../../.master.key', TOOL_DIR)
BOVEDA = File.join(TOOL_DIR, '.boveda.json')
SESION = File.join(TOOL_DIR, '.session')
ACCESS_LOG = File.join(TOOL_DIR, 'access.log')
# ─── Transición de Seguridad ─────────────────────────────────────────
if File.exist?(OLD_KEY) && !File.exist?(MASTER_KEY)
FileUtils.mv(OLD_KEY, MASTER_KEY)
FileUtils.chmod(0600, MASTER_KEY)
end
# ─── Colores ─────────────────────────────────────────────────────────
module C
RESET = "\e[0m"
BOLD = "\e[1m"
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 local: #{MASTER_KEY}#{C::RESET}"
end
File.read(MASTER_KEY)
end
# ─── Auditoría ───────────────────────────────────────────────────────
def registrar_acceso(accion, detalle = '')
usuario = ENV['USER'] || ENV['USERNAME'] || 'unknown'
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
log_entry = "[#{timestamp}] - user:#{usuario} - action:#{accion} - #{detalle}\n"
File.open(ACCESS_LOG, 'a') do |f|
f.write(log_entry)
end
FileUtils.chmod(0600, ACCESS_LOG) if File.exist?(ACCESS_LOG)
end
# ─── Gestión de Sesión (MFA) ─────────────────────────────────────────
def autorizar!
File.write(SESION, Time.now.to_i.to_s)
FileUtils.chmod(0600, SESION)
registrar_acceso('AUTHORIZE', 'Candado abierto (Sesión iniciada)')
$stderr.puts "#{C::GREEN}✓ Autorización concedida (Válida por 5 minutos).#{C::RESET}"
end
def cerrar_sesion!
if File.exist?(SESION)
FileUtils.rm_f(SESION)
registrar_acceso('LOCK', 'Candado cerrado (Sesión finalizada)')
$stderr.puts "#{C::YELLOW}🔒 Sesión cerrada. Candado puesto.#{C::RESET}"
end
end
def verificar_sesion!
unless File.exist?(SESION)
registrar_acceso('DENIED', 'Intento de acceso sin autorización')
abort "#{C::RED}✗ Error: El candado está puesto. Ejecutá 'ruby #{File.basename($0)} authorize' para abrirlo.#{C::RESET}"
end
inicio = File.read(SESION).to_i
if Time.now.to_i - inicio > 300 # 5 minutos
cerrar_sesion!
registrar_acceso('EXPIRED', 'Sesión expirada automáticamente')
abort "#{C::RED}✗ Sesión expirada. Por seguridad, volvé a autorizar.#{C::RESET}"
end
end
# ─── Comandos ────────────────────────────────────────────────────────
def cmd_abrir
puts "#{C::CYAN}🔐 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?
verificar_sesion!
token = boveda.get(clave)
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token.nil?
registrar_acceso('GET', "Clave: #{clave}")
puts cripto.decrypt(token)
end
def cmd_run(cripto, boveda, clave, *args)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
abort "#{C::RED}✗ Falta comando a ejecutar.#{C::RESET}" if args.empty? || args.all?(&:empty?)
# Retrocompatibilidad: candados run <clave> <VAR> <comando>
env_var_legacy = nil
if args.size >= 2 && args[0].match?(/^[A-Z_]+$/)
env_var_legacy = args.shift
end
comando = args
verificar_sesion!
token_pass = boveda.get(clave)
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token_pass.nil?
pass_value = cripto.decrypt(token_pass)
user_value = ''
# Buscar credenciales par (_user/_pass)
if clave.end_with?('_pass')
prefijo = clave.sub(/_pass$/, '')
token_user = boveda.get("#{prefijo}_user")
user_value = cripto.decrypt(token_user) if token_user
elsif clave.include?(':')
user_value = clave.split(':').last
else
token_user = boveda.get("#{clave}_user")
user_value = cripto.decrypt(token_user) if token_user
end
registrar_acceso('RUN', "Clave: #{clave}, Comando: #{comando.join(' ')}")
# Inyectar variables globalmente al entorno del comando
ENV['USR'] = user_value
ENV['PASS'] = pass_value
ENV[env_var_legacy] = pass_value if env_var_legacy
begin
if comando.length == 1
system(comando[0])
else
system(*comando)
end
exit $?.exitstatus || 0
ensure
ENV.delete('USR')
ENV.delete('PASS')
ENV.delete(env_var_legacy) if env_var_legacy
end
end
def cmd_set(cripto, boveda, clave, valor = nil)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
if valor.nil? || valor.empty?
$stderr.print "#{C::YELLOW}🔑 Valor para '#{clave}': #{C::RESET}"
valor = $stdin.noecho(&:gets)&.chomp
$stderr.puts
end
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_load(cripto, boveda, clave)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
verificar_sesion!
# Buscar credenciales por convención: <clave> contiene user y <clave> para pass,
# o <prefijo>_user y <prefijo>_pass si existen como par.
token_pass = boveda.get(clave)
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token_pass.nil?
pass_value = cripto.decrypt(token_pass)
user_value = ''
# Intentar encontrar el par _user/_pass automáticamente
if clave.end_with?('_pass')
prefijo = clave.sub(/_pass$/, '')
token_user = boveda.get("#{prefijo}_user")
user_value = cripto.decrypt(token_user) if token_user
elsif clave.include?(':')
# Formato "servidor:usuario" → USR es la parte después de :
user_value = clave.split(':').last
else
# Buscar si existe una clave hermana _user
token_user = boveda.get("#{clave}_user")
if token_user
user_value = cripto.decrypt(token_user)
end
end
registrar_acceso('LOAD', "Clave: #{clave}")
# Seguridad: NUNCA imprimir secretos a stdout/stderr.
# Escribir exports en un archivo temporal con permisos estrictos (600).
# A stdout solo emitimos el comando `source` + autoborrado.
require 'securerandom'
tmpfile_path = "/tmp/.candados_env_#{SecureRandom.hex(8)}"
safe_user = user_value.gsub("'", "'\\''")
safe_pass = pass_value.gsub("'", "'\\''")
File.write(tmpfile_path, "export USR='#{safe_user}'\nexport PASS='#{safe_pass}'\n")
File.chmod(0600, tmpfile_path)
# Solo el comando source + autoborrado va a stdout (evaluable por el shell)
puts "source '#{tmpfile_path}' && rm -f '#{tmpfile_path}'"
$stderr.puts "#{C::GREEN}✓ Credenciales cargadas en variables USR y PASS (archivo temporal seguro).#{C::RESET}"
end
# ─── Comando sudo (primitiva reutilizable) ─────────────────────
# Resolución inteligente de la clave sudo:
# 1. <hostname>:<usuario>:sudo (convención específica)
# 2. sudo (fallback genérico)
def cmd_sudo(cripto, boveda, comando_args)
abort "#{C::RED}✗ Falta comando a ejecutar con sudo.#{C::RESET}" if comando_args.nil? || comando_args.empty?
verificar_sesion!
# Resolver la clave sudo con búsqueda inteligente
hostname = `hostname -s 2>/dev/null`.strip
usuario = ENV['USER'] || ENV['USERNAME'] || 'root'
# Generar variantes de hostname para búsqueda flexible
# Ej: srvNS8 → ["srvNS8", "srvns8", "ns8", "NS8"]
variantes = [hostname]
variantes << hostname.downcase
variantes << hostname.sub(/^srv[-_]?/i, '') # sin prefijo srv/srv-/srv_
variantes << hostname.sub(/^srv[-_]?/i, '').downcase
variantes.uniq!
# Buscar la primera clave que exista en la bóveda
token = nil
clave_usada = nil
variantes.each do |h|
clave = "#{h}:#{usuario}:sudo"
if boveda.get(clave)
token = boveda.get(clave)
clave_usada = clave
break
end
end
# Fallback genérico
unless token
if boveda.get('sudo')
token = boveda.get('sudo')
clave_usada = 'sudo'
end
end
if token.nil?
buscadas = variantes.map { |h| "#{h}:#{usuario}:sudo" }.join("', '")
$stderr.puts "#{C::RED}✗ No se encontró clave sudo en la bóveda.#{C::RESET}"
$stderr.puts " Buscadas: '#{buscadas}', 'sudo'"
$stderr.puts " Registrala con: ruby candados.rb set #{variantes.first}:#{usuario}:sudo"
abort
end
sudo_pass = cripto.decrypt(token)
registrar_acceso('SUDO', "Clave: #{clave_usada}, Cmd: #{comando_args.join(' ')}")
# Ejecutar con sudo -S (hereda stdout/stderr, pipe solo en stdin)
cmd_full = comando_args.join(' ')
IO.popen("sudo -S #{cmd_full}", 'w') do |pipe|
pipe.puts sudo_pass
end
exit $?.exitstatus || 0
end
def cmd_help
puts <<~HELP
#{C::CYAN}🔐 candados — Gestión segura de secretos#{C::RESET}
#{C::GREEN}Uso:#{C::RESET} ruby adn/tools/seguridad/candados.rb <comando> [args]
#{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::YELLOW}━━━ Gestión de bóveda ━━━#{C::RESET}
authorize Abrir candado (autorización temporal 5 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::YELLOW}━━━ Utilidades ━━━#{C::RESET}
abrir Autorizar SSH (cargar passphrase)
encrypt <texto> Cifrar texto plano token
decrypt <token> 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}Ejemplos:#{C::RESET}
#{C::DIM}# 1. Autorizar (dura 5 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}# 3. Ejecutar con sudo#{C::RESET}
ruby candados.rb sudo tailscale set --operator=$USER
#{C::DIM}# 4. Cargar en shell interactivo (secundario)#{C::RESET}
eval $(ruby candados.rb load srv-dasu:rmonla)
#{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}
HELP
end
# ─── Main ────────────────────────────────────────────────────────────
if __FILE__ == $0
comando = ARGV[0]
arg = ARGV[1]
case comando
when 'authorize'
autorizar!
when 'cerrar'
cerrar_sesion!
when 'sudo'
cripto = Cripto.new(cargar_master_key)
boveda = Boveda.new(BOVEDA)
cmd_sudo(cripto, boveda, ARGV[1..])
when 'encrypt', 'decrypt', 'get', 'load', 'run', '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 'load' then cmd_load(cripto, boveda, arg)
when 'run' then cmd_run(cripto, boveda, arg, *ARGV[2..])
when 'set' then cmd_set(cripto, boveda, arg, ARGV[2])
when 'rm' then cmd_rm(boveda, arg)
when 'list' then cmd_list(boveda)
end
when 'help', 'ayuda', '-h', '--help', nil
cmd_help
else
$stderr.puts "#{C::RED}✗ Comando desconocido: '#{comando}'#{C::RESET}"
cmd_help
exit 1
end
end
-324
View File
@@ -1,324 +0,0 @@
#!/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)
# run <cl> <vr> <cmd> Ejecutar comando con secreto en variable de entorno <vr>
# 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__)
MASTER_KEY = File.join(TOOL_DIR, '.master.key')
OLD_KEY = File.expand_path('../../.master.key', TOOL_DIR)
BOVEDA = File.join(TOOL_DIR, '.boveda.json')
SESION = File.join(TOOL_DIR, '.session')
ACCESS_LOG = File.join(TOOL_DIR, 'access.log')
# ─── Transición de Seguridad ─────────────────────────────────────────
if File.exist?(OLD_KEY) && !File.exist?(MASTER_KEY)
FileUtils.mv(OLD_KEY, MASTER_KEY)
FileUtils.chmod(0600, MASTER_KEY)
end
# ─── 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 local: #{MASTER_KEY}#{C::RESET}"
end
File.read(MASTER_KEY)
end
# ─── Auditoría ───────────────────────────────────────────────────────
def registrar_acceso(accion, detalle = '')
usuario = ENV['USER'] || ENV['USERNAME'] || 'unknown'
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
log_entry = "[#{timestamp}] - user:#{usuario} - action:#{accion} - #{detalle}\n"
File.open(ACCESS_LOG, 'a') do |f|
f.write(log_entry)
end
FileUtils.chmod(0600, ACCESS_LOG) if File.exist?(ACCESS_LOG)
end
# ─── Gestión de Sesión (MFA) ─────────────────────────────────────────
def autorizar!
File.write(SESION, Time.now.to_i.to_s)
FileUtils.chmod(0600, SESION)
registrar_acceso('AUTHORIZE', 'Candado abierto (Sesión iniciada)')
$stderr.puts "#{C::GREEN}✓ Autorización concedida (Válida por 5 minutos).#{C::RESET}"
end
def cerrar_sesion!
if File.exist?(SESION)
FileUtils.rm_f(SESION)
registrar_acceso('LOCK', 'Candado cerrado (Sesión finalizada)')
$stderr.puts "#{C::YELLOW}🔒 Sesión cerrada. Candado puesto.#{C::RESET}"
end
end
def verificar_sesion!
unless File.exist?(SESION)
registrar_acceso('DENIED', 'Intento de acceso sin autorización')
abort "#{C::RED}✗ Error: El candado está puesto. Ejecutá 'ruby #{File.basename($0)} authorize' para abrirlo.#{C::RESET}"
end
inicio = File.read(SESION).to_i
if Time.now.to_i - inicio > 300 # 5 minutos
cerrar_sesion!
registrar_acceso('EXPIRED', 'Sesión expirada automáticamente')
abort "#{C::RED}✗ Sesión expirada. Por seguridad, volvé a autorizar.#{C::RESET}"
end
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?
verificar_sesion!
token = boveda.get(clave)
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token.nil?
registrar_acceso('GET', "Clave: #{clave}")
puts cripto.decrypt(token)
end
def cmd_run(cripto, boveda, clave, env_var, comando)
abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil?
abort "#{C::RED}✗ Falta nombre de variable de entorno.#{C::RESET}" if env_var.nil?
abort "#{C::RED}✗ Falta comando a ejecutar.#{C::RESET}" if comando.nil? || (comando.is_a?(Array) && comando.empty?) || (comando.is_a?(String) && comando.empty?)
verificar_sesion!
token = boveda.get(clave)
abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token.nil?
secret = cripto.decrypt(token)
registrar_acceso('RUN', "Clave: #{clave}, Comando: #{comando.is_a?(Array) ? comando.join(' ') : comando}")
# Ejecutar el comando inyectando la variable
ENV[env_var] = secret
begin
# Si comando es un array y tiene más de un elemento, lo ejecutamos directo (sin shell)
# Si es un string o array de 1, system lo pasará al shell.
if comando.is_a?(Array)
if comando.length == 1 && comando[0].include?(' ')
system(comando[0])
else
system(*comando)
end
else
system(comando)
end
exit $?.exitstatus || 0
ensure
ENV.delete(env_var)
end
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}
authorize Abrir candado (autorización temporal 5 min)
cerrar Cerrar candado manual (eliminar sesión)
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
run <cl> <vr> <cmd> Ejecutar comando con secreto en variable <vr>
set <clave> Guardar secreto (input seguro)
rm <clave> Eliminar secreto
list Listar claves disponibles
#{C::YELLOW}Ejemplos:#{C::RESET}
# Autorizar para operar
ruby ns8-candados.rb authorize
#{C::DIM}# Usar de forma segura inyectando en el entorno (recomendado)#{C::RESET}
ruby ns8-candados.rb run admindasu PASS "sshpass -e ssh user@host"
#{C::DIM}# Usar en scripts (salida limpia - menos seguro)#{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 'authorize'
autorizar!
when 'cerrar'
cerrar_sesion!
when 'encrypt', 'decrypt', 'get', 'run', '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 'run' then cmd_run(cripto, boveda, arg, ARGV[2], ARGV[3..])
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
@@ -0,0 +1,40 @@
function Log($msg) {
Write-Host $msg -ForegroundColor Cyan
if ($WZ_HOST) {
try {
Invoke-RestMethod -Uri "http://${WZ_HOST}/log" -Method Post -Body @{msg=$msg} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
} catch {}
}
}
Log "=========================================="
Log " REPARANDO PERMISOS E INICIANDO SSHD "
Log "=========================================="
Log "1) Bypass ExecutionPolicy para FixHostFilePermissions..."
try {
# Cambiamos la politica solo para este proceso
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
$out = & "C:\OpenSSH-Win64\FixHostFilePermissions.ps1" -Confirm:$false 2>&1
foreach ($line in $out) { Log " -> $line" }
Log "Script de permisos finalizado."
} catch {
Log "Error ejecutando script de permisos: $_"
}
Log "2) Iniciando servicio sshd..."
try {
Start-Service sshd -ErrorAction Stop
$svc = Get-Service sshd
Log "Servicio sshd. Estado actual: $($svc.Status) !!"
} catch {
Log "Fallo al iniciar el servicio: $_"
Log "Ejecutando sshd.exe -t para ver el error real de config:"
$test = & "C:\OpenSSH-Win64\sshd.exe" -t 2>&1
foreach ($line in $test) { Log " sshd -t: $line" }
}
Log "=========================================="
Log " FIN REPARACION "
Log "=========================================="
return
@@ -6,7 +6,11 @@ function Log($msg) {
$line = "[$ts] $msg"
Write-Host $line -ForegroundColor Cyan
Add-Content -Path $logFile -Value $line
try { (New-Object Net.WebClient).DownloadString("http://10.0.10.8:8000/log?msg=$msg") | Out-Null } catch {}
try {
if ($WZ_HOST) {
(New-Object Net.WebClient).DownloadString("http://${WZ_HOST}/log?msg=$msg") | Out-Null
}
} catch {}
}
Log "=========================================="
@@ -15,7 +19,7 @@ Log "=========================================="
$sshDir = "C:\OpenSSH-Win64"
$zipPath = "C:\OpenSSH-Win64.zip"
$sshUrl = "http://10.0.10.8:8000/payloads/OpenSSH-Win64.zip"
$sshUrl = "http://${WZ_HOST}/payloads/OpenSSH-Win64.zip"
try {
if (Test-Path "$sshDir\sshd.exe") {
@@ -2,7 +2,9 @@ function Log-Msg {
param([string]$Message)
Write-Host $Message -ForegroundColor Cyan
try {
Invoke-RestMethod -Uri "http://10.0.10.8:8000/log" -Method Post -Body @{msg=$Message} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
if ($WZ_HOST) {
Invoke-RestMethod -Uri "http://${WZ_HOST}/log" -Method Post -Body @{msg=$Message} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
}
} catch {}
}
@@ -15,6 +15,13 @@ TOOL_DIR = File.expand_path(__dir__)
PAYLOADS = File.join(TOOL_DIR, 'payloads')
LOG_FILE = File.join(TOOL_DIR, 'telemetria.log')
PORT = (ARGV.find { |a| a.match?(/^\d+$/) } || 8000).to_i
EXT_PORT = (ENV['EXT_PORT'] || PORT).to_i
require 'socket'
def get_ip
ENV['HOST_IP'] || Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }&.ip_address || '127.0.0.1'
end
HOST_IP = get_ip
PUBLIC_URL = "#{HOST_IP}:#{EXT_PORT}"
# Colores ANSI
C = { reset: "\e[0m", green: "\e[32m", cyan: "\e[36m", yellow: "\e[33m", red: "\e[31m", dim: "\e[2m" }
@@ -36,7 +43,7 @@ def banner
end
puts "#{C[:dim]}─────────────────────────────────────────────────────#{C[:reset]}"
puts "#{C[:yellow]}En la VM (PowerShell Admin):#{C[:reset]}"
puts " #{C[:green]}iwr 10.0.10.8:#{PORT}/zombi.ps1 -useb|iex#{C[:reset]}"
puts " #{C[:green]}iwr #{PUBLIC_URL}/zombi.ps1 -useb|iex#{C[:reset]}"
puts "#{C[:dim]}─────────────────────────────────────────────────────#{C[:reset]}"
puts ''
end
@@ -71,8 +78,9 @@ server = WEBrick::HTTPServer.new(
server.mount_proc '/zombi.ps1' do |_req, res|
# Genera un loader que apunta al payload activo
loader = <<~PS1
$WZ_HOST = "#{PUBLIC_URL}"
Write-Host "======================================================" -ForegroundColor Green
Write-Host " ZOMBI C2 LOOP — Conectado a srv-ns8:#{PORT} " -ForegroundColor Green
Write-Host " ZOMBI C2 LOOP — Conectado a $WZ_HOST " -ForegroundColor Green
Write-Host "======================================================" -ForegroundColor Green
Write-Host "Max 5 ciclos. CTRL+C para detener." -ForegroundColor DarkGray
Write-Host ""
@@ -82,7 +90,7 @@ server.mount_proc '/zombi.ps1' do |_req, res|
for ($ciclo = 1; $ciclo -le $maxCiclos; $ciclo++) {
Write-Host "[ZOMBI] Ciclo $ciclo/$maxCiclos" -ForegroundColor DarkGray
try {
$payload = Invoke-RestMethod -Uri "http://10.0.10.8:#{PORT}/payloads/activo.ps1" -UseBasicParsing -ErrorAction Stop
$payload = Invoke-RestMethod -Uri "http://#{PUBLIC_URL}/payloads/activo.ps1" -UseBasicParsing -ErrorAction Stop
if ($payload -and $payload.Trim() -ne "") {
try {
Invoke-Expression $payload
-115
View File
@@ -1,115 +0,0 @@
$ErrorActionPreference = "Continue"
$ZOMBI_URL = "http://10.0.10.8:8000"
function Log($msg) {
$ts = Get-Date -Format "HH:mm:ss"
Write-Host "[$ts] $msg" -ForegroundColor Cyan
try { Invoke-RestMethod -Uri "$ZOMBI_URL/log" -Method Post -Body @{msg="[$env:COMPUTERNAME] $msg"} -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null } catch {}
}
$DEST = "C:\SysDasuten"
$ZIP = "C:\TEMP\runSysDasuten.zip"
$SQL_SERVER = "sql-dasuten"
$SQL_DB = "sysdasuten"
Log "=========================================="
Log "DEPLOY SYSDASUTEN — $env:COMPUTERNAME"
Log "=========================================="
# --- PASO 1: Descargar ZIP ---
if (-not (Test-Path $DEST\Sistema\DasutenSQL.exe)) {
Log "PASO 1: Descargando runSysDasuten.zip (~268 MB)..."
New-Item -ItemType Directory -Path "C:\TEMP" -Force | Out-Null
try {
Invoke-WebRequest -Uri "$ZOMBI_URL/payloads/runSysDasuten.zip" -OutFile $ZIP -UseBasicParsing -ErrorAction Stop
Log "Descarga completada: $(((Get-Item $ZIP).Length / 1MB).ToString('N0')) MB"
} catch {
Log "ERROR descarga: $($_.Exception.Message)"
return
}
# --- PASO 2: Extraer ---
Log "PASO 2: Extrayendo a $DEST..."
New-Item -ItemType Directory -Path $DEST -Force | Out-Null
Expand-Archive -Path $ZIP -DestinationPath "C:\" -Force
# El zip contiene runSysDasuten/ como raiz, renombrar
if (Test-Path "C:\runSysDasuten") {
Copy-Item -Path "C:\runSysDasuten\*" -Destination $DEST -Recurse -Force
Remove-Item "C:\runSysDasuten" -Recurse -Force
}
Remove-Item $ZIP -Force -ErrorAction SilentlyContinue
Log "Extraido OK."
} else {
Log "PASO 1-2: SKIP — Ya existe $DEST\Sistema\DasutenSQL.exe"
}
# --- PASO 3: Modificar Kermet.ini ---
Log "PASO 3: Configurando Kermet.ini..."
$iniPaths = @("$DEST\Kermet.ini", "$DEST\Sistema\Kermet.ini")
foreach ($ini in $iniPaths) {
if (Test-Path $ini) {
$content = Get-Content $ini -Raw
$content = $content -replace 'SERVER=srvFENIX', "SERVER=$SQL_SERVER"
$content = $content -replace 'SERVER=172\.16\.9\.204\\SQL2017', "SERVER=$SQL_SERVER"
Set-Content $ini $content -Force
Log "Actualizado: $ini -> SERVER=$SQL_SERVER"
}
}
# --- PASO 4: Instalar Fuentes ---
Log "PASO 4: Instalando fuentes..."
$fontDir = "$DEST\Fonts\Fonts"
if (Test-Path $fontDir) {
$shell = New-Object -ComObject Shell.Application
$fontsFolder = $shell.NameSpace(0x14) # Windows Fonts folder
Get-ChildItem "$fontDir\*.ttf" | ForEach-Object {
$fontPath = $_.FullName
$fontName = $_.Name
if (-not (Test-Path "C:\Windows\Fonts\$fontName")) {
Copy-Item $fontPath "C:\Windows\Fonts\" -Force
$regKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
New-ItemProperty -Path $regKey -Name $fontName -Value $fontName -PropertyType String -Force | Out-Null
Log "Fuente instalada: $fontName"
} else {
Log "Fuente ya existe: $fontName"
}
}
} else {
Log "WARN: No se encontro directorio de fuentes"
}
# --- PASO 5: Verificar firewall SQL en sql-dasuten ---
Log "PASO 5: Probando conectividad SQL..."
$tcpTest = Test-NetConnection -ComputerName $SQL_SERVER -Port 1433 -WarningAction SilentlyContinue
if ($tcpTest.TcpTestSucceeded) {
Log "Conexion TCP a ${SQL_SERVER}:1433 OK"
} else {
Log "WARN: No se puede conectar a ${SQL_SERVER}:1433 — verificar firewall"
}
# --- PASO 6: Probar conexion SQL con sqlcmd (si existe) ---
$sqlcmd = Get-Command sqlcmd -ErrorAction SilentlyContinue
if ($sqlcmd) {
Log "PASO 6: Probando SQL con sqlcmd..."
$result = sqlcmd -S $SQL_SERVER -E -Q "SELECT DB_NAME() AS db_actual; SELECT name FROM sys.databases WHERE name = '$SQL_DB'" -W 2>&1
Log "SQL Result: $result"
} else {
Log "PASO 6: SKIP — sqlcmd no disponible (normal en Win10), probar con DasutenSQL.exe"
}
# --- PASO 7: Crear acceso directo ---
Log "PASO 7: Creando acceso directo en Escritorio..."
$desktopPath = [Environment]::GetFolderPath('CommonDesktopDirectory')
$shortcutPath = "$desktopPath\DasutenSQL.lnk"
$WshShell = New-Object -ComObject WScript.Shell
$shortcut = $WshShell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = "$DEST\Sistema\DasutenSQL.exe"
$shortcut.WorkingDirectory = "$DEST\Sistema"
$shortcut.Description = "Sistema DASUTEN"
$shortcut.Save()
Log "Acceso directo creado: $shortcutPath"
Log "=========================================="
Log "DEPLOY SYSDASUTEN COMPLETADO"
Log "Ejecutar desde: $DEST\Sistema\DasutenSQL.exe"
Log "=========================================="
@@ -1,3 +0,0 @@
NO
Debes usar las heramientas de adn/README.md y en su defecto si algo no te gusta te permito optimizar o mejorar la herramienta que necesites.
@@ -0,0 +1,4 @@
OK pc-dasu0 debemos poder acceder por ssh en el puerto 7022,no creo que esté implementado y para esto usabamos la sonda w-zombi de las herramientas.
Trata siempre de trabajar con herramientas definidas en ADN, mejorándolas y optimizándolas para poder reutilizarlas en el futuro. En su defecto, genera nuevas herramientas siguiendo estas mismas premisas, evitando el uso de soluciones temporales. Este enfoque está detallado en adn/README.md junto con el indice de herramientas ADN.
Recuerda que el flujo es actualizar el plan, bitácora web y recien ejecutas las tareas.
+1
View File
@@ -0,0 +1 @@
Trata siempre de trabajar con herramientas definidas en ADN, mejorándolas y optimizándolas para poder reutilizarlas en el futuro. En su defecto, genera nuevas herramientas siguiendo estas mismas premisas, evitando el uso de soluciones temporales. Este enfoque está detallado en adn/README.md.
@@ -3,7 +3,7 @@
**Fecha:** 11 de marzo de 2026
**Autor:** Sistema ADN
**Versión:** 1.0
**Ubicación:** `docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md`
**Ubicación:** `docs/proy/p2601_dasuten/plan/P2601_6.1.A - Red_SrvDasu.md`
**Estado:** ⏳ PLANIFICACIÓN
## 📋 Resumen Ejecutivo
@@ -25,7 +25,7 @@ Preparar la infraestructura de red para que `srv-dasu` funcione correctamente fu
#### Semana 1: Dependencias
- [x] **2.1:** Actualizar código: `adn/tools/cli/backup.rb` reemplazando `10.0.10.205` por la IP de Tailscale. (Referencias IP migradas a Tailscale 100.116.210.36)
- [x] **2.2:** Actualizar documentación: `nodos/srv-dasu.md` indicando la obsolescencia de la IP local. (Referencias IP migradas a Tailscale 100.116.210.36)
- [x] **2.3:** Actualizar documentación: `adn/03_seguridad.md` y `docs/proyectos/_hist/P2601_dasuten.md` reflejando rutas nuevas. (Referencias IP migradas a Tailscale 100.116.210.36)
- [x] **2.3:** Actualizar documentación: `adn/03_seguridad.md` y `docs/proy/p2601_dasuten/P2601_dasuten.md` reflejando rutas nuevas. (Referencias IP migradas a Tailscale 100.116.210.36)
### ✅ **FASE 3: Simulación de Mudanza**
@@ -0,0 +1,304 @@
# Plan: Unión de pc-dasu0 al Dominio DASUTeN (P2601 Fase 6.1.B)
**Fecha:** 11 de marzo de 2026
**Autor:** Sistema ADN
**Versión:** 2.0
**Ubicación:** `docs/proy/p2601_dasuten/plan/P2601_6.1.B - AD-Join: pc-dasu0 al Dominio.md`
**Estado:** ⏳ PLANIFICACIÓN
**Proyecto:** [P2601 DASUTEN](../../P2601_dasuten.md)
## 📋 Resumen Ejecutivo
Unir la PC física `pc-dasu0` (ubicada en la oficina DASUTeN) al dominio `dasuten.utnlr`, completando la Fase 6 del proyecto P2601. Este plan aborda los problemas de conectividad intermitente identificados en la red actual y establece un procedimiento validado basado en la experiencia exitosa con la VM de pruebas `pcv-dasu0`.
**Contexto Crítico:**
- `pc-dasu0` es la PC física (Ryzen 5, Windows 10) en la oficina DASUTeN
- `pcv-dasu0` (VM de pruebas) ya está unida exitosamente al dominio (validación previa)
- **INFORMACIÓN CRÍTICA:** `srv-dasu` y `pc-dasu0` comparten el mismo router ISP en la oficina DASUTeN
- Esta topología permite comunicación directa por capa 3 sin necesidad de Tailscale
- El dominio `dasuten.utnlr` opera en `dc-dasuten` (`10.0.100.10`)
- `srv-dasu` actúa como hypervisor con subred `10.0.100.0/24` y gateway `10.0.100.1`
## 🔧 Topología de Red Revisada
### Diagrama Conceptual
```
[Internet] ← Router ISP (Compartido) → [srv-dasu] ↔ [pc-dasu0]
│ │
└───────────────────────────────────────┘
Misma red local física
```
### Características Clave
1. **Router Compartido:** `srv-dasu` y `pc-dasu0` están detrás del mismo router del ISP
2. **Comunicación Directa Posible:** Conexión local por capa 3 sin intermediarios
3. **Simplificación de Conectividad:** Elimina dependencia exclusiva de Tailscale
4. **Latencia Reducida:** Comunicación local con latencia mínima (<1ms)
### Estrategias de Conectividad
#### Opción A: Comunicación Directa Local (PREFERIDA)
- **Ventajas:** Latencia mínima, sin dependencia externa, mayor estabilidad
- **Requisitos:** Configuración de red local adecuada, firewall permitido
- **Implementación:** IPs locales en misma subred, routing directo
#### Opción B: Tailscale como Backup
- **Ventajas:** Seguridad mejorada, tunelización cifrada, acceso remoto
- **Requisitos:** Tailscale funcionando en ambos nodos
- **Implementación:** Mantener como sistema de respaldo
#### Opción C: Híbrida (Directa + Tailscale)
- **Ventajas:** Resiliencia máxima, failover automático
- **Requisitos:** Configuración dual de red
- **Implementación:** Priorizar comunicación directa, fallback a Tailscale
#### 🔒 Red Tailscale Unificada (REQUERIMIENTO OBLIGATORIO)
Para asegurar la gestión y el monitoreo, todos los equipos del ecosistema deben coexistir en la misma red Tailscale:
- **Nodos:** `srv-ns8`, `srv-dasu`, `pc-dasu0`
- **Credenciales:** Gestionadas en la bóveda `candados` (clave: `tailscale_dasuten_user`)
- **Objetivo:** Garantizar visibilidad cruzada y túneles de administración remota persistentes.
- **Gestión ADN:** Implementado `./adn/tools/run tailscale switch <perfil>` para alternar entre cuentas.
- **Estado actual:**
- [x] `srv-ns8` (este equipo): Configurado con switch dual (dasuten/personal) ✅
- [x] `srv-dasu`: Migrado exitosamente a red `pcdasu0@frlr.utn.edu.ar` vía acceso local (IP: 100.112.46.104) ✅
- [ ] `pc-dasu0`: Offline en Tailscale, pendiente reactivar
## 🎯 Objetivos
1. **Primario:** Unir `pc-dasu0` al dominio `dasuten.utnlr` de forma estable y verificable
2. **Secundario:** Diagnosticar y resolver problemas de conectividad de red intermitente
3. **Validación:** Transferir configuración validada desde `pcv-dasu0` (VM de pruebas)
4. **Seguridad:** Establecer políticas de grupo y acceso seguro al sistema DASUTEN
5. **Topología:** Implementar comunicación directa local aprovechando router compartido
## 📅 Fases de Implementación
### 🔍 **FASE 1: Investigación de Topología de Red Actual**
**Hito 1.1:** Diagnóstico completo de conectividad entre `pc-dasu0` y la infraestructura DASUTEN
#### Semana 1: Análisis de Red y Topología
- [x] **1.1.1:** Verificar estado de Tailscale en `pc-dasu0` (IP actual: `100.100.145.51` - Estado: Offline)
- [x] **1.1.2:** Diagnosticar ruta de red desde `pc-dasu0` hacia `srv-dasu` (vía física/local a 192.168.1.27)
- [x] **1.1.3:** **CRÍTICO:** Probar conectividad directa local entre `pc-dasu0` y `srv-dasu`
- Identificar IPs locales asignadas por router ISP: `srv-dasu` (192.168.1.27) y `pc-dasu0` (192.168.1.13)
- Probar ping directo sin Tailscale (Éxito: < 2ms)
- Verificar acceso a puertos críticos localmente (RDP cerrado, firewall por defecto)
- [ ] **1.1.4:** Validar resolución DNS hacia `dc-dasuten.dasuten.utnlr` (`10.0.100.10`)
- [ ] **1.1.5:** Documentar problemas de conectividad intermitente y patrones identificados
- [x] **1.1.6:** **NUEVO:** Mapear topología de red local completa
- Identificar subred local del router ISP: `192.168.1.0/24`
- Documentar configuración DHCP del router: Asignando la `...13` y `...27`
- Verificar reglas de firewall locales
#### Semana 1: Preparación de Acceso
- [ ] **1.1.7:** Configurar acceso remoto seguro (RustDesk ID: `398 699 016`) -> Requiere acceso presencial o que RustDesk conecte para revisar Tailscale
- [x] **1.1.8:** Verificar credenciales de dominio en bóveda (`admindasu` guardada y operativa en candados)
- [ ] **1.1.9:** Establecer procedimiento de rollback (desunión de dominio)
- [ ] **1.1.10:** Preparar configuración de red dual (local + Tailscale)
- [x] **1.1.10a:** **CRÍTICO:** Migrar `srv-dasu` de red Tailscale `rmonla@gmail.com` a `pcdasu0@frlr.utn.edu.ar`
- [x] Ejecutar logout + login remoto (resuelto vía contingencia de puente manual desde la red local)
- [x] Autorizar URL de login manualmente
- [x] Verificar que `srv-dasu` resida en la nueva VPN (`100.112.46.104`)
- [x] **1.1.10b:** **CRÍTICO:** Instalar OpenSSH Server en `pc-dasu0` (puerto 7022)
- [x] Desplegar w-zombi en `srv-dasu` (192.168.1.27) como puente local
- [x] Activar payload `ssh_port7022.ps1` y `install_ssh_legacy.ps1` (instala SSH + configura puerto 7022 + firewall)
- [x] Ejecutar el agente zombi desde `pc-dasu0` via RustDesk
- [x] Verificar conectividad SSH a `pc-dasu0:7022` desde `srv-ns8` (Validado el estado Running via puente srv-dasu)
### 🛠 **FASE 2: Preparación de pc-dasu0**
**Hito 2.1:** Configuración base replicada desde `pcv-dasu0`
#### Semana 2: Configuración del Sistema y Red
- [ ] **2.1.1:** Verificar requisitos de Windows 10 (versión, updates, .NET Framework)
- [ ] **2.1.2:** **ACTUALIZADO:** Configurar adaptador de red con estrategia dual:
- **Configuración Local:** IP local según subred del router ISP
- **DNS Primario:** `10.0.100.10` (dc-dasuten) via comunicación directa
- **DNS Secundario:** `8.8.8.8` (fallback)
- **Gateway:** Router ISP local
- [ ] **2.1.3:** Aplicar configuración de red validada en `pcv-dasu0`
- [ ] **2.1.4:** Crear backup completo del sistema y perfiles de usuario actuales
- [ ] **2.1.5:** Preparar script de migración de perfiles locales a dominio
#### Semana 2: Validación Preliminar de Conectividad
- [ ] **2.1.6:** **ACTUALIZADO:** Probar conectividad a puertos críticos usando ambas rutas:
- **Ruta Local:** Comunicación directa sin Tailscale
- **Ruta Tailscale:** Via subnet router como backup
- **Puertos:** TCP 389, 636, 3268, 3269 (LDAP/GC)
- [ ] **2.1.7:** Verificar acceso a recursos compartidos del dominio
- [ ] **2.1.8:** Validar resolución de nombres NetBIOS (`DASUTEN`)
- [ ] **2.1.9:** **NUEVO:** Configurar reglas de firewall para comunicación local
- Permitir puertos de dominio en firewall local
- Configurar excepciones para subred local
### 🔗 **FASE 3: Unión al Dominio**
**Hito 3.1:** Unión exitosa de `pc-dasu0` a `dasuten.utnlr`
#### Semana 3: Procedimiento de Unión
- [ ] **3.1.1:** Ejecutar unión al dominio usando credenciales `DASUTEN\admindasu`
- [ ] **3.1.2:** Configurar nombre de equipo en dominio (`pc-dasu0.dasuten.utnlr`)
- [ ] **3.1.3:** Reiniciar y verificar autenticación en dominio
- [ ] **3.1.4:** Validar membresía en grupos de dominio apropiados
- [ ] **3.1.5:** Aplicar políticas de grupo (GPO) básicas del dominio
#### Semana 3: Migración de Perfiles y Configuración de Red Final
- [ ] **3.1.6:** Migrar perfiles de usuario local (Andrea/Romina) a cuentas de dominio
- [ ] **3.1.7:** Configurar mapeo de unidades de red y recursos compartidos
- [ ] **3.1.8:** Verificar acceso al sistema DASUTEN desde el dominio
- [ ] **3.1.9:** **NUEVO:** Optimizar configuración de red para comunicación directa
- Priorizar ruta local sobre Tailscale
- Configurar métricas de ruta apropiadas
- Establecer monitoreo de conectividad dual
### ✅ **FASE 4: Validación y Monitoreo**
**Hito 4.1:** Sistema operativo en producción estable
#### Semana 4: Pruebas Integrales
- [ ] **4.1.1:** Ejecutar suite de pruebas del sistema DASUTEN
- [ ] **4.1.2:** **ACTUALIZADO:** Validar conectividad persistente durante 24-48 horas usando:
- Comunicación directa local (métrica principal)
- Tailscale como respaldo (métrica secundaria)
- [ ] **4.1.3:** Monitorear logs de autenticación y eventos de dominio
- [ ] **4.1.4:** Verificar funcionamiento de aplicaciones críticas
#### Semana 4: Documentación y Cierre
- [ ] **4.1.5:** Actualizar documentación de `nodos/pc-dasu0.md` con nueva topología
- [ ] **4.1.6:** Registrar hitos en dashboard P2601
- [ ] **4.1.7:** Documentar lecciones aprendidas y procedimientos
- [ ] **4.1.8:** **NUEVO:** Establecer monitoreo continuo de conectividad dual
- Alertas por fallo de comunicación directa
- Métricas de performance de ambas rutas
- Procedimientos de failover automático
## 🚨 Procedimientos de Rollback
### Escenario 1: Fallo en Unión al Dominio
1. Desunir equipo del dominio usando credenciales locales administrativas
2. Restaurar configuración de red original
3. Recuperar perfiles de usuario desde backup
4. Revertir cambios en DNS y configuración de red
### Escenario 2: Problemas de Conectividad Persistente
1. **Prioridad 1:** Diagnosticar comunicación directa local
- Verificar configuración IP local
- Revisar reglas de firewall del router ISP
- Probar conectividad básica entre dispositivos
2. **Prioridad 2:** Habilitar acceso alternativo via Tailscale + RustDesk
3. **Prioridad 3:** Configurar acceso directo temporal si es necesario
4. **Prioridad 4:** Escalar diagnóstico a nivel de infraestructura de red
### Escenario 3: Fallo en Comunicación Directa Local
1. Activar automáticamente ruta Tailscale como primaria
2. Diagnosticar problemas de red local (router ISP, firewall, DHCP)
3. Reconfigurar IPs locales si es necesario
4. Verificar que ambos dispositivos estén en misma subred física
### Escenario 4: Fallo en Sistema DASUTEN
1. Revertir a VM de pruebas `pcv-dasu0` para operaciones críticas
2. Mantener acceso local a datos si es necesario
3. Ejecutar procedimientos de recuperación documentados
## 🔧 Notas Técnicas
### Configuración de Red Requerida (ACTUALIZADA)
#### Configuración Local (PRIMARIA)
```
Adaptador de Red pc-dasu0 (Local):
- IP: [DHCP o estática según subred del router ISP]
- Máscara: [Según configuración router]
- Gateway: [Router ISP local]
- DNS Primario: 10.0.100.10 (dc-dasuten) via ruta local
- DNS Secundario: 8.8.8.8 (fallback)
- Suffix DNS: dasuten.utnlr
```
#### Configuración Tailscale (BACKUP)
```
Tailscale en pc-dasu0:
- IP Tailscale: 100.65.62.44
- Ruta a 10.0.100.0/24: via srv-dasu (100.116.210.36)
- Subnet Router: srv-dasu
```
### Procedimientos para Comunicación Directa Local
#### Paso 1: Identificación de IPs Locales
```powershell
# En pc-dasu0
ipconfig /all
# Identificar IP local asignada por router ISP
# En srv-dasu (via SSH)
ip addr show
# Identificar IP local en interfaz física
```
#### Paso 2: Prueba de Conectividad Básica
```powershell
# Desde pc-dasu0 hacia srv-dasu (IP local)
ping [IP-local-srv-dasu]
Test-NetConnection -ComputerName [IP-local-srv-dasu] -Port 22
# Desde srv-dasu hacia pc-dasu0 (IP local)
ssh user@srv-dasu "ping [IP-local-pc-dasu0]"
```
#### Paso 3: Configuración de Firewall Local
```powershell
# Permitir puertos de dominio en firewall de Windows
New-NetFirewallRule -DisplayName "Dominio DASUTEN" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 389,636,3268,3269,445 `
-Action Allow `
-RemoteAddress [Subred-local]/24
```
### Puertos Críticos para Validación
- **TCP 389/636**: LDAP/LDAPS para autenticación de dominio
- **TCP 3268/3269**: Global Catalog
- **TCP 445**: SMB para recursos compartidos
- **TCP 135, 137-139, 445**: NetBIOS/SMB
- **UDP 123**: NTP para sincronización de tiempo
- **TCP 22**: SSH para administración de srv-dasu
### Dependencias Clave (ACTUALIZADAS)
1. **Router ISP Funcional**: Debe permitir comunicación entre dispositivos locales
2. **Comunicación Local**: Configuración correcta de IPs en misma subred
3. **Firewall Local**: Reglas que permitan puertos de dominio entre dispositivos
4. **Tailscale Subnet Router**: `srv-dasu` debe exponer `10.0.100.0/24` (backup)
5. **DNS Funcional**: `dc-dasuten` debe resolver nombres internos y externos
6. **Sincronización de Tiempo**: NTP configurado entre todos los nodos
7. **Credenciales**: Acceso a bóveda `ns8-candados` para `admindasu`
## 📊 Métricas de Éxito (ACTUALIZADAS)
1. **Conectividad Local**: Ping exitoso >99% del tiempo a `srv-dasu` via IP local
2. **Conectividad Dominio**: Ping exitoso >95% del tiempo a `dc-dasuten` (`10.0.100.10`)
3. **Autenticación**: Login en dominio exitoso en <15 segundos via ruta local
4. **Latencia**: <5ms en comunicación local entre `pc-dasu0` y `srv-dasu`
5. **Sistema DASUTEN**: Acceso completo y operativo al sistema productivo
6. **Estabilidad**: 0 incidentes críticos en primeras 72 horas post-implementación
7. **Usuario**: Perfiles migrados exitosamente sin pérdida de datos
8. **Resiliencia**: Failover automático a Tailscale en <30 segundos si falla ruta local
## 📚 Referencias
1. [Proyecto P2601 DASUTEN](../../P2601_dasuten.md)
2. [Nodo: pc-dasu0](../nodos/pc-dasu0.md)
3. [Nodo: pcv-dasu0](../nodos/pcv-dasu0.md) *(VM de referencia)*
4. [Nodo: dc-dasuten](../nodos/dc-dasuten.md)
5. [Nodo: srv-dasu](../nodos/srv-dasu.md)
6. [Plan 6.1.A: Red srv-dasu](P2601_6.1.A - Red_SrvDasu.md)
7. [ADN: Seguridad y Red](../adn/03_seguridad.md)
8. **NUEVO:** [Procedimiento de Comunicación Directa Local](../proc/P2601_6.1.B_comunicacion_directa_local.md)
---
**⚠️ ADVERTENCIA CRÍTICA:** Este plan sigue la filosofía DB-First del ADN.
**🔍 CAMBIO FUNDAMENTAL:** La topología de red compartida (mismo router ISP) cambia radicalmente la estrategia de conectividad, priorizando comunicación directa local sobre soluciones de tunelización como Tailscale. Esto representa una optimización significativa en performance y estabilidad.
@@ -0,0 +1,685 @@
# [PROC-2601-6.1.B] - Procedimiento de Comunicación Directa Local entre srv-dasu y pc-dasu0
## 1. Identificación y Objetivo
- **ID**: `PROC-2601-6.1.B`
- **Responsable**: Administrador de Infraestructura / Usuario Autorizado
- **Objetivo**: Establecer comunicación de red directa local entre `srv-dasu` (hypervisor Proxmox) y `pc-dasu0` (PC física) cuando ambos dispositivos comparten el mismo router ISP en la oficina DASUTeN, optimizando el rendimiento y reduciendo la dependencia de Tailscale para operaciones locales.
- **Frecuencia**: Manual / Bajo Demanda (cuando ambos dispositivos están en la misma red física)
## 2. Alcance y Prerrequisitos
- **Nodos Afectados**:
- `srv-dasu` (Hypervisor Proxmox VE - IP: 10.0.10.205 / Tailscale: 100.116.210.36)
- `pc-dasu0` (PC física Windows 10 - Tailscale: 100.65.62.44)
- `dc-dasuten` (VM 100 - Controlador de Dominio - IP: 10.0.100.10)
- **Sistemas**: Linux (Proxmox/Debian), Windows 10, Windows Server Core
- **Prerrequisitos**:
- [ ] Acceso SSH a `srv-dasu` con privilegios sudo
- [ ] Acceso administrativo a `pc-dasu0` (local o remoto)
- [ ] Ambos dispositivos conectados al mismo router ISP en oficina DASUTeN
- [ ] Tailscale operativo en ambos dispositivos (para failover)
- [ ] Backup previo de configuraciones de red
## 3. Flujo de Tareas (Ejecución)
Este flujo debe seguirse secuencialmente. Utilizar iconos de estado según el `adn/02_protocolo.md`.
| Paso | Tarea | Estado | Notas |
| :--- | :--- | :---: | :--- |
| 1 | **Diagnóstico de Topología de Red Local** | 📍 | Identificar configuración actual del router ISP |
| 2 | **Configuración de IPs Locales en srv-dasu** | ⏳ | Asignar IP local en subred del router |
| 3 | **Configuración de IP Local en pc-dasu0** | ⏳ | Asignar IP en misma subred |
| 4 | **Pruebas de Conectividad Básica** | ⏳ | Verificar ping bidireccional |
| 5 | **Configuración de DNS Local** | ⏳ | Apuntar a dc-dasuten via ruta local |
| 6 | **Validación de AD-Join** | ⏳ | Probar resolución de dominio |
| 7 | **Configuración de Failover Tailscale** | ⏳ | Mantener ruta Tailscale como backup |
| 8 | **Scripts de Validación Automática** | ⏳ | Crear e instalar herramientas de monitoreo |
### 3.1 Paso 1: Diagnóstico de Topología de Red Local
**Objetivo**: Determinar la subred local asignada por el router ISP.
**Procedimiento en pc-dasu0 (Windows):**
```powershell
# Obtener configuración IP actual
ipconfig /all
# Identificar:
# - Dirección IP asignada por DHCP
# - Máscara de subred
# - Gateway predeterminado
# - Servidores DNS
# Determinar rango de subred completa
$ipInfo = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceAlias -like "*Ethernet*"}
$subnetMask = $ipInfo.PrefixLength
$ipAddress = $ipInfo.IPAddress
$gateway = (Get-NetRoute -DestinationPrefix "0.0.0.0/0" -AddressFamily IPv4).NextHop
# Calcular subred
$subnet = [System.Net.IPAddress]::Parse($ipAddress).GetAddressBytes()
$mask = [System.Net.IPAddress]::Parse(([System.Net.IPAddress]::Parse("255.255.255.255").GetAddressBytes() | ForEach-Object {$_ -shl (32-$subnetMask)}))
$network = [System.Net.IPAddress]::new(($subnet | ForEach-Object {$_ -band $mask.GetAddressBytes()[$i]; $i++}))
Write-Host "Subred detectada: $network/$subnetMask"
Write-Host "Gateway: $gateway"
Write-Host "IP Local: $ipAddress"
# Escanear dispositivos en la red local
arp -a | Select-String "dynamic" | ForEach-Object {
$_.ToString().Split()[0]
} | Select-Object -First 10
```
**Procedimiento en srv-dasu (Linux via SSH):**
```bash
# Conectarse a srv-dasu via Tailscale
ssh -J root@100.116.210.36 rmonla@10.0.100.1
# Ver configuración de red actual
echo "=== Configuración de Interfaces ==="
ip addr show vmbr0
echo ""
echo "=== Tabla de Rutas ==="
ip route show
echo ""
echo "=== Información DHCP (si aplica) ==="
cat /var/lib/dhcp/dhclient.leases 2>/dev/null | grep -A5 -B5 "interface.*vmbr0" || echo "No hay leases DHCP disponibles"
echo ""
# Detectar gateway local automáticamente
GATEWAY=$(ip route show default | awk '/default/ {print $3}')
if [ -n "$GATEWAY" ]; then
echo "Gateway detectado: $GATEWAY"
echo "=== Probando conectividad a gateway ==="
ping -c 4 $GATEWAY
# Determinar subred del gateway
SUBNET=$(ip route show | grep "link src" | awk '{print $1}' | head -1)
if [ -n "$SUBNET" ]; then
echo "Subred detectada: $SUBNET"
# Escanear dispositivos en la subred local
echo "=== Dispositivos en subred local (primeros 5) ==="
nmap -sn $SUBNET 2>/dev/null | grep "Nmap scan report" | head -5 || \
echo "Instale nmap para escaneo completo: sudo apt install nmap"
fi
else
echo "⚠️ No se detectó gateway predeterminado"
echo "Verifique conectividad física y configuración DHCP"
fi
# Verificar si hay IP local ya configurada
echo "=== IPs locales configuradas ==="
ip addr show vmbr0 | grep -E "inet (192\.168|10\.0\.0|172\.(1[6-9]|2[0-9]|3[0-1]))"
```
**Salida Esperada**:
- Subred local identificada (ej: 192.168.1.0/24, 10.0.0.0/24)
- Gateway del router ISP
- Rango de IPs disponibles en la subred
- Dispositivos detectados en la red local
- Confirmación de que srv-dasu y pc-dasu0 están en la misma subred física
**Registro de Diagnóstico**:
```bash
# Crear archivo de diagnóstico
sudo tee /tmp/red-local-diagnostico-$(date +%Y%m%d).txt << EOF
=== DIAGNÓSTICO DE RED LOCAL ===
Fecha: $(date)
Hostname: $(hostname)
CONFIGURACIÓN ACTUAL:
$(ip addr show vmbr0)
TABLA DE RUTAS:
$(ip route show)
GATEWAY DETECTADO: $GATEWAY
SUBRED DETECTADA: $SUBNET
DISPOSITIVOS EN RED (si se escaneó):
$(nmap -sn $SUBNET 2>/dev/null | grep "Nmap scan report" | head -10)
RECOMENDACIÓN DE IP LOCAL:
# Usar IP fuera del rango DHCP del router
# Ejemplo: Si router asigna 192.168.1.100-200
# Usar: 192.168.1.205 para srv-dasu
# Usar: 192.168.1.100 para pc-dasu0 (si está disponible)
EOF
```
### 3.2 Paso 2: Configuración de IP Local en srv-dasu
**Objetivo**: Asignar IP estática en subred local manteniendo configuración existente.
**Procedimiento en srv-dasu:**
```bash
# 1. Backup de configuración actual
sudo cp /etc/network/interfaces /etc/network/interfaces.backup.$(date +%Y%m%d)
# 2. Editar configuración de red
sudo nano /etc/network/interfaces
```
**Configuración Recomendada** (adaptar según subred local detectada):
```bash
# Configuración actual de vmbr0 (mantener)
auto vmbr0
iface vmbr0 inet dhcp
bridge-ports enp3s0
bridge-stp off
bridge-fd 0
# Red interna para VMs (mantener)
post-up ip addr add 10.0.100.1/24 dev vmbr0
pre-down ip addr del 10.0.100.1/24 dev vmbr0
# IP estática local (agregar - adaptar según subred)
post-up ip addr add 192.168.1.205/24 dev vmbr0
pre-down ip addr del 192.168.1.205/24 dev vmbr0
```
**Aplicar cambios:**
```bash
# 3. Reiniciar servicio de red
sudo systemctl restart networking
# 4. Verificar configuración
ip addr show vmbr0
# Debe mostrar:
# - IP DHCP (de la red facultad si está conectado)
# - 10.0.100.1/24 (red interna VMs)
# - 192.168.1.205/24 (IP local)
# 5. Probar conectividad local
ping -c 4 192.168.1.1 # Gateway del router
```
### 3.3 Paso 3: Configuración de IP Local en pc-dasu0
**Objetivo**: Configurar IP estática en pc-dasu0 en misma subred.
**Procedimiento en pc-dasu0 (Windows):**
```powershell
# 1. Abrir configuración de red
ncpa.cpl
# 2. Propiedades del adaptador Ethernet -> IPv4
# Configurar:
# - IP: 192.168.1.100 (ejemplo, usar IP disponible)
# - Máscara: 255.255.255.0
# - Gateway: 192.168.1.1 (router ISP)
# - DNS Primario: 10.0.100.10 (dc-dasuten via ruta local)
# - DNS Secundario: 8.8.8.8
# 3. Aplicar cambios
```
**Alternativa via PowerShell (remoto):**
```powershell
# Si se tiene acceso remoto via Tailscale
$adapter = Get-NetAdapter -Name "Ethernet"
New-NetIPAddress -InterfaceIndex $adapter.ifIndex `
-IPAddress 192.168.1.100 `
-PrefixLength 24 `
-DefaultGateway 192.168.1.1
Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex `
-ServerAddresses ("10.0.100.10", "8.8.8.8")
```
### 3.4 Paso 4: Pruebas de Conectividad Básica
**Objetivo**: Validar comunicación bidireccional.
**Desde pc-dasu0:**
```powershell
# Probar conectividad a srv-dasu
ping 192.168.1.205
ping 10.0.100.1 # Gateway interno VMs
# Probar conectividad a dc-dasuten (via srv-dasu)
ping 10.0.100.10
# Verificar ruta
tracert 10.0.100.10
# Debe mostrar: pc-dasu0 -> router -> srv-dasu -> dc-dasuten
```
**Desde srv-dasu:**
```bash
# Probar conectividad a pc-dasu0
ping -c 4 192.168.1.100
# Verificar que VMs pueden comunicarse
ping -c 4 10.0.100.10 # dc-dasuten
```
### 3.5 Paso 5: Configuración de DNS Local
**Objetivo**: Configurar resolución DNS directa al controlador de dominio.
**En pc-dasu0:**
```powershell
# Verificar resolución DNS
nslookup dc-dasuten.dasuten.utnlr
# Debe resolver a 10.0.100.10
# Probar resolución inversa
nslookup 10.0.100.10
# Debe resolver a dc-dasuten.dasuten.utnlr
# Verificar registro en DNS del dominio
nslookup -type=SRV _ldap._tcp.dasuten.utnlr
```
**Configuración de Suffix DNS (si es necesario):**
```powershell
# Agregar suffix DNS al adaptador
Set-DnsClientGlobalSetting -SuffixSearchList @("dasuten.utnlr")
```
### 3.6 Paso 6: Validación de AD-Join
**Objetivo**: Probar funcionalidad de unión al dominio.
**Pruebas preliminares:**
```powershell
# 1. Verificar que se puede alcanzar el dominio
Test-Connection dc-dasuten.dasuten.utnlr -Count 4
# 2. Probar autenticación (sin unir aún)
Test-ComputerSecureChannel -Server dc-dasuten.dasuten.utnlr
# 3. Verificar puertos LDAP/Kerberos
Test-NetConnection dc-dasuten.dasuten.utnlr -Port 389 # LDAP
Test-NetConnection dc-dasuten.dasuten.utnlr -Port 88 # Kerberos
Test-NetConnection dc-dasuten.dasuten.utnlr -Port 445 # SMB
```
**Procedimiento de unión al dominio (si aplica):**
```powershell
# Unir al dominio (solo si es necesario)
Add-Computer -DomainName "dasuten.utnlr" `
-Credential (Get-Credential) `
-Restart
```
### 3.7 Paso 7: Configuración de Failover Tailscale
**Objetivo**: Mantener Tailscale como ruta de respaldo.
**Verificar estado Tailscale:**
```bash
# En srv-dasu
tailscale status
# Debe mostrar: 100.116.210.36
# En pc-dasu0 (via PowerShell)
tailscale status
# Debe mostrar: 100.65.62.44
```
**Configurar métricas de ruta (en srv-dasu):**
```bash
# Verificar rutas actuales
ip route show
# Tailscale debe tener métrica más alta (menos preferida)
# La ruta local (192.168.1.0/24) debe tener métrica más baja
```
**Script de monitoreo de conectividad:**
```bash
#!/bin/bash
# /usr/local/bin/check-local-connectivity.sh
LOCAL_IP="192.168.1.100"
TAILSCALE_IP="100.65.62.44"
if ping -c 2 -W 1 $LOCAL_IP > /dev/null 2>&1; then
echo "✅ Conectividad LOCAL activa"
exit 0
else
echo "⚠️ Fallo conectividad local, usando Tailscale"
# Aquí se podrían activar reglas de failover
exit 1
fi
```
### 3.8 Paso 8: Scripts de Validación Automática
**Crear script de validación completo:**
**Script para srv-dasu (`/usr/local/bin/validate-local-network.sh`):**
*Nota: También disponible en `docs/procedimientos/scripts/diagnostico_red_local.sh`*
```bash
#!/bin/bash
# Script de validación de red local
LOG_FILE="/var/log/local-network-validation.log"
echo "=== Validación de Red Local $(date) ===" | tee -a $LOG_FILE
# 1. Verificar IPs locales
echo "1. Verificando IPs configuradas..." | tee -a $LOG_FILE
ip addr show vmbr0 | grep -E "192\.168\.1|10\.0\.100\.1" | tee -a $LOG_FILE
# 2. Probar conectividad a pc-dasu0
echo "2. Probando conectividad a pc-dasu0..." | tee -a $LOG_FILE
if ping -c 3 -W 2 192.168.1.100 > /dev/null 2>&1; then
echo "✅ Ping exitoso a pc-dasu0" | tee -a $LOG_FILE
else
echo "❌ Fallo ping a pc-dasu0" | tee -a $LOG_FILE
fi
# 3. Verificar rutas
echo "3. Verificando tabla de rutas..." | tee -a $LOG_FILE
ip route show | grep -E "192\.168\.1|10\.0\.100" | tee -a $LOG_FILE
# 4. Verificar estado Tailscale
echo "4. Verificando estado Tailscale..." | tee -a $LOG_FILE
tailscale status --json | jq -r '.Self.TailscaleIPs[0]' | tee -a $LOG_FILE
echo "=== Validación completada ===" | tee -a $LOG_FILE
```
**Script para pc-dasu0 (`C:\Scripts\Validate-LocalNetwork.ps1`):**
*Nota: También disponible en `docs/procedimientos/scripts/Diagnose-LocalNetwork.ps1`*
```powershell
# Script de validación para Windows
$LogFile = "C:\Logs\LocalNetwork-Validation.log"
$LocalIP = "192.168.1.205" # srv-dasu
$DomainController = "10.0.100.10"
"=== Validación de Red Local $(Get-Date) ===" | Out-File -FilePath $LogFile -Append
# 1. Verificar IP local
"1. Configuración IP local:" | Out-File -FilePath $LogFile -Append
Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.IPAddress -like "192.168.1.*"} |
Select-Object IPAddress, PrefixLength | Out-File -FilePath $LogFile -Append
# 2. Probar conectividad
"2. Probando conectividad:" | Out-File -FilePath $LogFile -Append
if (Test-Connection -ComputerName $LocalIP -Count 2 -Quiet) {
"✅ Conectividad local OK" | Out-File -FilePath $LogFile -Append
} else {
"❌ Fallo conectividad local" | Out-File -FilePath $LogFile -Append
}
# 3. Verificar DNS
"3. Verificando resolución DNS:" | Out-File -FilePath $LogFile -Append
try {
$DnsResult = Resolve-DnsName -Name "dc-dasuten.dasuten.utnlr" -ErrorAction Stop
"✅ DNS resuelve: $($DnsResult.IPAddress)" | Out-File -FilePath $LogFile -Append
} catch {
"❌ Fallo resolución DNS" | Out-File -FilePath $LogFile -Append
}
# 4. Verificar puertos críticos
"4. Verificando puertos del dominio:" | Out-File -FilePath $LogFile -Append
$Ports = @(389, 88, 445, 53)
foreach ($Port in $Ports) {
$Test = Test-NetConnection -ComputerName $DomainController -Port $Port -WarningAction SilentlyContinue
if ($Test.TcpTestSucceeded) {
"✅ Puerto $Port abierto" | Out-File -FilePath $LogFile -Append
} else {
"❌ Puerto $Port cerrado" | Out-File -FilePath $LogFile -Append
}
}
```
## 4. Verificación y Validación
- [ ] Ping bidireccional exitoso entre `pc-dasu0` (192.168.1.100) y `srv-dasu` (192.168.1.205)
- [ ] Resolución DNS de `dc-dasuten.dasuten.utnlr` a `10.0.100.10`
- [ ] Conexión LDAP/Kerberos al controlador de dominio
- [ ] Tailscale operativo como respaldo (IPs: 100.116.210.36 y 100.65.62.44)
- [ ] Scripts de validación ejecutados sin errores críticos
- **Resultado Esperado**: ✅ Comunicación local establecida con latencia <5ms, failover Tailscale configurado, capacidad de AD-Join verificada
## 5. Plan de Reversión (Hombre Muerto)
> [!IMPORTANT]
> En caso de fallo crítico o pérdida de conectividad, seguir estos pasos inmediatamente.
1. **Gatillo de Reversión**:
- Error en paso 4 (fallo ping bidireccional tras 3 intentos)
- Pérdida completa de conectividad a internet
- Incapacidad para acceder a recursos críticos por más de 5 minutos
2. **Acciones de Reversión**:
- **En pc-dasu0**: Restaurar configuración DHCP automática
```powershell
Set-NetIPInterface -Dhcp Enabled
Set-DnsClientServerAddress -ResetServerAddresses
```
- **En srv-dasu**: Remover IP local manteniendo configuración base
```bash
sudo ip addr del 192.168.1.205/24 dev vmbr0
sudo systemctl restart networking
```
- **Forzar uso de Tailscale**: Verificar que ambas máquinas están conectadas a Tailscale
```bash
# En srv-dasu
tailscale up --reset
# En pc-dasu0 (via PowerShell si hay acceso)
tailscale up --reset
```
3. **Verificación post-reversión**:
- Confirmar conectividad via Tailscale entre nodos
- Verificar que servicios críticos (dc-dasuten) son accesibles
- Documentar incidente y causas raíz identificadas
---
## 6. Anexos Técnicos
### Anexo 0: Scripts de Diagnóstico Automático
**Scripts disponibles en `docs/procedimientos/scripts/`:**
1. **`diagnostico_red_local.sh`** (para srv-dasu - Linux):
```bash
# Copiar y hacer ejecutable
sudo cp docs/procedimientos/scripts/diagnostico_red_local.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/diagnostico_red_local.sh
# Ejecutar diagnóstico
sudo diagnostico_red_local.sh vmbr0
```
2. **`Diagnose-LocalNetwork.ps1`** (para pc-dasu0 - Windows PowerShell):
```powershell
# Ejecutar como administrador
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
.\docs\procedimientos\scripts\Diagnose-LocalNetwork.ps1 -Detailed $true
```
**Características de los scripts:**
- Diagnóstico automático de topología de red
- Detección de subred local y gateway
- Verificación de conectividad a recursos críticos
- Validación de configuración DNS
- Comprobación de estado Tailscale
- Generación de reportes detallados
- Recomendaciones específicas para configuración local
### Anexo A: Configuración de Red de Referencia
**Topología Final Objetivo**:
```
Router ISP (Oficina DASUTeN)
├── Subred Local: 192.168.1.0/24
│ ├── srv-dasu: 192.168.1.205/24 (IP local) + 10.0.100.1/24 (gateway VMs)
│ └── pc-dasu0: 192.168.1.100/24
└── Conexión WAN a Internet
└── Tailscale VPN (Failover)
├── srv-dasu: 100.116.210.36
└── pc-dasu0: 100.65.62.44
```
**Tabla de IPs Críticas**:
| Nodo | IP Local | IP Tailscale | IP Interna VMs | Propósito |
| :--- | :--- | :--- | :--- | :--- |
| srv-dasu | 192.168.1.205 | 100.116.210.36 | 10.0.100.1 | Hypervisor Proxmox |
| pc-dasu0 | 192.168.1.100 | 100.65.62.44 | - | PC física cliente |
| dc-dasuten | - | - | 10.0.100.10 | Controlador de Dominio |
| sql-dasuten | - | - | 10.0.100.11 | SQL Server |
### Anexo B: Comandos de Diagnóstico Rápido
**Diagnóstico de Conectividad Local**:
```bash
# Desde srv-dasu
ping -c 4 192.168.1.100
traceroute 192.168.1.100
ss -tulpn | grep :22 # Verificar SSH
# Desde pc-dasu0 (PowerShell)
Test-NetConnection 192.168.1.205 -Port 22
Test-NetConnection 10.0.100.10 -Port 389
Resolve-DnsName dc-dasuten.dasuten.utnlr
```
**Diagnóstico de Rutas**:
```bash
# En srv-dasu
ip route show
ip rule show
tailscale status --json | jq '.Self.TailscaleIPs'
# En pc-dasu0
route print
Get-NetRoute -AddressFamily IPv4
```
### Anexo C: Script de Monitoreo Automático
**Script para crontab (srv-dasu)**:
```bash
#!/bin/bash
# /usr/local/bin/monitor-local-network.sh
LOCAL_PC="192.168.1.100"
TAILSCALE_PC="100.65.62.44"
LOG="/var/log/network-monitor.log"
ALERT_THRESHOLD=3
check_connectivity() {
local target=$1
local type=$2
if ping -c 2 -W 1 "$target" > /dev/null 2>&1; then
echo "$(date): ✅ $type conectividad OK a $target" >> "$LOG"
return 0
else
echo "$(date): ⚠️ $type conectividad FALLIDA a $target" >> "$LOG"
return 1
fi
}
# Monitoreo principal
if check_connectivity "$LOCAL_PC" "LOCAL"; then
# Conectividad local OK
exit 0
else
# Fallo local, verificar Tailscale
if check_connectivity "$TAILSCALE_PC" "TAILSCALE"; then
echo "$(date): 🔄 Cambiando a ruta Tailscale" >> "$LOG"
# Aquí se podrían activar reglas de failover automático
else
echo "$(date): 🚨 FALLO CRÍTICO - Sin conectividad local ni Tailscale" >> "$LOG"
# Notificación de alerta
fi
fi
```
**Configurar en crontab**:
```bash
# Ejecutar cada 5 minutos
*/5 * * * * /usr/local/bin/monitor-local-network.sh
```
### Anexo D: Solución de Problemas Comunes
**Problema 1: No hay respuesta de ping entre dispositivos**
- **Causas posibles**: Firewall bloqueando ICMP, subred incorrecta, VLANs
- **Solución**:
```bash
# En srv-dasu
sudo iptables -I INPUT -s 192.168.1.0/24 -j ACCEPT
sudo iptables -I OUTPUT -d 192.168.1.0/24 -j ACCEPT
# En pc-dasu0 (PowerShell como Admin)
New-NetFirewallRule -DisplayName "Allow-Local-ICMP" `
-Direction Inbound -Protocol ICMPv4 -IcmpType 8 `
-RemoteAddress 192.168.1.0/24 -Action Allow
```
**Problema 2: DNS no resuelve dc-dasuten**
- **Causas posibles**: Ruta no configurada, firewall bloqueando puerto 53
- **Solución**:
```powershell
# Verificar que la ruta existe
Test-NetConnection 10.0.100.10 -Port 53
# Forzar registro DNS
ipconfig /flushdns
ipconfig /registerdns
# Probar con nslookup especificando servidor
nslookup dc-dasuten.dasuten.utnlr 10.0.100.10
```
**Problema 3: Conflictos de IP**
- **Causas posibles**: IP duplicada en la red
- **Solución**:
```bash
# En srv-dasu
sudo arping -c 3 -I vmbr0 192.168.1.205
# Si hay respuesta, cambiar IP
sudo ip addr del 192.168.1.205/24 dev vmbr0
sudo ip addr add 192.168.1.206/24 dev vmbr0
```
### Anexo E: Checklist de Implementación
**Pre-Implementación**:
- [ ] Backup de configuraciones de red actuales
- [ ] Documentar IPs actuales de todos los dispositivos
- [ ] Verificar que Tailscale está operativo en ambos nodos
- [ ] Comunicar ventana de mantenimiento a usuarios afectados
**Implementación**:
- [ ] Configurar IP local en srv-dasu (Paso 2)
- [ ] Configurar IP local en pc-dasu0 (Paso 3)
- [ ] Validar conectividad básica (Paso 4)
- [ ] Configurar DNS (Paso 5)
- [ ] Validar AD-Join (Paso 6)
- [ ] Configurar failover Tailscale (Paso 7)
- [ ] Instalar scripts de validación (Paso 8)
**Post-Implementación**:
- [ ] Ejecutar scripts de validación completos
- [ ] Documentar configuración final
- [ ] Probar failover manualmente
- [ ] Monitorear durante 24 horas
---
## 7. Referencias
1. **Documentación Relacionada**:
- [Plan P2601_6.1.B - AD-Join: pc-dasu0 al Dominio](../plan/P2601_6.1.B%20-%20AD-Join:%20pc-dasu0%20al%20Dominio.md)
- [Nodo srv-dasu](../../nodos/srv-dasu.md)
- [Nodo pc-dasu0](../../nodos/pc-dasu0.md)
- [ADN 03 - Seguridad y Red](../../adn/03_seguridad.md)
2. **Scripts de Soporte**:
- `docs/procedimientos/scripts/diagnostico_red_local.sh` - Diagnóstico Linux
- `docs/procedimientos/scripts/Diagnose-LocalNetwork.ps1` - Diagnóstico Windows
- Scripts de validación incluidos en este documento
3. **Herramientas Utilizadas**:
- Tailscale: VPN mesh para failover
- OpenSSH: Administración remota de srv-dasu
- PowerShell: Automatización en Windows
- iptables/nftables: Gestión de firewall en Linux
4. **Estándares de Red**:
- RFC 1918: Direccionamiento IP privado
- Best Practices para redes híbridas
- Protocolos AD DS: LDAP (389), Kerberos (88), DNS (53)
---
*Documento generado bajo el ADN del proyecto srv-ns8.*
*Última actualización: $(date +%Y-%m-%d)*
*Versión: 1.0*
@@ -0,0 +1,399 @@
# Script de diagnóstico de red local para pc-dasu0
# Uso: .\Diagnose-LocalNetwork.ps1 [-InterfaceAlias "Ethernet"] [-Detailed $true]
param(
[string]$InterfaceAlias = "Ethernet",
[bool]$Detailed = $true,
[string]$LogPath = "C:\Logs\NetworkDiagnostic"
)
# Configuración inicial
$ErrorActionPreference = "Stop"
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$LogFile = Join-Path $LogPath "NetworkDiagnostic_$Timestamp.log"
$ReportFile = Join-Path $LogPath "NetworkReport_$Timestamp.md"
# Crear directorio de logs si no existe
if (-not (Test-Path $LogPath)) {
New-Item -ItemType Directory -Path $LogPath -Force | Out-Null
}
# Funciones de logging
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$FormattedMessage = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message"
switch ($Level) {
"ERROR" { Write-Host $FormattedMessage -ForegroundColor Red }
"WARNING" { Write-Host $FormattedMessage -ForegroundColor Yellow }
"SUCCESS" { Write-Host $FormattedMessage -ForegroundColor Green }
default { Write-Host $FormattedMessage -ForegroundColor Cyan }
}
$FormattedMessage | Out-File -FilePath $LogFile -Append
}
function Write-Report {
param([string]$Content)
$Content | Out-File -FilePath $ReportFile -Append
}
function Test-Command {
param([string]$Command)
try {
Get-Command $Command -ErrorAction Stop | Out-Null
return $true
} catch {
return $false
}
}
# Inicio del diagnóstico
Write-Log "========================================"
Write-Log " DIAGNÓSTICO DE RED LOCAL - pc-dasu0"
Write-Log "========================================"
Write-Log ""
Write-Report "# Reporte de Diagnóstico de Red Local"
Write-Report "Fecha: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Report "Hostname: $env:COMPUTERNAME"
Write-Report "Usuario: $env:USERNAME"
Write-Report ""
# 1. Verificar adaptador de red
Write-Log "1. Verificando adaptador de red '$InterfaceAlias'..."
Write-Report "## 1. Información del Adaptador de Red"
try {
$NetworkAdapter = Get-NetAdapter -Name $InterfaceAlias -ErrorAction Stop
Write-Log " Adaptador encontrado: $($NetworkAdapter.Name)"
Write-Log " Estado: $($NetworkAdapter.Status)"
Write-Log " Velocidad: $($NetworkAdapter.LinkSpeed)"
Write-Report "### Adaptador: $($NetworkAdapter.Name)"
Write-Report "- Estado: $($NetworkAdapter.Status)"
Write-Report "- Velocidad: $($NetworkAdapter.LinkSpeed)"
Write-Report "- MAC Address: $($NetworkAdapter.MacAddress)"
Write-Report "- Interface Description: $($NetworkAdapter.InterfaceDescription)"
Write-Report ""
if ($NetworkAdapter.Status -ne "Up") {
Write-Log " ADVERTENCIA: El adaptador no está activo" -Level "WARNING"
}
} catch {
Write-Log " ERROR: No se encontró el adaptador '$InterfaceAlias'" -Level "ERROR"
Write-Log " Adaptadores disponibles:"
Get-NetAdapter | ForEach-Object {
Write-Log " - $($_.Name): $($_.Status)"
}
Write-Report "ERROR: Adaptador '$InterfaceAlias' no encontrado"
exit 1
}
# 2. Obtener configuración IP
Write-Log "2. Obteniendo configuración IP..."
Write-Report "## 2. Configuración IP"
try {
$IPConfiguration = Get-NetIPConfiguration -InterfaceAlias $InterfaceAlias -ErrorAction Stop
Write-Log " IPv4 Address: $($IPConfiguration.IPv4Address.IPAddress)"
Write-Log " Subnet Mask: $($IPConfiguration.IPv4Address.PrefixLength)"
Write-Log " Gateway: $($IPConfiguration.IPv4DefaultGateway.NextHop)"
Write-Report "### Configuración IPv4"
Write-Report "- Dirección IP: $($IPConfiguration.IPv4Address.IPAddress)"
Write-Report "- Prefijo: /$($IPConfiguration.IPv4Address.PrefixLength)"
Write-Report "- Gateway: $($IPConfiguration.IPv4DefaultGateway.NextHop)"
Write-Report ""
# Calcular subred
$IPAddress = $IPConfiguration.IPv4Address.IPAddress
$PrefixLength = $IPConfiguration.IPv4Address.PrefixLength
$SubnetMask = [System.Net.IPAddress]::Parse(([System.Net.IPAddress]::Parse("255.255.255.255").GetAddressBytes() |
ForEach-Object { $_ -shl (32 - $PrefixLength) }))
$NetworkAddress = [System.Net.IPAddress]::new((
[System.Net.IPAddress]::Parse($IPAddress).GetAddressBytes() |
ForEach-Object { $_ -band $SubnetMask.GetAddressBytes()[$i]; $i++ }))
$Subnet = "$NetworkAddress/$PrefixLength"
Write-Log " Subred calculada: $Subnet"
Write-Report "- Subred: $Subnet"
Write-Report ""
# Servidores DNS
Write-Log " Servidores DNS:"
$DNSServers = $IPConfiguration.DNSServer.ServerAddresses
if ($DNSServers) {
$i = 1
foreach ($DNS in $DNSServers) {
Write-Log " $i. $DNS"
Write-Report "- DNS $($i): $DNS"
$i++
}
} else {
Write-Log " ADVERTENCIA: No hay servidores DNS configurados" -Level "WARNING"
Write-Report "- DNS: No configurado"
}
Write-Report ""
} catch {
Write-Log " ERROR: No se pudo obtener configuración IP" -Level "ERROR"
Write-Report "ERROR: No se pudo obtener configuración IP"
}
# 3. Probar conectividad
Write-Log "3. Probando conectividad de red..."
Write-Report "## 3. Pruebas de Conectividad"
# Gateway
$Gateway = $IPConfiguration.IPv4DefaultGateway.NextHop
if ($Gateway) {
Write-Log " Probando gateway $Gateway..."
$GatewayTest = Test-NetConnection -ComputerName $Gateway -InformationLevel Quiet
if ($GatewayTest) {
Write-Log " ✅ Gateway alcanzable" -Level "SUCCESS"
Write-Report "- Gateway ($Gateway): ✅ Alcanzable"
} else {
Write-Log " ❌ Gateway no alcanzable" -Level "ERROR"
Write-Report "- Gateway ($Gateway): ❌ No alcanzable"
}
} else {
Write-Log " ADVERTENCIA: No hay gateway configurado" -Level "WARNING"
Write-Report "- Gateway: No configurado"
}
# Internet
Write-Log " Probando conectividad a internet (8.8.8.8)..."
$InternetTest = Test-NetConnection -ComputerName "8.8.8.8" -InformationLevel Quiet -WarningAction SilentlyContinue
if ($InternetTest) {
Write-Log " ✅ Internet alcanzable" -Level "SUCCESS"
Write-Report "- Internet (8.8.8.8): ✅ Alcanzable"
} else {
Write-Log " ❌ Internet no alcanzable" -Level "ERROR"
Write-Report "- Internet (8.8.8.8): ❌ No alcanzable"
}
# srv-dasu (si se conoce la IP)
Write-Log " Probando conectividad a srv-dasu (192.168.1.205)..."
$SrvDasuTest = Test-NetConnection -ComputerName "192.168.1.205" -InformationLevel Quiet -WarningAction SilentlyContinue
if ($SrvDasuTest) {
Write-Log " ✅ srv-dasu alcanzable localmente" -Level "SUCCESS"
Write-Report "- srv-dasu (192.168.1.205): ✅ Alcanzable"
} else {
Write-Log " ⚠️ srv-dasu no alcanzable localmente" -Level "WARNING"
Write-Report "- srv-dasu (192.168.1.205): ⚠️ No alcanzable"
}
Write-Report ""
# 4. Verificar DNS
Write-Log "4. Verificando resolución DNS..."
Write-Report "## 4. Pruebas de DNS"
# Resolución de dominio DASUTEN
Write-Log " Probando resolución de dc-dasuten.dasuten.utnlr..."
try {
$DNSResult = Resolve-DnsName -Name "dc-dasuten.dasuten.utnlr" -ErrorAction Stop
if ($DNSResult.IPAddress) {
Write-Log " ✅ DNS resuelve a: $($DNSResult.IPAddress)" -Level "SUCCESS"
Write-Report "- dc-dasuten.dasuten.utnlr: ✅ $($DNSResult.IPAddress)"
} else {
Write-Log " ❌ DNS no devolvió IP" -Level "ERROR"
Write-Report "- dc-dasuten.dasuten.utnlr: ❌ Sin respuesta"
}
} catch {
Write-Log " ❌ Error en resolución DNS: $_" -Level "ERROR"
Write-Report "- dc-dasuten.dasuten.utnlr: ❌ Error: $_"
}
# Resolución inversa
Write-Log " Probando resolución inversa de 10.0.100.10..."
try {
$ReverseDNS = Resolve-DnsName -Name "10.0.100.10" -Type PTR -ErrorAction Stop
if ($ReverseDNS.NameHost) {
Write-Log " ✅ Resolución inversa: $($ReverseDNS.NameHost)" -Level "SUCCESS"
Write-Report "- 10.0.100.10 (PTR): ✅ $($ReverseDNS.NameHost)"
}
} catch {
Write-Log " ⚠️ No se pudo resolver PTR para 10.0.100.10" -Level "WARNING"
Write-Report "- 10.0.100.10 (PTR): ⚠️ No resuelto"
}
Write-Report ""
# 5. Verificar puertos críticos del dominio
Write-Log "5. Verificando puertos críticos del dominio..."
Write-Report "## 5. Puertos del Dominio DASUTEN"
$DomainController = "10.0.100.10"
$CriticalPorts = @(
@{Port=389; Service="LDAP"},
@{Port=88; Service="Kerberos"},
@{Port=445; Service="SMB"},
@{Port=53; Service="DNS"},
@{Port=135; Service="RPC"},
@{Port=636; Service="LDAPS"}
)
foreach ($PortInfo in $CriticalPorts) {
Write-Log " Probando puerto $($PortInfo.Port) ($($PortInfo.Service))..."
$PortTest = Test-NetConnection -ComputerName $DomainController -Port $PortInfo.Port -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
if ($PortTest.TcpTestSucceeded) {
Write-Log " ✅ Puerto $($PortInfo.Port) abierto" -Level "SUCCESS"
Write-Report "- $($PortInfo.Service) (puerto $($PortInfo.Port)): ✅ Abierto"
} else {
Write-Log " ❌ Puerto $($PortInfo.Port) cerrado" -Level "ERROR"
Write-Report "- $($PortInfo.Service) (puerto $($PortInfo.Port)): ❌ Cerrado"
}
}
Write-Report ""
# 6. Verificar Tailscale
Write-Log "6. Verificando estado de Tailscale..."
Write-Report "## 6. Estado de Tailscale"
if (Test-Command "tailscale") {
try {
$TailscaleStatus = & tailscale status --json 2>$null | ConvertFrom-Json
if ($TailscaleStatus.Self.TailscaleIPs) {
$TailscaleIP = $TailscaleStatus.Self.TailscaleIPs[0]
Write-Log " ✅ Tailscale conectado: $TailscaleIP" -Level "SUCCESS"
Write-Report "- Estado: ✅ Conectado"
Write-Report "- IP Tailscale: $TailscaleIP"
# Verificar peers
$PeerCount = ($TailscaleStatus.Peer | Measure-Object).Count
Write-Log " Peers conectados: $PeerCount"
Write-Report "- Peers conectados: $PeerCount"
} else {
Write-Log " ⚠️ Tailscale instalado pero no conectado" -Level "WARNING"
Write-Report "- Estado: ⚠️ Instalado pero no conectado"
}
} catch {
Write-Log " ⚠️ No se pudo obtener estado de Tailscale" -Level "WARNING"
Write-Report "- Estado: ⚠️ Error al obtener estado"
}
} else {
Write-Log " ⚠️ Tailscale no está instalado" -Level "WARNING"
Write-Report "- Estado: ⚠️ No instalado"
}
Write-Report ""
# 7. Escanear red local (opcional)
if ($Detailed) {
Write-Log "7. Escaneando dispositivos en red local..."
Write-Report "## 7. Dispositivos en Red Local"
try {
# Usar ARP para detectar dispositivos
$ARPTable = arp -a | Select-String "dynamic" | ForEach-Object {
$Line = $_.ToString().Trim()
$Parts = $Line -split '\s+'
[PSCustomObject]@{
IP = $Parts[0]
MAC = $Parts[1]
Type = $Parts[2]
}
}
$DeviceCount = ($ARPTable | Measure-Object).Count
Write-Log " Dispositivos detectados en ARP: $DeviceCount"
Write-Report "- Dispositivos en tabla ARP: $DeviceCount"
if ($DeviceCount -gt 0) {
Write-Report ""
Write-Report "### Primeros 10 dispositivos:"
$ARPTable | Select-Object -First 10 | ForEach-Object {
Write-Report "- $($_.IP) ($($_.MAC))"
}
}
} catch {
Write-Log " ⚠️ No se pudo escanear red local" -Level "WARNING"
Write-Report "- Escaneo: ⚠️ No disponible"
}
Write-Report ""
}
# 8. Generar recomendaciones
Write-Log "8. Generando recomendaciones..."
Write-Report "## 8. Recomendaciones para Comunicación Local"
$BaseNetwork = $IPAddress -replace '\.\d+$', ''
$RecommendedSrvIP = "$BaseNetwork.205"
$RecommendedPcIP = "$BaseNetwork.100"
Write-Report "### Configuración recomendada para comunicación local:"
Write-Report ""
Write-Report "1. **IPs estáticas locales:**"
Write-Report " - srv-dasu: $RecommendedSrvIP/24"
Write-Report " - pc-dasu0: $RecommendedPcIP/24"
Write-Report ""
Write-Report "2. **Configuración de red en pc-dasu0:**"
Write-Report " - IP: $RecommendedPcIP"
Write-Report " - Máscara: 255.255.255.0"
Write-Report " - Gateway: $Gateway"
Write-Report " - DNS Primario: 10.0.100.10 (dc-dasuten)"
Write-Report " - DNS Secundario: 8.8.8.8"
Write-Report " - Suffix DNS: dasuten.utnlr"
Write-Report ""
Write-Report "3. **Verificaciones previas:**"
Write-Report " - Confirmar que $RecommendedSrvIP y $RecommendedPcIP no están en uso"
Write-Report " - Verificar firewall (permitir ICMP, puertos 389, 88, 445, 53)"
Write-Report " - Mantener Tailscale activo como respaldo"
Write-Report ""
Write-Report "4. **Comandos de prueba post-configuración:**"
Write-Report " ```powershell"
Write-Report " # Probar conectividad local"
Write-Report " Test-NetConnection $RecommendedSrvIP"
Write-Report " "
Write-Report " # Probar resolución DNS"
Write-Report " Resolve-DnsName dc-dasuten.dasuten.utnlr"
Write-Report " "
Write-Report " # Probar puertos del dominio"
Write-Report " Test-NetConnection 10.0.100.10 -Port 389"
Write-Report " ```"
Write-Report ""
# 9. Resumen final
Write-Log "9. Generando resumen final..."
Write-Report "## 9. Resumen del Diagnóstico"
Write-Report "- **Adaptador de red**: $($NetworkAdapter.Name) ($($NetworkAdapter.Status))"
Write-Report "- **IP Configurada**: $IPAddress/$PrefixLength"
Write-Report "- **Gateway**: $(if($Gateway) {$Gateway} else {"No configurado"})"
Write-Report "- **Subred**: $Subnet"
Write-Report "- **DNS Servers**: $(if($DNSServers) {$DNSServers -join ', '} else {"No configurado"})"
Write-Report "- **Internet**: $(if($InternetTest) {"✅ Alcanzable"} else {"❌ No alcanzable"})"
Write-Report "- **srv-dasu local**: $(if($SrvDasuTest) {"✅ Alcanzable"} else {"⚠️ No alcanzable"})"
Write-Report "- **Tailscale**: $(if(Test-Command "tailscale") {"✅ Instalado"} else {"⚠️ No instalado"})"
Write-Report ""
Write-Report "### Archivos generados:"
Write-Report "- Log detallado: $LogFile"
Write-Report "- Reporte completo: $ReportFile"
Write-Report ""
Write-Report "### Próximos pasos:"
Write-Report "1. Si la conectividad local falla, verificar configuración del router ISP"
Write-Report "2. Configurar IPs estáticas según recomendaciones"
Write-Report "3. Probar AD-Join después de configurar comunicación local"
Write-Report "4. Mantener Tailscale como respaldo para acceso remoto"
Write-Report ""
Write-Report "*Diagnóstico completado: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')*"
@@ -0,0 +1,337 @@
#!/bin/bash
# Script de diagnóstico automático para red local
# Uso: ./diagnostico_red_local.sh [interface]
# Ejemplo: ./diagnostico_red_local.sh vmbr0
set -e
# Colores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Variables
INTERFACE="${1:-vmbr0}"
LOG_FILE="/tmp/diagnostico_red_local_$(date +%Y%m%d_%H%M%S).log"
REPORT_FILE="/tmp/reporte_red_local_$(date +%Y%m%d_%H%M%S).md"
# Funciones de utilidad
log() {
echo -e "${BLUE}[INFO]${NC} $1" | tee -a "$LOG_FILE"
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE"
}
warning() {
echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
}
check_dependencies() {
local deps=("ip" "ping" "awk" "grep" "tee")
local missing=()
for dep in "${deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then
missing+=("$dep")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
error "Dependencias faltantes: ${missing[*]}"
return 1
fi
# Dependencias opcionales
if command -v "nmap" &> /dev/null; then
HAS_NMAP=true
else
HAS_NMAP=false
warning "nmap no encontrado. El escaneo de red será limitado."
fi
if command -v "jq" &> /dev/null; then
HAS_JQ=true
else
HAS_JQ=false
fi
return 0
}
check_interface() {
if ! ip link show "$INTERFACE" &> /dev/null; then
error "Interfaz $INTERFACE no encontrada"
echo "Interfaces disponibles:"
ip link show | awk -F': ' '/^[0-9]+:/ {print $2}' | grep -v lo
return 1
fi
if [ "$(cat /sys/class/net/"$INTERFACE"/operstate 2>/dev/null)" != "up" ]; then
warning "Interfaz $INTERFACE no está activa (operstate: $(cat /sys/class/net/"$INTERFACE"/operstate 2>/dev/null))"
fi
return 0
}
get_interface_info() {
log "Obteniendo información de la interfaz $INTERFACE..."
echo "=== INFORMACIÓN DE INTERFAZ ===" | tee -a "$REPORT_FILE"
ip addr show "$INTERFACE" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
# Extraer IPs configuradas
IPS=$(ip addr show "$INTERFACE" | awk '/inet / {print $2}')
if [ -z "$IPS" ]; then
warning "No hay IPs configuradas en $INTERFACE"
else
success "IPs configuradas:"
echo "$IPS" | while read -r ip; do
echo " - $ip"
done | tee -a "$REPORT_FILE"
fi
}
get_routing_info() {
log "Obteniendo información de enrutamiento..."
echo "=== TABLA DE RUTAS ===" | tee -a "$REPORT_FILE"
ip route show | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
# Detectar gateway predeterminado
DEFAULT_GW=$(ip route show default | awk '/default/ {print $3}')
if [ -n "$DEFAULT_GW" ]; then
success "Gateway predeterminado: $DEFAULT_GW"
echo "Gateway: $DEFAULT_GW" | tee -a "$REPORT_FILE"
# Probar conectividad al gateway
log "Probando conectividad al gateway $DEFAULT_GW..."
if ping -c 3 -W 2 "$DEFAULT_GW" &> /dev/null; then
success "Gateway alcanzable"
echo "Gateway alcanzable: Sí" | tee -a "$REPORT_FILE"
else
warning "Gateway no alcanzable"
echo "Gateway alcanzable: No" | tee -a "$REPORT_FILE"
fi
else
warning "No se encontró gateway predeterminado"
echo "Gateway: No encontrado" | tee -a "$REPORT_FILE"
fi
# Detectar subred local
LOCAL_SUBNET=$(ip route show | grep "link src" | awk '{print $1}' | head -1)
if [ -n "$LOCAL_SUBNET" ]; then
success "Subred local detectada: $LOCAL_SUBNET"
echo "Subred local: $LOCAL_SUBNET" | tee -a "$REPORT_FILE"
else
warning "No se pudo detectar subred local"
echo "Subred local: No detectada" | tee -a "$REPORT_FILE"
fi
}
check_dhcp_info() {
log "Verificando información DHCP..."
echo "=== INFORMACIÓN DHCP ===" | tee -a "$REPORT_FILE"
# Verificar archivos de lease DHCP
DHCP_FILES=("/var/lib/dhcp/dhclient.leases" "/var/lib/dhclient/dhclient.leases")
for file in "${DHCP_FILES[@]}"; do
if [ -f "$file" ]; then
log "Analizando $file..."
if grep -q "interface.*$INTERFACE" "$file" 2>/dev/null; then
success "Se encontraron leases DHCP para $INTERFACE"
echo "Archivo DHCP: $file" | tee -a "$REPORT_FILE"
# Extraer información relevante
grep -A10 -B2 "interface.*$INTERFACE" "$file" | \
grep -E "(lease|starts|ends|option|fixed-address)" | \
head -20 | tee -a "$REPORT_FILE"
break
fi
fi
done
if ! grep -q "Archivo DHCP:" "$REPORT_FILE"; then
warning "No se encontraron leases DHCP para $INTERFACE"
echo "DHCP: No se encontraron leases" | tee -a "$REPORT_FILE"
fi
echo "" | tee -a "$REPORT_FILE"
}
scan_local_network() {
if [ -z "$LOCAL_SUBNET" ]; then
warning "No se puede escanear red local (subred no detectada)"
return 1
fi
log "Escaneando red local $LOCAL_SUBNET..."
echo "=== ESCANEO DE RED LOCAL ===" | tee -a "$REPORT_FILE"
if [ "$HAS_NMAP" = true ]; then
# Escaneo rápido con nmap
log "Ejecutando escaneo rápido con nmap..."
nmap -sn "$LOCAL_SUBNET" 2>/dev/null | \
grep "Nmap scan report" | \
head -20 | \
tee -a "$REPORT_FILE"
# Contar dispositivos
DEVICE_COUNT=$(nmap -sn "$LOCAL_SUBNET" 2>/dev/null | grep "Nmap scan report" | wc -l)
success "Dispositivos detectados: $DEVICE_COUNT"
echo "Total dispositivos: $DEVICE_COUNT" | tee -a "$REPORT_FILE"
else
# Método alternativo usando ping y arp
warning "Usando método básico de detección (sin nmap)..."
# Ping a broadcast (puede no funcionar en todas las redes)
log "Probando detección básica..."
ping -c 2 -b "$(echo "$LOCAL_SUBNET" | cut -d'/' -f1 | sed 's/0$/255/')" &> /dev/null || true
# Mostrar tabla ARP
ip neigh show | grep -v "FAILED" | \
head -20 | \
tee -a "$REPORT_FILE"
DEVICE_COUNT=$(ip neigh show | grep -v "FAILED" | wc -l)
echo "Dispositivos en tabla ARP: $DEVICE_COUNT" | tee -a "$REPORT_FILE"
fi
echo "" | tee -a "$REPORT_FILE"
}
check_tailscale() {
log "Verificando estado de Tailscale..."
echo "=== ESTADO TAILSCALE ===" | tee -a "$REPORT_FILE"
if command -v tailscale &> /dev/null; then
tailscale status 2>/dev/null | tee -a "$REPORT_FILE"
# Extraer IP de Tailscale
TAILSCALE_IP=$(tailscale status --json 2>/dev/null | \
$([ "$HAS_JQ" = true ] && echo "jq -r '.Self.TailscaleIPs[0]'" || echo "grep -oE '100\.[0-9]+\.[0-9]+\.[0-9]+' | head -1"))
if [ -n "$TAILSCALE_IP" ]; then
success "Tailscale IP: $TAILSCALE_IP"
echo "IP Tailscale: $TAILSCALE_IP" | tee -a "$REPORT_FILE"
else
warning "Tailscale no parece estar conectado"
echo "Tailscale: No conectado" | tee -a "$REPORT_FILE"
fi
else
warning "Tailscale no está instalado"
echo "Tailscale: No instalado" | tee -a "$REPORT_FILE"
fi
echo "" | tee -a "$REPORT_FILE"
}
generate_recommendations() {
log "Generando recomendaciones..."
echo "=== RECOMENDACIONES ===" | tee -a "$REPORT_FILE"
# Recomendación de IP local
if [ -n "$LOCAL_SUBNET" ]; then
BASE_NET=$(echo "$LOCAL_SUBNET" | cut -d'/' -f1 | cut -d'.' -f1-3)
echo "Para configuración local entre srv-dasu y pc-dasu0:" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo "1. **IPs recomendadas (fuera de rango DHCP común):**" | tee -a "$REPORT_FILE"
echo " - srv-dasu: ${BASE_NET}.205/24" | tee -a "$REPORT_FILE"
echo " - pc-dasu0: ${BASE_NET}.100/24" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo "2. **Configuración de red:**" | tee -a "$REPORT_FILE"
echo " - Gateway: $(echo "$DEFAULT_GW" || echo "[gateway-del-router]")" | tee -a "$REPORT_FILE"
echo " - Máscara: 255.255.255.0 (/24)" | tee -a "$REPORT_FILE"
echo " - DNS Primario: 10.0.100.10 (dc-dasuten)" | tee -a "$REPORT_FILE"
echo " - DNS Secundario: 8.8.8.8" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo "3. **Verificaciones previas:**" | tee -a "$REPORT_FILE"
echo " - Confirmar que ${BASE_NET}.205 y ${BASE_NET}.100 no están en uso" | tee -a "$REPORT_FILE"
echo " - Verificar que el firewall permite ICMP y puertos necesarios" | tee -a "$REPORT_FILE"
echo " - Tailscale debe permanecer activo como respaldo" | tee -a "$REPORT_FILE"
else
echo "No se pudo generar recomendaciones específicas (subred no detectada)" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo "Recomendaciones generales:" | tee -a "$REPORT_FILE"
echo "1. Identificar manualmente la subred del router ISP" | tee -a "$REPORT_FILE"
echo "2. Usar IPs estáticas fuera del rango DHCP del router" | tee -a "$REPORT_FILE"
echo "3. Mantener Tailscale como ruta de failover" | tee -a "$REPORT_FILE"
fi
echo "" | tee -a "$REPORT_FILE"
}
generate_summary() {
echo "=== RESUMEN DEL DIAGNÓSTICO ===" | tee -a "$REPORT_FILE"
echo "Fecha: $(date)" | tee -a "$REPORT_FILE"
echo "Hostname: $(hostname)" | tee -a "$REPORT_FILE"
echo "Interfaz analizada: $INTERFACE" | tee -a "$REPORT_FILE"
echo "Estado interfaz: $(cat /sys/class/net/"$INTERFACE"/operstate 2>/dev/null || echo "desconocido")" | tee -a "$REPORT_FILE"
echo "Gateway: ${DEFAULT_GW:-No detectado}" | tee -a "$REPORT_FILE"
echo "Subred local: ${LOCAL_SUBNET:-No detectada}" | tee -a "$REPORT_FILE"
echo "Tailscale: $(if command -v tailscale &> /dev/null; then echo "Instalado"; else echo "No instalado"; fi)" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo "Archivos generados:" | tee -a "$REPORT_FILE"
echo " - Log detallado: $LOG_FILE" | tee -a "$REPORT_FILE"
echo " - Reporte completo: $REPORT_FILE" | tee -a "$REPORT_FILE"
}
main() {
echo "========================================="
echo " DIAGNÓSTICO DE RED LOCAL - srv-dasu"
echo "========================================="
echo ""
# Inicializar archivos
> "$LOG_FILE"
> "$REPORT_FILE"
# Verificar dependencias
if ! check_dependencies; then
exit 1
fi
# Verificar interfaz
if ! check_interface; then
exit 1
fi
# Ejecutar diagnósticos
get_interface_info
get_routing_info
check_dhcp_info
scan_local_network
check_tailscale
generate_recommendations
generate_summary
echo ""
success "Diagnóstico completado exitosamente"
echo ""
echo "Para configurar IP local en $INTERFACE, edite:"
echo " sudo nano /etc/network/interfaces"
echo ""
echo "Agregue las líneas (adaptando la IP según recomendaciones):"
echo " post-up ip addr add 192.168.1.205/24 dev $INTERFACE"
echo " pre-down ip addr del 192.168.1.205/24 dev $INTERFACE"
echo ""
echo "Luego reinicie el servicio de red:"
echo " sudo systemctl restart networking"
}
# Ejecutar script principal
main "$@"
@@ -1,42 +0,0 @@
# P2601-6.1.A - Proyecto P2601-6.1.A
**Estado:** 📍 Definido
**Fecha Inicio:** 2026-03-11
**Fecha Fin Estimada:** *Especificar*
## Objetivo
*Describir el propósito y qué problema resuelve*
## Stakeholders
| Rol | Nombre | Contacto |
| :--- | :--- | :--- |
| *Ej: Coordinador* | *Nombre* | *email/tel* |
## 🌐 Topología y Contexto de Red
*Describir ubicación de nodos, subredes, requisitos de acceso*
## Nodos Involucrados
### Nodos Exclusivos (Core)
- *Listar nodos centrales del proyecto*
### Nodos de Soporte/Infraestructura
- *Listar nodos de infraestructura compartida*
## Herramientas
- *Software, scripts y servicios utilizados*
## Hitos Clave
| ID | Hito | Estado | Fecha |
| :--- | :--- | :---: | :--- |
| 1 | *Primer hito* | 📍 | *Fecha* |
## Dashboard
*URL del panel de seguimiento web (si aplica)*
## Referencias
- [ADN del Proyecto](../adn/00_indice.md)
- *Otras referencias relevantes*
---
*Manifiesto generado automáticamente el 2026-03-11*
+2 -2
View File
@@ -1,6 +1,6 @@
server {
server_name ns8.frlr.utn.edu.ar 100.88.252.26 190.114.205.18 srvns8.tail982245.ts.net;
server_name ns8.frlr.utn.edu.ar 100.88.252.26 190.114.205.18 srvns8.tail982245.ts.net srv-ns8.tail.dasuten srv-ns8.tail.rmonla;
root /var/www/html;
index index.html index.htm;
@@ -99,7 +99,7 @@ server {
listen 80;
server_name ns8.frlr.utn.edu.ar 100.88.252.26 190.114.205.18 srvns8.tail982245.ts.net 10.0.10.8;
server_name ns8.frlr.utn.edu.ar 100.88.252.26 190.114.205.18 srvns8.tail982245.ts.net srv-ns8.tail.dasuten srv-ns8.tail.rmonla 10.0.10.8;
location /apps/ {
client_max_body_size 20G;