Conexiones SSH y Túneles

Técnicas avanzadas de SSH para administración remota segura y túneles de red

Módulo 9 ⏱️ 45-50 min 🔐 SSH 🌐 Túneles 📊 Avanzado

SSH Avanzado y Túneles

SSH (Secure Shell) es mucho más que un protocolo para conexiones remotas seguras. Sus capacidades avanzadas incluyen la creación de túneles, port forwarding, proxies SOCKS, y multiplexado de conexiones. Estas funcionalidades son fundamentales para administradores de sistemas que trabajan con infraestructuras complejas y necesitan acceso seguro a recursos de red.

Los túneles SSH permiten encapsular tráfico de red inseguro dentro de conexiones SSH cifradas, proporcionando una capa adicional de seguridad y permitiendo el acceso a servicios que no están directamente disponibles desde nuestra ubicación de red.

Casos de Uso Comunes
  • Acceso a bases de datos: Conectar de forma segura a DB en redes privadas
  • Navegación segura: Usar el servidor como proxy para navegación web
  • Bypass de firewalls: Acceder a servicios bloqueados por firewalls corporativos
  • Desarrollo remoto: Acceder a servicios de desarrollo en servidores remotos

SSH Básico y Configuración

Conexiones SSH Básicas

Repaso de los comandos SSH fundamentales con opciones avanzadas.

SSH básico y opciones Copiar
# Conexión SSH básica
ssh [email protected]

# Especificar puerto personalizado
ssh -p 2222 [email protected]

# Usar clave SSH específica
ssh -i ~/.ssh/mi_clave_privada [email protected]

# Conexión con forwarding de X11 (aplicaciones gráficas)
ssh -X [email protected]

# Conexión con compresión (útil para conexiones lentas)
ssh -C [email protected]

# Modo verbose para debugging
ssh -v [email protected]

# Forzar autenticación por clave pública únicamente
ssh -o PreferredAuthentications=publickey [email protected]

# Conexión con timeout personalizado
ssh -o ConnectTimeout=10 [email protected]

# Ejecutar comando remoto y salir
ssh [email protected] "ls -la && df -h"

# Conexión sin verificación de host (solo para testing)
ssh -o StrictHostKeyChecking=no [email protected]

Configuración SSH Cliente

Optimizar la configuración SSH para uso profesional.

~/.ssh/config Copiar
# Configuración SSH profesional
# ~/.ssh/config

# Configuración global
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    TCPKeepAlive yes
    Compression yes
    
# Servidor de producción
Host prod-web
    HostName 192.168.1.100
    User admin
    Port 2222
    IdentityFile ~/.ssh/prod_key
    ForwardAgent yes
    
# Servidor de desarrollo
Host dev-web
    HostName dev.miempresa.com
    User developer
    IdentityFile ~/.ssh/dev_key
    LocalForward 3000 localhost:3000
    LocalForward 5432 localhost:5432
    
# Servidor jump/bastion
Host bastion
    HostName bastion.miempresa.com
    User sysadmin
    Port 22
    IdentityFile ~/.ssh/bastion_key
    
# Servidores detrás del bastion
Host prod-db
    HostName 10.0.1.50
    User dbadmin
    ProxyJump bastion
    IdentityFile ~/.ssh/db_key
    
# Servidor con múltiples túneles
Host tunnel-server
    HostName servidor.com
    User tunneluser
    IdentityFile ~/.ssh/tunnel_key
    LocalForward 8080 localhost:80
    LocalForward 8443 localhost:443
    LocalForward 3306 localhost:3306
    DynamicForward 1080
# Con la configuración anterior, ahora puedes usar:
$ ssh prod-web     # Se conecta automáticamente con todos los parámetros
$ ssh dev-web      # Incluye los port forwards automáticamente
$ ssh prod-db      # Usa bastion como jump host automáticamente

Port Forwarding (Túneles SSH)

Local Port Forwarding

Reenvía puertos desde tu máquina local a un servidor remoto.

Local Port Forwarding Copiar
# Sintaxis básica: ssh -L puerto_local:destino:puerto_destino usuario@servidor

# Ejemplo: Acceder a base de datos MySQL remota
ssh -L 3306:localhost:3306 [email protected]
# Ahora puedes conectar a localhost:3306 para acceder a la DB remota

