17. November 2021 · Comments Off on GnuPG encryption/decryption helper · Categories: Linux, Linux Admin, Linux Scripts, Linux Security · Tags: , , , ,

I have had many ruminations on the best way to encrypt and secure files. A good reason for wanting to encrypt your files might be; you keep a USB stick on your keychain with your taxes or customer data on it so its accesable to you, but you’re are worried about loosing it. In situations like that, file encryption is exactly what you want! I previously have written a scripts and jotted notes on encrypting block devices; of which I am not even convinced is best practice. Encrypting block devices is all well and good but it’s not very practical for encrypting individual files. I recently noticed that Cisco uses GPG to encrypt their backup files on their ISE appliance, which convinced me that GPG is probabbly the smartest way to encrypt files. Unfortunately there is a lot of commandline options and reading you have to do inorder to get it to work properly. I figured this was perfect thing to write a script for and make public. My goal is to make GnuPG a little bit more accesable to a broader audience.

The following script is intended to make it simple for a lay Linux user to use GPG to encrypt/decrypt files and directories. As a bonus I’m keeping the debug block that I used, and can be removed, in order to test encryption and decryption while I was writting the script and testing commands. If you do not already have a public-private key pair, the script will generate one for you and ask you if you want to do a backup. If you allow the script to create backup files of your keys I strongly recommend you remove them as soon as possible! The script can also perform a restore of your keys from backup if you put them back in the root of your home directory. Additionally the script doesn’t see any keys it will perform symmetric encryption against the follow directory. Symmetric encryption just uses a simple passphrase to perform the encryption and decryption. If you create the following variable:

export gpg_method="symmetric"

With the above variable set; the script will ignore your keys and will do symmetric only encryption.

gpg_crypt.sh: a /bin/env bash script, ASCII text executable, with very long lines (382)

#!/bin/env bash
## GnuPG encryption/decryption helper.
## 2021 - Script from www.davideaves.com
 
# Enable for debuging
# set -x
 
export GPG_TTY=$(tty)
 
# Verify script requirements
for req in curl gpg jq
 do type ${req} >/dev/null 2>&1 || { echo >&2 "$(basename "${0}"): I require ${req} but it's not installed. Aborting."; exit 1; }
done && umask 0077
 
# Fetch existing keys
keyid=( `gpg --list-keys --keyid-format 0xLONG | awk '/^sub.*[E]/{gsub("[]|[]|/", " "); print $3,$NF}'` )
 
# Help manage keys
if [ -z "${keyid}" ] && [ -z "${gpg_method}" ]
 then [ -f "${HOME}/bin/rc_files/gpg.conf" -a ! -f "${HOME}/.gnupg/gpg.conf" ] && \
       { cat "${HOME}/bin/rc_files/gpg.conf" > "${HOME}/.gnupg/gpg.conf"; }
 
      # Generate a new keys
      read -p "(G)enerate new or (R)estore keys? (g/r) " -n 1 -r; echo
      if [[ "${REPLY}" =~ ^[Gg]$ ]]
       then gpg --full-generate-key || gpg --gen-key
 
            # Backup keys
            read -p "Export a backup of keys? " -n 1 -r; echo
            if [[ "${REPLY}" =~ ^[Yy]$ ]]
             then gpg --armor --export-secret-key > "${HOME}/gpg_secret-key.asc"
                  gpg --armor --export-secret-subkeys > "${HOME}/gpg_secret-subkeys.asc"
                  gpg --armor --export > "${HOME}/gpg_public-key.asc"
                  gpg --armor --export-ownertrust > "${HOME}/gpg_ownertrust.txt"
            fi
 
      # Restore existing keys
      elif [[ "${REPLY}" =~ ^[Rr]$ ]]
       then for asc in ${HOME}/gpg_*.asc
             do gpg --import "${asc}"
            done && gpg --import-ownertrust "${HOME}/gpg_ownertrust.txt"
      fi && unset ${REPLY}
 elif [ -n "${gpg_method}" ]
  then unset keyid
fi
 
# Exit if no debug or user input
if [[ ! "$SHELLOPTS" =~ "xtrace" ]] && [[ -z "${@}" ]]
 then echo "$(basename "${0}"): GnuPG encryption/decryption helper."
      echo "File or Directory input is required to continue!"
 exit 0
fi
 
