From b13a389568ae26d82cd0ddda09fe64b66839f9e6 Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Sat, 14 Mar 2026 10:29:54 -0300 Subject: [PATCH] =?UTF-8?q?[Core/Infra]=20Provisionamiento=20SSH=20en=20pc?= =?UTF-8?q?-dasu0=20y=20unificaci=C3=B3n=20Nginx=20tailscale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adn/03_seguridad.md | 16 +- adn/05_ia.md | 2 +- adn/README.md | 5 +- adn/tools/cli/ayuda.rb | 12 + adn/tools/cli/backup.rb | 13 +- adn/tools/cli/commit.rb | 9 + adn/tools/cli/db/evento.rb | 76 +- adn/tools/cli/estados.rb | 4 +- adn/tools/cli/tailscale.rb | 202 ++++++ adn/tools/core/conciliador.rb | 2 +- adn/tools/db/core/bitacora_db.rb | 2 +- adn/tools/run | 4 + adn/tools/seguridad/.boveda.json | 5 +- adn/tools/seguridad/.session | 2 +- adn/tools/seguridad/README.md | 131 +++- adn/tools/seguridad/candados.rb | 490 +++++++++++++ adn/tools/seguridad/ns8-candados.rb | 324 --------- adn/tools/{sondas => sonda_w-zombi}/README.md | 0 .../payloads/.gitignore | 0 adn/tools/sonda_w-zombi/payloads/activo.ps1 | 40 + .../payloads/deploy_sysdasuten.ps1 | 0 .../payloads/install_ssh.ps1 | 0 .../payloads/install_ssh_fixtemp.ps1 | 0 .../payloads/install_ssh_legacy.ps1 | 8 +- .../payloads/setup.ps1 | 0 .../payloads/sonda.ps1 | 0 .../payloads/sql_sysadmin_fix.ps1 | 0 .../payloads/ssh_port7022.ps1 | 4 +- .../payloads/zombi_agent.bat | 0 .../{sondas => sonda_w-zombi}/servidor.rb | 14 +- adn/tools/sondas/payloads/activo.ps1 | 115 --- docs/prompt/260311-1758_Usa_adn-README.md | 3 - docs/prompt/260313-1813_Pido_Usar_w-zombi.md | 4 + docs/prompt/Usa_Herramientas_ADN.md | 1 + .../P2602_backups}/P2602_backups.md | 0 .../P2603_bitacoras}/P2603_bitacoras.md | 0 .../p2601_dasuten}/P2601_dasuten.md | 0 .../plan/P2601_6.1.A - Red_SrvDasu.md} | 4 +- ...01_6.1.B - AD-Join: pc-dasu0 al Dominio.md | 304 ++++++++ .../P2601_6.1.B_comunicacion_directa_local.md | 685 ++++++++++++++++++ .../proc/scripts/Diagnose-LocalNetwork.ps1 | 399 ++++++++++ .../proc/scripts/diagnostico_red_local.sh | 337 +++++++++ ...60304-1959_dashboard_P2601_en_bitacoras.md | 0 .../260305-0930_PruebasDasuten.md | 0 .../260305-1010_ContextoADN_Dasuten.md | 0 .../260305-1017_Despliegue_runSysDasuten.md | 0 ...305-1300_Plan_Fase_6_Fisico_Dominio_GPO.md | 0 .../P2605_Migracion_Solo_Bitacoras_Web.md | 0 .../P2601-6.1.A_proyecto_p2601-6.1.a.md | 42 -- servicios/nginx/conf.d/ns8.conf | 4 +- 50 files changed, 2702 insertions(+), 561 deletions(-) create mode 100644 adn/tools/cli/commit.rb create mode 100644 adn/tools/cli/tailscale.rb create mode 100755 adn/tools/seguridad/candados.rb delete mode 100755 adn/tools/seguridad/ns8-candados.rb rename adn/tools/{sondas => sonda_w-zombi}/README.md (100%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/.gitignore (100%) create mode 100644 adn/tools/sonda_w-zombi/payloads/activo.ps1 rename adn/tools/{sondas => sonda_w-zombi}/payloads/deploy_sysdasuten.ps1 (100%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/install_ssh.ps1 (100%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/install_ssh_fixtemp.ps1 (100%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/install_ssh_legacy.ps1 (92%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/setup.ps1 (100%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/sonda.ps1 (100%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/sql_sysadmin_fix.ps1 (100%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/ssh_port7022.ps1 (94%) rename adn/tools/{sondas => sonda_w-zombi}/payloads/zombi_agent.bat (100%) rename adn/tools/{sondas => sonda_w-zombi}/servidor.rb (90%) delete mode 100644 adn/tools/sondas/payloads/activo.ps1 delete mode 100644 docs/prompt/260311-1758_Usa_adn-README.md create mode 100644 docs/prompt/260313-1813_Pido_Usar_w-zombi.md create mode 100644 docs/prompt/Usa_Herramientas_ADN.md rename docs/{proyectos => proy/P2602_backups}/P2602_backups.md (100%) rename docs/{proyectos => proy/P2603_bitacoras}/P2603_bitacoras.md (100%) rename docs/{proyectos/_hist => proy/p2601_dasuten}/P2601_dasuten.md (100%) rename docs/{plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md => proy/p2601_dasuten/plan/P2601_6.1.A - Red_SrvDasu.md} (94%) create mode 100644 docs/proy/p2601_dasuten/plan/P2601_6.1.B - AD-Join: pc-dasu0 al Dominio.md create mode 100644 docs/proy/p2601_dasuten/proc/P2601_6.1.B_comunicacion_directa_local.md create mode 100644 docs/proy/p2601_dasuten/proc/scripts/Diagnose-LocalNetwork.ps1 create mode 100644 docs/proy/p2601_dasuten/proc/scripts/diagnostico_red_local.sh rename docs/{plan/_hist => proy/p2601_dasuten/sinclasificar}/260304-1959_dashboard_P2601_en_bitacoras.md (100%) rename docs/{plan/_hist => proy/p2601_dasuten/sinclasificar}/260305-0930_PruebasDasuten.md (100%) rename docs/{plan/_hist => proy/p2601_dasuten/sinclasificar}/260305-1010_ContextoADN_Dasuten.md (100%) rename docs/{plan/_hist => proy/p2601_dasuten/sinclasificar}/260305-1017_Despliegue_runSysDasuten.md (100%) rename docs/{plan/_hist => proy/p2601_dasuten/sinclasificar}/260305-1300_Plan_Fase_6_Fisico_Dominio_GPO.md (100%) rename docs/{proyectos/_hist => proy/p2605_bitacoras-web}/P2605_Migracion_Solo_Bitacoras_Web.md (100%) delete mode 100644 docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md diff --git a/adn/03_seguridad.md b/adn/03_seguridad.md index 1ada7a03..ca1e13c7 100644 --- a/adn/03_seguridad.md +++ b/adn/03_seguridad.md @@ -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 [args] +ruby adn/tools/seguridad/candados.rb [args] ``` | Comando | Descripción | @@ -51,7 +51,7 @@ ruby adn/tools/seguridad/ns8-candados.rb [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" diff --git a/adn/05_ia.md b/adn/05_ia.md index 6d6d615d..a68c6cee 100644 --- a/adn/05_ia.md +++ b/adn/05_ia.md @@ -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. diff --git a/adn/README.md b/adn/README.md index f943f455..8105eab5 100644 --- a/adn/README.md +++ b/adn/README.md @@ -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 )`). 7. **Topología primero**: Antes de conectarte a un nodo, consultar `./adn/tools/run contexto ` 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) ``` diff --git a/adn/tools/cli/ayuda.rb b/adn/tools/cli/ayuda.rb index a29e2d8a..ec355fc9 100644 --- a/adn/tools/cli/ayuda.rb +++ b/adn/tools/cli/ayuda.rb @@ -34,6 +34,7 @@ class SubcomandoAyuda generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD) generar nodo Generar ficha de nodo #{Color::YELLOW}generar proyecto #{Color::RESET} Generar manifiesto de proyecto + #{Color::YELLOW}tailscale #{Color::RESET} Gestión de identidades Tailscale #{Color::YELLOW}backup [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo #{Color::YELLOW}db #{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 [perfil]" + puts "" + puts "Gestión de identidades y perfiles de Tailscale:" + puts " • switch 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." diff --git a/adn/tools/cli/backup.rb b/adn/tools/cli/backup.rb index aa08c75d..447b44e2 100644 --- a/adn/tools/cli/backup.rb +++ b/adn/tools/cli/backup.rb @@ -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...") diff --git a/adn/tools/cli/commit.rb b/adn/tools/cli/commit.rb new file mode 100644 index 00000000..0627f7c2 --- /dev/null +++ b/adn/tools/cli/commit.rb @@ -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 diff --git a/adn/tools/cli/db/evento.rb b/adn/tools/cli/db/evento.rb index c05d4f50..c762d88a 100644 --- a/adn/tools/cli/db/evento.rb +++ b/adn/tools/cli/db/evento.rb @@ -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 [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 diff --git a/adn/tools/cli/estados.rb b/adn/tools/cli/estados.rb index a862558c..c5252f5b 100644 --- a/adn/tools/cli/estados.rb +++ b/adn/tools/cli/estados.rb @@ -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" diff --git a/adn/tools/cli/tailscale.rb b/adn/tools/cli/tailscale.rb new file mode 100644 index 00000000..6c310d3d --- /dev/null +++ b/adn/tools/cli/tailscale.rb @@ -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 [perfil] + + #{Color::YELLOW}Acciones:#{Color::RESET} + switch 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 diff --git a/adn/tools/core/conciliador.rb b/adn/tools/core/conciliador.rb index e420abdf..946fc345 100644 --- a/adn/tools/core/conciliador.rb +++ b/adn/tools/core/conciliador.rb @@ -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) diff --git a/adn/tools/db/core/bitacora_db.rb b/adn/tools/db/core/bitacora_db.rb index 2a98eec6..09ebc87d 100644 --- a/adn/tools/db/core/bitacora_db.rb +++ b/adn/tools/db/core/bitacora_db.rb @@ -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 diff --git a/adn/tools/run b/adn/tools/run index 088b1801..4f76e478 100755 --- a/adn/tools/run +++ b/adn/tools/run @@ -19,6 +19,7 @@ ENV['TZ'] = 'America/Argentina/Buenos_Aires' # generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD) # generar nodo Generar ficha de nodo # generar proyecto Generar manifiesto de proyecto +# tailscale [opciones] Gestión de identidades Tailscale # conocimiento asimilar Asimilar documentación al ADN # evolucion 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 diff --git a/adn/tools/seguridad/.boveda.json b/adn/tools/seguridad/.boveda.json index 5d511e0f..e71532cb 100644 --- a/adn/tools/seguridad/.boveda.json +++ b/adn/tools/seguridad/.boveda.json @@ -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=" } diff --git a/adn/tools/seguridad/.session b/adn/tools/seguridad/.session index 53b4adf3..d635ea22 100644 --- a/adn/tools/seguridad/.session +++ b/adn/tools/seguridad/.session @@ -1 +1 @@ -1773303097 \ No newline at end of file +1773452033 \ No newline at end of file diff --git a/adn/tools/seguridad/README.md b/adn/tools/seguridad/README.md index afdcca72..1891d70a 100644 --- a/adn/tools/seguridad/README.md +++ b/adn/tools/seguridad/README.md @@ -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 `: 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. `::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_`) 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 ` | ✅ Nada expuesto | **★ PRIMARIO** - Ejecuta con USR/PASS inyectados | +| `sudo ` | ✅ Nada expuesto | Ejecuta con privilegios desde bóveda | +| `load ` | ✅ Archivo temporal 600 | Carga en shell (usar con `eval`) | +| `get ` | ⚠ Imprime a stdout | Obtener valor directo (usar con cuidado) | +| `authorize` | ✅ | Abrir candado (5 min) | +| `set ` | ✅ 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* diff --git a/adn/tools/seguridad/candados.rb b/adn/tools/seguridad/candados.rb new file mode 100755 index 00000000..25f5bc7b --- /dev/null +++ b/adn/tools/seguridad/candados.rb @@ -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 '' +# → Inyecta USR y PASS en el entorno del subproceso. Nada se imprime. +# +# COMANDO SECUNDARIO (USO INTERACTIVO): +# eval $(ruby candados.rb load ) +# → 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 [args] +# +# Comandos principales: +# run Ejecutar comando con USR y PASS inyectados (PRIMARIO) +# sudo Ejecutar comando con sudo desde la bóveda +# load Cargar USR/PASS en shell (eval, archivo temporal seguro) +# +# Comandos de gestión: +# authorize Abrir candado (sesión temporal 5 min) +# set Guardar secreto (input seguro, sin eco) +# get Obtener secreto (⚠ imprime en stdout) +# rm 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 + 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: contiene user y para pass, + # o _user y _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. ::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 [args] + + #{C::YELLOW}━━━ Comandos principales ━━━#{C::RESET} + #{C::BOLD}run #{C::RESET} #{C::GREEN}★ PRIMARIO#{C::RESET} Ejecutar comando con USR y PASS inyectados + #{C::BOLD}sudo #{C::RESET} Ejecutar con privilegios (resuelve clave automáticamente) + #{C::BOLD}load #{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 Guardar secreto (input seguro, sin eco) + get Obtener secreto (⚠ imprime a stdout) + rm Eliminar secreto + list Listar claves disponibles + + #{C::YELLOW}━━━ Utilidades ━━━#{C::RESET} + abrir Autorizar SSH (cargar passphrase) + encrypt Cifrar texto plano → token + decrypt 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} + ::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 diff --git a/adn/tools/seguridad/ns8-candados.rb b/adn/tools/seguridad/ns8-candados.rb deleted file mode 100755 index 15dec78a..00000000 --- a/adn/tools/seguridad/ns8-candados.rb +++ /dev/null @@ -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 [args] -# -# Comandos: -# abrir Autorizar SSH (cargar llave con passphrase) -# encrypt Cifrar texto plano -# decrypt Descifrar payload -# get Obtener secreto de la bóveda (salida: solo el valor) -# run Ejecutar comando con secreto en variable de entorno -# set Guardar secreto (pide input interactivo, sin eco) -# rm 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 [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 Cifrar texto plano → token - decrypt Descifrar token → texto plano - get Obtener secreto de la bóveda - run Ejecutar comando con secreto en variable - set Guardar secreto (input seguro) - rm 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 diff --git a/adn/tools/sondas/README.md b/adn/tools/sonda_w-zombi/README.md similarity index 100% rename from adn/tools/sondas/README.md rename to adn/tools/sonda_w-zombi/README.md diff --git a/adn/tools/sondas/payloads/.gitignore b/adn/tools/sonda_w-zombi/payloads/.gitignore similarity index 100% rename from adn/tools/sondas/payloads/.gitignore rename to adn/tools/sonda_w-zombi/payloads/.gitignore diff --git a/adn/tools/sonda_w-zombi/payloads/activo.ps1 b/adn/tools/sonda_w-zombi/payloads/activo.ps1 new file mode 100644 index 00000000..2cbd8047 --- /dev/null +++ b/adn/tools/sonda_w-zombi/payloads/activo.ps1 @@ -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 diff --git a/adn/tools/sondas/payloads/deploy_sysdasuten.ps1 b/adn/tools/sonda_w-zombi/payloads/deploy_sysdasuten.ps1 similarity index 100% rename from adn/tools/sondas/payloads/deploy_sysdasuten.ps1 rename to adn/tools/sonda_w-zombi/payloads/deploy_sysdasuten.ps1 diff --git a/adn/tools/sondas/payloads/install_ssh.ps1 b/adn/tools/sonda_w-zombi/payloads/install_ssh.ps1 similarity index 100% rename from adn/tools/sondas/payloads/install_ssh.ps1 rename to adn/tools/sonda_w-zombi/payloads/install_ssh.ps1 diff --git a/adn/tools/sondas/payloads/install_ssh_fixtemp.ps1 b/adn/tools/sonda_w-zombi/payloads/install_ssh_fixtemp.ps1 similarity index 100% rename from adn/tools/sondas/payloads/install_ssh_fixtemp.ps1 rename to adn/tools/sonda_w-zombi/payloads/install_ssh_fixtemp.ps1 diff --git a/adn/tools/sondas/payloads/install_ssh_legacy.ps1 b/adn/tools/sonda_w-zombi/payloads/install_ssh_legacy.ps1 similarity index 92% rename from adn/tools/sondas/payloads/install_ssh_legacy.ps1 rename to adn/tools/sonda_w-zombi/payloads/install_ssh_legacy.ps1 index cda1e397..4337a445 100644 --- a/adn/tools/sondas/payloads/install_ssh_legacy.ps1 +++ b/adn/tools/sonda_w-zombi/payloads/install_ssh_legacy.ps1 @@ -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") { diff --git a/adn/tools/sondas/payloads/setup.ps1 b/adn/tools/sonda_w-zombi/payloads/setup.ps1 similarity index 100% rename from adn/tools/sondas/payloads/setup.ps1 rename to adn/tools/sonda_w-zombi/payloads/setup.ps1 diff --git a/adn/tools/sondas/payloads/sonda.ps1 b/adn/tools/sonda_w-zombi/payloads/sonda.ps1 similarity index 100% rename from adn/tools/sondas/payloads/sonda.ps1 rename to adn/tools/sonda_w-zombi/payloads/sonda.ps1 diff --git a/adn/tools/sondas/payloads/sql_sysadmin_fix.ps1 b/adn/tools/sonda_w-zombi/payloads/sql_sysadmin_fix.ps1 similarity index 100% rename from adn/tools/sondas/payloads/sql_sysadmin_fix.ps1 rename to adn/tools/sonda_w-zombi/payloads/sql_sysadmin_fix.ps1 diff --git a/adn/tools/sondas/payloads/ssh_port7022.ps1 b/adn/tools/sonda_w-zombi/payloads/ssh_port7022.ps1 similarity index 94% rename from adn/tools/sondas/payloads/ssh_port7022.ps1 rename to adn/tools/sonda_w-zombi/payloads/ssh_port7022.ps1 index 50108189..7eb13f78 100644 --- a/adn/tools/sondas/payloads/ssh_port7022.ps1 +++ b/adn/tools/sonda_w-zombi/payloads/ssh_port7022.ps1 @@ -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 {} } diff --git a/adn/tools/sondas/payloads/zombi_agent.bat b/adn/tools/sonda_w-zombi/payloads/zombi_agent.bat similarity index 100% rename from adn/tools/sondas/payloads/zombi_agent.bat rename to adn/tools/sonda_w-zombi/payloads/zombi_agent.bat diff --git a/adn/tools/sondas/servidor.rb b/adn/tools/sonda_w-zombi/servidor.rb similarity index 90% rename from adn/tools/sondas/servidor.rb rename to adn/tools/sonda_w-zombi/servidor.rb index d76e86ff..f94e3ee8 100755 --- a/adn/tools/sondas/servidor.rb +++ b/adn/tools/sonda_w-zombi/servidor.rb @@ -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 diff --git a/adn/tools/sondas/payloads/activo.ps1 b/adn/tools/sondas/payloads/activo.ps1 deleted file mode 100644 index 631d867e..00000000 --- a/adn/tools/sondas/payloads/activo.ps1 +++ /dev/null @@ -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 "==========================================" diff --git a/docs/prompt/260311-1758_Usa_adn-README.md b/docs/prompt/260311-1758_Usa_adn-README.md deleted file mode 100644 index 6fe6f7ce..00000000 --- a/docs/prompt/260311-1758_Usa_adn-README.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/docs/prompt/260313-1813_Pido_Usar_w-zombi.md b/docs/prompt/260313-1813_Pido_Usar_w-zombi.md new file mode 100644 index 00000000..31861f44 --- /dev/null +++ b/docs/prompt/260313-1813_Pido_Usar_w-zombi.md @@ -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. \ No newline at end of file diff --git a/docs/prompt/Usa_Herramientas_ADN.md b/docs/prompt/Usa_Herramientas_ADN.md new file mode 100644 index 00000000..5da67089 --- /dev/null +++ b/docs/prompt/Usa_Herramientas_ADN.md @@ -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. \ No newline at end of file diff --git a/docs/proyectos/P2602_backups.md b/docs/proy/P2602_backups/P2602_backups.md similarity index 100% rename from docs/proyectos/P2602_backups.md rename to docs/proy/P2602_backups/P2602_backups.md diff --git a/docs/proyectos/P2603_bitacoras.md b/docs/proy/P2603_bitacoras/P2603_bitacoras.md similarity index 100% rename from docs/proyectos/P2603_bitacoras.md rename to docs/proy/P2603_bitacoras/P2603_bitacoras.md diff --git a/docs/proyectos/_hist/P2601_dasuten.md b/docs/proy/p2601_dasuten/P2601_dasuten.md similarity index 100% rename from docs/proyectos/_hist/P2601_dasuten.md rename to docs/proy/p2601_dasuten/P2601_dasuten.md diff --git a/docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md b/docs/proy/p2601_dasuten/plan/P2601_6.1.A - Red_SrvDasu.md similarity index 94% rename from docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md rename to docs/proy/p2601_dasuten/plan/P2601_6.1.A - Red_SrvDasu.md index 84296aeb..b20a91ff 100644 --- a/docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md +++ b/docs/proy/p2601_dasuten/plan/P2601_6.1.A - Red_SrvDasu.md @@ -3,7 +3,7 @@ **Fecha:** 11 de marzo de 2026 **Autor:** Sistema ADN **Versión:** 1.0 -**Ubicación:** `docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md` +**Ubicación:** `docs/proy/p2601_dasuten/plan/P2601_6.1.A - Red_SrvDasu.md` **Estado:** ⏳ PLANIFICACIÓN ## 📋 Resumen Ejecutivo @@ -25,7 +25,7 @@ Preparar la infraestructura de red para que `srv-dasu` funcione correctamente fu #### Semana 1: Dependencias - [x] **2.1:** Actualizar código: `adn/tools/cli/backup.rb` reemplazando `10.0.10.205` por la IP de Tailscale. (Referencias IP migradas a Tailscale 100.116.210.36) - [x] **2.2:** Actualizar documentación: `nodos/srv-dasu.md` indicando la obsolescencia de la IP local. (Referencias IP migradas a Tailscale 100.116.210.36) -- [x] **2.3:** Actualizar documentación: `adn/03_seguridad.md` y `docs/proyectos/_hist/P2601_dasuten.md` reflejando rutas nuevas. (Referencias IP migradas a Tailscale 100.116.210.36) +- [x] **2.3:** Actualizar documentación: `adn/03_seguridad.md` y `docs/proy/p2601_dasuten/P2601_dasuten.md` reflejando rutas nuevas. (Referencias IP migradas a Tailscale 100.116.210.36) ### ✅ **FASE 3: Simulación de Mudanza** diff --git a/docs/proy/p2601_dasuten/plan/P2601_6.1.B - AD-Join: pc-dasu0 al Dominio.md b/docs/proy/p2601_dasuten/plan/P2601_6.1.B - AD-Join: pc-dasu0 al Dominio.md new file mode 100644 index 00000000..fd2e1e45 --- /dev/null +++ b/docs/proy/p2601_dasuten/plan/P2601_6.1.B - AD-Join: pc-dasu0 al Dominio.md @@ -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 ` 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. \ No newline at end of file diff --git a/docs/proy/p2601_dasuten/proc/P2601_6.1.B_comunicacion_directa_local.md b/docs/proy/p2601_dasuten/proc/P2601_6.1.B_comunicacion_directa_local.md new file mode 100644 index 00000000..5b731f3f --- /dev/null +++ b/docs/proy/p2601_dasuten/proc/P2601_6.1.B_comunicacion_directa_local.md @@ -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* \ No newline at end of file diff --git a/docs/proy/p2601_dasuten/proc/scripts/Diagnose-LocalNetwork.ps1 b/docs/proy/p2601_dasuten/proc/scripts/Diagnose-LocalNetwork.ps1 new file mode 100644 index 00000000..3dd38a9e --- /dev/null +++ b/docs/proy/p2601_dasuten/proc/scripts/Diagnose-LocalNetwork.ps1 @@ -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')*" diff --git a/docs/proy/p2601_dasuten/proc/scripts/diagnostico_red_local.sh b/docs/proy/p2601_dasuten/proc/scripts/diagnostico_red_local.sh new file mode 100644 index 00000000..a6faf07a --- /dev/null +++ b/docs/proy/p2601_dasuten/proc/scripts/diagnostico_red_local.sh @@ -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 "$@" diff --git a/docs/plan/_hist/260304-1959_dashboard_P2601_en_bitacoras.md b/docs/proy/p2601_dasuten/sinclasificar/260304-1959_dashboard_P2601_en_bitacoras.md similarity index 100% rename from docs/plan/_hist/260304-1959_dashboard_P2601_en_bitacoras.md rename to docs/proy/p2601_dasuten/sinclasificar/260304-1959_dashboard_P2601_en_bitacoras.md diff --git a/docs/plan/_hist/260305-0930_PruebasDasuten.md b/docs/proy/p2601_dasuten/sinclasificar/260305-0930_PruebasDasuten.md similarity index 100% rename from docs/plan/_hist/260305-0930_PruebasDasuten.md rename to docs/proy/p2601_dasuten/sinclasificar/260305-0930_PruebasDasuten.md diff --git a/docs/plan/_hist/260305-1010_ContextoADN_Dasuten.md b/docs/proy/p2601_dasuten/sinclasificar/260305-1010_ContextoADN_Dasuten.md similarity index 100% rename from docs/plan/_hist/260305-1010_ContextoADN_Dasuten.md rename to docs/proy/p2601_dasuten/sinclasificar/260305-1010_ContextoADN_Dasuten.md diff --git a/docs/plan/_hist/260305-1017_Despliegue_runSysDasuten.md b/docs/proy/p2601_dasuten/sinclasificar/260305-1017_Despliegue_runSysDasuten.md similarity index 100% rename from docs/plan/_hist/260305-1017_Despliegue_runSysDasuten.md rename to docs/proy/p2601_dasuten/sinclasificar/260305-1017_Despliegue_runSysDasuten.md diff --git a/docs/plan/_hist/260305-1300_Plan_Fase_6_Fisico_Dominio_GPO.md b/docs/proy/p2601_dasuten/sinclasificar/260305-1300_Plan_Fase_6_Fisico_Dominio_GPO.md similarity index 100% rename from docs/plan/_hist/260305-1300_Plan_Fase_6_Fisico_Dominio_GPO.md rename to docs/proy/p2601_dasuten/sinclasificar/260305-1300_Plan_Fase_6_Fisico_Dominio_GPO.md diff --git a/docs/proyectos/_hist/P2605_Migracion_Solo_Bitacoras_Web.md b/docs/proy/p2605_bitacoras-web/P2605_Migracion_Solo_Bitacoras_Web.md similarity index 100% rename from docs/proyectos/_hist/P2605_Migracion_Solo_Bitacoras_Web.md rename to docs/proy/p2605_bitacoras-web/P2605_Migracion_Solo_Bitacoras_Web.md diff --git a/docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md b/docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md deleted file mode 100644 index 6b15f805..00000000 --- a/docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md +++ /dev/null @@ -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* diff --git a/servicios/nginx/conf.d/ns8.conf b/servicios/nginx/conf.d/ns8.conf index f5fa2fa1..ea69be60 100644 --- a/servicios/nginx/conf.d/ns8.conf +++ b/servicios/nginx/conf.d/ns8.conf @@ -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;