# Ejemplo: Acceder a aplicación web interna
ssh -L 8080:192.168.1.50:80 [email protected]
# Accede vía http://localhost:8080

# Múltiples port forwards en una sola conexión
ssh -L 3306:db.interno.com:3306 -L 8080:web.interno.com:80 [email protected]

# Port forward en background
ssh -f -N -L 3306:localhost:3306 [email protected]

# Port forward con autenticación por clave
ssh -i ~/.ssh/mi_clave -L 5432:postgres-server:5432 [email protected]

Remote Port Forwarding

Permite que el servidor remoto acceda a servicios en tu máquina local.

Remote Port Forwarding Copiar
# Sintaxis: ssh -R puerto_remoto:destino:puerto_local usuario@servidor

# Ejemplo: Compartir servidor web local con servidor remoto
ssh -R 8080:localhost:80 [email protected]
# El servidor remoto puede acceder a tu web via localhost:8080

# Ejemplo: Permitir acceso remoto a servicio local
ssh -R 3000:localhost:3000 [email protected]
# Útil para demos o desarrollo colaborativo

# Remote forward en background
ssh -f -N -R 9000:localhost:8000 [email protected]

# Ejemplo práctico: Acceso a servicio detrás de NAT
ssh -R 2222:localhost:22 [email protected]
# Permite SSH reverso desde el servidor público

Dynamic Port Forwarding (SOCKS Proxy)

Crear un proxy SOCKS para enrutar todo el tráfico a través del servidor SSH.

SOCKS Proxy Copiar
# Crear proxy SOCKS5
ssh -D 1080 [email protected]

# Proxy SOCKS en background
ssh -f -N -D 1080 [email protected]

# Proxy SOCKS con puerto específico
ssh -D 127.0.0.1:1080 [email protected]

# Combinado con otros forwards
ssh -D 1080 -L 3306:localhost:3306 [email protected]

# Usar el proxy SOCKS con curl
curl --socks5 localhost:1080 http://sitio-bloqueado.com

# Configurar Firefox para usar el proxy SOCKS
# Preferences → Network → Settings → Manual proxy configuration
# SOCKS Host: localhost, Port: 1080, SOCKS v5

Scripts Avanzados para SSH

Gestor de Túneles SSH

Script completo para gestionar múltiples túneles SSH de forma automatizada.

ssh_tunnel_manager.sh Copiar
#!/bin/bash

# SSH Tunnel Manager - Gestión avanzada de túneles SSH
# Uso: ./ssh_tunnel_manager.sh [start|stop|status|list] [profile]

CONFIG_FILE="/etc/ssh_tunnels.conf"
PID_DIR="/var/run/ssh_tunnels"
LOG_FILE="/var/log/ssh_tunnels.log"

# Configuración por defecto
DEFAULT_CONFIG='
# Configuración de túneles SSH
# Formato: PROFILE:TYPE:LOCAL_PORT:REMOTE_HOST:REMOTE_PORT:SSH_HOST:SSH_USER:SSH_KEY

TUNNEL_PROFILES=(
    "web-dev:local:8080:localhost:80:webserver.com:admin:/home/admin/.ssh/web_key"
    "database:local:3306:db.internal.com:3306:gateway.com:dbadmin:/home/admin/.ssh/db_key"
    "proxy:dynamic:1080:::proxy.server.com:user:/home/admin/.ssh/proxy_key"
    "reverse-ssh:remote:2222:localhost:22:remote.server.com:admin:/home/admin/.ssh/reverse_key"
)
'

# Cargar configuración
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE" || echo "$DEFAULT_CONFIG" > "$CONFIG_FILE"

# Colores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

log_message() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
    
    case $level in
        ERROR)   echo -e "${RED}[ERROR]${NC} $message" ;;
        SUCCESS) echo -e "${GREEN}[SUCCESS]${NC} $message" ;;
        WARNING) echo -e "${YELLOW}[WARNING]${NC} $message" ;;
        INFO)    echo -e "${BLUE}[INFO]${NC} $message" ;;
    esac
}

# Crear directorios necesarios
setup_directories() {
    mkdir -p "$PID_DIR" "$(dirname "$LOG_FILE")"
}

