BBlack has submitted this change and it was merged.
Change subject: protoproxy/sslcert/cache: nginx ssl_stapling_file support
......................................................................
protoproxy/sslcert/cache: nginx ssl_stapling_file support
Bug: T86666
Change-Id: If19dc78a8743cdcfff18b702a6c4502eeedcf393
---
M manifests/role/cache.pp
M manifests/role/protoproxy.pp
A modules/protoproxy/files/update-ocsp-all
M modules/protoproxy/manifests/localssl.pp
A modules/protoproxy/manifests/ocsp_updater.pp
M modules/protoproxy/templates/localssl.erb
A modules/sslcert/files/update-ocsp
M modules/sslcert/manifests/init.pp
8 files changed, 316 insertions(+), 13 deletions(-)
Approvals:
BBlack: Verified; Looks good to me, approved
diff --git a/manifests/role/cache.pp b/manifests/role/cache.pp
index 25b8418..d71d100 100644
--- a/manifests/role/cache.pp
+++ b/manifests/role/cache.pp
@@ -631,7 +631,7 @@
}
}
- define localssl($certname, $server_name=$::fqdn, $server_aliases=[],
$default_server=false) {
+ define localssl($certname, $do_ocsp=false, $server_name=$::fqdn,
$server_aliases=[], $default_server=false) {
# Assumes that LVS service IPs are setup elsewhere
install_certificate { $certname:
@@ -644,6 +644,7 @@
default_server => $default_server,
server_name => $server_name,
server_aliases => $server_aliases,
+ do_ocsp => $do_ocsp,
}
}
diff --git a/manifests/role/protoproxy.pp b/manifests/role/protoproxy.pp
index ddce309..f92f916 100644
--- a/manifests/role/protoproxy.pp
+++ b/manifests/role/protoproxy.pp
@@ -35,17 +35,6 @@
content => template('nginx/logrotate'),
tag => 'nginx', # workaround PUP-2689, can remove w/ puppetmaster
3.6.2+
}
-
- # reload protoproxies once a day for ticket keys
- # on legacy cache boxes (to be removed when no matching
- # hosts in the if clause here).
- if ! os_version('debian >= jessie') {
- cron { 'nginx_reload_daily':
- command => '/usr/sbin/service nginx reload >/dev/null 2>/dev/null',
- hour => fqdn_rand(24),
- minute => fqdn_rand(60),
- }
- }
}
class role::protoproxy::ssl::beta::common {
diff --git a/modules/protoproxy/files/update-ocsp-all
b/modules/protoproxy/files/update-ocsp-all
new file mode 100644
index 0000000..ea3a7ea
--- /dev/null
+++ b/modules/protoproxy/files/update-ocsp-all
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Executes update-ocsp for all existing OCSP files,
+# continuing through the list even if some fail, and then
+# reloads nginx configuration to apply updates and exits
+# with a status that reflects whether any updates failed
+
+if [ $# != 1 ]; then
+ echo One argument required: the proxy hostname:port
+ exit 1
+fi
+
+PROXY=$1
+OCSP_DIR=/var/cache/ocsp
+LCERT_DIR=/etc/ssl/localcerts
+
+some_failed=0
+for existing in ${OCSP_DIR}/*.ocsp; do
+ bn=$(basename $existing)
+ certname=${bn%.ocsp}
+ /usr/local/sbin/update-ocsp -c ${LCERT_DIR}/${certname}.crt -o $existing
-p $proxy
+ if [ $? -ne 0 ]; then
+ echo OCSP update failed for $certname
+ some_failed=1
+ fi
+done
+
+service nginx reload
+
+exit $some_failed
diff --git a/modules/protoproxy/manifests/localssl.pp
b/modules/protoproxy/manifests/localssl.pp
index 87f8279..51dfc6a 100644
--- a/modules/protoproxy/manifests/localssl.pp
+++ b/modules/protoproxy/manifests/localssl.pp
@@ -17,14 +17,22 @@
# [*default_server*]
# Boolean. Adds the 'default_server' option to the listen statement.
# Exactly one instance should have this set to true.
+#
+# [*do_ocsp*]
+# Boolean. Sets up OCSP Stapling for this server. This both enables the
+# correct configuration directives in the site's nginx config file as well
+# as creates the OCSP data file itself and ensures a cron is running to
+# keep it up to date.
define protoproxy::localssl(
$proxy_server_cert_name,
$server_name = $::fqdn,
$server_aliases = [],
$default_server = false,
- $upstream_port = '80'
+ $upstream_port = '80',
+ $do_ocsp = false
) {
+ require ::sslcert
# Ensure that exactly one definition exists with default_server = true
# if multiple defines have default_server set to true, this
@@ -38,6 +46,21 @@
# for localssl.erb below
$ssl_protos = 'ssl spdy'
+ if $do_ocsp {
+ include ::protoproxy::ocsp_updater
+
+ $certpath = "/etc/ssl/localcerts/${proxy_server_cert_name}.crt"
+ $output = "/var/cache/ocsp/${proxy_server_cert_name}.ocsp"
+ $proxy = "webproxy.${::site}.wmnet:8080"
+
+ exec { "${title}-create-ocsp":
+ command => "/usr/local/sbin/update-ocsp -c $cert -o $output -p
$proxy",
+ creates => $output,
+ require => Sslcert::Certificate[$proxy_server_cert_name],
+ before => Service['nginx']
+ }
+ }
+
nginx::site { $name:
require => Notify['protoproxy localssl default_server'], # Ensure a
default_server has been defined
content => template('protoproxy/localssl.erb')
diff --git a/modules/protoproxy/manifests/ocsp_updater.pp
b/modules/protoproxy/manifests/ocsp_updater.pp
new file mode 100644
index 0000000..4668885
--- /dev/null
+++ b/modules/protoproxy/manifests/ocsp_updater.pp
@@ -0,0 +1,59 @@
+# == Class: protoproxy::ocsp_updater
+#
+# This class defines a machine-global cronjob which updates
+# any existing OCSP files in /var/cache/ocsp once every two
+# hours, randomly splayed per-machine.
+#
+# It is intended to be used as "include protoproxy::ocsp_updater"
+# any time an ocsp file is defined for creation on a given machine.
+# See protoproxy::localssl for example.
+#
+# Note that everything about how we time/check this stuff today makes
+# assumptions based on GlobalSign's OCSP validity time windows. In the
+# future, it would be better to find a way to make the cron/check -timing
+# a bit more adaptive...
+#
+
+class protoproxy::ocsp_updater {
+ require ::sslcert
+
+ file { '/usr/local/sbin/update-ocsp-all':
+ mode => '0555',
+ owner => 'root',
+ group => 'root',
+ source => 'puppet:///modules/protoproxy/update-ocsp-all',
+ }
+
+ # This is "0" or "1" randomly by-host, used below with Linux crontab
+ # syntax to get every-two-hours timing with hosts splayed into even/odd
hours
+ $fqr01 = fqdn_rand(2, '97e54956f8c8e861')
+
+ cron { 'update-ocsp-all':
+ command => "/usr/local/sbin/update-ocsp-all
webproxy.${::site}.wmnet:8080",
+ minute => fqdn_rand(60, '1adf3dd699e51805'),
+ hour => "${fqr01}-23/2",
+ require => [
+ File['/usr/local/sbin/update-ocsp-all'],
+ Service['nginx'],
+ ],
+ }
+
+ # Generate icinga alert if OCSP files falling out of date due to errors
+ #
+ # The cron above attempts to get fresh data every 2 hours, and a good
+ # fresh fetch of data has a 12H lifetime with the windows we're seeing
+ # from GlobalSign today.
+ #
+ # The crit/warn values of 29100 and 14700 correspond are "8h5m" and
+ # "4h5m", so those are basically warning if two updates in a row failed
+ # for a given cert, and critical if 4 updates in a row fail (at which
+ # point we have 4h left to fix the situation before the validity window
+ # expires).
+
+ $check_args = '-c 29100 -w 14700 -d /var/cache/ocsp -g "*.ocsp"'
+ nrpe::monitor_service { 'ocsp-freshness':
+ description => 'Freshness of OCSP Stapling files',
+ nrpe_command => "/usr/lib/nagios/plugins/check-fresh-files-in-dir.py
$check_args",
+ require =>
File['/usr/lib/nagios/plugins/check-fresh-files-in-dir.py'],
+ }
+}
diff --git a/modules/protoproxy/templates/localssl.erb
b/modules/protoproxy/templates/localssl.erb
index 08e123e..2265f10 100644
--- a/modules/protoproxy/templates/localssl.erb
+++ b/modules/protoproxy/templates/localssl.erb
@@ -13,6 +13,11 @@
ssl_certificate /etc/ssl/localcerts/<%= @proxy_server_cert_name
%>.chained.crt;
ssl_certificate_key /etc/ssl/private/<%= @proxy_server_cert_name %>.key;
+ <% if @do_ocsp -%>
+ ssl_stapling on;
+ ssl_stapling_file /var/cache/ocsp/<%= @proxy_server_cert_name %>.ocsp;
+ <% end -%>
+
keepalive_timeout 60;
location / {
diff --git a/modules/sslcert/files/update-ocsp
b/modules/sslcert/files/update-ocsp
new file mode 100644
index 0000000..5f2ab69
--- /dev/null
+++ b/modules/sslcert/files/update-ocsp
@@ -0,0 +1,188 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# update-ocsp - creates or updates an OCSP stapling file for an SSL cert
+#
+# Copyright 2015 Brandon Black
+# Copyright 2015 Wikimedia Foundation, Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import errno
+import argparse
+from subprocess import check_output
+import glob
+import tempfile
+import datetime
+
+
+def file_exists(fname):
+ """Helper for argparse to do check if a filename argument exists"""
+ if not os.path.exists(fname):
+ raise argparse.ArgumentTypeError("{0} does not exist".format(fname))
+ return fname
+
+
+def parse_options():
+ """Parse command-line options, return args hash"""
+ parser = argparse.ArgumentParser(description="OCSP Fetcher")
+ parser.add_argument('--certificate', '-c', dest="cert",
+ type=file_exists,
+ help="certificate filename",
+ required=True)
+ parser.add_argument('--output', '-o', dest="output",
+ help="output filename",
+ required=True)
+ parser.add_argument('--proxy', '-p', dest="proxy",
+ help="HTTP proxy server URL to use for OCSP request",
+ default=None)
+ parser.add_argument('--ca-certs', '-d', dest="cadir",
+ help="SSL CA certificates directory",
+ default='/etc/ssl/certs')
+ parser.add_argument('--time-offset-start', '-s', dest="offset_start",
+ help="validate thisUpdate <= X secs in the future",
+ type=int, default=60)
+ parser.add_argument('--time-offset-end', '-e', dest="offset_end",
+ help="validate nextUpdate >= X secs in the future",
+ type=int, default=3600)
+
+ return parser.parse_args()
+
+
+def cert_x509_option(filename, attrib):
+ """Returns output of an openssl x509 cert option w/ noout"""
+ return check_output([
+ "openssl", "x509", "-noout",
+ "-in", filename,
+ "-" + attrib,
+ ]).rstrip()
+
+
+def cert_x509_option_kv(filename, attrib):
+ """As above, but returns the value when output is k=v"""
+ k, v = cert_x509_option(filename, attrib).split("=", 1)
+ assert k == attrib
+ return v
+
+
+def cert_get_issuer_filename(cert, cadir):
+ """Get the filename of the immediate issuer of the given cert"""
+
+ # Note, this uses the pre-0.9.6 algorithm - it can be confused if
+ # there are 2+ distinct possible issuers in cadir with identical subjects!
+ # (is there a way to resolve that ambiguity that isn't unreasonable?)
+
+ issuer_subject = cert_x509_option_kv(cert, "issuer")
+ issuer_hash = cert_x509_option(cert, "issuer_hash")
+ for issuer in glob.glob(os.path.join(cadir, issuer_hash + '.[0-9]')):
+ if cert_x509_option_kv(issuer, "subject") == issuer_subject:
+ return issuer
+ raise Exception("No matching issuer file found at %s for %s",
+ (issuer_glob, cert))
+
+
+def cert_fetch_ocsp(cert, cadir, outfile, proxy):
+ """Fetch validated OCSP response for cert"""
+
+ issuer_path = cert_get_issuer_filename(cert, cadir)
+ ocsp_uri = cert_x509_option(cert, "ocsp_uri")
+
+ cmd = [
+ "openssl", "ocsp", "-nonce",
+ "-respout", outfile,
+ "-issuer", issuer_path,
+ "-cert", cert,
+ ]
+
+ if proxy:
+ cmd.extend([
+ "-path", ocsp_uri,
+ "-host", proxy,
+ ])
+ else:
+ cmd.extend([
+ "-url", ocsp_uri,
+ ])
+
+ return check_output(cmd)
+
+
+def ocsp_text_datetime_field(ocsp_text, field):
+ """Create datetime object from field:datetsring line in OCSP text"""
+ for line in ocsp_text.split("\n"):
+ if ":" in line:
+ k, v = line.strip().split(":", 1)
+ if k == field:
+ return datetime.datetime.strptime(v.lstrip(),
+ "%b %d %H:%M:%S %Y %Z")
+ raise Exception("Did not find OCSP datetime field for '%s'" % field)
+
+
+def ocsp_validate_window(ocspfile, offset_start, offset_end):
+ """Validate the validity range of the OCSP response"""
+
+ ocsp_text = check_output([
+ "openssl", "ocsp", "-noverify",
+ "-respin", ocspfile,
+ "-resp_text",
+ ])
+
+ thisup_dt = ocsp_text_datetime_field(ocsp_text, "This Update")
+ nextup_dt = ocsp_text_datetime_field(ocsp_text, "Next Update")
+ now_dt = datetime.datetime.utcnow()
+
+ thisup_notafter = now_dt + datetime.timedelta(0, offset_start)
+ if thisup_dt > thisup_notafter:
+ raise Exception("OCSP thisUpdate more than %i secs in the future"
+ % offset_start)
+
+ nextup_notbefore = now_dt + datetime.timedelta(0, offset_end)
+ if nextup_dt < nextup_notbefore:
+ raise Exception("OCSP nextUpdate less than %i secs in the future"
+ % offset_end)
+
+ return 0
+
+
+def mkdir_p(path):
+ try:
+ os.makedirs(path)
+ except OSError as exc:
+ if exc.errno == errno.EEXIST and os.path.isdir(path):
+ pass
+ else:
+ raise
+
+
+def main():
+ args = parse_options()
+
+ os.umask(022)
+ out_fn = os.path.basename(args.output)
+ out_basedir = os.path.dirname(args.output)
+ mkdir_p(out_basedir)
+ out_tempdir = tempfile.mkdtemp(".tmp", "update-ocsp-", out_basedir)
+ out_tempfile = os.path.join(out_tempdir, out_fn)
+
+ cert_fetch_ocsp(args.cert, args.cadir, out_tempfile, args.proxy)
+ ocsp_validate_window(out_tempfile, args.offset_start, args.offset_end)
+
+ os.rename(out_tempfile, args.output)
+ os.rmdir(out_tempdir)
+
+
+if __name__ == '__main__':
+ main()
+
+# vim: set ts=4 sw=4 et:
diff --git a/modules/sslcert/manifests/init.pp
b/modules/sslcert/manifests/init.pp
index d671fe9..619cf26 100644
--- a/modules/sslcert/manifests/init.pp
+++ b/modules/sslcert/manifests/init.pp
@@ -30,6 +30,14 @@
require => Package['ssl-cert'],
}
+ # generic script for fetching the OCSP file for a given cert
+ file { '/usr/local/sbin/update-ocsp':
+ mode => '0555',
+ owner => 'root',
+ group => 'root',
+ source => 'puppet:///modules/sslcert/update-ocsp',
+ }
+
# Limit AppArmor support to just Ubuntu, for now
if $::operatingsystem == 'Ubuntu' {
include apparmor
--
To view, visit https://gerrit.wikimedia.org/r/198110
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: If19dc78a8743cdcfff18b702a6c4502eeedcf393
Gerrit-PatchSet: 21
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: BBlack <[email protected]>
Gerrit-Reviewer: BBlack <[email protected]>
Gerrit-Reviewer: Faidon Liambotis <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits