Changeset - 0b86d3da5a29
[Not reviewed]
0 5 0
Branko Majic (branko) - 6 years ago 2018-07-23 22:27:52
branko@majic.rs
MAR-133: Improve output for certificate checks:

- Do not produce warnings in case no certificates have been configured
for checking.
- Only send out mails about certificates that are about to expire.
- Include information in how many days a certificate is going to
expire.
- Include information on whether the certificates has already expired.
5 files changed with 70 insertions and 19 deletions:
0 comments (0 inline, 0 general)
docs/releasenotes.rst
Show inline comments
 
@@ -24,24 +24,29 @@ Breaking changes:
 
New features/improvements:
 

	
 
* Tests have been updated to work with latest Molecule/Testinfra as
 
  part of the Ansible upgrade process.
 

	
 
* ``common`` role
 

	
 
  * The ``pip`` requirements upgrade checks are now performed once per
 
    day instead of once per hour.
 
  * The ``pip`` requirements upgrade checks now do not output warning
 
    in case deployed ``.in`` file does not have a matching ``.txt``
 
    file.
 
  * Certificate expiration check is less verbose. No mails are sent
 
    out any longer in case no certificates have been configured for
 
    checking, nor in cases where all certificates have passed the
 
    check. E.g. mails are sent out only in case some of the configured
 
    certificates will expire within next 30 days.
 

	
 

	
 
2.0.0
 
-----
 

	
 
Upgrade to Ansible 2.3.x, minor bug fixes and updates needed for the upgrade.
 

	
 
Breaking changes:
 

	
 
* Switched to Ansible 2.3.x, removing support for Ansible 1.9.x. All
 
  documentation has been updated.
 

	
docs/rolereference.rst
Show inline comments
 
@@ -273,25 +273,27 @@ The role implements the following:
 
  can reach them provided they have appropriate read/write rights on the file
 
  itself, and provided they know the exact path of the file.
 
* Deploys CA certificate files, normally used for truststore purposes, to
 
  ``/usr/local/share/ca-certificates/``.
 
* Installs ``ferm`` (for iptables management), configuring a basic firewall
 
  which allows ICMP echo requests (PING), incoming connection on TCP port 22
 
  (SSH), and also introduces rate-limitting for incoming ICMP echo request
 
  pacakges and (new) TCP connections. The rate-limitting is based on the source
 
  IP address, using the ``iptables hashlimit`` module.
 
* Sets-up system for performing checks on certificates (currently only if they
 
  expire within less than 30 days). Roles that want their certificates checked
 
  should deploy a ``.conf`` to directory ``/etc/check_certificate/`` with paths
 
  to certificate files, one per line. Certificates are checked on daily basis.
 
  to certificate files, one per line. Certificates are checked on
 
  daily basis, using crontab (resulting in failures being sent out to
 
  the ``root`` user).
 
* Deploys ``apticron`` package that performs checks for available package
 
  upgrades on daily basis. Mails are delivered to local ``root`` account, and
 
  can be redirected elsewhere via aliases. If using ``mail_forwarder`` or
 
  ``mail_server`` roles on the same server, aliases can be set-up through them.
 
* Sets-up system for performing checks on pip requirements files. Roles that
 
  want their requirements files checked should create a sub-directory inside of
 
  ``/etc/pip_check_requirements_upgrades``, and place ``.txt`` and ``.in`` files
 
  inside (with same base name). The ``.txt`` files should be standard
 
  requirements files with fixed versions (the ones installed by the role). The
 
  ``.in`` files should contain only the top-level packages (no
 
  dependencies). Avoid hard-coding versions in the ``.in`` file unless really
 
  needed. For packages where you want to stick to stable/LTS version branch, you
docs/usage.rst
Show inline comments
 
@@ -585,29 +585,30 @@ one up first. This includes both the LDAP *server* and *client* configuration.
 
6. Ok, time to re-run the playbooks again... Wait a minute, something
 
   is missing here... Oh, right, I forgot to mention one thing - Majic
 
   Ansible Roles use TLS throughout wherever possible. In other words,
 
   you *must* have TLS private keys and certificates issued by some CA
 
   for all servers in order to be able to use most of the roles
 
   (actually, you need them issued per *service*). This includes
 
   ``ldap_server`` too. So, let's make a slight detour to create a CA
 
   of our own, plus the necessary server certificate for the LDAP
 
   service...
 

	
 
   .. note::
 
      Another useful feature the roles implement is a check to see if
 
      certificates will expire within the next 30 days. This check is performed
 
      via cronjob at midnight, and results will end-up being delivered to the
 
      ``root`` user on local server. Later on, once you have configured the mail
 
      server, you should be able to set-up the necessary aliases to have the
 
      mails delivered to non-local accounts too.
 
      certificates will expire within the next 30 days. This check is
 
      performed via cronjob at midnight, and failing results will
 
      end-up being delivered to the ``root`` user on local
 
      server. Later on, once you have configured the mail server, you
 
      should be able to set-up the necessary aliases to have the mails
 
      delivered to non-local accounts too.
 

	
 
   1. Let's first install a couple of more tools on the Ansible server, since we
 
      will be using ``certtool`` for our improvised CA needs (run this as
 
      ``root``)::
 

	
 
        apt-get install -y gnutls-bin
 

	
 
   2. Create directory where the private keys and certificates will be stored
 
      at (you can switch back to the ``ansible`` user now)::
 

	
 
        mkdir ~/mysite/tls/
 

	
