From 38e97f9d092cd6ced9b28efd9aeedfdd00e9ea99 Mon Sep 17 00:00:00 2001 From: Andrea Dell'Amico Date: Mon, 22 Jun 2020 13:41:05 +0200 Subject: [PATCH] Import the old haproxy role. --- README.md | 64 ++- defaults/main.yml | 29 +- files/check_haproxy_stats | 225 +++++++++ files/haproxy-letsencrypt.sh | 29 ++ handlers/main.yml | 13 +- meta/main.yml | 56 +-- tasks/haproxy-letsencrypt-acme-sh.yml | 16 + tasks/haproxy-nagios.yml | 10 + tasks/haproxy-service.yml | 63 +++ tasks/haproxy-ssl.yml | 17 + tasks/main.yml | 32 +- templates/hapos-upd.j2 | 571 +++++++++++++++++++++++ templates/haproxy-letsencrypt-acme.sh.j2 | 50 ++ templates/lb.cfg.j2 | 4 + 14 files changed, 1111 insertions(+), 68 deletions(-) create mode 100644 files/check_haproxy_stats create mode 100644 files/haproxy-letsencrypt.sh create mode 100644 tasks/haproxy-letsencrypt-acme-sh.yml create mode 100644 tasks/haproxy-nagios.yml create mode 100644 tasks/haproxy-service.yml create mode 100644 tasks/haproxy-ssl.yml create mode 100644 templates/hapos-upd.j2 create mode 100644 templates/haproxy-letsencrypt-acme.sh.j2 create mode 100644 templates/lb.cfg.j2 diff --git a/README.md b/README.md index 3637db8..e54c9cd 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,59 @@ Role Name ========= -A brief description of the role goes here. - -Requirements ------------- - -Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. +A role that installs Haproxy, . Role Variables -------------- -A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. +The most important variables are listed below: + +``` yaml +haproxy_latest_release: True +haproxy_version: 2.0 +haproxy_repo_key: 'http://haproxy.debian.net/bernat.debian.org.gpg' +haproxy_debian_latest_repo: "deb http://haproxy.debian.net {{ ansible_lsb.codename }}-backports-{{ haproxy_version }} main" +haproxy_ubuntu_latest_repo: "ppa:vbernat/haproxy-{{ haproxy_version }}" +haproxy_pkg_state: latest +haproxy_enabled: True +haproxy_k_bind_non_local_ip: True + +haproxy_default_port: 80 +haproxy_terminate_tls: False +haproxy_ssl_port: 443 +haproxy_admin_port: 8880 +haproxy_admin_socket: /run/haproxy/admin.sock + +haproxy_letsencrypt_managed: True +haproxy_cert_dir: '{{ pki_dir }}/haproxy' + +haproxy_nagios_check: False +# It's a percentage +haproxy_nagios_check_w: 70 +haproxy_nagios_check_c: 90 + +haproxy_check_interval: 3s +haproxy_backend_maxconn: 2048 + +haproxy_sysctl_conntrack_max: 131072 +``` + +Additional tasks +------------ + + The user of this role will need to write a haproxy.cfg template and install it with a dedicated task. Something like + +```yaml +- name: Configure haproxy + template: src=haproxy.cfg.j2 dest=/etc/haproxy/haproxy.cfg owner=root group=haproxy mode=0440 + notify: Reload haproxy + tags: [ 'haproxy', 'haproxy_conf' ] +``` Dependencies ------------ -A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. - -Example Playbook ----------------- - -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: - - - hosts: servers - roles: - - { role: username.rolename, x: 42 } +* letsencrypt-acme-sh License ------- @@ -35,4 +63,4 @@ EUPL-1.2 Author Information ------------------ -An optional section for the role authors to include contact information, or a website (HTML is not allowed). +Andrea Dell'Amico, diff --git a/defaults/main.yml b/defaults/main.yml index 95d3c70..415e3c6 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,2 +1,29 @@ --- -# defaults file for ansible-role-template \ No newline at end of file +haproxy_latest_release: True +haproxy_version: 2.0 +haproxy_repo_key: 'http://haproxy.debian.net/bernat.debian.org.gpg' +haproxy_debian_latest_repo: "deb http://haproxy.debian.net {{ ansible_lsb.codename }}-backports-{{ haproxy_version }} main" +haproxy_ubuntu_latest_repo: "ppa:vbernat/haproxy-{{ haproxy_version }}" +haproxy_pkg_state: latest +haproxy_enabled: True +haproxy_k_bind_non_local_ip: True + +haproxy_default_port: 80 +haproxy_terminate_tls: False +haproxy_ssl_port: 443 +haproxy_admin_port: 8880 +haproxy_admin_socket: /run/haproxy/admin.sock + +haproxy_letsencrypt_managed: True +haproxy_cert_dir: '{{ pki_dir }}/haproxy' + +haproxy_nagios_check: False +# It's a percentage +haproxy_nagios_check_w: 70 +haproxy_nagios_check_c: 90 + +haproxy_check_interval: 3s +haproxy_backend_maxconn: 2048 + +haproxy_sysctl_conntrack_max: 131072 + diff --git a/files/check_haproxy_stats b/files/check_haproxy_stats new file mode 100644 index 0000000..aed3ce3 --- /dev/null +++ b/files/check_haproxy_stats @@ -0,0 +1,225 @@ +#!/usr/bin/env perl +# vim: se et ts=4: + +# +# Copyright (C) 2012, Giacomo Montagner +# +# This program is free software; you can redistribute it and/or modify it +# under the same terms as Perl 5.10.1. +# For more details, see http://dev.perl.org/licenses/artistic.html +# +# This program is distributed in the hope that it will be +# useful, but without any warranty; without even the implied +# warranty of merchantability or fitness for a particular purpose. +# + +our $VERSION = "1.0.1"; + +# CHANGELOG: +# 1.0.0 - first release +# 1.0.1 - fixed empty message if all proxies are OK +# + +use strict; +use warnings; +use 5.010.001; +use File::Basename qw/basename/; +use IO::Socket::UNIX; +use Getopt::Long; + +sub usage { + my $me = basename $0; + print <. + $me is distributed under GPL and the Artistic License 2.0 + +SEE ALSO + Check out online haproxy documentation at + +EOU +} + +my %check_statuses = ( + UNK => "unknown", + INI => "initializing", + SOCKERR => "socket error", + L4OK => "layer 4 check OK", + L4CON => "connection error", + L4TMOUT => "layer 1-4 timeout", + L6OK => "layer 6 check OK", + L6TOUT => "layer 6 (SSL) timeout", + L6RSP => "layer 6 protocol error", + L7OK => "layer 7 check OK", + L7OKC => "layer 7 conditionally OK", + L7TOUT => "layer 7 (HTTP/SMTP) timeout", + L7RSP => "layer 7 protocol error", + L7STS => "layer 7 status error", +); + +my @status_names = (qw/OK WARNING CRITICAL UNKNOWN/); + +# Defaults +my $swarn = 80.0; +my $scrit = 90.0; +my $sock = "/run/haproxy/admin.sock"; +my $dump; +my $proxy; +my $help; + +# Read command line +Getopt::Long::Configure ("bundling"); +GetOptions ( + "c|critical=i" => \$scrit, + "d|dump" => \$dump, + "h|help" => \$help, + "p|proxy=s" => \$proxy, + "s|sock|socket=s" => \$sock, + "w|warning=i" => \$swarn, +); + +# Want help? +if ($help) { + usage; + exit 3; +} + +# Connect to haproxy socket and get stats +my $haproxy = new IO::Socket::UNIX ( + Peer => $sock, + Type => SOCK_STREAM, +); +die "Unable to connect to haproxy socket: $@" unless $haproxy; +print $haproxy "show stat\n" or die "Print to socket failed: $!"; + +# Dump stats and exit if requested +if ($dump) { + while (<$haproxy>) { + print; + } + exit 0; +} + +# Get labels from first output line and map them to their position in the line +my $labels = <$haproxy>; +chomp($labels); +$labels =~ s/^# // or die "Data format not supported."; +my @labels = split /,/, $labels; +{ + no strict "refs"; + my $idx = 0; + map { $$_ = $idx++ } @labels; +} + +# Variables I will use from here on: +our $pxname; +our $svname; +our $status; + +my @proxies = split ',', $proxy if $proxy; +my $exitcode = 0; +my $msg; +my $checked = 0; +while (<$haproxy>) { + chomp; + next if /^[[:space:]]*$/; + my @data = split /,/, $_; + if (@proxies) { next unless grep {$data[$pxname] eq $_} @proxies; }; + + # Is session limit enforced? + our $slim; + if ($data[$slim]) { + # Check current session # against limit + our $scur; + my $sratio = $data[$scur]/$data[$slim]; + if ($sratio >= $scrit || $sratio >= $swarn) { + $exitcode = $sratio >= $scrit ? 2 : + $exitcode < 2 ? 1 : $exitcode; + $msg .= sprintf "%s:%s sessions: %.2f%%; ", $data[$pxname], $data[$svname], $sratio; + } + } + + # Check of BACKENDS + if ($data[$svname] eq 'BACKEND') { + if ($data[$status] ne 'UP') { + $msg .= sprintf "BACKEND: %s is %s; ", $data[$pxname], $data[$status]; + $exitcode = 2; + } + # Check of FRONTENDS + } elsif ($data[$svname] eq 'FRONTEND') { + if ($data[$status] ne 'OPEN') { + $msg .= sprintf "FRONTEND: %s is %s; ", $data[$pxname], $data[$status]; + $exitcode = 2; + } + # Check of servers + } else { + if ($data[$status] ne 'UP') { + next if $data[$status] eq 'no check'; # Ignore server if no check is configured to be run + $exitcode = 2; + our $check_status; + $msg .= sprintf "server: %s:%s is %s", $data[$pxname], $data[$svname], $data[$status]; + $msg .= sprintf " (check status: %s)", $check_statuses{$data[$check_status]} if $check_statuses{$data[$check_status]}; + $msg .= "; "; + } + } + ++$checked; +} + +unless ($msg) { + $msg = @proxies ? sprintf("checked proxies: %s", join ', ', sort @proxies) : "checked $checked proxies."; +} +say "Check haproxy $status_names[$exitcode] - $msg"; +exit $exitcode; + diff --git a/files/haproxy-letsencrypt.sh b/files/haproxy-letsencrypt.sh new file mode 100644 index 0000000..a540458 --- /dev/null +++ b/files/haproxy-letsencrypt.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +LE_SERVICES_SCRIPT_DIR=/usr/local/lib/letsencrypt +LE_CERTS_DIR=/etc/letsencrypt/live/$HOSTNAME +LE_LOG_DIR=/var/log/letsencrypt +HAPROXY_CERTDIR=/etc/pki/haproxy +HAPROXY_CERTFILE=$HAPROXY_CERTDIR/haproxy.pem +DATE=$( date ) +echo "$DATE" >> $LE_LOG_DIR/haproxy.log + +if [ -f /etc/default/letsencrypt ] ; then + . /etc/default/letsencrypt +else + echo "No letsencrypt default file" >> $LE_LOG_DIR/haproxy.log +fi + +[ ! -d $HAPROXY_CERTDIR ] && mkdir $HAPROXY_CERTDIR + +echo "Building the new certificate file" >> $LE_LOG_DIR/haproxy.log +cat ${LE_CERTS_DIR}/{fullchain.pem,privkey.pem} > ${HAPROXY_CERTFILE} +chmod 440 ${HAPROXY_CERTFILE} +chgrp haproxy ${HAPROXY_CERTFILE} + +echo "Reload the haproxy service" >> $LE_LOG_DIR/haproxy.log +service haproxy reload >/dev/null 2>&1 +echo "Done." >> $LE_LOG_DIR/haproxy.log + +exit 0 + diff --git a/handlers/main.yml b/handlers/main.yml index 27474e0..a3cb82b 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -1,2 +1,13 @@ --- -# handlers file for ansible-role-template \ No newline at end of file +- name: Restart haproxy + service: name=haproxy state=restarted + when: haproxy_enabled + +- name: Reload haproxy + service: name=haproxy state=reloaded + when: haproxy_enabled + +- name: Reload rsyslog + service: name=rsyslog state=reloaded + when: haproxy_enabled + diff --git a/meta/main.yml b/meta/main.yml index 1126a5e..af2e58e 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,61 +1,23 @@ galaxy_info: - author: your name - description: your description + author: Andrea Dell'Amico + description: Systems Architect company: ISTI-CNR - # If the issue tracker for your role is not on github, uncomment the - # next line and provide a value issue_tracker_url: https://redmine-s2i2s.isti.cnr.it/projects/provisioning - # Some suggested licenses: - # - BSD (default) - # - MIT - # - GPLv2 - # - GPLv3 - # - Apache - # - CC-BY - license: EUPL-1.2 + license: EUPL 1.2+ min_ansible_version: 2.8 - # If this a Container Enabled role, provide the minimum Ansible Container version. - # min_ansible_container_version: - - # Optionally specify the branch Galaxy will use when accessing the GitHub - # repo for this role. During role install, if no tags are available, - # Galaxy will use this branch. During import Galaxy will access files on - # this branch. If Travis integration is configured, only notifications for this - # branch will be accepted. Otherwise, in all cases, the repo's default branch - # (usually master) will be used. - #github_branch: - - # - # Provide a list of supported platforms, and for each platform a list of versions. - # If you don't wish to enumerate all versions for a particular platform, use 'all'. # To view available platforms and versions (or releases), visit: # https://galaxy.ansible.com/api/v1/platforms/ # - # platforms: - # - name: Fedora - # versions: - # - all - # - 25 - # - name: SomePlatform - # versions: - # - all - # - 1.0 - # - 7 - # - 99.99 + platforms: + - name: Ubuntu + versions: + - bionic - galaxy_tags: [] - # List tags for your role here, one per line. A tag is a keyword that describes - # and categorizes the role. Users find roles by searching for tags. Be sure to - # remove the '[]' above, if you add tags to this list. - # - # NOTE: A tag is limited to a single word comprised of alphanumeric characters. - # Maximum 20 tags per role. + galaxy_tags: + - haproxy dependencies: [] - # List your role dependencies here, one per line. Be sure to remove the '[]' above, - # if you add dependencies to this list. - diff --git a/tasks/haproxy-letsencrypt-acme-sh.yml b/tasks/haproxy-letsencrypt-acme-sh.yml new file mode 100644 index 0000000..aa0cb5b --- /dev/null +++ b/tasks/haproxy-letsencrypt-acme-sh.yml @@ -0,0 +1,16 @@ +--- +- block: + - name: Create the acme hooks directory if it does not yet exist + file: dest={{ letsencrypt_acme_sh_services_scripts_dir }} state=directory owner=root group=root + + - name: Install a script that fix the letsencrypt certificate for haproxy and then reload the service + template: src=haproxy-letsencrypt-acme.sh.j2 dest={{ letsencrypt_acme_sh_services_scripts_dir }}/haproxy owner=root group=root mode=4555 + + - name: When we are going to install letsencrypt certificates, create a preliminary path and a self signed cert. Now handle the haproxy special case + shell: mkdir {{ pki_dir }}/haproxy ; cat {{ letsencrypt_acme_user_home | default(omit) }}/live/{{ ansible_fqdn }}/privkey {{ letsencrypt_acme_user_home | default(omit) }}/live/{{ ansible_fqdn }}/cert > {{ pki_dir }}/haproxy/haproxy.pem + args: + creates: '{{ pki_dir }}/haproxy/haproxy.pem' + tags: [ 'pki', 'ssl', 'letsencrypt', 'haproxy', 'letsencrypt_acme_sh' ] + + when: letsencrypt_acme_sh_install + tags: [ 'haproxy', 'letsencrypt', 'letsencrypt_acme_sh' ] diff --git a/tasks/haproxy-nagios.yml b/tasks/haproxy-nagios.yml new file mode 100644 index 0000000..272e0e0 --- /dev/null +++ b/tasks/haproxy-nagios.yml @@ -0,0 +1,10 @@ +--- +- name: Install the haproxy NRPE nagios check + copy: src=check_haproxy_stats dest={{ nagios_local_plugdir }}/check_haproxy_stats owner=root group=root mode=0555 + when: haproxy_nagios_check + +- name: Install the haproxy NRPE command configuration + template: src=lb.cfg.j2 dest={{ nrpe_include_dir }}/lb.cfg owner=root group=root mode=0444 + notify: Reload NRPE server + when: haproxy_nagios_check + diff --git a/tasks/haproxy-service.yml b/tasks/haproxy-service.yml new file mode 100644 index 0000000..d310add --- /dev/null +++ b/tasks/haproxy-service.yml @@ -0,0 +1,63 @@ +--- +- name: Get the haproxy repo key + apt_key: url={{ haproxy_repo_key }} state=present + when: haproxy_latest_release + tags: haproxy + +- name: Define the haproxy repository + apt_repository: repo='{{ haproxy_ubuntu_latest_repo }}' state=present update_cache=yes + when: + - haproxy_latest_release + - is_ubuntu + tags: haproxy + +- name: Define the haproxy repository + apt_repository: repo='{{ haproxy_debian_latest_repo }}' state=present update_cache=yes + when: + - haproxy_latest_release + - is_debian + tags: haproxy + +- name: Install the haproxy package + apt: name=haproxy state=present default_release={{ ansible_lsb.codename }}-backports update_cache=yes cache_valid_time=3600 + when: not haproxy_latest_release + register: install_haproxy + tags: haproxy + +- name: Install the haproxy package + apt: name=haproxy state=latest default_release={{ ansible_lsb.codename }}-backports-{{ haproxy_version }} update_cache=yes cache_valid_time=3600 + when: + - haproxy_latest_release + - is_debian + register: install_haproxy + tags: haproxy + +- name: Install the haproxy package + apt: name=haproxy state=latest update_cache=yes cache_valid_time=3600 + when: + - haproxy_latest_release + - is_ubuntu + register: install_haproxy + tags: haproxy + +- name: Enable kernel binding non local IP addresses + sysctl: name={{ item }} value=1 reload=yes state=present + with_items: + - net.ipv4.ip_nonlocal_bind + when: haproxy_k_bind_non_local_ip + tags: [ 'haproxy', 'haproxy_sysctl' ] + +- name: Disable kernel binding non local IP addresses + sysctl: name={{ item }} value=0 reload=yes state=present + with_items: + - net.ipv4.ip_nonlocal_bind + when: not haproxy_k_bind_non_local_ip + tags: [ 'haproxy', 'haproxy_sysctl' ] + +- name: Increase the connection tracking table capacity + sysctl: name={{ item }} value={{ haproxy_sysctl_conntrack_max }} reload=yes state=present + with_items: + - net.nf_conntrack_max + when: is_not_debian9 + tags: [ 'haproxy', 'haproxy_sysctl' ] + diff --git a/tasks/haproxy-ssl.yml b/tasks/haproxy-ssl.yml new file mode 100644 index 0000000..f873d46 --- /dev/null +++ b/tasks/haproxy-ssl.yml @@ -0,0 +1,17 @@ +--- +- block: + - name: Install the socat binary needed to talk to the haproxy socket + apt: name=socat state=latest update_cache=yes cache_valid_time=3600 + + - name: Install a script that refreshes the OCSP configuration and reloads haproxy if needed + template: src=hapos-upd.j2 dest=/usr/local/bin/hapos-upd owner=root group=root mode=0755 + + - name: Install a cron job that refreshes the OCSP configuration + cron: + name: "Refresh the haproxy OCSP information" + user: root + special_time: daily + job: "/usr/local/bin/hapos-upd --cert {{ haproxy_cert_dir }}/haproxy.pem -v {{ letsencrypt_acme_certs_dir }}/fullchain -s {{ haproxy_admin_socket }} >/var/log/hapos-upd.log 2>&1" + + tags: [ 'haproxy', 'letsencrypt', 'ssl', 'ssl_ocsp' ] + diff --git a/tasks/main.yml b/tasks/main.yml index 53c6cae..70e8d24 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,2 +1,32 @@ --- -# tasks file for ansible-role-template \ No newline at end of file +- import_tasks: haproxy-service.yml +- import_tasks: haproxy-letsencrypt-acme-sh.yml + when: + - haproxy_letsencrypt_managed + - letsencrypt_acme_sh_install +- import_tasks: haproxy-ssl.yml + when: + - haproxy_letsencrypt_managed + +- import_tasks: haproxy-nagios.yml + when: + - nagios_enabled is defined + - nagios_enabled + +- name: Ensure that haproxy is enabled and started + service: name=haproxy state=restarted enabled=yes + when: haproxy_enabled + ignore_errors: True + tags: haproxy + +- name: Haproxy puts a new rsyslog directive. Restart rsyslog to activate it. Reload is not sufficient + service: name=rsyslog state=restarted + when: + - haproxy_enabled + - install_haproxy is changed + tags: haproxy + +- name: Ensure that haproxy is stopped and disabled if needed + service: name=haproxy state=stopped enabled=no + when: not haproxy_enabled + tags: haproxy diff --git a/templates/hapos-upd.j2 b/templates/hapos-upd.j2 new file mode 100644 index 0000000..4b399bd --- /dev/null +++ b/templates/hapos-upd.j2 @@ -0,0 +1,571 @@ +#!/bin/bash + +# HAProxy OCSP Stapling Updater +# Copyright (c) 2015 Pier Carlo Chiodi - http://www.pierky.com +# +# https://github.com/pierky/haproxy-ocsp-stapling-updater + +set -o nounset + +VERSION="0.4.1-pre1" + +PROGNAME="hapos-upd" + +if [ -z ${OPENSSL_BIN+x} ]; then + OPENSSL_BIN="openssl" +fi + +SOCAT_BIN="socat" + +CERT="" +VAFILE="" +HAPROXY_ADMIN_SOCKET_DEFAULT="/run/haproxy/admin.sock" +HAPROXY_ADMIN_SOCKET="$HAPROXY_ADMIN_SOCKET_DEFAULT" +GOOD_ONLY=0 +SYSLOG_PRIO="" +DEBUG=0 +KEEP_TEMP=0 +OCSP_URL="" +OCSP_HOST="" +VERIFY=1 +TMP="" +SKIP_UPDATE=0 +PARTIAL_CHAIN="" + +function Quit() { + if [ $KEEP_TEMP -eq 0 ]; then + if [ -n "$TMP" ]; then + rm -r $TMP &>/dev/null + fi + fi + exit $1 +} + +function LogError() { + MSG="$1" + + if [ -z "$SYSLOG_PRIO" ]; then + echo "$MSG" >&2 + else + logger -p "$SYSLOG_PRIO" -s -- "$PROGNAME - $MSG" + fi + + echo "$MSG" >>$TMP/log +} + +function Error() { + if [ $1 -eq 9 ]; then + MSG="Error: $2" + else + MSG="Error processing '$CERT': $2" + fi + + LogError "$MSG" + + if [ $1 -eq 9 ]; then + echo "Run $PROGNAME -h for help" >&2 + fi + + Quit $1 +} + +function Debug() { + if [ $DEBUG -eq 1 ]; then + echo "$1" + fi + echo "$1" >>$TMP/log +} + +function Trap() { + Debug "Aborting" + Quit 9 +} + +function Usage() { + echo " +HAProxy OCSP Stapling Updater - $VERSION +Copyright (c) 2015 Pier Carlo Chiodi - http://www.pierky.com + +https://github.com/pierky/haproxy-ocsp-stapling-updater + +Usage: + $PROGNAME [options] --cert crt_full_path + +This script extracts and queries the OCSP server present in a +certificate to obtain its revocation status, then updates HAProxy by +writing the '.issuer' and the '.ocsp' files and by sending it the +'set ssl ocsp-response' command through the local UNIX admin socket. + +The crt_full_path argument is the full path to the certificate bundle +used in haproxy 'crt' setting. End-entity (EE) certificate plus any +intermediate CA certificates must be concatenated there. +An OCSP query is sent to the OCSP server given on the command line +(--ocsp-url and --ocsp-host argument); if these arguments are missing, +URL and Host header values are automatically extracted from the +certificate. +If the '.issuer' file already exists it's used to build the OCSP +request, otherwise the chain is extracted from crt_full_path and used +to identify the issuer. +Finally, it writes the related '.issuer' and .'ocsp' files and updates +haproxy, using 'socat' and the local UNIX socket (--socket argument, +default $HAPROXY_ADMIN_SOCKET_DEFAULT). + +Exit codes: + 0 OK + 1 openssl certificates handling error + 2 OCSP server URL not found + 3 string parsing / PEM manipulation error + 4 OCSP error + 5 haproxy management error + 9 program error (wrong arguments, missing dependencies) + +Options: + + -d, --debug : don't do anything, print debug messages only. + + --keep-temp : keep temporary directory after exiting (for + debug purposes). + + -g, --good-only : do not update haproxy if OCSP response + certificate status value is not 'good'. + + -l, --syslog priority : log errors to syslog system log module. + The priority may be specified numerically + or as a facility.level pair (e.g. + local7.error). + + --ocsp-url url : OCSP server URL; use this instead of the + one in the EE certificate. + + --ocsp-host host : OCSP server hostname to be used in the + 'Host:' header; use this instead of the one + extracted from the OCSP server URL. + + --partial-chain : Allow partial certificate chain if at least one certificate + is in trusted store. Useful when validating an intermediate + certificate without the root CA. + + -s, --socket file : haproxy admin socket. If omitted, + $HAPROXY_ADMIN_SOCKET_DEFAULT is used by default. + This script is distributed with only one + method to update haproxy: using 'socat' + with a local admin-level UNIX socket. + Feel free to implement other mechanisms as + needed! The right section in the code is + \"UPDATE HAPROXY\", at the end of the script. + + -v, --VAfile file : same as the openssl ocsp -VAfile option + with 'file' as argument. For more details: + 'man ocsp'. + If file = \"-\" then the chain extracted + from the certificate's bundle (or .issuer + file) is used (useful for OCSP responses + that don't include the signer certificate). + + --noverify : Do not verify OCSP response. + + -S, --skip-update : Do not notify haproxy of the new OCSP response. + + -h, --help : this help." +} + +trap Trap INT TERM + +TMP="`mktemp -d -q -t $PROGNAME.XXXXXXXXXX`" + +# COMMAND LINE PROCESSING +# ---------------------------------- + +while [[ $# > 0 ]] +do + + case "$1" in + -h|--help) + Usage + Quit 0 + ;; + + -d|--debug) + DEBUG=1 + ;; + + --keep-temp) + KEEP_TEMP=1 + ;; + + -g|--good-only) + GOOD_ONLY=1 + ;; + + --noverify) + VERIFY=0 + ;; + + --partial-chain) + PARTIAL_CHAIN="-partial_chain" + ;; + + -l|--syslog) + if [ $# -le 1 ]; then + Error 9 "mandatory value is missing for $1 argument" + fi + SYSLOG_PRIO="$2" + shift + ;; + + --ocsp-url) + if [ $# -le 1 ]; then + Error 9 "mandatory value is missing for $1 argument" + fi + OCSP_URL="$2" + shift + ;; + + --ocsp-host) + if [ $# -le 1 ]; then + Error 9 "mandatory value is missing for $1 argument" + fi + OCSP_HOST="$2" + shift + ;; + + -c|--cert) + if [ $# -le 1 ]; then + Error 9 "mandatory value is missing for $1 argument" + fi + CERT="$2" + shift + ;; + + -v|--VAfile) + if [ $# -le 1 ]; then + Error 9 "mandatory value is missing for $1 argument" + fi + VAFILE="$2" + if [ "$VAFILE" == "-" ]; then + VAFILE="$TMP/chain.pem" + else + if [ ! -e "$VAFILE" ]; then + Error 9 "VAfile does not exists: $VAFILE" + fi + fi + shift + ;; + + -s|--socket) + if [ $# -le 1 ]; then + Error 9 "mandatory value is missing for $1 argument" + fi + HAPROXY_ADMIN_SOCKET="$2" + shift + ;; + + -S|--skip-update) + SKIP_UPDATE=1 + ;; + + *) + Error 9 "unknown option: $1" + esac + + shift +done + +Debug "Temporary directory: $TMP" + +$OPENSSL_BIN version | grep OpenSSL &>>$TMP/log + +if [ $? -ne 0 ]; then + Error 9 "openssl binary not found; adjust OPENSSL_BIN variable in the script" +fi + +$SOCAT_BIN -V | grep socat &>>$TMP/log + +if [ $? -ne 0 ]; then + Error 9 "socat binary not found; adjust SOCAT_BIN variable in the script" +fi + +if [ -z "$CERT" ]; then + Error 9 "certificate not provided (--cert argument)" +fi + +# CURRENT RESPONSE EXPIRED? +# ---------------------------------- + +ISNEW=1 +if [ -e $CERT.ocsp ]; then + ISNEW=0 + Debug "An OCSP response already exists: checking its expiration." + + $OPENSSL_BIN ocsp -respin $CERT.ocsp -text -noverify | \ + grep "Next Update:" &>>$TMP/log + + if [ $? -eq 0 ]; then + CURR_EXP=`$OPENSSL_BIN ocsp -respin $CERT.ocsp -text -noverify | grep "Next Update:" | cut -d ':' -f 2-` + CURR_EXP_EPOCH=`date --date="$CURR_EXP" +%s` + + if [ $? -ne 0 ]; then + Error 3 "can't parse Next Update from current OCSP response" + fi + + if [ $CURR_EXP_EPOCH -lt `date +%s` ]; then + Debug "Current OCSP response expiration: $CURR_EXP - expired" + LogError "current OCSP response is expired: please consider running this script more frequently" + else + Debug "Current OCSP response expiration: $CURR_EXP - NOT expired" + fi + fi +fi + +# EXTRACT EE CERTIFICATE INFO +# ---------------------------------- + +# extract EE certificate +$OPENSSL_BIN x509 -in $CERT -outform PEM -out $TMP/ee.pem &>>$TMP/log + +if [ $? -ne 0 ]; then + Error 1 "can't extract EE certificate from $CERT" +fi + +# get OCSP server URL +if [ -z "$OCSP_URL" ]; then + OCSP_URL="`$OPENSSL_BIN x509 -in $TMP/ee.pem -ocsp_uri -noout`" + + if [ $? -ne 0 ]; then + Error 1 "can't obtain OCSP server URL from $CERT" + fi + + if [ -z "$OCSP_URL" ]; then + Error 2 "OCSP server URL not found in the EE certificate" + fi + + Debug "OCSP server URL found: $OCSP_URL" +else + Debug "Using OCSP server URL from command line: $OCSP_URL" +fi + +# check OCSP server URL format (http:// or https://) +echo "$OCSP_URL" | egrep -i "(http://|https://)" &>/dev/null + +if [ $? -ne 0 ]; then + Error 3 "OCSP server URL not in http[s]:// format" +fi + +# get OCSP server URL host name +if [ -z "$OCSP_HOST" ]; then + OCSP_HOST="`echo "$OCSP_URL" | egrep -i "(http://|https://)" | cut -d'/' -f 3`" + + if [ $? -ne 0 -o -z "$OCSP_HOST" ]; then + Error 3 "can't extract hostname from OCSP server URL $OCSP_URL" + fi + + Debug "OCSP server hostname: $OCSP_HOST" +else + Debug "Using OCSP server hostname from command line: $OCSP_HOST" +fi + +# EXTRACT CHAIN INFO +# ---------------------------------- + +if [ -e $CERT.issuer ]; then + Debug "Using existing chain ($CERT.issuer)" + + # copy .issuer file to temporary chain.pem + cp $CERT.issuer $TMP/chain.pem &>>$TMP/log + + if [ $? -ne 0 ]; then + Error 3 "can't copy current chain from $CERT.issuer" + fi +else + Debug "Extracting chain from certificates bundle" + + # get EE certificate's fingerprint + FP_EE="`$OPENSSL_BIN x509 -fingerprint -noout -in $TMP/ee.pem`" + + if [ $? -ne 0 -o -z "$FP_EE" ]; then + Error 1 "can't obtain EE certificate's fingerprint" + fi + + Debug "EE certificate's fingerprint: $FP_EE" + + # get BEGIN CERTIFICATE and END CERTIFICATE separators + PEM_BEGIN_CERT="`head $TMP/ee.pem -n 1`" + PEM_END_CERT="`tail $TMP/ee.pem -n 1`" + + # get number of certificates in the bundle file + NUM_OF_CERTS=`cat $CERT | grep -e "$PEM_BEGIN_CERT" | wc -l` + + if [ $NUM_OF_CERTS -le 1 ]; then + Error 3 "can't obtain the number of certificates in the chain" + fi + + Debug "$NUM_OF_CERTS certificates found in the bundle" + + # save each certificate in the bundle into $TMP/chain-X.pem + cat $CERT | \ + sed -n -e "/$PEM_BEGIN_CERT/,/$PEM_END_CERT/p" | \ + awk "/$PEM_BEGIN_CERT/{x=\"$TMP/chain-\" ++i \".pem\";}{print > x;}" &>>$TMP/log + + if [ $? -ne 0 ]; then + Error 3 "can't extract certificates from bundle" + fi + + # for each certificate that is extracted from the bundle check if + # it's the EE certificate, otherwise uses it to build the chain file + for c in `seq 1 $NUM_OF_CERTS`; + do + # check fingerprint of current and EE certificates + FP="`$OPENSSL_BIN x509 -fingerprint -noout -in $TMP/chain-$c.pem`" + if [ $? -ne 0 -o -z "$FP" ]; then + Error 1 "can't obtain the fingerprint of the certificate n. $c in the bundle" + else + if [ ! "$FP" == "$FP_EE" ]; then + Debug "Bundle certificate n. $c fingerprint: $FP - it's part of the chain" + + # current certificate is not the same as the EE; append to the chain + cat $TMP/chain-$c.pem >> $TMP/chain.pem + else + Debug "Bundle certificate n. $c fingerprint: $FP - EE certificate" + fi + fi + done +fi + +# check if the EE certificate validates against the chain +$OPENSSL_BIN verify $PARTIAL_CHAIN -CAfile $TMP/chain.pem $TMP/ee.pem &>>$TMP/log + +if [ $? -ne 0 ]; then + if [ -e $CERT.issuer ]; then + Error 1 "can't validate the EE certificate against the existing chain; if it has been changed recently consider removing the current $CERT.issuer file and let this script to figure out a new one" + else + Error 1 "can't validate the EE certificate against the extracted chain" + fi +fi + +# OCSP +# ---------------------------------- + +# query the OCSP server and save its response + +$OPENSSL_BIN version | grep "OpenSSL 1.0" &>/dev/null +if [ $? -eq 0 ]; then + # OpenSSL 1.0.x + + $OPENSSL_BIN ocsp $PARTIAL_CHAIN -issuer $TMP/chain.pem -cert $TMP/ee.pem \ + -respout $TMP/ocsp.der -noverify \ + -no_nonce -url $OCSP_URL -header "Host" "$OCSP_HOST" &>>$TMP/log +else + $OPENSSL_BIN ocsp $PARTIAL_CHAIN -issuer $TMP/chain.pem -cert $TMP/ee.pem \ + -respout $TMP/ocsp.der -noverify \ + -no_nonce -url $OCSP_URL -header "Host=$OCSP_HOST" &>>$TMP/log +fi + +if [ $? -ne 0 ]; then + Error 1 "can't receive the OCSP server response" +fi + +# process the OCSP response +VERIFYOPT="" +if [ $VERIFY -eq 0 ]; then + VERIFYOPT="-noverify" +fi +if [ -z "$VAFILE" ]; then + $OPENSSL_BIN ocsp $PARTIAL_CHAIN $VERIFYOPT -issuer $TMP/chain.pem -cert $TMP/ee.pem \ + -respin $TMP/ocsp.der -no_nonce -CAfile $TMP/chain.pem \ + -out $TMP/ocsp.txt &>>$TMP/ocsp-verify.txt +else + $OPENSSL_BIN ocsp $PARTIAL_CHAIN $VERIFYOPT -issuer $TMP/chain.pem -cert $TMP/ee.pem \ + -respin $TMP/ocsp.der -no_nonce -CAfile $TMP/chain.pem \ + -VAfile $VAFILE \ + -out $TMP/ocsp.txt &>>$TMP/ocsp-verify.txt +fi + +if [ $? -ne 0 ]; then + Error 1 "can't receive OCSP response" +fi + +if [ $VERIFY -eq 1 ]; then + Debug "OCSP response verification results: `cat $TMP/ocsp-verify.txt`" + + cat $TMP/ocsp-verify.txt | grep "Response verify OK" &>>$TMP/log + + if [ $? -ne 0 ]; then + grep "signer certificate not found" $TMP/ocsp-verify.txt &>/dev/null + + if [ $? -eq 0 ]; then + Error 4 "OCSP response verification failure: signer certificate not found; try with '--VAfile -' or '--VAfile OCSP-response-signing-certificate-file' arguments" + else + Error 4 "OCSP response verification failure." + fi + fi +fi + +Debug "OCSP response: `cat $TMP/ocsp.txt`" + +if [ $GOOD_ONLY -eq 1 ]; then + cat $TMP/ocsp.txt | head -n 1 | grep ": good" &>>$TMP/log + + if [ $? -ne 0 ]; then + Error 4 "OCSP response, certificate status not good" + fi +fi + +# UPDATE HAPROXY +# ---------------------------------- + +# Status: +# - $TMP/ocsp.der contains the OCSP response, DER format +# - $TMP/ocsp.txt contains the textual OCSP response as produced +# by openssl +# - the OCSP response has been verified against the chain or +# the --VAfile + +if [ $DEBUG -eq 0 ]; then + # update .ocsp and .issuer files + + cp $TMP/ocsp.der $CERT.ocsp &>>$TMP/log + + if [ $? -ne 0 ]; then + Error 5 "can't update $CERT.ocsp file" + fi + + if [ ! -e $CERT.issuer ]; then + cp $TMP/chain.pem $CERT.issuer &>>$TMP/log + + if [ $? -ne 0 ]; then + Error 5 "can't update $CERT.issuer file" + fi + fi + + if [ $SKIP_UPDATE -eq 0 ]; then + if [ $ISNEW -eq 1 ]; then + # no .ocsp file found, maybe it's an initial run + Debug "Reloading haproxy." + + service haproxy reload + + if [ $? -ne 0 ]; then + Error 5 "can't reload haproxy with 'service haproxy reload'" + fi + else + # update haproxy via local UNIX socket + Debug "Updating haproxy." + + echo "set ssl ocsp-response `base64 -w 0 $TMP/ocsp.der`" | $SOCAT_BIN stdio $HAPROXY_ADMIN_SOCKET &>>$TMP/log + + if [ $? -ne 0 ]; then + Error 5 "can't update haproxy ssl ocsp-response using $HAPROXY_ADMIN_SOCKET socket" + fi + fi + else + Debug "Not notifying haproxy because skip-update is set." + fi + +else + Debug "Debug mode: haproxy update skipped." +fi + +# remove temporary files and quit with success +Quit 0 + +# vim: set tabstop=4 shiftwidth=4 expandtab: diff --git a/templates/haproxy-letsencrypt-acme.sh.j2 b/templates/haproxy-letsencrypt-acme.sh.j2 new file mode 100644 index 0000000..1aaa92b --- /dev/null +++ b/templates/haproxy-letsencrypt-acme.sh.j2 @@ -0,0 +1,50 @@ +#!/bin/bash + +H_NAME="{{ letsencrypt_acme_sh_certs_data_prefix }}" +LE_SERVICES_SCRIPT_DIR=/usr/lib/acme/hooks +LE_CERTS_DIR=/var/lib/acme/live/$H_NAME +LE_LOG_DIR=/var/log/letsencrypt +HAPROXY_CERTDIR=/etc/pki/haproxy +HAPROXY_CERTFILE=$HAPROXY_CERTDIR/haproxy.pem +DATE=$( date ) + +[ ! -d $HAPROXY_CERTDIR ] && mkdir -p $HAPROXY_CERTDIR +[ ! -d $LE_LOG_DIR ] && mkdir $LE_LOG_DIR +echo "$DATE" >> $LE_LOG_DIR/haproxy.log + +{% if letsencrypt_acme_install %} +LE_ENV_FILE=/etc/default/letsencrypt +{% endif %} +{% if letsencrypt_acme_sh_install %} +LE_ENV_FILE=/etc/default/acme_sh_request_env +{% endif %} +if [ -f "$LE_ENV_FILE" ] ; then + . "$LE_ENV_FILE" +else + echo "No letsencrypt default file" >> $LE_LOG_DIR/haproxy.log +fi + +echo "Building the new certificate file" >> $LE_LOG_DIR/haproxy.log +cat ${LE_CERTS_DIR}/{fullchain,privkey} > ${HAPROXY_CERTFILE} +chmod 440 ${HAPROXY_CERTFILE} +chgrp haproxy ${HAPROXY_CERTFILE} + +echo "Reload the haproxy service" >> $LE_LOG_DIR/haproxy.log +if [ -x /bin/systemctl ] ; then + systemctl reload haproxy >> $LE_LOG_DIR/haproxy.log 2>&1 +else + service haproxy reload >> $LE_LOG_DIR/haproxy.log 2>&1 +fi + +# Run the OCSP stapling script +if [ -x /usr/local/bin/hapos-upd ] ; then + echo "Run the OCSP stapling updater script" >> $LE_LOG_DIR/haproxy.log + /usr/local/bin/hapos-upd --cert {{ haproxy_cert_dir }}/haproxy.pem -v ${LE_CERTS_DIR}/fullchain -s {{ haproxy_admin_socket }} -v - >> $LE_LOG_DIR/haproxy.log 2>&1 +else + echo "No OCPS stapling updater script" >> $LE_LOG_DIR/haproxy.log +fi + +echo "Done." >> $LE_LOG_DIR/haproxy.log + +exit 0 + diff --git a/templates/lb.cfg.j2 b/templates/lb.cfg.j2 new file mode 100644 index 0000000..509d441 --- /dev/null +++ b/templates/lb.cfg.j2 @@ -0,0 +1,4 @@ +# Check the haproxy backends status +command[lb_check_bk_status]=/usr/bin/sudo {{ nagios_local_plugdir }}/check_haproxy_stats -s {{ haproxy_admin_socket }} -w {{ haproxy_nagios_check_w }} -c {{ haproxy_nagios_check_c }} + +