# Create debug test file
if [[ "$SHELLOPTS" =~ "xtrace" ]] && [[ -z "${@}" ]]
 then debug_b64="/9j/4AAQSkZJRgABAQAAZABkAAD/2wCEABQQEBkSGScXFycyJh8mMi4mJiYmLj41NTU1NT5EQUFBQUFBREREREREREREREREREREREREREREREREREREREQBFRkZIBwgJhgYJjYmICY2RDYrKzZERERCNUJERERERERERERERERERERERERERERERERERERERERERERERERERP/AABEIAAEAAQMBIgACEQEDEQH/xABMAAEBAAAAAAAAAAAAAAAAAAAABQEBAQAAAAAAAAAAAAAAAAAABQYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJQA9Yv/2Q=="
 
      # Create temp file
      if debug_file="$(mktemp)"
       then trap "{ if [ -e "${debug_file}*" ]; then rm -rf "${debug_file}*"; fi }" \
         SIGINT SIGTERM ERR EXIT
       else echo "Failure, exit status: ${?}"
            exit ${?};
      fi && echo -n "${debug_b64}" | base64 -d > "${debug_file}"
 
      MimeDB=( "https://raw.githubusercontent.com/jshttp/mime-db/master/db.json" "${HOME}/.mime.json" )
      [ ! -s "${MimeDB[1]}" ] && { curl -s "${MimeDB[0]}" | jq > "${MimeDB[1]}"; }
 
      debug_mime="$(file -b --mime-type "${debug_file}")"
      debug_hash="$(sha1sum "${debug_temp}" | awk '{print $1}')"
      debug_ext="$(jq -r --arg MIME "${debug_mime}" '.[$MIME].extensions[0] // empty' "${MimeDB[1]}")"
      debug_output="${debug_hash}.${debug_ext}"
 
      mv "${debug_file}" "${debug_file}.${debug_ext}"
      set -- "$@" "${debug_file}.${debug_ext}"
 
      file "${debug_file}.${debug_ext}"
fi
 
### BEGIN ###
 
for input in "${@}"
 do file_ext="$(basename "${input##*.}")"
 
    ## symmetric encryption: file ##
    if [ -z "${keyid[0]}" ] && [ -f "${input}" ]
     then if [ "${file_ext}" != "gpg" ]
           then gpg --batch --yes --quiet --output "$(basename "${input:-'null'}").gpg" --symmetric "${input}"
          ## decrypt file ##
          elif [[ "${input%.*}" =~ ".tgz"$ ]]
           then gpg --batch --yes --quiet --decrypt "${input}" | tar xzfv -
           else gpg --batch --yes --quiet --output "$(basename "${input%.*}")" --decrypt "${input}"
          fi
 
    ## symmetric encryption: directory ##
    elif [ -z "${keyid[0]}" ] && [ -d "${input}" ]
     then tar czfv - "${input}" | gpg --batch --yes --quiet --output "$(basename "${input:-'null'}").tgz.gpg" --symmetric
 
    ## key encryption: file ##
    elif [ -n "${keyid[0]}" ] && [ -f "${input}" ]
     then if [ "${file_ext}" != "gpg" ]
           then gpg --batch --yes --quiet --output "$(basename "${input:-'null'}").gpg" --recipient ${keyid[0]} --encrypt "${input}"
          ## decrypt file ##
          elif [[ "${input%.*}" =~ ".tgz"$ ]]
           then gpg --batch --yes --quiet --recipient ${keyid[0]} --decrypt "${input}" | tar xzfv -
           else gpg --batch --yes --quiet --output "$(basename "${input%.*}")" --recipient ${keyid[0]} --decrypt "${input}"
          fi
 
    ## key encryption: directory ##
    elif [ -n "${keyid[0]}" ] && [ -d "${input}" ]
     then tar czfv - "${input}" | gpg --batch --yes --quiet --recipient ${keyid[0]} --encrypt > "$(basename "${input:-'null'}").tgz.gpg"
    fi
done
 
### FINISH ###

GnuPG user configuration options

If you use GnuPG I recommend updating your configuration to give it an affinity for stronger ciphers. Riseup.net created a very good best practice config that is a good starting place. I’ve made a few modifications to it, but have left it relatively unchanged. If the above script sees the user config missing and can find the following config file in a repo direcory, it will go ahead and copy the configuration to where it needs to be… Feel free to use the below config as an optional reference.

gpg.conf: ASCII text

## GnuPG Options
 
# Assume that command line arguments are given as UTF8 strings.
utf8-strings
 
#
# This is an implementation of the Riseup OpenPGP Best Practices
# https://help.riseup.net/en/security/message-security/openpgp/best-practices
#
 
#-----------------------------
# default key
#-----------------------------
 
# The default key to sign with. If this option is not used, the default key is the first key found in the secret keyring
#default-key 0xD8692123C4065DEA5E0F3AB5249B39D24F25E3B6
 
#-----------------------------
# behavior
#-----------------------------
 
# Disable inclusion of the version string in ASCII armored output
no-emit-version
 
# Disable comment string in clear text signatures and ASCII armored messages
no-comments
 
# Display long key IDs
keyid-format 0xlong
 
# List all keys (or the specified ones) along with their fingerprints
with-fingerprint
 
# Display the calculated validity of user IDs during key listings
list-options show-uid-validity
verify-options show-uid-validity
 
# Try to use the GnuPG-Agent. With this option, GnuPG first tries to connect to the agent before it asks for a passphrase.
use-agent
 
#-----------------------------
# algorithm and ciphers
#-----------------------------
 
# list of personal digest preferences. When multiple digests are supported by all recipients, choose the strongest one
personal-cipher-preferences AES256 AES192 AES CAST5
 
# list of personal digest preferences. When multiple ciphers are supported by all recipients, choose the strongest one
personal-digest-preferences SHA512 SHA384 SHA256 SHA224
 
# message digest algorithm used when signing a key
cert-digest-algo SHA512
 
# This preference list is used for new keys and becomes the default for "setpref" in the edit menu
default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed
 
# Use a specified algorithm as the symmetric cipher
cipher-algo AES256
21. February 2021 · Comments Off on Targeted network monitoring using only fping and rrdtool. · Categories: Linux, Linux Admin, Linux Scripts, Networking · Tags: , , , , , ,

I’ve been very unhappy with the state of network monitoring applications lately. Most network monitoring tools are either too big or too arbitrary to be helpful for application support. This can be an issue when focusing on a specific application with components separated into various tiers, datacenters or locations. When network performance is in question the most helpful data is the active latency between a node and its other components (during or leading up to the time in question). If a network monitoring tool lacks any specificity of the application in question it will be viewed as too dense or cerebral to be useful; at worse it will harm troubleshooting. The quicker hard data can be accessed proving out the network layer, the quicker troubleshooting can move up the stack towards resolution.

Monolithic tools like Cacti are sometimes useful, however the lighter the script is, the more nimbly it can be deployed on a wide variety of nodes. Because both FPing and RRDTool are small, useful and standard Linux packages they are ideal, so l wrote the following bash script that leverages only those 2 tools together. The data collected is roughly identical to SmokePing but has the benefit of not dirtying a system with unnecessary packages. The script can easily be deployed by any devops deployment and is ran via crontab. Graph data can be created when or if they are needed.

fping_rrd.sh: Bourne-Again shell script, ASCII text executable

#!/usr/bin/env bash
## FPing data collector for RRDTOOL
#
# Crontab:
#   */5 * * * *     fping_rrd.sh
#
## Requires: fping, rrdtool
## 2021 - Script from www.davideaves.com
 
# Enable for debuging
#set -x
 
STEP=300 # 5min
PINGS=20 # 20 pings
 
# The first ping is usually an outlier; adding an extra ping to drop the first result.
 
fping_opts="-C $((PINGS+1)) -q -B1 -r1 -i10"
fping_hosts="172.31.3.1 172.31.3.3 172.31.4.1 172.31.4.10 172.31.15.1 172.31.15.4"
rrd_path="/var/lib/fping"
rrd_timestamp=$(date +%s)
 
calc_median() {
    awk '{ if ( $1 != "-" ) { fping[NR] = $1 }
           else { NR-- }
         }
     END { asort(fping);
           if (NR % 2) { print fping[(NR + 1) / 2] }
           else { print (fping[(NR / 2)] + fping[(NR / 2) + 1]) / 2.0 }
         }'
}
 