# Parsear perfil de túnel
parse_profile() {
    local profile_string="$1"
    IFS=':' read -r profile type local_port remote_host remote_port ssh_host ssh_user ssh_key <<< "$profile_string"
    
    echo "PROFILE=$profile"
    echo "TYPE=$type"
    echo "LOCAL_PORT=$local_port"
    echo "REMOTE_HOST=$remote_host"
    echo "REMOTE_PORT=$remote_port"
    echo "SSH_HOST=$ssh_host"
    echo "SSH_USER=$ssh_user"
    echo "SSH_KEY=$ssh_key"
}

# Construir comando SSH para túnel
build_tunnel_command() {
    local type=$1
    local local_port=$2
    local remote_host=$3
    local remote_port=$4
    local ssh_host=$5
    local ssh_user=$6
    local ssh_key=$7
    
    local ssh_cmd="ssh -f -N -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes"
    
    # Agregar clave SSH si se especifica
    [[ -n "$ssh_key" && -f "$ssh_key" ]] && ssh_cmd="$ssh_cmd -i '$ssh_key'"
    
    # Agregar tipo de túnel
    case $type in
        local)
            ssh_cmd="$ssh_cmd -L ${local_port}:${remote_host}:${remote_port}"
            ;;
        remote)
            ssh_cmd="$ssh_cmd -R ${local_port}:${remote_host}:${remote_port}"
            ;;
        dynamic)
            ssh_cmd="$ssh_cmd -D $local_port"
            ;;
        *)
            log_message "ERROR" "Tipo de túnel desconocido: $type"
            return 1
            ;;
    esac
    
    ssh_cmd="$ssh_cmd ${ssh_user}@${ssh_host}"
    echo "$ssh_cmd"
}

# Iniciar túnel
start_tunnel() {
    local profile_string="$1"
    eval "$(parse_profile "$profile_string")"
    
    local pid_file="$PID_DIR/${PROFILE}.pid"
    
    # Verificar si ya está ejecutándose
    if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
        log_message "WARNING" "Túnel $PROFILE ya está ejecutándose"
        return 1
    fi
    
    log_message "INFO" "Iniciando túnel: $PROFILE"
    
    # Construir y ejecutar comando
    local tunnel_cmd=$(build_tunnel_command "$TYPE" "$LOCAL_PORT" "$REMOTE_HOST" "$REMOTE_PORT" "$SSH_HOST" "$SSH_USER" "$SSH_KEY")
    
    if [[ -z "$tunnel_cmd" ]]; then
        log_message "ERROR" "No se pudo construir comando para túnel $PROFILE"
        return 1
    fi
    
    log_message "INFO" "Ejecutando: $tunnel_cmd"
    
    # Ejecutar comando y guardar PID
    if eval "$tunnel_cmd"; then
        # Encontrar PID del proceso SSH creado
        local ssh_pid=$(pgrep -f "$SSH_HOST.*$LOCAL_PORT" | tail -1)
        
        if [[ -n "$ssh_pid" ]]; then
            echo "$ssh_pid" > "$pid_file"
            log_message "SUCCESS" "Túnel $PROFILE iniciado (PID: $ssh_pid)"
            
            # Verificar que el túnel funciona
            sleep 2
            if verify_tunnel "$TYPE" "$LOCAL_PORT"; then
                log_message "SUCCESS" "Túnel $PROFILE verificado correctamente"
            else
                log_message "WARNING" "Túnel $PROFILE iniciado pero la verificación falló"
            fi
            
            return 0
        else
            log_message "ERROR" "No se pudo obtener PID del túnel $PROFILE"
            return 1
        fi
    else
        log_message "ERROR" "Falló al iniciar túnel $PROFILE"
        return 1
    fi
}

# Detener túnel
stop_tunnel() {
    local profile_name="$1"
    local pid_file="$PID_DIR/${profile_name}.pid"
    
    if [[ ! -f "$pid_file" ]]; then
        log_message "WARNING" "Archivo PID no encontrado para $profile_name"
        return 1
    fi
    
    local pid=$(cat "$pid_file")
    
    if kill -0 "$pid" 2>/dev/null; then
        log_message "INFO" "Deteniendo túnel $profile_name (PID: $pid)"
        
        if kill "$pid"; then
            rm -f "$pid_file"
            log_message "SUCCESS" "Túnel $profile_name detenido"
            return 0
        else
            log_message "ERROR" "No se pudo detener túnel $profile_name"
            return 1
        fi
    else
        log_message "WARNING" "Proceso $pid para túnel $profile_name no existe"
        rm -f "$pid_file"
        return 1
    fi
}

