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