rrd_create() {
    rrdtool create "${fping_rrd}" \
     --start now-2h --step $((STEP)) \
     DS:loss:GAUGE:$((STEP*2)):0:$((PINGS)) \
     DS:median:GAUGE:$((STEP*2)):0:180 \
     $(seq -f " DS:ping%g:GAUGE:$((STEP*2)):0:180" 1 $((PINGS++))) \
     RRA:AVERAGE:0.5:1:1008 \
     RRA:AVERAGE:0.5:12:4320 \
     RRA:MIN:0.5:12:4320 \
     RRA:MAX:0.5:12:4320 \
     RRA:AVERAGE:0.5:144:720 \
     RRA:MAX:0.5:144:720 \
     RRA:MIN:0.5:144:720
}
 
rrd_update() {
    rrd_loss=0
    rrd_median=""
    rrd_rev=$((PINGS))
    rrd_name=""
    rrd_value="${rrd_timestamp}"
 
    for rrd_idx in $(seq 1 $((rrd_rev)))
     do
        rrd_name="${rrd_name}$([[ ${rrd_idx} -gt "1" ]] && echo ":")ping$((rrd_idx))"
        rrd_value="${rrd_value}:${fping_array[-$((rrd_rev))]}"
        rrd_median="${fping_array[-$((rrd_rev))]}\n${rrd_median}"
 
        [ "${fping_array[-$((rrd_rev))]}" == "-" ] && (( rrd_loss++ ))
 
        (( rrd_rev-- ))
    done
    rrd_median=$(printf ${rrd_median} | calc_median)
 
    rrdtool update "${fping_rrd}" --template $(echo ${rrd_name}:median:loss ${rrd_value}:${rrd_median}:${rrd_loss} | sed 's/-/U/g')
    unset rrd_loss rrd_median rrd_rev rrd_name rrd_value
}
 
fping ${fping_opts} ${fping_hosts} 2>&1 | while read fping_line;
 do fping_array=( ${fping_line} )
    fping_rrd="${rrd_path}/fping_${fping_array[0],,}.rrd"
 
    # Create RRD file.
    if [ ! -f "${fping_rrd}" ]
     then rrd_create
    fi
 
    # Update RRD file.
    if [ -f "${fping_rrd}" ]
     then rrd_last=$(( ${rrd_timestamp} - $(rrdtool last "${fping_rrd}") ))
          [[ $((rrd_last)) -ge $((STEP)) ]] && rrd_update
    fi && unset rrd_last
done

Creating Network Monitoring Graphs

The following are 3 example scripts that use rrdtool to create graphs from the RRD files.

Mini Graph

fping_172.31.15.4_mini.png

graph_mini.sh: Bourne-Again shell script, ASCII text executable