# Verificar túnel
verify_tunnel() {
    local type="$1"
    local port="$2"
    
    case $type in
        local|dynamic)
            # Verificar que el puerto local está en escucha
            if netstat -ln | grep -q ":$port "; then
                return 0
            else
                return 1
            fi
            ;;
        remote)
            # Para remote tunnels, verificar conexión SSH
            return 0
            ;;
    esac
}

# Mostrar estado de túneles
show_status() {
    echo -e "${BLUE}=== Estado de Túneles SSH ===${NC}"
    printf "%-15s %-8s %-10s %-20s %-10s %s\n" "PERFIL" "TIPO" "PUERTO" "DESTINO" "PID" "ESTADO"
    echo "─────────────────────────────────────────────────────────────────────────────"
    
    for profile_string in "${TUNNEL_PROFILES[@]}"; do
        eval "$(parse_profile "$profile_string")"
        
        local pid_file="$PID_DIR/${PROFILE}.pid"
        local pid_status="No ejecutándose"
        local pid_value="N/A"
        
        if [[ -f "$pid_file" ]]; then
            local pid=$(cat "$pid_file")
            if kill -0 "$pid" 2>/dev/null; then
                pid_status="✅ Ejecutándose"
                pid_value="$pid"
            else
                pid_status="❌ Proceso muerto"
                rm -f "$pid_file"
            fi
        fi
        
        local destination="$REMOTE_HOST:$REMOTE_PORT"
        [[ "$TYPE" == "dynamic" ]] && destination="SOCKS Proxy"
        
        printf "%-15s %-8s %-10s %-20s %-10s %s\n" "$PROFILE" "$TYPE" "$LOCAL_PORT" "$destination" "$pid_value" "$pid_status"
    done
}

# Listar perfiles disponibles
list_profiles() {
    echo -e "${BLUE}=== Perfiles Disponibles ===${NC}"
    
    for profile_string in "${TUNNEL_PROFILES[@]}"; do
        eval "$(parse_profile "$profile_string")"
        
        echo -e "${GREEN}$PROFILE${NC}"
        echo "  Tipo: $TYPE"
        echo "  Puerto local: $LOCAL_PORT"
        [[ "$TYPE" != "dynamic" ]] && echo "  Destino: $REMOTE_HOST:$REMOTE_PORT"
        echo "  SSH: $SSH_USER@$SSH_HOST"
        echo
    done
}

# Iniciar todos los túneles
start_all() {
    log_message "INFO" "Iniciando todos los túneles..."
    
    local success_count=0
    local total_count=0
    
    for profile_string in "${TUNNEL_PROFILES[@]}"; do
        ((total_count++))
        if start_tunnel "$profile_string"; then
            ((success_count++))
        fi
    done
    
    log_message "INFO" "Túneles iniciados: $success_count/$total_count"
}

# Detener todos los túneles
stop_all() {
    log_message "INFO" "Deteniendo todos los túneles..."
    
    for profile_string in "${TUNNEL_PROFILES[@]}"; do
        eval "$(parse_profile "$profile_string")"
        stop_tunnel "$PROFILE"
    done
}

# Reiniciar túnel específico
restart_tunnel() {
    local profile_name="$1"
    
    log_message "INFO" "Reiniciando túnel: $profile_name"
    
    # Buscar perfil
    local found_profile=""
    for profile_string in "${TUNNEL_PROFILES[@]}"; do
        eval "$(parse_profile "$profile_string")"
        if [[ "$PROFILE" == "$profile_name" ]]; then
            found_profile="$profile_string"
            break
        fi
    done
    
    if [[ -z "$found_profile" ]]; then
        log_message "ERROR" "Perfil no encontrado: $profile_name"
        return 1
    fi
    
    stop_tunnel "$profile_name"
    sleep 2
    start_tunnel "$found_profile"
}

