27. November 2017 · Comments Off on Preventing brute force login attempts by merging geoiplookup and hosts_access · Categories: Ansible, Linux, Linux Admin, Linux Scripts, Linux Security, Networking · Tags: , , , , , ,

To me networking and UNIX run hand-in-hand; as I have been getting into Ansible to do network automation I am also using it to automate and enforce consistency between all of my other hosts. The following is a simple playbook to configure Debian based hosts to perform a geoiplookup against all incoming hosts to prevent brute force login attempts using host_access. I have to admit the original script and idea are not mine; they originated from the following blog post: Limit your SSH logins using GeoIP. As more and more systems are migrated into cloud environments automating and enforcing security controls like this one are of critical importance.

geowrapper.yaml: a /usr/local/bin/ansible-playbook script, ASCII text executable

#!/usr/local/bin/ansible-playbook
## Configure Debian OS family to geoiplookup against all incoming hosts.
## 2017 (v.01) - Playbook from www.davideaves.com
---
- name: GEO Wrapper
  hosts: all
  become: yes
  gather_facts: yes
  tags: host_access

  vars:
    geocountries: "US"
    geofilter: "/opt/geowrapper.sh"

  tasks:
  - name: "Fail if OS family not Debian"
    fail:
      msg: "Distribution not supported"
    when: ansible_os_family != "Debian"

  - name: "Fetch geoip packages"
    apt:
      name:
        - geoip-bin
        - geoip-database
      state: latest
      update_cache: yes
    register: geoip
    when: ansible_os_family == "Debian"

  - name: "Wrapper script {{ geofilter }}"
    copy:
      content: |
        #!/bin/bash
        # Ansible Managed: GeoIP aclexec script for Linux TCP wrappers.
        ## Source: http://www.axllent.org/docs/view/ssh-geoip
 
        # UPPERCASE space-separated country codes to ACCEPT
        ALLOW_COUNTRIES="{{ geocountries }}"
 
        if [ $# -ne 1 ]; then
          echo "Usage:  `basename $0` ip" 1>&2
          exit 0 # return true in case of config issue
        fi
 
        COUNTRY=`/usr/bin/geoiplookup $1 | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1`
 
        [[ $COUNTRY = "IP Address not found" || $ALLOW_COUNTRIES =~ $COUNTRY ]] && RESPONSE="ALLOW" || RESPONSE="DENY"
 
        if [ $RESPONSE = "ALLOW" ]
        then
          exit 0
        else
          logger "$RESPONSE connection from $1 ($COUNTRY)"
          exit 1
        fi
      dest: "{{ geofilter }}"
      mode: "0755"
      owner: root
      group: root
    ignore_errors: yes
    register: geowrapper
    when: geoip|success

  - name: "Mappings in /etc/hosts.allow"
    blockinfile:
      path: /etc/hosts.allow
      state: present
      content: |
        ALL: 10.0.0.0/8
        ALL: 172.16.0.0/12
        ALL: 192.168.0.0/16
        ALL: ALL: aclexec {{ geofilter }} %a
    when: geowrapper|success

  - name: "Mappings in /etc/hosts.deny"
    blockinfile:
      path: /etc/hosts.deny
      state: present
      content: "ALL: ALL"
    when: geowrapper|success

About a year ago I wrote two F5 scripts for mapping and filtering VIPs on an F5 BigIP. I use those scripts often and the f5filter script I continue to tweak/correct minor bugs; which I post in the comments. The following script is cut from same cloth as the previous 2 scripts, except this script search for an F5 VIP by Node; even if the VIP directs traffic to the node via a Policy or iRule.

f5node.sh: Bourne-Again shell script text executable

#!/bin/bash
## Reverse filter a single Node on a BigIP.
## 2017 (v1.0) - Script from www.davideaves.com
 
F5CONFIG="$1"
F5NODE="$2"
 
# Fix broken TERM variable if using screen.
[ "$TERM" == "screen-256color" ] && { export TERM="xterm-256color"; }
 
# FUNCTION: End Script if error.
DIE() {
 echo "ERROR: Validate \"$_\" is installed and working on your system."
 exit 0
}
 
# ANSI color variables.
esc="$(echo -e '\E')";  cCLS="${esc}[0m"
cfBLACK="${esc}[30m";   cbBLACK="${esc}[40m"
cfRED="${esc}[31m";     cbRED="${esc}[41m"
cfGREEN="${esc}[32m";   cbGREEN="${esc}[42m"
cfYELLOW="${esc}[33m";  cbYELLOW="${esc}[43m"
cfBLUE="${esc}[34m";    cbBLUE="${esc}[44m"
cfMAGENTA="${esc}[35m"; cbMAGENTA="${esc}[45m"
cfCYAN="${esc}[36m";    cbCYAN="${esc}[46m"
cfWHITE="${esc}[37m";   cbWHITE="${esc}[47m"
c1BOLD="${esc}[1m";     c0BOLD="${esc}[22m"
 
### Print Syntax if arguments are not provided. ###
if [ ! -e "$F5CONFIG" ] || [ -z "$F5NODE" ]
 then
 echo "Usage: $0 bigip.conf 10.1.1.10"
 exit 0;
fi
 
### Check to see if we are running this on an F5. ###
type -p tmsh > /dev/null && if [ "$(cat /var/prompt/ps1)" == "Active" ]
  then STATE=$cbGREEN
  else STATE=$cbRED
fi
 
### The function that does all the filtering. ###
F5FILTER() {
 if [[ "$(file "$F5CONFIG")" == *"ASCII"* ]]
  then cat "$F5CONFIG"
 elif [[ "$(file "$F5CONFIG")" == *"gzip compressed data"* ]]
  then tar -xOvf "$F5CONFIG" config/bigip.conf 2> /dev/null
 fi | sed -n -e '/^ltm '"$(echo $F5STANZA)"'.*/,/^}$/p' | sed 's/^}/}|/g' | tr -d '[:cntrl:]' |\
  sed 's/|/\n/g;s/ \{1,\}/ /g' | grep "$F5DIG"
}
 
### Display status icon back to the user. ###
STATUS() {
 if    [ "$STATUS" == "available enabled" ]; then printf -- "($c0BOLD$cfBLACK$cbGREEN@$cCLS) "
  elif [ "$STATUS" == "available disabled" ]; then printf -- "($c0BOLD$cfBLACK$cbBLUE@$cCLS) "
  elif [ "$STATUS" == "unknown enabled" ]; then printf -- "[$c0BOLD$cfBLACK$cbBLUE#$cCLS] "
  elif [ "$STATUS" == "offline disabled" ]; then printf -- "< $c0BOLD$cfBLACK$cbWHITE-$cCLS> "
  elif [ "$STATUS" == "offline enabled" ]; then printf -- "< $c0BOLD$cfBLACK$cbRED!$cCLS> "
  elif [ "$STATUS" == "available disabled-by-parent" ]; then printf -- "($c1BOLD$cfBLACK$cbWHITE@$cCLS) "
  elif [ "$STATUS" == "unknown disabled-by-parent" ]; then printf -- "[$c1BOLD$cfBLACK$cbWHITE#$cCLS] "
  elif [ "$STATUS" == "offline disabled-by-parent" ]; then printf -- "< $c1BOLD$cfBLACK$cbWHITE-$cCLS> "
  else printf "[ $STATUS ] "
 fi
}
 
### Display the final result back to the user. ###
PRINT() {
 if [ ! -z "$STATE" ]
  then ### We are running this script on the F5. ###
   printf "$STATE$cfBLACK$(cat /var/prompt/hostname)$cCLS "
 
   # Collect status of VIRTUAL and print.
   printf ">>> "
   STATUS="$(tmsh show ltm virtual $VIRTUAL 2> /dev/null | grep -e "Availability" -e "State" | awk -F':' '{printf $NF" "}' | sed 's/ \+/ /g;s/^[ \t]*//;s/[ \t]*$//')"
   STATUS
   printf "$c1BOLD$VIRTUAL$cCLS < << $cf$POLICY$cCLS\n  "
 
   # Collect status of POOL and print.
   STATUS="$(tmsh show ltm pool $POOL 2> /dev/null | grep -e "Availability" -e "State" | awk -F':' '{printf $NF" "}' | sed 's/ \+/ /g;s/^[ \t]*//;s/[ \t]*$//')"
   STATUS
   printf "$cfCYAN$POOL$cCLS\n"
 
   # Collect status of Nodes and print.
   for NODE in $NODES;
    do STATUS="$(tmsh show ltm node $NODE 2> /dev/null | grep -e "Availability" -e "State" | awk -F':' '{printf $NF" "}' | sed 's/ \+/ /g;s/^[ \t]*//;s/[ \t]*$//')"
       STATUS && printf "$NODE "
   done | sed 's/\] /\]_/g' | xargs -n4 | column -t | awk '{print "  ",$0}' |\
   sed 's/'"$F5NODE"'/'"$cfYELLOW$F5NODE$cCLS "'/g;s/\]_/\] /g'
 
 else ### We are not Running on an F5 ###
   printf ">>> $c1BOLD$VIRTUAL$cCLS < << $cf$POLICY$cCLS\n  $cfCYAN$POOL$cCLS\n"
   printf "$NODES\n" | xargs -n4 | column -t | awk '{print "  ",$0}' |\
   sed 's/'"$F5NODE"'/'"$cfYELLOW$F5NODE$cCLS "'/g'
 fi; echo
}
 
# Main Loop.
F5STANZA="pool"
F5DIG="$F5NODE "
 
for i in $(F5FILTER)
 do
  if [ "$i" == "pool" ]
   then echo; FLAG="$i"
  elif [ "$i" == "address" ]
   then FLAG="$i"
  elif [ ! -z "${FLAG}" ]
   then printf "$i " | sed 's/^\/.*\///'
   unset FLAG
  fi
done | grep -v ^$ | while read POOL NODES
 do
 
  # Dig polices and iRules binding the pool to VIPs.
  for F5STANZA in policy profile rule
   do F5DIG="$POOL"
 
      if [ "$F5STANZA" == "policy" ]
       then COLOR="$c1BOLD$cfGREEN"
      elif [ "$F5STANZA" == "rule" ]
       then COLOR="$c0BOLD$cfGREEN"
      else
       COLOR="$c0BOLD$cfMAGENTA"
      fi
 
      F5FILTER | sed 's/profile httpclass/httpclass/g' | awk '{print $3}' | sed 's/\/.*\///g' | while read POLICY;
       do F5STANZA="virtual "
          F5DIG="$POLICY"
          F5FILTER | awk '{print $3}' | sed 's/\/.*\///g' | while read VIRTUAL
            do POLICY="$COLOR$POLICY"
               PRINT
          done
      done
  done
 
  # Dig VIPs that are bound to the pool.
  F5STANZA="virtual "
  F5DIG="$POOL"
  F5FILTER | awk '{print $3}' | sed 's/\/.*\///g' | while read VIRTUAL
   do PRINT
  done
 
done
09. November 2017 · Comments Off on YAML Templating Engine · Categories: Cisco, Firewall, Linux, Linux Scripts, Load Balancing, Networking · Tags: , , , ,

About a month ago I left my previous position and took a new engineering position. In my new role I am more focused on devops/orchestration for a new a network team. One major gap that I commonly see missing from most infrastructure/ops teams is the sharing of basic templates; ideally using a templating engine to maintain consistency. A couple years ago I stumbled upon a templating-engine by mxtommy on github that would read XML data and spit out a config. Aside from being a little rough, you would have to create your templates in a rough XML format plus it no longer seems to work with the latest version of PHP.

I decided that it was time to rewrite it; this time making it so it would read a YAML data input structure. YAML is a human-readable data serialization standard and is commonly used in orchestration tools like Ansible. Just like before this web script searches the current directory for template files (*.yaml). It then parses the questions to ask and creates the appropriate html input fields. The user answers the questions and submits them back to the script. The script then replaces the user responses with the placeholders to create a constant and repeatable configuration… For example, last year I posted a config snippet for configuring a Cisco UCS-E module on a 4000 series router. A template created for that config would make for constant and rapid deployments of UCS-E blades by infrastructure staff. Additionally you could use it to create a bootstrap config to quickly bring a host online so Ansible can take over management of it.

This web script requires the YAML-1.1 parser and emitter for PHP…

  • Debian: apt-get install php-yaml
  • RHEL: yum install php-pecl-yaml

template.php: Template Engine / PHP script.

< ?php
/*
   # yaml-templating-engine
 
   This web script searches the current directory for .yaml files. It will then ask
   questions based on questions defined in the template file, and then insert the info
   at the appropriate places in the template.
 
   Concept based on: https://github.com/mxtommy/templating-engine
 
   Rewritten to consume YAML and works with PHP7+.
 
   *** This is script not intended for use on a public server. ***
 
   2017 (v1.0) - Script from www.davideaves.com
*/
 
if (basename($_SERVER["PHP_SELF"]) == basename(__FILE__) && isset($_GET["source"])) {
  header("Content-type: text/plain");
  exit(file_get_contents(basename($_SERVER["PHP_SELF"])));
}
 
?>
< !DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>< ?php echo gethostname(); ?>: YAML Template Generator</title>
    <link href="template.css" rel="stylesheet"/>
  </head>
<body>
 
<table align="center"><tr>
	<td><h1>YAML Templating Engine</h1></td>
</tr></table>
 
<div id="TEMPLATE">
<fieldset>< ?php
 
// SELECT THE TEMPLATE
if (!isset($_REQUEST['template'])) {
  echo "<legend>Template Select\n";
  echo "<p>Please select one of the following templates:</p>\n<ul class=\"twocols\">\n";
  $templates = glob("*.yaml");
  foreach ($templates as $file) {
	print ("\t<li><a href='".$_SERVER['PHP_SELF']."?template=".$file."'>$file</a></li>\n");
  }
  echo "</ul>";
 
}
 
// ASK TEMPLATE QUESTIONS
elseif (!isset($_REQUEST['submitted'])) {
  echo "<legend>Template Input</legend>";
  echo "<h2>" . $_REQUEST['template'] . "</h2>\n";
 
  $yaml_template = yaml_parse_file($_REQUEST['template']);
  //print_r($yaml_template);
 
  echo "<form action=\"" . $_SERVER['PHP_SELF'] . "\" method=\"post\">\n";
  echo "<input type=\"hidden\" name=\"template\" value=\"" . $_REQUEST['template'] . "\"/>\n";	
 
  foreach($yaml_template['questions'] as $question) {
 
    // type is text
    if ($question['type'] == 'text') {
      echo "<div class=\"line\"><label>" . $question['description'] . ":</label><input type=\"text\" name=\"" . $question['name'] . "\" value=\"" . $question['default'] . "\"/></div>\n";
    }
 
    // type is select
    elseif($question['type'] == 'select') {
      echo "<div class=\"line\"><label>" . $question['description'] . ":</label><select name=\"" . $question['name'] . "\">\n";
      foreach ($question['option'] as $option) {
        echo "\t<option value=\"" . $option['name'] . "\"";
          if ($question['default'] == $option['name']) { echo " selected"; }
          echo ">" . $option['description'] . "</option>\n";
      }
      echo "</select></div>\n";
    }
 
    // type is checkbox
    elseif($question['type'] == 'checkbox') {
      echo "<div class=\"line\"><label>" . $question['description'] . ":</label><input type=\"checkbox\" name=\"" . $question['name'] . "\"";
      if ( isset($question['default']) ) { echo " checked"; }
      echo "/></div>\n";
    }
  }
  echo "<br /><input type=\"submit\" name=\"submitted\"/>\n</form>\n";
} 
 
// DISPLAY THE TEMPLATE
else {
  echo "<legend>Template Output</legend>";
  echo "<h2>" . $_REQUEST['template'] . "</h2>\n";
 
  $vars[] = "";
  $yaml_template = yaml_parse_file($_POST['template']);
 
  foreach ($yaml_template['questions'] as $i => $row) {
 
      $TMP = $yaml_template['questions'][$i];
 
      // Input type is text.
      if($TMP['type'] == "text") {
	$vars = $vars + array( "{{ ".$TMP['name']." }}" => "<mark>" . $_POST[$TMP['name']] . "</mark>" );
      }
 
      // Input type is select.
      elseif ($TMP['type'] == "select" ) {
        foreach ($TMP['option'] as $i => $row) {
          if($row['name'] == $_POST[$TMP['name']]) {
            foreach ($TMP['option'][$i]['option'] as $i => $row) {
              $vars = $vars + array( "{{ ".$row['name']." }}" => "<mark>" . $row['text'] . "</mark>" );
      } } } }
 
      // Input type is a checkbox.
      elseif ($TMP['type'] == "checkbox" ) {
        if(isset($_POST[$TMP['name']])) {
          $vars = $vars + array( "{{ ".$TMP['name']." }}" => "<mark>" . $TMP['checked'] . "</mark>" );
        } elseif (isset($TMP['unchecked'])) {
          $vars = $vars + array( "{{ ".$TMP['name']." }}" => "<mark>" . $TMP['unchecked'] . "</mark>" );
        } else {
          $vars = $vars + array( "{{ ".$TMP['name']." }}" => "" );
      } }
  }
 
  array_shift($vars);
  echo "<span class=\"config\">";
  echo strtr($yaml_template['config'], $vars);
  echo "</span>";
 
}
 
?></fieldset>
</div>
 
</body>
</html>

template.css: Example CSS file for the template generator.

/* TEMPLATE CSS */
#TEMPLATE {width:800px; margin:20px auto; background: #EEE; border: solid 1px #999; position: float; vertical-align: middle; border-radius: 5px;}
#TEMPLATE fieldset {border-radius: 5px; border-width:2px; border-style:dotted; margin:5px;}
#TEMPLATE h2 {margin:0; font-family:Arial, Sans-Serif; background:#5f9be3; color:#fff; white-space: nowrap; font-weight:bold; padding:0px; text-align: center;}
#TEMPLATE span.config {background-color:black; color:white; font-family:monospace; white-space:pre; display:block;}
#TEMPLATE mark {background-color:black; color:yellow; font-style:italic;}
#TEMPLATE form {margin-top: 15px; margin-left: 15px; margin-right: 15px;}
#TEMPLATE form label {display:inline-block; width:48%;}
#TEMPLATE form a {color: blue; text-decoration: none; margin: 0px;}
#TEMPLATE form input[type="text"] {width:50%;}
#TEMPLATE form input[type="submit"] {width:100%;}
#TEMPLATE form select {width:50.6%;}
#TEMPLATE form .line {clear:both;}
 
/* twocols layout */
.twocols {
  -webkit-column-count: 2; /* Chrome, Safari, Opera */
  -moz-column-count: 2; /* Firefox */
  column-count: 2;
}

Overall the templates are very easy to read and create making it so even non-technical support staff can contribute to the template repository this web script reads from.

example.yaml: Example yaml template file.

---
questions:

  - name: "hostname"
    description: "Hostname"
    type: "text"
    default: ROUTER

  - name: "mgmt_net"
    description: "Management Network"
    type: "select"
    default: dev
    option:
    - name: corp
      description: "Corporate"
      option:
      - name: VLAN
        text: 1000
      - name: VLAN-NAME
        text: PROD_VLAN
      - name: GATEWAY
        text: 192.168.50.1
    - name: dev
      description: "Development"
      option:
      - name: VLAN
        text: 2000
      - name: VLAN-NAME
        text: DEV_VLAN
      - name: GATEWAY
        text: 192.168.100.1

  - name: "service_encrypt"
    description: "Password Encryption"
    type: "checkbox"
    default: checked
    checked: service password-encryption
    unchecked: no service password-encryption

config: |
  {{ service_encrypt }}
  !
  h0stname {{ hostname }}
  !
  vlan {{ VLAN }}
   name {{ VLAN-NAME }}
  !
  ip default gateway {{ GATEWAY }}