#!/usr/bin/env bash
## Create a  mini graph from a RRD file
## Requires: rrdtool
## 2021 - Script from www.davideaves.com
 
# Enable for debuging
#set -x
 
fping_rrd="${1}"
COLOR=( "FF5500" )
 
rrd_graph_cmd() {
cat << EOF
rrdtool graph "$(basename ${fping_rrd%.*})_mini.png"
--start "${START}" --end "${END}"
--title "$(date -d "${START}") ($(awk -v TIME=$TIME 'BEGIN {printf "%.1f hr", TIME/3600}'))"
--height 65 --width 600
--vertical-label "Seconds"
--color BACK#F3F3F3
--color CANVAS#FDFDFD
--color SHADEA#CBCBCB
--color SHADEB#999999
--color FONT#000000
--color AXIS#2C4D43
--color ARROW#2C4D43
--color FRAME#2C4D43
--border 1
--font TITLE:10:"Arial"
--font AXIS:8:"Arial"
--font LEGEND:8:"Courier"
--font UNIT:8:"Arial"
--font WATERMARK:6:"Arial"
--imgformat PNG
EOF
}
 
rrd_graph_opts() {
rrd_idx=0
cat << EOF
DEF:median$((rrd_idx))="${fping_rrd}":median:AVERAGE
DEF:loss$((rrd_idx))="${fping_rrd}":loss:AVERAGE
$(for ((i=1;i<=PINGS;i++)); do echo "DEF:ping$((rrd_idx))p$((i))=\"${fping_rrd}\":ping$((i)):AVERAGE"; done)
CDEF:ploss$((rrd_idx))=loss$((rrd_idx)),20,/,100,*
CDEF:dm$((rrd_idx))=median$((rrd_idx)),0,100000,LIMIT
$(for ((i=1;i<=PINGS;i++)); do echo "CDEF:p$((rrd_idx))p$((i))=ping$((rrd_idx))p$((i)),UN,0,ping$((rrd_idx))p$((i)),IF"; done)
$(echo -n "CDEF:pings$((rrd_idx))=$((PINGS)),p$((rrd_idx))p1,UN"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),UN,+"; done; echo ",-")
$(echo -n "CDEF:m$((rrd_idx))=p$((rrd_idx))p1"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),+"; done; echo ",pings$((rrd_idx)),/")
$(echo -n "CDEF:sdev$((rrd_idx))=p$((rrd_idx))p1,m$((rrd_idx)),-,DUP,*"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),m$((rrd_idx)),-,DUP,*,+"; done; echo ",pings$((rrd_idx)),/,SQRT")
CDEF:dmlow$((rrd_idx))=dm$((rrd_idx)),sdev$((rrd_idx)),2,/,-
CDEF:s2d$((rrd_idx))=sdev$((rrd_idx))
AREA:dmlow$((rrd_idx))
AREA:s2d$((rrd_idx))#${COLOR}30:STACK
LINE1:dm$((rrd_idx))#${COLOR}:"$(basename ${fping_rrd%.*} | awk -F'_' '{print $NF}')\t"
VDEF:avmed$((rrd_idx))=median$((rrd_idx)),AVERAGE
VDEF:avsd$((rrd_idx))=sdev$((rrd_idx)),AVERAGE
CDEF:msr$((rrd_idx))=median$((rrd_idx)),POP,avmed$((rrd_idx)),avsd$((rrd_idx)),/
VDEF:avmsr$((rrd_idx))=msr$((rrd_idx)),AVERAGE
GPRINT:avmed$((rrd_idx)):"Median RTT\: %5.2lfms"
GPRINT:ploss$((rrd_idx)):AVERAGE:"Loss\: %5.1lf%%"
GPRINT:avsd$((rrd_idx)):"Std Dev\: %5.2lfms"
GPRINT:avmsr$((rrd_idx)):"Ratio\: %5.1lfms\\j"
COMMENT:"Probe\: $((PINGS)) pings every $((STEP)) seconds"
COMMENT:"${fping_rrd}\\j"
EOF
}
 
if [ ! -r "${fping_rrd}" ]
 then printf "${0} \"file.rrd\"\n"
 else
      STEP=$(rrdtool info "${fping_rrd}" | awk '/^step/{print $NF}')
      PINGS=$(rrdtool info "${fping_rrd}" | awk '/^ds.ping.*index/{count++} END{print count}')
 
      START="$([ -z "${2}" ] && echo "-9 hours" || echo "${2}")"
      END="$([ -z "${3}" ] && echo "now" || echo "${3}")"
      TIME=$(( $(date -d "${END}" +%s) - $(date -d "${START}" +%s) ))
 
      eval $(rrd_graph_cmd; rrd_graph_opts)
fi

Combined (multi) Graph

fping_graph.png

graph_multi.sh: Bourne-Again shell script, ASCII text executable

#!/usr/bin/env bash
## Create a mini graph from multiple RRDs
## Requires: rrdtool
## 2021 - Script from www.davideaves.com
 
# Enable for debuging
#set -x
 
START="-9 hours"
END="now"
 
png_file="${1}"
rrd_files="${*:2}"
 