# Mostrar ayuda
show_help() {
    cat << EOF
Uso: $0 COMANDO [PERFIL]

COMANDOS:
    start PERFIL       - Iniciar túnel específico
    stop PERFIL        - Detener túnel específico
    restart PERFIL     - Reiniciar túnel específico
    start-all          - Iniciar todos los túneles
    stop-all           - Detener todos los túneles
    status             - Mostrar estado de túneles
    list               - Listar perfiles disponibles

EJEMPLOS:
    $0 start web-dev
    $0 stop database
    $0 restart proxy
    $0 start-all
    $0 status

CONFIGURACIÓN:
    Archivo de configuración: $CONFIG_FILE
EOF
}

# Función principal
main() {
    setup_directories
    
    case "${1:-}" in
        start)
            if [[ -z "$2" ]]; then
                log_message "ERROR" "Nombre de perfil requerido"
                exit 1
            fi
            
            # Buscar perfil por nombre
            local found_profile=""
            for profile_string in "${TUNNEL_PROFILES[@]}"; do
                eval "$(parse_profile "$profile_string")"
                if [[ "$PROFILE" == "$2" ]]; then
                    found_profile="$profile_string"
                    break
                fi
            done
            
            if [[ -z "$found_profile" ]]; then
                log_message "ERROR" "Perfil no encontrado: $2"
                exit 1
            fi
            
            start_tunnel "$found_profile"
            ;;
        stop)
            if [[ -z "$2" ]]; then
                log_message "ERROR" "Nombre de perfil requerido"
                exit 1
            fi
            stop_tunnel "$2"
            ;;
        restart)
            if [[ -z "$2" ]]; then
                log_message "ERROR" "Nombre de perfil requerido"
                exit 1
            fi
            restart_tunnel "$2"
            ;;
        start-all)
            start_all
            ;;
        stop-all)
            stop_all
            ;;
        status)
            show_status
            ;;
        list)
            list_profiles
            ;;
        -h|--help|help)
            show_help
            exit 0
            ;;
        *)
            log_message "ERROR" "Comando desconocido: ${1:-}"
            show_help
            exit 1
            ;;
    esac
}

# Ejecutar si es llamado directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

SSH Jump Hosts (Bastion Hosts)

ProxyJump y Conexiones en Cadena

Técnicas para conectar a servidores a través de hosts intermedios.

Jump Hosts Copiar
# Método moderno: ProxyJump (OpenSSH 7.3+)
ssh -J [email protected] [email protected]

# Múltiples saltos
ssh -J [email protected],[email protected] [email protected]

# Con configuración en ~/.ssh/config
Host bastion
    HostName bastion.miempresa.com
    User admin
    IdentityFile ~/.ssh/bastion_key

Host servidor-interno
    HostName 10.0.1.100
    User appuser
    ProxyJump bastion
    IdentityFile ~/.ssh/interno_key

# Ahora solo necesitas: ssh servidor-interno

# Método tradicional: ProxyCommand
ssh -o ProxyCommand="ssh -W %h:%p [email protected]" usuario@servidor-interno

# Port forwarding a través de jump host
ssh -J bastion -L 3306:db-server:3306 usuario@app-server

# SFTP a través de jump host
sftp -o ProxyJump=bastion usuario@servidor-interno

Multiplexado de Conexiones SSH

Connection Sharing

Optimizar conexiones SSH reutilizando conexiones existentes.

SSH Multiplexing Copiar
# Configuración en ~/.ssh/config para multiplexing
Host *
    ControlMaster auto
    ControlPath ~/.ssh/connections/%r@%h:%p
    ControlPersist 600

# Crear directorio para control sockets
mkdir -p ~/.ssh/connections

# Primera conexión crea el master
ssh [email protected]

# Conexiones subsiguientes reutilizan el master (mucho más rápidas)
ssh [email protected] "uptime"
scp archivo.txt [email protected]:/tmp/
sftp [email protected]

# Verificar conexiones activas
ssh -O check [email protected]

# Cerrar master connection
ssh -O exit [email protected]

# Listar conexiones activas
ls -la ~/.ssh/connections/

Scripts de Administración SSH

Sistema de Gestión SSH Completo

Script para administrar múltiples servidores SSH:

