PfCentinel V 1.0 Beta – Manager Update Platform/Packages for pfSense Multisite


  • Hola

    Una aportación a la causa :)

    pfCentinel V 1.0 Beta – Manager Update Platform/Packages for pfSense Multisite
    pfCentinel V 1.0 Beta – Gestor de actualizaciones de plataforma/paquetes para varios sitios pfSense

    Esta utilidad sirve para tener un control de actualizaciones de la plataforma y los paquetes de varios pfSense.

    Son dos scripts en php que se ejecutan desde un host pfSense (al que llamaré central) y efectua vía php conexiones ssh2 sobre los hosts pfSense remotos a controlar sus actualizaciones con comandos ssh/bash

    Condiciones necesarias:
    Tanto el host pfSense (central) que corre los scripts como los hosts pfSense remotos para comprobar las actualizaciones deben de tener activada la Consola ssh
    ( Enable Secure Shell – sshd ) y ser accesibles vía puerto tcp22.

    (Esto es fácil de comprobar: hacer desde la shell del pfSense central un ssh sobre otro pfSense remoto
    para el usuario root: # ssh root@ip–otro-pfSense y ver si hay conectividad y acceso/validación ok)

    Condiciones recomendables:
    Se recomienda que el host que ejecuta los scripts se conecte a los otros hosts vía Tunel (openVPN o IPsec), ya que se supone que los otros hosts son remotos y externos a la LAN del pfSense Central. Y por defecto pfSense bloquea en su interfaz WAN este tipo de acceso.
    Es decir evitar dejar un acceso ssh vía WAN a los pfSense y trabajar de forma segura vía tunnel.

    La utilidad se compone de 2 scripts php

    1.- pfCentinel-Setup.php
    2.- pfCentinel-central.php

    El 1.- pfCentinel-Setup.php sirve para crear la base de datos de los hosts a controlar o añadir más host a esa bbdd.

    La bbdd es un simpe fichero csv, alojado en la ruta /scripts/machines.csv

    La bbdd guarda en sus registros los campos:

    “descr”;”host”;”platform”;”packages”;”user”;”password”

    -Descripción del pfSense remoto, por ejemplo: pfMadrid
    -Host del pfSense remoto, la IP del pfSense remoto (altamente recomendable que sea una IP de un tunel openVPN o IPSec)
    -Plataforma: la plataforma del pfSense remoto y si necesita update y la versión disponible del update
    Valores posibles (por ejemplo):

    CURRENT-2.3.1_5 = Esta al día, no hay update
    2.3-UPDATE-to-2.3.1_5 = HAY UN UPDATE (versión actual – UPDATE a – versión disponible)
    NO_CONEXION = No hay conectividad al host remoto
    ERROR_AUTENTICACION = Hay conectividad pero hay un error de validación ssh2 de usurio:clave en host remoto
    -Packages: Si existe algún paquete para actualizar
    Valores posibles:

    CURRENT = Está al día, no hay updates de paquetes
    NO_CONEXION = No hay conectividad al host remoto
    ERROR_AUTENTICACION = Hay conectividad pero hay un error de validación ssh2 de usurio:clave en host remoto
    UPDATES = Hay actualizaciones de paquetes disponibles.
    -User, el usuario para conectarse vía ssh2 al host remoto, usualmente root
    -Password, la clave de User para acceder vía ssh2 al host remoto.
    En este campo se guarda la clave encriptada para cada host

    Se recomienda alojar este script php cli: pfCentinel-Setup.php en un directorio no accesible desde web gui, como por ejemplo el directorio /scripts , o /tmp

    Para ejecutar este script (se hace vía shell), si estuviera alojado en la carpeta scripts:

    /usr/local/bin/php /scripts/pfCentinel-Setup.php
    

    Al ejecutar viá shell o cli o consola el script pfCentinel-Setup.php, la primera vez, nos irá preguntando por los datos de cada host
    (cuando pregunte la clave, no se verá por consola, y se guarda de forma encriptada en machines.csv, por seguridad)

    [2.3-RELEASE][root@pfLEON.localdomain.local]/root: /usr/local/bin/php /scripts/pfCentinel-Setup.php
    ###################################################
    #        Setup    pfCentinel      2016        #
    ###################################################
    #by Javier Castan?on.javcasta https://javcasta.com#
    ###################################################
    ###################################################
    #  host pfCentinel-Setup.php  in /tmp  folder    #
    ###################################################

    run via shell: # php /tmp/pfCentinel-Setup.php

    ###################################################

    Introduccion de datos para Host 1
    Introduce descripcion Host (p.e: pfMadrid): pftest1
    Introduce IP Host (p.e: 10.0.0.254): 10.20.10.204
    Introduce el usuario para el host pftest1 (p.e: root): root
    Introduce la clave para el usuario root:
    Testeando conectividad, usurio:clave ...
    Host: 10.20.10.204

    NO CONEXION
    An?adimos pftest1 a /scripts/machines.csv ?(y/n):n
    Continuamos? (y/n): y
    ...
    Introduccion de datos para Host 2
    Introduce descripcion Host (p.e: pfMadrid):
    ...

    Y hará un test de conectividad, si este da NO CONEXION es que no hay conectividad a ese host vía ssh2,
    pero se puede añadir a la bbdd ( An?adimos pftest1 a /scripts/machines.csv ?(y/n):y ) u optar por no añadirlo

    Si da un ERROR DE AUTENTICACION, idem al caso anterior, lo normal seria no añadirlo a la bbdd hasta que nos de un Test OK

    Introduccion de datos para Host 3
    Introduce descripcion Host (p.e: pfMadrid): pferraut
    Introduce IP Host (p.e: 10.0.0.254): 192.168.56.243
    Introduce el usuario para el host pferraut (p.e: root): root2
    Introduce la clave para el usuario root2:
    Testeando conectividad, usurio:clave …
    Host: 192.168.56.243

    ERROR DE AUTENTICACION
    An?adimos pferraut a /scripts/machines.csv ?(y/n):n
    Continuamos? (y/n):y

    Y si el test es OK nos mostrará el estado de UPDATES de plataforma y paquetes


    Introduccion de datos para Host 1
    Introduce descripcion Host (p.e: pfMadrid): pfTest5
    Introduce IP Host (p.e: 10.0.0.254): 192.168.56.243
    Introduce el usuario para el host pfTest5 (p.e: root): root
    Introduce la clave para el usuario root:
    Testeando conectividad, usurio:clave ...
    Host: 192.168.56.243

    HAY UPDATES PAQUETES: Paquetes a actualizar:
    pfSense-pkg-openvpn-client-export-1.3.7_1
    pfSense-pkg-pfBlockerNG-2.0.10
    pfSense-pkg-squid-0.4.16_2
    pfSense-pkg-squidGuard-1.14_2
    pfSense-pkg-suricata-3.0_6

    Version pfSense actual: 2.3

    Version pfSense disponible: 2.3.1_5

    HAY UPDATE PLATAFORMA

    An?adimos pfTest5 a /scripts/machines.csv ?(y/n):y

    Para cortar el bucle del script, a la pregunta:

    Continuamos? (y/n):n

    Contestar n ( = no), y para continuar, contestar y ( = yes)

    Si ya existe el fichero de la bbdd /scripts/machines.csv al volver a ejecutarse este script,
    nos avisará mostrando la bbdd y dandonos opción a añadir más hosts

    Una vez tengamos la bbdd lista, ya podemos ejecutar desde web gui el script:

    2.- pfCentinel-central.php

    Este script hay que alojarlo en la ruta:

    /usr/local/www/
        o
        /usr/local/www/pfCentinel/ (u otra subcarpeta debajo de www)

    para que sea accesible vía web gui.

    Accedemos a la web gui del pfSense central, nos validamos user:pass, y en el navegador indicamos la ruta al script:
    Por ejemplo:

    https://10.20.10.203/pfCentinel/pfCentinel-central.php

    si lo hubiésemos alojado el script en /usr/local/www/pfCentinel-central.php el link seria:

    https://10.20.10.203/pfCentinel-central.php

    Para 5 hosts tardará de media 1 minuto o menos el script pfCenter-central.php
    Por cada host se toma de 10 a 30 sg, en mi entorno de red, para unos 9 host me ha tardado un minuto (58sg).
    Hasta que el script no haya recorrido todos los registros de /scripts/machines.csv no va a mostrar nada en el navegador
    así que paciencia 🙂

    Así que recomiendop no tener una bbdd de más de 15 registros, ya que puede dar el error Gateway Time Out el script y no mostrar nada.

    Por lo que para empezar yo probaria la primera vez el script con no más de 10 host pfSense en la bbdd, con 5 seria un buen test.
    (De todas formas pocos admins tienen más de 15 pfSense que administrar 🙂 , aunque haberlos haylos)

    (En otra versión más adelante cuando controle mejor AJAX (solo con php no se puede mostrar cada registro uno a uno),
    podré subsanar esto haciendo que se vaya mostrando cada registro en el navegador en tiempo real)

    Una vez termine el scriopt se mostrarán los resultados, y en caso de algún registro con un UPDATE presentará el link para su actualización
    (el link funcionará si hay una web gui validada a ese host pfSense)

    Se podria automatizar la actualización de la platforma y/o paquetes vía script, pero creo que es un asunto delicado dejar automatizada
    updates en un firewall, ya que es la piedra angular o factor crítico de cualquier infraestructura de una red.
    Mejor que sea asistida por un técnico o admin, siempre pueden haber sorpresas… 🙂

    La página que muestra tiene un campo: Remote Date Last refresh. Muestra la última comprobación de Update del script, pero con la fecha y hora remota
    ,no la del host central

    La tabla que se muestra en la página se puede ordenar clikando en los campos, como por ejemplo en el campo: Remote Date Last refresh

    La página tiene un botón Refresh, para volver a lanzar el script, no recomiendo hacer esto más de una  o dos veces al día en un pfSense central en
    producción con alta carga de red, ya que el script abre sockets (tcp22) y consume tiempo de cpu y algo de cantidad de ram, y ya se sabe que un firewall, cuanto
    menos se le sature, mejor.

    Si se ejecutara este script pfCentinel-central.php sin que exista /scripts/machines.csv daria el aviso:

    Ejecute vía shell pfCentinel-Setup.php para crear fichero machines.csv, y depues de crearlo, refresque esta página.

    EXECUTE via shell pfCentinel-setup.php for CREATE FILE machines.csv:
        after create it, refresh this page

    Salu2

    Referencia:
    https://www.javcasta.com/pfcentinel-v-1-0-beta-manager-update-platformpackages-for-pfsense-multisite/

    Descarga/Download pfCentinel-V1-Beta: http://www.javcasta.com/?smd_process_download=1&download_id=33256


  • Me funciona bien. Andaba buscando algo parecido hace tiempo. Gracias


  • Hola.

    Gracias a ti por comunicar su funcionalidad. :)

    Salu2


  • Hola

    Posteo el código (está en mi site, pero a modo de backup y poder leerlo sin hacer download es lo mejor)

    /scripts/pfCentinel-Setup.php

    
    /*
    	pfCentinel-Setup.php
    
    	Copyright (c) 2016 Javier Castañon
      javier@javcasta.com - https://javcasta.com/
    	All rights reserved.
    
    	Redistribution and use in source and binary forms, with or without
    	modification, are permitted provided that the following conditions are met:
    
    	1\. Redistributions of source code must retain the above copyright notice,
    	   this list of conditions and the following disclaimer.
    
    	2\. Redistributions in binary form must reproduce the above copyright
    	   notice, this list of conditions and the following disclaimer in the
    	   documentation and/or other materials provided with the distribution.
    
    	THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
    	INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    	AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    	AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
    	OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    	POSSIBILITY OF SUCH DAMAGE.
    */
    
    set_time_limit(0);
    ini_set('max_execution_time', '0');
    //si no existe dir /scripts lo creamos
    if(!is_dir('/scripts')) {
      mkdir('/scripts');
    }
    
    $existemachines = false;
    if (file_exists('/scripts/machines.csv')) {
      $existemachines = true;    
    }
    
    echo "\033[34m################################################### \033[0m \n";
    echo "\033[31m#         Setup     pfCentinel       2016         # \033[0m \n";
    echo "\033[34m################################################### \033[0m \n";
    echo "\033[34m#by Javier Castan?on.javcasta https://javcasta.com# \033[0m \n";
    echo "\033[34m################################################### \033[0m \n";
    echo "\033[34m################################################### \033[0m \n";
    echo "\033[34m#   host pfCentinel-Setup.php  in /tmp  folder    # \033[0m \n";
    echo "\033[34m################################################### \033[0m \n";
    echo "\033[34m# run via shell: # php /tmp/pfCentinel-Setup.php  # \033[0m \n";
    echo "\033[34m################################################### \033[0m \n";
    // rutina introduccion datos > /scrips/machines.csv
    $goon = true;
    $i = 0;
    
    // condiciones necesarias:
    // cada host tenga habilitado sshd (Enable Secure Shell) en puerto tcp22
    // y no tengan restricciones a conectarse vía ssh al tcp22 desde otro host
    // por lo que es muy recomendable que las IPs de los hosts sean accesibles
    // vía tunel openVPN o IPSec , etc
    
    //Para 5 hosts de media tardará 1 minuto el script pfCenter-central.php
    //por cada host se toma de 10 a 30 sg, para 5 hosts: [ 50 .. 150 ] sg
    
    if ($existemachines) { 
      echo "\n Ya existe /scripts/machines.csv \n";
      $muestra = shell_exec("/bin/cat /scripts/machines.csv");
      echo $muestra . "\n";
      $adddata = readline("Introducimos mas hosts a /scripts/machines.csv ? (y/n): ");
      if ($adddata == "n") die("\nFin.\n");
    }
    
    while ( $goon ) {
      //&& $i <= 4
      $i++;
      echo "...\n";
      echo "\033[31m Introduccion de datos para Host $i \033[0m \n";
      $descripcion = readline("Introduce descripcion Host (p.e: pfMadrid): ");
      $elhost = readline("Introduce IP Host (p.e: 10.0.0.254): ");
      $eluser = readline("Introduce el usuario para el host $descripcion (p.e: root): ");
      echo "Introduce la clave para el usuario $eluser: ";
      `/bin/stty -echo`;
      $lapass = readline();
      `/bin/stty echo`;
      echo "\n";
      echo "Testeando conectividad, usurio:clave ...\n";
      @fdoit($elhost, 22, $eluser, $lapass);
      $escribir = readline("An?adimos $descripcion a /scripts/machines.csv ?(y/n):");
      if ($escribir == "y") fmachinesupdate($descripcion, $elhost, "xxx", "xxx", $eluser, fenydesencripta($lapass, true)); 
      $continuar = readline("Continuamos? (y/n): ");
      if ( $continuar == "n" ) $goon = false;
    
    }
    
    echo "END.\n";
    
    function fdoit($host, $port, $username, $password) {
      $paquetes = "";
      $comando = "pkg version > /tmp/pfcenter-status.tmp";
      fshh($host, $port, $username, $password, $comando);
      echo "Host: $host\n";
      echo "=====================\n";
      $comando = "cat /tmp/pfcenter-status.tmp | grep '^pfSense-pkg' | grep '<' | cut -f 1 -d ' '";
      $paquetes = @fshh($host, $port, $username, $password, $comando);
      if (strpos($paquetes, "ERROR_AUTENTICACION") !== false) { echo "ERROR DE AUTENTICACION\n"; return "ERROR DE AUTENTICACION"; }
      if (strpos($paquetes, "NO_CONEXION") !== false) { echo "NO CONEXION\n"; return "NO CONEXION"; }
      if (strlen($paquetes) < 5) { echo "NO HAY UPDATES PAQUETES\n"; echo " \n"; }
      else echo "HAY UPDATES PAQUETES: Paquetes a actualizar:\n" . $paquetes . "\n";
      $comando = "cat /tmp/pfcenter-status.tmp | grep '^pfSense-2' | cut -c 9- | cut -f 1 -d ' '";
      $vactual = fshh($host, $port, $username, $password, $comando);
      echo "Version pfSense actual: " . $vactual . "\n";
      $comando = "pkg rquery %v pfSense";
      $vdisponible = fshh($host, $port, $username, $password, $comando);
      echo "Version pfSense disponible: " . $vdisponible . "\n";
      if ($vactual !== $vdisponible) echo "HAY UPDATE PLATAFORMA\n";
      echo " \n";
    }
    
    function fpingssh($phost, $pport) {
      $waitTimeoutInSeconds = 2;
      try {
      if($fp = @fsockopen($phost,$pport,$errCode,$errStr,$waitTimeoutInSeconds)){   
       // It worked 
       return true;
      } else {
       // It didn't work
       return false; 
      } 
      fclose($fp);
      } catch(Exception $e) { return false; }
    }
    
    function fshh ( $vhost, $vport, $vuser, $vpass, $vcomando ){
      //if(!$connection) $connection = ssh2_connect($vhost, $vport)or die("The SSH2 connection could not be established.");
      if(fpingssh($vhost, $vport)) {
        set_time_limit(30);
        if(!$connection) {
          $connection = ssh2_connect($vhost, $vport);
          if (!$authentication) {
            $errorautenticacion = false;
            $authentication = @ssh2_auth_password($connection, $vuser, $vpass) or $errorautenticacion = true; //die("Could not authenticate '{$vuser}'");
            if ($errorautenticacion) return "ERROR_AUTENTICACION";
            $stream = ssh2_exec($connection, $vcomando) or die("Error comando...");
            stream_set_blocking( $stream, true );
            $cmd = stream_get_contents($stream);
            return $cmd;
            fclose($stream);
            sleep(1);
          } else return "NO_CONEXION";
        }
        } else {
          return "NO_CONEXION";
        }
      //flush();
    }  
    
    function fenydesencripta($vcadena, $modo) {
      //AES-256 / CBC / ZeroBytePadding - ref http://php.net/manual/es/function.mcrypt-encrypt.php
      $key = pack('H*', "dcb34c7d113acd07b53763052cef08cc66ace029fddbae4e1d427a1cfb2a10b2");
      $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
      $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
      if ($modo) {
        // $modo = true => encripta
        $ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $vcadena, MCRYPT_MODE_CBC, $iv);
        $ciphertext = $iv . $ciphertext;
        $ciphertext_base64 = base64_encode($ciphertext);
        return $ciphertext_base64;
      } else {
        // $modo = false => desencripta
        $ciphertext_dec = base64_decode($vcadena);
        $iv_dec = substr($ciphertext_dec, 0, $iv_size);
        $ciphertext_dec = substr($ciphertext_dec, $iv_size);
        $plaintext_dec = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $ciphertext_dec, MCRYPT_MODE_CBC, $iv_dec);
        return $plaintext_dec;
      }
    }
    
    function fmachinesupdate($vdescr, $vhost, $vplatform, $vpackage, $vuser, $vpass) {
      $cabeza = '"descr";"host";"platform";"packages";"user";"password"'."\n";
      if (!file_exists('/scripts/machines.csv'))  
      file_put_contents('/scripts/machines.csv', $cabeza );
      $vlinea = '"'.$vdescr.'"'.';'.'"'.$vhost.'"'.';'.'"'.$vplatform.'"'.';'.'"'.$vpackage.'"'.';'.'"'.$vuser.'"'.';'.'"'.$vpass.'"'."\n";
      file_put_contents('/scripts/machines.csv', $vlinea, FILE_APPEND | LOCK_EX);
    }
    
    ?>[/code]
    
    Salu2
    

  • Hola
    Y el código de /usr/local/www/pfCentinel-central.php

    
    Este script php, tarda entre 10sg a 30sg por host... paciencia :)"."[Feel Free to paypal me][0] [ --- By Javier Castan?on - @JavCasta - 2016][1]";
    
    $form = new Form;
    $section = new Form_Section('pfCentinel: CENTRAL (highly recommended to work with tunnels openVPN or IPsec)');
    
    $form->add($section);
    
    //si no existe dir /scripts lo creamos
    if(!is_dir('/scripts')) {
      mkdir('/scripts');
    }
    
    $existemachines = false;
    if (file_exists('/scripts/machines.csv')) {
      $existemachines = true;    
    }
    
    //csv file to array
    function csv_in_array($url, $delm=";", $encl="\"", $head=true) {
        $out = Array();
        //ref http://php.net/manual/es/function.file.php#105772
        
        $csvxrow = file($url);   // ---- csv rows to array ----
       
        $csvxrow[0] = chop($csvxrow[0]);
        $csvxrow[0] = str_replace($encl,'',$csvxrow[0]);
        $keydata = explode($delm,$csvxrow[0]);
        $keynumb = count($keydata);
       
        if ($head === true) {
        $anzdata = count($csvxrow);
        $z=0;
        for($x=1; $x
    

    Salu2


  • Estimado. Si pongo nombre de máquina en algún caso en vez de la ip de tunel de la máquina no funciona, da no conexion.
    que puede ser?. Gracias por su ayuda.


  • Hola

    Si pones nombre de máquina o fqdn en el campo host, dependiendo de los registros de tu servicio dns, traducirá ese nombre de host tal vez a una ip pública o una privada (que no sea la del tunel), y puede que en esa interfaz de ese pfSense no tengas habilitado vía reglas del firewall el acceso a sshd (tcp22). Revisa los logs del firewall. Por eso es preferible poner la IP del tunel del host.

    Por cierto, no se recomienda dejar acceso a sshd vía ip pública wan en un firewall pfsense, ya que siempre hay scaneos e intentos de acceso vía ssh por fuerza bruta y si sale un zero day para ssh/freebsd, pues peor todavía :) .

    Salu2


  • Gracias estimado.


  • Hola

    De nada.

    En unos días (o semanas, depende). Pondré la versión 2 de este script.

    En lugar de un fichero csv, usaré un fichero de bbdd sqlite3 y espero implementar bién lo que me falta de AJAX para que se vea en tiempo real el progreso de todos los registros de los hosts.

    Salu2


  • Hola

    Una duda que me ha llegado por correo (email).

    Me preguntan si el propio pfSense (central) que ejecuta el script se puede incluir en la bbdd machines.csv.

    La respuesta es (siempre que se defina para ese host en machines.csv la IP de una interfaz LAN o de Tunel y no haya restricciones al tcp/22)

    Salu2


  • Con poner como ip la de localhost 127.0.0.1 ya va bien para el central. lo he comprobado.


  • Hola

    Pues gracias por el dato, no lo había probado con 127.0.0.1 :)

    Salu2


  • jajaja, era de cajón. gracias por el tool.
    ;D


  • Ya tenés  la v2 de la tool?


  • Hola

    Estoy en ello, tengo nivel medio en php (backend), pero poco nivel en AJAX y javascript (frontend), así que tardaré un poco más :)

    Salu2


  • +1


  • Ok. Otra forma de desir que andás en vacaciones.  ;D  Esperamos


  • Hola

    Tras la actualización de pfSense a la v 2.3.2, el script, si se ejecuta desde un pfSense 2.3.2,  ya no soporta usar localhost (127.0.0.1) como uno de los registros de machines.csv.

    También he comprobado que si se ejecuta el script desde un pfSense con versión menor a 2.3.2, la conexión php ssh (ssh2_connect) contra un pfSense 2.3.2 da error, esto es debido a que pfSense 2.3.2 está usando como algoritmo de intercambio de key ssh2 a curve25519

    Por lo tanto, si alguien usa este script, conviene que sepa estas circunstancias y lo ejecute desde un pfSense actualizado a su última versión (desde un 2.3.2 la conexión php ssh a una versión menor 2.3 o 2.3.1 o 2.3.1_5 va bien, lo he testeado).

    El script no lo he testeado con versiones 2.2.6 y menores. En un firewall, dejar de actualizar durante mucho tiempo o dejar pasar varias versiones es una brecha de seguridad potencial :)

    Salu2