rrd_graph_cmd() {
cat << EOF
rrdtool graph "${png_file}"
--start "${START}" --end "${END}"
--title "$(date -d "${START}") ($(awk -v TIME=$TIME 'BEGIN {printf "%.1f hr", TIME/3600}'))"
--height 115 --width 600
--vertical-label "Seconds"
--color BACK#F3F3F3
--color CANVAS#FDFDFD
--color SHADEA#CBCBCB
--color SHADEB#999999
--color FONT#000000
--color AXIS#2C4D43
--color ARROW#2C4D43
--color FRAME#2C4D43
--border 1
--font TITLE:10:"Arial"
--font AXIS:8:"Arial"
--font LEGEND:8:"Courier"
--font UNIT:8:"Arial"
--font WATERMARK:6:"Arial"
--imgformat PNG
EOF
}
 
rrd_graph_opts() {
rrd_idx=0
for fping_rrd in ${rrd_files}
do COLOR=$(openssl rand -hex 3)
STEP=$(rrdtool info "${fping_rrd}" | awk '/^step/{print $NF}')
PINGS=$(rrdtool info "${fping_rrd}" | awk '/^ds.ping.*index/{count++} END{print count}')
cat << EOF
DEF:median$((rrd_idx))="${fping_rrd}":median:AVERAGE
DEF:loss$((rrd_idx))="${fping_rrd}":loss:AVERAGE
$(for ((i=1;i<=PINGS;i++)); do echo "DEF:ping$((rrd_idx))p$((i))=\"${fping_rrd}\":ping$((i)):AVERAGE"; done)
CDEF:ploss$((rrd_idx))=loss$((rrd_idx)),20,/,100,*
CDEF:dm$((rrd_idx))=median$((rrd_idx)),0,100000,LIMIT
$(for ((i=1;i<=PINGS;i++)); do echo "CDEF:p$((rrd_idx))p$((i))=ping$((rrd_idx))p$((i)),UN,0,ping$((rrd_idx))p$((i)),IF"; done)
$(echo -n "CDEF:pings$((rrd_idx))=$((PINGS)),p$((rrd_idx))p1,UN"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),UN,+"; done; echo ",-")
$(echo -n "CDEF:m$((rrd_idx))=p$((rrd_idx))p1"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),+"; done; echo ",pings$((rrd_idx)),/")
$(echo -n "CDEF:sdev$((rrd_idx))=p$((rrd_idx))p1,m$((rrd_idx)),-,DUP,*"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),m$((rrd_idx)),-,DUP,*,+"; done; echo ",pings$((rrd_idx)),/,SQRT")
CDEF:dmlow$((rrd_idx))=dm$((rrd_idx)),sdev$((rrd_idx)),2,/,-
CDEF:s2d$((rrd_idx))=sdev$((rrd_idx))
AREA:dmlow$((rrd_idx))
AREA:s2d$((rrd_idx))#${COLOR}30:STACK
LINE1:dm$((rrd_idx))#${COLOR}:"$(basename ${fping_rrd%.*} | awk -F'_' '{print $NF}')\t"
VDEF:avmed$((rrd_idx))=median$((rrd_idx)),AVERAGE
VDEF:avsd$((rrd_idx))=sdev$((rrd_idx)),AVERAGE
CDEF:msr$((rrd_idx))=median$((rrd_idx)),POP,avmed$((rrd_idx)),avsd$((rrd_idx)),/
VDEF:avmsr$((rrd_idx))=msr$((rrd_idx)),AVERAGE
GPRINT:avmed$((rrd_idx)):"Median RTT\: %5.2lfms"
GPRINT:ploss$((rrd_idx)):AVERAGE:"Loss\: %5.1lf%%"
GPRINT:avsd$((rrd_idx)):"Std Dev\: %5.2lfms"
GPRINT:avmsr$((rrd_idx)):"Ratio\: %5.1lfms\\j"
EOF
(( rrd_idx++ ))
done && unset rrd_idx
}
 
if [ -z "${rrd_files}" ]
 then printf "${0} \"file.png\" { file1.rrd ... file6.rrd }\n"
 else TIME=$(( $(date -d "${END}" +%s) - $(date -d "${START}" +%s) ))
      eval $(rrd_graph_cmd; rrd_graph_opts)
fi

SmokePing like Graph

fping_172.31.15.4_smoke.png

graph_smoke.sh: Bourne-Again shell script, ASCII text executable

#!/usr/bin/env bash
## Create a SmokePing like graph from a RRD file
## Requires: rrdtool
## 2021 - Script from www.davideaves.com
 
# Enable for debuging
#set -x
 
fping_rrd="${1}"
COLOR=( "0F0f00" "00FF00" "00BBFF" "0022FF" "8A2BE2" "FA0BE2" "C71585" "FF0000" )
LINE=".5"
 