ssh_admin.sh Copiar
#!/bin/bash

# SSH Administration Tool
# Gestión centralizada de múltiples servidores SSH

SERVERS_CONFIG="/etc/ssh_servers.conf"
LOG_FILE="/var/log/ssh_admin.log"

# Configuración por defecto
DEFAULT_CONFIG='
# Configuración de servidores
# Formato: ALIAS:HOSTNAME:USER:PORT:KEY_FILE:DESCRIPTION

SERVERS=(
    "web1:web1.miempresa.com:admin:22:/root/.ssh/web_key:Servidor Web Principal"
    "web2:web2.miempresa.com:admin:22:/root/.ssh/web_key:Servidor Web Backup"
    "db1:db1.miempresa.com:dbadmin:2222:/root/.ssh/db_key:Base de Datos Principal"
    "jump:bastion.miempresa.com:sysadmin:22:/root/.ssh/jump_key:Servidor Bastion"
)
'

# Cargar configuración
[[ -f "$SERVERS_CONFIG" ]] && source "$SERVERS_CONFIG" || echo "$DEFAULT_CONFIG" > "$SERVERS_CONFIG"

# Colores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

log_message() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
    
    case $level in
        ERROR)   echo -e "${RED}[ERROR]${NC} $message" ;;
        SUCCESS) echo -e "${GREEN}[SUCCESS]${NC} $message" ;;
        WARNING) echo -e "${YELLOW}[WARNING]${NC} $message" ;;
        INFO)    echo -e "${BLUE}[INFO]${NC} $message" ;;
    esac
}

# Parsear configuración de servidor
parse_server() {
    local server_string="$1"
    IFS=':' read -r alias hostname user port key_file description <<< "$server_string"
    
    echo "ALIAS=$alias"
    echo "HOSTNAME=$hostname"
    echo "USER=$user"
    echo "PORT=$port"
    echo "KEY_FILE=$key_file"
    echo "DESCRIPTION=$description"
}

# Conectar a servidor
connect_server() {
    local server_alias="$1"
    
    # Buscar servidor
    local found_server=""
    for server_string in "${SERVERS[@]}"; do
        eval "$(parse_server "$server_string")"
        if [[ "$ALIAS" == "$server_alias" ]]; then
            found_server="$server_string"
            break
        fi
    done
    
    if [[ -z "$found_server" ]]; then
        log_message "ERROR" "Servidor no encontrado: $server_alias"
        return 1
    fi
    
    log_message "INFO" "Conectando a $ALIAS ($HOSTNAME)"
    
    local ssh_cmd="ssh -p $PORT"
    [[ -f "$KEY_FILE" ]] && ssh_cmd="$ssh_cmd -i $KEY_FILE"
    ssh_cmd="$ssh_cmd ${USER}@${HOSTNAME}"
    
    eval "$ssh_cmd"
}

# Ejecutar comando en servidor(es)
execute_command() {
    local server_alias="$1"
    local command="$2"
    
    if [[ "$server_alias" == "all" ]]; then
        log_message "INFO" "Ejecutando comando en todos los servidores: $command"
        
        for server_string in "${SERVERS[@]}"; do
            eval "$(parse_server "$server_string")"
            execute_on_single_server "$server_string" "$command"
        done
    else
        # Buscar servidor específico
        local found_server=""
        for server_string in "${SERVERS[@]}"; do
            eval "$(parse_server "$server_string")"
            if [[ "$ALIAS" == "$server_alias" ]]; then
                found_server="$server_string"
                break
            fi
        done
        
        if [[ -z "$found_server" ]]; then
            log_message "ERROR" "Servidor no encontrado: $server_alias"
            return 1
        fi
        
        execute_on_single_server "$found_server" "$command"
    fi
}

# Ejecutar comando en un servidor específico
execute_on_single_server() {
    local server_string="$1"
    local command="$2"
    
    eval "$(parse_server "$server_string")"
    
    log_message "INFO" "Ejecutando en $ALIAS: $command"
    
    local ssh_cmd="ssh -p $PORT -o ConnectTimeout=10 -o BatchMode=yes"
    [[ -f "$KEY_FILE" ]] && ssh_cmd="$ssh_cmd -i $KEY_FILE"
    ssh_cmd="$ssh_cmd ${USER}@${HOSTNAME} \"$command\""
    
    echo -e "\n${BLUE}=== $ALIAS ($HOSTNAME) ===${NC}"
    
    if eval "$ssh_cmd"; then
        log_message "SUCCESS" "Comando ejecutado exitosamente en $ALIAS"
    else
        log_message "ERROR" "Comando falló en $ALIAS"
    fi
}

