[Core/Infra] Provisionamiento SSH en pc-dasu0 y unificación Nginx tailscale
This commit is contained in:
+8
-8
@@ -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
@@ -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
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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...")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
1773303097
|
||||
1773452033
|
||||
@@ -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*
|
||||
|
||||
Executable
+490
@@ -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
|
||||
@@ -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
-2
@@ -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") {
|
||||
+3
-1
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+2
-2
@@ -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*
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user