rrd_graph_cmd() {
cat << EOF
rrdtool graph "$(basename ${fping_rrd%.*})_smoke.png"
--start "${START}" --end "${END}"
--title "$(basename ${fping_rrd%.*} | awk -F'_' '{print $NF}')"
--height 95 --width 600
--vertical-label "Seconds"
--color BACK#F3F3F3
--color CANVAS#FDFDFD
--color SHADEA#CBCBCB
--color SHADEB#999999
--color FONT#000000
--color AXIS#2C4D43
--color ARROW#2C4D43
--color FRAME#2C4D43
--border 1
--font TITLE:10:"Arial"
--font AXIS:8:"Arial"
--font LEGEND:9:"Courier"
--font UNIT:8:"Arial"
--font WATERMARK:7:"Arial"
--imgformat PNG
EOF
}
 
rrd_graph_opts() {
cat << EOF
DEF:median$((rrd_idx))="${fping_rrd}":median:AVERAGE
DEF:loss$((rrd_idx))="${fping_rrd}":loss:AVERAGE
$(for ((i=1;i<=PINGS;i++)); do echo "DEF:ping$((rrd_idx))p$((i))=\"${fping_rrd}\":ping$((i)):AVERAGE"; done)
CDEF:ploss$((rrd_idx))=loss$((rrd_idx)),20,/,100,*
CDEF:dm$((rrd_idx))=median$((rrd_idx)),0,100000,LIMIT
$(for ((i=1;i<=PINGS;i++)); do echo "CDEF:p$((rrd_idx))p$((i))=ping$((rrd_idx))p$((i)),UN,0,ping$((rrd_idx))p$((i)),IF"; done)
$(echo -n "CDEF:pings$((rrd_idx))=$((PINGS)),p$((rrd_idx))p1,UN"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),UN,+"; done; echo ",-")
$(echo -n "CDEF:m$((rrd_idx))=p$((rrd_idx))p1"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),+"; done; echo ",pings$((rrd_idx)),/")
$(echo -n "CDEF:sdev$((rrd_idx))=p$((rrd_idx))p1,m$((rrd_idx)),-,DUP,*"; for ((i=2;i<=PINGS;i++)); do echo -n ",p$((rrd_idx))p$((i)),m$((rrd_idx)),-,DUP,*,+"; done; echo ",pings$((rrd_idx)),/,SQRT")
CDEF:dmlow$((rrd_idx))=dm$((rrd_idx)),sdev$((rrd_idx)),2,/,-
CDEF:s2d$((rrd_idx))=sdev$((rrd_idx))
AREA:dmlow$((rrd_idx))
AREA:s2d$((rrd_idx))#${COLOR[0]}30:STACK
\
VDEF:avmed$((rrd_idx))=median$((rrd_idx)),AVERAGE
VDEF:avsd$((rrd_idx))=sdev$((rrd_idx)),AVERAGE
CDEF:msr$((rrd_idx))=median$((rrd_idx)),POP,avmed$((rrd_idx)),avsd$((rrd_idx)),/
VDEF:avmsr$((rrd_idx))=msr$((rrd_idx)),AVERAGE
LINE3:avmed$((rrd_idx))#${COLOR[1]}15:
\
COMMENT:"\t\t"
COMMENT:"Average"
COMMENT:"Maximum"
COMMENT:"Minimum"
COMMENT:"Current"
COMMENT:"Std Dev"
COMMENT:" \\j"
\
COMMENT:"Median RTT\:\t"
GPRINT:avmed$((rrd_idx)):"%.2lf"
GPRINT:median$((rrd_idx)):MAX:"%.2lf"
GPRINT:median$((rrd_idx)):MIN:"%.2lf"
GPRINT:median$((rrd_idx)):LAST:"%.2lf"
GPRINT:avsd$((rrd_idx)):"%.2lf"
COMMENT:" \\j"
\
COMMENT:"Packet Loss\:\t"
GPRINT:ploss$((rrd_idx)):AVERAGE:"%.2lf%%"
GPRINT:ploss$((rrd_idx)):MAX:"%.2lf%%"
GPRINT:ploss$((rrd_idx)):MIN:"%.2lf%%"
GPRINT:ploss$((rrd_idx)):LAST:"%.2lf%%"
COMMENT:"  -  "
COMMENT:" \\j"
\
COMMENT:"Loss Colors\:\t"
CDEF:me0=loss$((rrd_idx)),-1,GT,loss$((rrd_idx)),0,LE,*,1,UNKN,IF,median$((rrd_idx)),*
CDEF:meL0=me0,${LINE},-
CDEF:meH0=me0,0,*,${LINE},2,*,+
AREA:meL0
STACK:meH0#${COLOR[1]}:" 0/$((PINGS))"
CDEF:me1=loss$((rrd_idx)),0,GT,loss$((rrd_idx)),1,LE,*,1,UNKN,IF,median$((rrd_idx)),*
CDEF:meL1=me1,${LINE},-
CDEF:meH1=me1,0,*,${LINE},2,*,+
AREA:meL1
STACK:meH1#${COLOR[2]}:" 1/$((PINGS))"
CDEF:me2=loss$((rrd_idx)),1,GT,loss$((rrd_idx)),2,LE,*,1,UNKN,IF,median$((rrd_idx)),*
CDEF:meL2=me2,${LINE},-
CDEF:meH2=me2,0,*,${LINE},2,*,+
AREA:meL2
STACK:meH2#${COLOR[3]}:" 2/$((PINGS))"
CDEF:me3=loss$((rrd_idx)),2,GT,loss$((rrd_idx)),3,LE,*,1,UNKN,IF,median$((rrd_idx)),*
CDEF:meL3=me3,${LINE},-
CDEF:meH3=me3,0,*,${LINE},2,*,+
AREA:meL3
STACK:meH3#${COLOR[4]}:" 3/$((PINGS))"
CDEF:me4=loss$((rrd_idx)),3,GT,loss$((rrd_idx)),4,LE,*,1,UNKN,IF,median$((rrd_idx)),*
CDEF:meL4=me4,${LINE},-
CDEF:meH4=me4,0,*,${LINE},2,*,+
AREA:meL4
STACK:meH4#${COLOR[5]}:" 4/$((PINGS))"
CDEF:me10=loss$((rrd_idx)),4,GT,loss$((rrd_idx)),10,LE,*,1,UNKN,IF,median$((rrd_idx)),*
CDEF:meL10=me10,${LINE},-
CDEF:meH10=me10,0,*,${LINE},2,*,+
AREA:meL10
STACK:meH10#${COLOR[6]}:"10/$((PINGS))"
CDEF:me19=loss$((rrd_idx)),10,GT,loss$((rrd_idx)),19,LE,*,1,UNKN,IF,median$((rrd_idx)),*
CDEF:meL19=me19,${LINE},-
CDEF:meH19=me19,0,*,${LINE},2,*,+
AREA:meL19
STACK:meH19#${COLOR[7]}:"19/$((PINGS))\\j"
\
COMMENT:"Probe\: $((PINGS)) pings every $((STEP)) seconds"
COMMENT:"$(date -d "${START}" | sed 's/\:/\\\:/g') ($(awk -v TIME=$TIME 'BEGIN {printf "%.1f hr", TIME/3600}'))\\j"
EOF
}
 