roles/common/files/check_certificate.sh
Show inline comments
 
@@ -50,24 +50,26 @@ lines will be ignored.
 
$program accepts the following options:
 

	
 
    -e period
 
        Number of days before certificate expires after which the certificate
 
        should be considered as about to expire. Value is used in the following
 
        check types: expiration.
 

	
 
    -c certificate_file
 
        Path to certificate file for which the checks should be run. This option
 
        can be specified multiple times on the command line in order to verify
 
        multiple certificates.
 

	
 
    -q
 
        Enable quiet mode. Output only warnings and errors.
 

	
 
    -d
 
        Enable debug output.
 

	
 
    -v
 
        Show script version and licensing information.
 

	
 
    -h
 
        Show usage help.
 

	
 

	
 
Please report bugs and send feature requests to <branko@majic.rs>.
 
@@ -112,103 +114,144 @@ if [[ -t 1 ]] && (( ${_color_terminal} > 0 )); then
 
else
 
    _text_bold=""
 
    _text_white=""
 
    _text_blue=""
 
    _text_green=""
 
    _text_yellow=""
 
    _text_red=""
 
    _text_reset=""
 
fi
 

	
 
# Set-up functions for printing coloured messages.
 
function debug() {
 
    if [[ $DEBUG != 0 ]]; then
 
	echo "${_text_bold}${_text_blue}[DEBUG]${_text_reset}" "$@"
 
    fi
 
    [[ $DEBUG != 0 ]] && echo "${_text_bold}${_text_blue}[DEBUG]${_text_reset}" "$@"
 
}
 

	
 
function info() {
 
    echo "${_text_bold}${_text_white}[INFO] ${_text_reset}" "$@"
 
    [[ $QUIET == 0 ]] && echo "${_text_bold}${_text_white}[INFO] ${_text_reset}" "$@"
 
}
 

	
 
function success() {
 
    echo "${_text_bold}${_text_green}[OK]   ${_text_reset}" "$@"
 
    [[ $QUIET == 0 ]] && echo "${_text_bold}${_text_green}[OK]   ${_text_reset}" "$@"
 
}
 

	
 
function warning() {
 
    echo "${_text_bold}${_text_yellow}[WARN] ${_text_reset}" "$@"
 
}
 

	
 
function error() {
 
    echo "${_text_bold}${_text_red}[ERROR]${_text_reset}" "$@" >&2
 
}
 

	
 
#
 
# Checks expiration of passed-in certificate file.
 
#
 
# Arguments:
 
#
 
#   $1 - Path to certificate file to check
 
#
 
# Returns:
 
#
 
#   0 if check has passed, 1 if check has not passed.
 
#
 
function check_expiration() {
 
    local certificate_file="$1"
 
    local certificate_file_expiration_date
 
    local certificate_expiration_date certificate_expiration_date_in_seconds certificate_expires_in
 
    local current_date_in_seconds
 
    local expiration_period_seconds
 
    local status
 

	
 
    let expiration_period_seconds="$expiration_period"*24*60*60
 

	
 
    debug "Running expiration check for file: $certificate_file"
 
    debug "Expiration period set to: $expiration_period"
 

	
 
    certificate_file_expiration_date=$(openssl x509 -enddate -noout -in "$certificate_file" | sed -e 's/^notAfter=//')
 
    certificate_expiration_date=$(openssl x509 -enddate -noout -in "$certificate_file" | sed -e 's/^notAfter=//')
 
    certificate_expiration_date_in_seconds=$(date -d "$certificate_expiration_date" "+%s")
 
    current_date_in_seconds=$(date "+%s")
 
    let certificate_expires_in="$certificate_expiration_date_in_seconds-$current_date_in_seconds"
 

	
 
    if (( $certificate_expires_in >= 0 )); then
 
        status="expires"
 
    else
 
        status="expired"
 
    fi
 

	
 
    if openssl x509 -noout -in "$certificate_file" -checkend "$expiration_period_seconds" > /dev/null; then
 
	success "Expiration check ($expiration_period days) passed for $certificate_file (expires on $certificate_file_expiration_date)."
 
        success "Certificate $certificate_file $status on $certificate_expiration_date, $(print_relative_period "$certificate_expires_in")."
 
	return 0
 
    else
 
	error "Expiration check ($expiration_period days) failed for $certificate_file (expires on $certificate_file_expiration_date)."
 
        error "Certificate $certificate_file $status on $certificate_expiration_date, $(print_relative_period "$certificate_expires_in")."
 
	return 1
 
    fi
 
}
 

	
 
