How we shaved the poodle

CFEngineAgentIn this post I’ll describe how we used CFEngine to apply fixes to apache and nginx to defuse the infamous poodle bug. The post is a bit rushed, in the hope it may still be useful to someone. The policies use bundles and bodies from either the standard library or from our own. The libraries are not shown here but the names speak for themselves… hopefully 🙂

As you’ll probably know, the “trick” on the server side is not to allow secure (erm…) connections to use anything older than TLSv1. In order to do that, we decided to

  • deploy a conf.d snippet to set the appropriate protocol versions as a default;
  • disable the same directive in existing configuration files to avoid weaker directives take priority;
  • restart the server if/when the configuration gets fixed.

Apache

For Apache this was quite straightforward, thanks to the IfModule directive that prevents the directive to be read if mod_ssl is not active, and thanks to the ability to specify “all protocols but…”.

The configuration fragment is four lines, comments included:

# Ref: http://httpd.apache.org/docs/2.2/mod/mod_ssl.html#sslprotocol
<IfModule mod_ssl.c>
        SSLProtocol all -SSLv2 -SSLv3
</IfModule>

The policy we used is below. In very short: it checks if “interesting” apache directories are present and, in case, it takes action. Nothing will happen if none of those directories exists.

bundle agent sanitize_apache_ssl
{
  meta:
      # Notice that meta variables have their own context, that is:
      # "$(this.bundle)_meta". If you want to use bundle variables in meta
      # you need to fully qualify them with the bundle context, or you'll
      # get strange errors.
      "task"
          comment => "What this bundle is for",
	  string => "Sanitize SSL protocols in Apache" ;

  vars:
      "dirs"
	  comment => "Directories to sanitize",
	  slist => { "/etc/apache2/conf.d",
		     "/etc/apache2/conf-available",
		     "/etc/apache2/mods-available",
		     "/etc/apache2/sites-available" } ;

      "cdir[$(dirs)]"
	  comment => "Mapping dirname => canonified_dirname",
	  string => canonify("has_dir$(dirs)") ;

      "sanitizer_src"
	  comment => "Source for the sanitizer script",
	  string => "$(site.services)/ssl/sources/apache" ;


  classes:
    any::
      "$(cdir[$(dirs)])"
	  comment => "Define class if the directory is present",
	  expression => isdir("$(dirs)") ;

      "must_sanitize_apache_ssl"
	  comment => "Check if we have anything to sanitize",
	  expression => classmatch("has_dir_etc_apache2_.+") ;


  files:
    must_sanitize_apache_ssl::
      "$(dirs)/(?!sanitize_ssl).*"
	  comment => "Sanitize files in $(dirs)",
	  edit_line  => comment_lines_matching("(?i)\s*SSLProtocol\s+.*",
					       "# DISABLED "),
	  edit_defaults => no_backup,
	  classes    => if_repaired("apply_apache_ssl_fixes"),
	  ifvarclass => "$(cdir[$(dirs)])" ;

    has_dir_etc_apache2_conf_d::
      "/etc/apache2/conf.d/sanitize_ssl.conf"
	  comment => "This will keep SSL sane here",
	  perms     => root_readable,
	  copy_from => digest_cp("$(sanitizer_src)"),
	  classes   => if_repaired("apply_apache_ssl_fixes") ;

    has_dir_etc_apache2_conf_available::
      "/etc/apache2/conf-available/sanitize_ssl.conf"
	  comment => "This will keep SSL sane here",
	  perms     => root_readable,
	  copy_from => digest_cp("$(sanitizer_src)"),
	  classes   => if_repaired("apply_apache_ssl_fixes") ;

      "/etc/apache2/conf-enabled/sanitize_ssl.conf"
	  comment => "This will link sanitize_ssl in the appropriate place",
	  link_from => ln_s("/etc/apache2/conf-available/sanitize_ssl.conf"),
	  classes   => if_repaired("apply_apache_ssl_fixes") ;


  commands:
    apply_apache_ssl_fixes::
      "/etc/init.d/apache2 restart" ;


  reports:
    report_always.apply_apache_ssl_fixes::
      "[$(this.bundle) REPAIRED] $($(this.bundle)_meta.task)" ;
}

Nginx

For nginx things were a bit more complicated. There is no IfModule, and you can’t say “all protocols but…”. In addition, older versions don’t support anything better than TLSv1 and choke if the configuration files mention TLSv1.1 and TLSv1.2. That’s why the configuration snippet had to be built via a template:

# Ref. http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_protocols
ssl_protocols $(nginx_info.nginx_ssl_protocols) ;

To analyse the version of nginx, the presence of the ssl module and the ssl capabilities we resorted to the simple Perl module below, nginx_info. It basically checks if the binary is there and, if it is, it runs it with the -V option to capture all the information needed and return it to the agent in the form of classes and variables.

#!/usr/bin/perl

use strict ;
use warnings ;
#use v5.6.0 ;

my $nginx = q{/usr/sbin/nginx} ;
print "=nginx_command=$nginx\n" ;

# Do we have nginx?
if ( not -x $nginx ) {
    print "+nginx_not_found\n" ;
    exit 0 ;
}

print "+has_nginx_binary\n" ;

# Can we get something out of it?
my $nginx_V_output = qx{$nginx -V 2>&1} ;
my $rc = $? ;

if ( $rc != 0 ) {
    print "+nginx_V_failed\n" ;
    print "=nginx_rc=$rc\n" ;
    exit 0 ;
}