if [ ! -r "${fping_rrd}" ]
 then printf "${0} \"file.rrd\"\n"
 else
      STEP=$(rrdtool info "${fping_rrd}" | awk '/^step/{print $NF}')
      PINGS=$(rrdtool info "${fping_rrd}" | awk '/^ds.ping.*index/{count++} END{print count}')
 
      START="$([ -z "${2}" ] && echo "-7 hours" || echo "${2}")"
      END="$([ -z "${3}" ] && echo "now" || echo "${3}")"
      TIME=$(( $(date -d "${END}" +%s) - $(date -d "${START}" +%s) ))
 
      eval $(rrd_graph_cmd; rrd_graph_opts)
fi
19. December 2018 · Comments Off on Ansible playbook to provision Netscaler VIPs. · Categories: Ansible, Linux, Linux Admin, Load Balancing, NetScaler, Networking · Tags: , , ,

The following playbook will create a fully functional VIP; including the supporting monitor, service-group (pool) and servers (nodes) on a netscaler loadbalancer. Additionally, the same playbook has the ability to fully deprovision a VIP and all its supporting artifacts. To do all this I use the native Netscaler Ansible modules. When it comes to using the netscaler_servicegroup module, since the number of servers are not always consistent; I create that task with a Jinja2 template, where its imported back into the play.

netscaler_provision.yaml: a /usr/bin/ansible-playbook -f 10 script text executable, ASCII text

#!/usr/bin/ansible-playbook -f 10
## Ansible playbook to provision Netscaler VIPs.
# Requires: nitrosdk-python
# 2018 (v.01) - Playbook from www.davideaves.com
---
- name: Netscaler VIP provision
  hosts: netscaler
  connection: local
  gather_facts: False

  vars:

    ansible_connection: "local"
    ansible_python_interpreter: "/usr/bin/env python"

    state: 'present'

    lbvip:
      name: testvip
      address: 203.0.113.1
      server:
        - name: 'server-1'
          address: '192.0.2.1'
          description: 'Ansible Test Server 1'
          disabled: 'true'
        - name: 'server-2'
          address: '192.0.2.2'
          description: 'Ansible Test Server 2'
          disabled: 'true'
        - name: 'server-3'
          address: '192.0.2.3'
          description: 'Ansible Test Server 3'
          disabled: 'true'
        - name: 'server-4'
          address: '192.0.2.4'
          description: 'Ansible Test Server 4'
          disabled: 'true'
        - name: 'server-5'
          address: '192.0.2.5'
          description: 'Ansible Test Server 5'
          disabled: 'true'
        - name: 'server-6'
          address: '192.0.2.6'
          description: 'Ansible Test Server 6'
          disabled: 'true'
        - name: 'server-7'
          address: '192.0.2.7'
          description: 'Ansible Test Server 7'
          disabled: 'true'
        - name: 'server-8'
          address: '192.0.2.8'
          description: 'Ansible Test Server 8'
          disabled: 'true'
      vserver:
        - port: '80'
          description: 'Generic service running on 80'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '443'
          description: 'Generic service running on 443'
          type: 'SSL_BRIDGE'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8080'
          description: 'Generic service running on 8080'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8081'
          description: 'Generic service running on 8081'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8443'
          description: 'Generic service running on 8443'
          type: 'SSL_BRIDGE'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'

  tasks:

    - name: Build lbvip and all related componets.
      block:
      - local_action:
          module: netscaler_server
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "{{ item.name }}"
          ipaddress: "{{ item.address }}"
          comment: "{{ item.description | default('Ansible Created') }}"
          disabled: "{{ item.disabled | default('false') }}"
        with_items: "{{ lbvip.server }}"
      - local_action:
          module: netscaler_lb_monitor
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
          type: TCP
          destport: "{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
        no_log: false
      - local_action:
          module: copy
          content: "{{ lookup('template', 'templates/netscaler_servicegroup.j2') }}"
          dest: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
          mode: "0644"
        with_items: "{{ lbvip.vserver }}"
        changed_when: false
      - include_tasks: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: file
          state: absent
          path: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
        with_items: "{{ lbvip.vserver }}"
        changed_when: false
      - local_action:
          module: netscaler_lb_vserver
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "vs_{{ lbvip.name }}_{{ item.port }}"
          servicetype: "{{ item.type }}"
          ipv46: "{{ lbvip.address }}"
          port: "{{ item.port }}"
          lbmethod: "{{ item.method | default('LEASTCONNECTION') }}"
          persistencetype: "{{ item.persistence | default('SOURCEIP') }}"
          servicegroupbindings:
            - servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      when: state == "present"

    - name: Destroy lbvip and all related componets.
      block:
      - local_action:
          module: netscaler_lb_vserver
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "vs_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_servicegroup
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_lb_monitor
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
          type: TCP
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_server
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "{{ item.name }}"
        with_items: "{{ lbvip.server }}"
      when: state == "absent"