#
 
# Outputs period relative to current time in human-readable format
 
# with granularity in days.
 
#
 
# Arguments:
 
#
 
#   $1 - Time period in seconds. Can be negative to denote past.
 
#
 
function print_relative_period() {
 
    local seconds="$1"
 
    local days leftover
 

	
 
    let days="$seconds/(60*60*24)"
 

	
 
    if (( $days == 1 )); then
 
        echo "in $days day"
 
    elif (( $days > 1 )); then
 
        echo "in $days days"
 
    elif (( $days == 0 && $seconds > 0 )); then
 
        echo "in less than a day"
 
    elif (( $days == 0 && $seconds < 0 )); then
 
        echo "less than a day ago"
 
    elif (( $days == -1 )); then
 
        echo "one day ago"
 
    elif (( $days < -1 )); then
 
        echo "${days#-} days ago"
 
    fi
 
}
 

	
 
# Exit codes
 
ERROR_SUCCESS=0
 
ERROR_PARAMETERS=1
 
ERROR_FAILED_CHECK=2
 

	
 
# If no arguments were given, just show usage help.
 
if [[ -z $1 ]]; then
 
    usage
 
    exit $ERROR_SUCCESS
 
fi
 

	
 
# Disable debug by default.
 
DEBUG=0
 
QUIET=0
 

	
 
# Set-up default option values.
 
let expiration_period=30
 

	
 
# List of certificate files to check.
 
certificate_files=()
 
configuration_directory="/etc/check_certificate"
 

	
 
# Parse the arguments
 
while getopts "e:c:C:xdvh" opt; do
 
while getopts "e:c:C:xqdvh" opt; do
 
    case "$opt" in
 
	e) let expiration_period="$OPTARG";;
 
	c) certificate_files+=("$OPTARG");;
 
	C) configuration_directory="$OPTARG";;
 
        q) QUIET=1;;
 
	d) DEBUG=1;;
 
        v) version
 
           exit $ERROR_SUCCESS;;
 
        h) usage
 
           exit $ERROR_SUCCESS;;
 
        *) usage
 
           exit $ERROR_PARAMETERS;;
 
    esac
 
done
 
i=$OPTIND
 
shift $(($i-1))
 

	
 
@@ -229,27 +272,27 @@ done
 
if [[ ${#certificate_files[@]} == 0 ]]; then
 
    for configuration_file in "$configuration_directory"/*.conf; do
 
	if [[ -f $configuration_file ]]; then
 
	    DONE=false
 
	    until "$DONE"; do
 
		read line || DONE=true
 
		[[ ! $line =~ ^[[:blank:]]*$ ]] && certificate_files+=("$line")
 
	    done < "$configuration_file"
 
	fi
 
    done
 
fi
 

	
 
# Log a warning if list of certificates is empty.
 
# Inform user that no certificates have been configured for checking.
 
if [[ ${#certificate_files[@]} == 0 ]]; then
 
    warning "No certificate files were specified for checking."
 
    info "No certificate files were specified for checking."
 
fi
 

	
 
# Process the certificate files.
 
result=$ERROR_SUCCESS
 
for certificate_file in "${certificate_files[@]}"; do
 
    for check in "$@"; do
 
	if ! check_"$check" "${certificate_file}"; then
 
	    result=$ERROR_FAILED_CHECK
 
	fi
 
    done
 
done
 

	
roles/common/tasks/main.yml
Show inline comments
 
@@ -258,25 +258,25 @@
 
    path: "/etc/check_certificate"
 
    state: "directory"
 
    owner: root
 
    group: root
 
    mode: 0755
 

	
 
- name: Deploy crontab entry for checking certificates
 
  cron:
 
    name: "check_certificate"
 
    cron_file: "check_certificate"
 
    hour: 0
 
    minute: 0
 
    job: "/usr/local/bin/check_certificate.sh expiration"
 
    job: "/usr/local/bin/check_certificate.sh -q expiration"
 
    state: present
 
    user: nobody
 

	
 
- name: Install apticron (for checking available upgrades)
 
  apt:
 
    name: apticron
 
    state: present
 

	
 
# Implementation for checking pip requirements files via via pip-tools.
 
- name: Install virtualenv for pip requirements checks
 
  apt:
 
    name: virtualenv
0 comments (0 inline, 0 general)