# parse version number:
print "+nginx_V_info_available\n" ;
my ( $nginx_version ) =
    ( $nginx_V_output =~ m{nginx version: nginx/([\d\.]+)} ) ;

if ( not defined $nginx_version ) {
    print "+nginx_is_custom\n" ;
    $nginx_version = "0.0.0" ;
}

print "=nginx_version=$nginx_version\n" ;

# Set the variable $(nginx_ssl_protocols) if the ssl module is enabled
if ( $nginx_V_output =~ m{with-http_ssl_module} ) {
    print "+nginx_has_ssl_module\n" ;

    my ($major,$minor,$patch) = ( $nginx_version =~ m{^(\d+)\.(\d+)\.(\d+)} ) ;

    my $nginx_ssl_protocols ;
# TLS v1.1, v1.2 supported only from version 1.1.13 onwards
    if ( ( $major > 1 ) or
	 ( $major == 1 and $minor > 1 ) or
	 ( $major == 1 and $minor == 1 and $patch >= 13 ) ) {
	$nginx_ssl_protocols = q{TLSv1 TLSv1.1 TLSv1.2} ;
    }

    else {
	$nginx_ssl_protocols = q{TLSv1} ;
    }

    print "=nginx_ssl_protocols=$nginx_ssl_protocols\n" ;
}

# SSL support not enabled
else {
    print "+nginx_ssl_module_not_available\n" ;
}

exit 0 ;

The policy below will run the module and apply patches, in case. It’s longer and a bit more complicated due to the number of cases it has to keep into consideration.

bundle agent sanitize_nginx_ssl
{
  meta:
      # Notice that meta variables have their own context, that is:
      # "$(this.bundle)_meta". If you want to use bundle variables in meta
      # you need to fully qualify them with the bundle context, or you'll
      # get strange errors.
      "task"
          comment => "What this bundle is for",
	  string => "Sanitize SSL protocols in NGINX" ;

  vars:
      "dirs"
	  comment => "Directories to sanitize",
	  slist => { "/etc/nginx/conf.d",
		     "/etc/nginx/sites-available" } ;

      "cdir[$(dirs)]"
	  comment => "Mapping dirname => canonified_dirname",
	  string => canonify("has_dir$(dirs)") ;

      "sanitizer_tmpl_src"
	  comment => "Source for the sanitizer template",
	  string => "$(site.services)/ssl/sources/nginx.tmpl" ;

      "sanitizer_tmpl_dst"
	  comment => "Local cache for the sanitizer template",
	  string => "$(site.lservices)/ssl/sources/nginx.tmpl" ;


  classes:
    nginx_has_ssl_module::
      "$(cdir[$(dirs)])"
	  comment => "Define class if the directory is present",
	  expression => isdir("$(dirs)") ;

      "must_sanitize_nginx_ssl"
	  comment => "Check if we have anything to sanitize",
	  expression => classmatch("has_dir_etc_nginx_.+") ;


  files:
    any::
      "$(site.lmodules)/nginx_info"
	  perms => root_executable,
	  copy_from => digest_cp("$(site.modules)/nginx_info") ;

      "$(sanitizer_tmpl_dst)"
	  copy_from => digest_cp("$(sanitizer_tmpl_src)") ;

    must_sanitize_nginx_ssl::
      "$(dirs)/(?!sanitize_ssl).*"
	  comment => "Sanitize files in $(dirs)",
	  edit_line  => comment_lines_matching("\s*ssl_protocols\s+.*",
					       "# DISABLED "),
	  edit_defaults => no_backup,
	  classes    => if_repaired("apply_nginx_ssl_fixes"),
	  ifvarclass => "$(cdir[$(dirs)])" ;

      "/etc/nginx/conf.d/sanitize_ssl.conf"
	  comment => "This will keep SSL sane here",
	  create    => "yes",
	  perms     => root_readable,
	  edit_line => expand_template("$(sanitizer_tmpl_dst)"),
	  edit_defaults => empty,
	  classes   => if_repaired("apply_nginx_ssl_fixes") ;


  commands:
    any::
      "$(site.lmodules)/nginx_info" module => "yes" ;
      
    apply_nginx_ssl_fixes::
      "/etc/init.d/nginx restart" ;


  reports:
    report_minimum.nginx_not_found::
      "[$(this.bundle) INFO] $(nginx_info.nginx_command) not found" ;

    report_minimum.has_nginx_binary::
      "[$(this.bundle) INFO] nginx found in $(nginx_info.nginx_command)" ;

    report_always.nginx_V_failed::
      "[$(this.bundle) WARNING] $(nginx_info.nginx_command) -V failed with code $(nginx_info.nginx_rc)" ;

    report_minimum.nginx_V_info_available::
      "[$(this.bundle) INFO] nginx version is $(nginx_info.nginx_version)" ;

    report_minimum.nginx_is_custom::
      "[$(this.bundle) INFO] custom build of nginx detected, version set to $(nginx_info.nginx_version)" ;

    report_always.nginx_ssl_module_not_available::
      "[$(this.bundle) KEPT] $(nginx_info.nginx_command) doesn't support SSL" ;

    report_minimum.nginx_has_ssl_module::
      "[$(this.bundle) INFO] $(nginx_info.nginx_command) supports SSL" ;

    report_always.apply_nginx_ssl_fixes::
      "[$(this.bundle) REPAIRED] $($(this.bundle)_meta.task)" ;
}

Conclusion

I hope you find this useful and clear enough. If it’s not, please use the comments to request clarifications and I’ll update the post.

Advertisements

2 thoughts on “How we shaved the poodle

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s