The following is the Jinja2 template that creates the netscaler_servicegroup task. An important thing to note is my use of the RAW block. When the task is created and stored in /tmp it does not contain any account credentials, instead I preserve the variable in the raw to prevent leaking sensitive information to anyone who may be snooping around on the server while the playbook is running.

templates/netscaler_servicegroup.j2: ASCII text, with CRLF line terminators

---
- local_action:
    module: netscaler_servicegroup
    nsip: {% raw %}"{{ inventory_hostname }}"
{% endraw %}
    nitro_user: {% raw %}"{{ nitro_user }}"
{% endraw %}
    nitro_pass: {% raw %}"{{ nitro_pass }}"
{% endraw %}
    nitro_protocol: "https"
    validate_certs: no

    state: "{{ state | default('present') }}"

    servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
    comment: "{{ item.description | default('Ansible Created') }}"
    servicetype: "{{ item.type }}"
    servicemembers:
{% for i in lbvip.server %}
      - servername: "{{ i.name }}"
        port: "{{ item.port }}"
{% endfor %}
    monitorbindings:
      - monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
13. June 2018 · Comments Off on Using netaddr in Ansible to manipulate network IP, CIDR, MAC and prefix. · Categories: Ansible, Cloud, Linux Admin, Networking · Tags: , , , , , , , , , , , ,

The following ansible playbook is an example that demonstrates using netaddr to manipulate network IP, CIDR, MAC and prefix. Additional examples can be found in the Ansible docs or if your looking to do manipulation in python the following are the docs for netaddr.

#!/usr/local/bin/ansible-playbook
## Using netaddr in Ansible to manipulate network IP, CIDR, MAC and prefix
## 2018 (v.01) - Playbook from www.davideaves.com
---
- hosts: localhost
  gather_facts: false

  vars:
  - IP: 172.31.3.13/23
  - CIDR: 192.168.0.0/16
  - MAC: 1a:2b:3c:4d:5e:6f
  - PREFIX: 18

  tasks:
    - debug: msg="___ {{ IP }} ___ ADDRESS {{ IP | ipaddr('address') }}"
    - debug: msg="___ {{ IP }} ___ BROADCAST {{ IP | ipaddr('broadcast') }}"
    - debug: msg="___ {{ IP }} ___ NETMASK {{ IP | ipaddr('netmask') }}"
    - debug: msg="___ {{ IP }} ___ NETWORK {{ IP | ipaddr('network') }}"
    - debug: msg="___ {{ IP }} ___ PREFIX {{ IP | ipaddr('prefix') }}"
    - debug: msg="___ {{ IP }} ___ SIZE {{ IP | ipaddr('size') }}"
    - debug: msg="___ {{ IP }} ___ WILDCARD {{ IP | ipaddr('wildcard') }}"
    - debug: msg="___ {{ IP }} ___ RANGE {{ IP | ipaddr('range_usable') }}"
    - debug: msg="___ {{ IP }} ___ REVERSE DNS {{ IP | ipaddr('revdns') }}"
    - debug: msg="___ {{ IP }} ___ HEX {{ IP | ipaddr('address') | ip4_hex() }}"
    - debug: msg="___ {{ MAC }} ___ CISCO {{ MAC | hwaddr('cisco') }}"
    - debug: msg="___ {{ CIDR }} ___ Last /20 CIDR {{ CIDR | ipsubnet(20, -1) }}"
    - debug: msg="___ {{ CIDR }} ___ 1st IP {{ CIDR | ipaddr(1) }}"
    - debug: msg="___ {{ CIDR }} ___ 3rd from last IP {{ CIDR | ipaddr(-3) }}"
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