# Verificar conectividad
check_connectivity() {
    local server_alias="$1"
    
    if [[ "$server_alias" == "all" ]]; then
        log_message "INFO" "Verificando conectividad a todos los servidores"
        
        echo -e "${BLUE}=== Test de Conectividad ===${NC}"
        printf "%-10s %-25s %-15s %-10s %s\n" "ALIAS" "HOSTNAME" "USER" "PUERTO" "ESTADO"
        echo "─────────────────────────────────────────────────────────────────────"
        
        for server_string in "${SERVERS[@]}"; do
            eval "$(parse_server "$server_string")"
            
            local ssh_cmd="ssh -p $PORT -o ConnectTimeout=5 -o BatchMode=yes"
            [[ -f "$KEY_FILE" ]] && ssh_cmd="$ssh_cmd -i $KEY_FILE"
            
            if $ssh_cmd "${USER}@${HOSTNAME}" "echo 'OK'" &>/dev/null; then
                printf "%-10s %-25s %-15s %-10s %s\n" "$ALIAS" "$HOSTNAME" "$USER" "$PORT" "✅ OK"
            else
                printf "%-10s %-25s %-15s %-10s %s\n" "$ALIAS" "$HOSTNAME" "$USER" "$PORT" "❌ FALLO"
            fi
        done
    else
        # Verificar servidor específico
        local found_server=""
        for server_string in "${SERVERS[@]}"; do
            eval "$(parse_server "$server_string")"
            if [[ "$ALIAS" == "$server_alias" ]]; then
                found_server="$server_string"
                break
            fi
        done
        
        if [[ -z "$found_server" ]]; then
            log_message "ERROR" "Servidor no encontrado: $server_alias"
            return 1
        fi
        
        eval "$(parse_server "$found_server")"
        
        local ssh_cmd="ssh -p $PORT -o ConnectTimeout=10 -v"
        [[ -f "$KEY_FILE" ]] && ssh_cmd="$ssh_cmd -i $KEY_FILE"
        
        log_message "INFO" "Probando conectividad a $ALIAS"
        $ssh_cmd "${USER}@${HOSTNAME}" "echo 'Conexión exitosa a $ALIAS'"
    fi
}

# Listar servidores
list_servers() {
    echo -e "${BLUE}=== Servidores Configurados ===${NC}"
    printf "%-10s %-25s %-15s %-10s %s\n" "ALIAS" "HOSTNAME" "USER" "PUERTO" "DESCRIPCIÓN"
    echo "──────────────────────────────────────────────────────────────────────────────────"
    
    for server_string in "${SERVERS[@]}"; do
        eval "$(parse_server "$server_string")"
        printf "%-10s %-25s %-15s %-10s %s\n" "$ALIAS" "$HOSTNAME" "$USER" "$PORT" "$DESCRIPTION"
    done
}

# Transferir archivo
transfer_file() {
    local direction="$1"    # upload o download
    local server_alias="$2"
    local source="$3"
    local destination="$4"
    
    # Buscar servidor
    local found_server=""
    for server_string in "${SERVERS[@]}"; do
        eval "$(parse_server "$server_string")"
        if [[ "$ALIAS" == "$server_alias" ]]; then
            found_server="$server_string"
            break
        fi
    done
    
    if [[ -z "$found_server" ]]; then
        log_message "ERROR" "Servidor no encontrado: $server_alias"
        return 1
    fi
    
    eval "$(parse_server "$found_server")"
    
    local scp_cmd="scp -P $PORT"
    [[ -f "$KEY_FILE" ]] && scp_cmd="$scp_cmd -i $KEY_FILE"
    
    case $direction in
        upload)
            log_message "INFO" "Subiendo $source a $ALIAS:$destination"
            scp_cmd="$scp_cmd \"$source\" \"${USER}@${HOSTNAME}:$destination\""
            ;;
        download)
            log_message "INFO" "Descargando $ALIAS:$source a $destination"
            scp_cmd="$scp_cmd \"${USER}@${HOSTNAME}:$source\" \"$destination\""
            ;;
        *)
            log_message "ERROR" "Dirección inválida: $direction (usa upload o download)"
            return 1
            ;;
    esac
    
    if eval "$scp_cmd"; then
        log_message "SUCCESS" "Transferencia completada"
    else
        log_message "ERROR" "Transferencia falló"
        return 1
    fi
}

# Mostrar ayuda
show_help() {
    cat << EOF
SSH Administration Tool

Uso: $0 COMANDO [OPCIONES]

COMANDOS:
    connect ALIAS              - Conectar a servidor
    exec ALIAS "COMANDO"       - Ejecutar comando en servidor
    exec all "COMANDO"         - Ejecutar comando en todos los servidores
    check ALIAS               - Verificar conectividad a servidor
    check all                 - Verificar conectividad a todos
    upload ALIAS SRC DEST     - Subir archivo a servidor
    download ALIAS SRC DEST   - Descargar archivo de servidor
    list                      - Listar servidores configurados

EJEMPLOS:
    $0 connect web1
    $0 exec db1 "systemctl status mysql"
    $0 exec all "uptime"
    $0 check all
    $0 upload web1 ./app.tar.gz /tmp/
    $0 download db1 /var/log/mysql.log ./
    $0 list

CONFIGURACIÓN:
    Archivo: $SERVERS_CONFIG
EOF
}

# Función principal
main() {
    case "${1:-}" in
        connect)
            [[ -z "$2" ]] && { log_message "ERROR" "Alias de servidor requerido"; exit 1; }
            connect_server "$2"
            ;;
        exec)
            [[ -z "$2" || -z "$3" ]] && { log_message "ERROR" "Alias y comando requeridos"; exit 1; }
            execute_command "$2" "$3"
            ;;
        check)
            [[ -z "$2" ]] && { log_message "ERROR" "Alias de servidor requerido (o 'all')"; exit 1; }
            check_connectivity "$2"
            ;;
        upload)
            [[ -z "$2" || -z "$3" || -z "$4" ]] && { log_message "ERROR" "Alias, fuente y destino requeridos"; exit 1; }
            transfer_file "upload" "$2" "$3" "$4"
            ;;
        download)
            [[ -z "$2" || -z "$3" || -z "$4" ]] && { log_message "ERROR" "Alias, fuente y destino requeridos"; exit 1; }
            transfer_file "download" "$2" "$3" "$4"
            ;;
        list)
            list_servers
            ;;
        -h|--help|help)
            show_help
            exit 0
            ;;
        *)
            log_message "ERROR" "Comando desconocido: ${1:-}"
            show_help
            exit 1
            ;;
    esac
}

# Ejecutar si es llamado directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

Ejercicios Prácticos Avanzados

Ejercicio 1: Túnel SSH para Base de Datos
  1. Configura un túnel SSH para acceder a una base de datos remota
  2. Crea un script que inicie el túnel automáticamente
  3. Implementa verificación de que el túnel está funcionando
  4. Añade logging y notificaciones de fallos
Proyecto: VPN Casera con SSH

Implementa una solución VPN completa usando SSH:

  • Proxy SOCKS5 permanente
  • Script de inicio/parada automático
  • Configuración automática del navegador
  • Monitoreo de la conexión
  • Fallback a múltiples servidores
Consideraciones de Seguridad
  • Claves SSH: Usa claves específicas para cada propósito
  • Permisos: Configura permisos restrictivos (600) en claves privadas
  • Timeouts: Configura timeouts apropiados para conexiones
  • Auditoría: Registra todas las conexiones y transferencias
  • Firewall: Limita acceso SSH solo desde IPs autorizadas
Casos de Uso Empresariales
  • Desarrollo: Acceso seguro a entornos de desarrollo
  • DevOps: Despliegues automatizados a través de bastion hosts
  • Monitoreo: Acceso a sistemas de monitoreo internos
  • Backup: Transferencias seguras de backups
  • Troubleshooting: Acceso de emergencia a sistemas críticos