Adds SSL/TLS support 85/66285/16
authorTim Rozet <trozet@redhat.com>
Wed, 6 Dec 2017 16:52:21 +0000 (11:52 -0500)
committerDaniel Farrell <dfarrell@redhat.com>
Tue, 2 Jan 2018 19:01:13 +0000 (14:01 -0500)
Allows configuration for requiring TLS communication across Northbound
REST and OVSDB/OF with OVS.  The TLS configuration enables two
keystores, "controller" and "truststore".  The controller keystore is
used to hold the private key and certificate, which may be
auto-generated or provided as input parameters.  The truststore holds
trusted certificates for clients connecting to ODL and may also be
provided via a parameter.  Additionally when providing the private key
and certificate to the controller keystore, a public certificate
authority certificate may be linked.

Change-Id: I079fc0759bb42888472ef95ca239c3ca67db8a56
Signed-off-by: Tim Rozet <trozet@redhat.com>
Signed-off-by: Daniel Farrell <dfarrell@redhat.com>
19 files changed:
CHANGELOG
README.markdown
files/org.opendaylight.ovsdb.library.cfg [new file with mode: 0644]
lib/puppet/functions/convert_cert_to_string.rb [new file with mode: 0644]
lib/puppet/provider/odl_keystore/jks.rb [new file with mode: 0644]
lib/puppet/type/odl_keystore.rb [new file with mode: 0644]
manifests/config.pp
manifests/init.pp
manifests/params.pp
manifests/post_config.pp [new file with mode: 0644]
metadata.json
spec/acceptance/class_spec.rb
spec/classes/opendaylight_spec.rb
spec/spec_helper.rb
spec/spec_helper_acceptance.rb
spec/unit/provider/jks_spec.rb [new file with mode: 0644]
spec/unit/type/odl_keystore_spec.rb [new file with mode: 0644]
templates/aaa-cert-config.xml.erb [new file with mode: 0644]
templates/default-openflow-connection-config.xml.erb [new file with mode: 0644]

index cd187b2c02ae5c01b0ada4c0596e2b16d0dd5afa..285d7adc1185cf2a2bd46cb3d83515e920e78343 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -52,3 +52,5 @@
 - Allow full customization of ODL RPM repo
 2017-11-17 Release 6.1.0
 - Configure websocket address
+2017-01-02 Release 6.2.0
+- Adds SSL/TLS support
index 6b260878db789ea31559ff1b53f1770d4e71d4b5..4fbd3336ae287409f11c4350f324eff8b29b2dc5 100644 (file)
@@ -178,6 +178,30 @@ class { 'opendaylight':
 Websocket address can be configured to the IP of ODL rather than default 0.0.0.0. This IP will
 be defined by `odl_bind_ip`.
 
+### Enabling TLS with OpenDaylight
+
+It is possible to enable TLS encrypted communication for OpenDaylight Northbound REST
+along with Southbound OVSDB/OpenFlow communication with Open vSwitch. To enable
+TLS, use the `enable_tls` flag. This option will create two keystores in OpenDaylight
+which are stored in '/opt/opendaylight/configuration/ssl'. The first keystore
+is the controller keystore, which will hold the private key and ODL certificate,
+along with the Certificate Authority (CA) certificate if provided. The second
+keystore is the trust keystore, which will hold the trusted OVS switch certificates.
+
+In order to enable TLS, it is required to provide the `tls_keystore_password`
+parameter. This represents the password to use for the controller and truststore
+keystores. With only providing these parameters, ODL will generate the
+controller keystore with a random private key and self-signed certficate.
+
+Additionally the `tls_key_file` and `tls_cert_file` parameters may be provided.
+These represent ODL's private key file and certificate file to be used when building
+the controller keystore. Optionally the `tls_ca_cert_file` may be provided which
+will chain the CA certificate to the keystore for client validation.
+
+`tls_trusted_certs` may be provided as an array of trusted certificates to be
+added to the trusted keystore. This allows OpenDaylight to identify trusted
+clients which may connect to ODL Southbound and Northbound.
+
 ## Reference
 
 ### Classes
diff --git a/files/org.opendaylight.ovsdb.library.cfg b/files/org.opendaylight.ovsdb.library.cfg
new file mode 100644 (file)
index 0000000..cdb9b17
--- /dev/null
@@ -0,0 +1,29 @@
+#********************************************************************************************
+#                               Boot Time Configuration                                     *
+#                   Config knob changes will require controller restart                     *
+#********************************************************************************************
+
+#Ovsdb plugin's (OVS, HwVtep) support both active and passive connections. OVSDB library by
+#default listens on port 6640 for switch initiated connection. Please use following config
+#knob for changing this default port.
+ovsdb-listener-port = 6640
+
+#This flag will be enforced across all the connection's (passive and active) if set to true
+use-ssl = true
+
+#Set Json Rpc decoder max frame length value. If the OVSDB node contains large configurations
+#that can cause connection related issue while reading the configuration from the OVSDB node
+#database. Increasing the max frame lenge helps resolve the issue. Please see following bug
+#report for more details ( https://bugs.opendaylight.org/show_bug.cgi?id=2732 &
+#https://bugs.opendaylight.org/show_bug.cgi?id=2487). Default value set to 100000.
+json-rpc-decoder-max-frame-length = 100000
+
+
+#********************************************************************************************
+#                               Run Time Configuration                                      *
+#                   Config knob changes doesn't require controller resart                   *
+#********************************************************************************************
+#Timeout value (in millisecond) after which OVSDB rpc task will be cancelled.Default value is
+#set to 1000ms, please uncomment and override the value if requires.Changing the value don't
+#require controller restart.
+ovsdb-rpc-task-timeout = 1000
diff --git a/lib/puppet/functions/convert_cert_to_string.rb b/lib/puppet/functions/convert_cert_to_string.rb
new file mode 100644 (file)
index 0000000..e40b8ef
--- /dev/null
@@ -0,0 +1,19 @@
+Puppet::Functions.create_function(:convert_cert_to_string) do
+  dispatch :convert_cert_to_string do
+    param 'String', :cert_file
+  end
+
+  def convert_cert_to_string(cert_file)
+    unless File.file?(cert_file)
+      raise puppet::ParseError, "Certificate file not found: #{cert_file}"
+    end
+    text=File.readlines(cert_file)
+    cert_string = ''
+    text.each do |line|
+      unless line.include? '-----'
+        cert_string += line.strip
+      end
+    end
+    return cert_string
+  end
+end
diff --git a/lib/puppet/provider/odl_keystore/jks.rb b/lib/puppet/provider/odl_keystore/jks.rb
new file mode 100644 (file)
index 0000000..4338cc2
--- /dev/null
@@ -0,0 +1,107 @@
+Puppet::Type.type(:odl_keystore).provide(:jks) do
+  commands :keytool => 'keytool'
+
+  require 'fileutils'
+  require 'openssl'
+
+  def remove_p12_ks
+    keystore_dir = File.dirname(@resource[:keystore_path])
+    if File.file?("#{keystore_dir}/ctl.p12")
+      FileUtils.rm("#{keystore_dir}/ctl.p12")
+    end
+  end
+
+  def create
+    keystore_dir = File.dirname(@resource[:keystore_path])
+    unless File.directory?(keystore_dir)
+      FileUtils.mkdir_p(keystore_dir, :mode => 0755)
+      FileUtils.chown('odl', 'odl', keystore_dir)
+    end
+    # create p12 keystore
+    key = OpenSSL::PKey::RSA.new File.read(@resource[:key_file])
+    raw_cert = File.read(@resource[:cert_file])
+    certificate = OpenSSL::X509::Certificate.new(raw_cert)
+    if @resource[:ca_file]
+      p12_ks = OpenSSL::PKCS12.create(@resource[:password], @resource[:name], \
+                                      key, certificate, [@resource[:ca_file]])
+    else
+      p12_ks = OpenSSL::PKCS12.create(@resource[:password], @resource[:name], \
+                                      key, certificate)
+    end
+    open "#{keystore_dir}/ctl1.p12", 'w', 0644 do |io|
+      io.write p12_ks.to_der()
+    end
+    # convert to jks
+    keytool('-importkeystore', '-deststorepass', @resource[:password], \
+            '-destkeypass', @resource[:password], '-destkeystore', \
+            @resource[:keystore_path], '-srckeystore', "#{keystore_dir}/ctl1.p12", \
+            '-srcstoretype', 'PKCS12', '-srcstorepass', @resource[:password], \
+            '-alias', @resource[:name])
+    remove_p12_ks
+    unless File.file?(@resource[:keystore_path])
+      raise Puppet::Error, 'JKS keystore creation failed'
+    end
+    FileUtils.chown('odl', 'odl', @resource[:keystore_path])
+  end
+
+  def destroy
+    FileUtils.rm(@resource[:keystore_path])
+  end
+
+  def exists?
+    return File.file?(@resource[:keystore_path])
+  end
+
+  def key_file
+    return @resource[:key_file]
+  end
+
+  def key_file=(key_file)
+    destroy
+    create
+  end
+
+  def cert_file
+    return @resource[:cert_file]
+  end
+
+  def cert_file=(cert_file)
+    destroy
+    create
+  end
+
+  def ca_file
+    return @resource[:ca_file]
+  end
+
+  def ca_file=(ca_file)
+    destroy
+    create
+  end
+
+  def keystore_path
+    if exists?
+      return @resource[:keystore_path]
+    end
+  end
+
+  def keystore_path=(keystore_path)
+    destroy
+    create
+  end
+
+  def password
+    begin
+      keytool('-list', '-keystore', @resource[:keystore_path], '-storepass', \
+              @resource[:password])
+      return @resource[:password]
+    rescue
+      return false
+    end
+  end
+
+  def password=(password)
+    destroy
+    create
+  end
+end
diff --git a/lib/puppet/type/odl_keystore.rb b/lib/puppet/type/odl_keystore.rb
new file mode 100644 (file)
index 0000000..b0e2568
--- /dev/null
@@ -0,0 +1,74 @@
+Puppet::Type.newtype(:odl_keystore) do
+
+  ensurable
+
+  newparam(:name, :namevar => true) do
+    desc "Name of the keystore"
+    newvalues(/^\w+$/)
+  end
+
+  newproperty(:ca_file) do
+    desc "CA authority certificate file path"
+    validate do |value|
+      if value
+        if !value.is_a?(String)
+          raise ArgumentError, "CA cert file path must be a string"
+        end
+        unless File.file?(value)
+          raise ArgumentError, "CA cert file not found: #{value}"
+        end
+      end
+    end
+  end
+
+  newproperty(:password) do
+    desc "Password for the keystore"
+    validate do |value|
+      if !value.is_a?(String)
+        raise ArgumentError, "Passwords must be a string"
+      end
+
+      if value.length < 6
+        raise ArgumentError, "Password must be at least 6 characters"
+      end
+    end
+
+    def change_to_s(current, desire)
+      "Keystore Password changed"
+    end
+  end
+
+  newproperty(:cert_file) do
+    desc "Certificate filepath"
+    validate do |value|
+      if !value.is_a?(String)
+        raise ArgumentError, "Certificate file path must be a string"
+      end
+      unless File.file?(value)
+        raise ArgumentError, "Certificate file not found: #{value}"
+      end
+    end
+  end
+
+  newproperty(:key_file) do
+    desc "Private key file path"
+    validate do |value|
+      if !value.is_a?(String)
+        raise ArgumentError, "Key file path must be a string"
+      end
+      unless File.file?(value)
+        raise ArgumentError, "Key file not found: #{value}"
+      end
+    end
+  end
+
+  newproperty(:keystore_path) do
+    desc "Filepath for the keystore"
+    defaultto '/opt/opendaylight/configuration/ssl/ctl.jks'
+    validate do |value|
+      if !value.is_a?(String)
+          raise ArgumentError, "Keystore file path must be a string"
+      end
+    end
+  end
+end
index 1654a756f0a5becc3d675fc2b0d4e989787d2a46..824829e510f1e3c0d4568091bf602ca366abe1ec 100644 (file)
@@ -19,13 +19,131 @@ class opendaylight::config {
     match => '^featuresBoot=.*$',
   }
 
+  file { 'org.ops4j.pax.web.cfg':
+    ensure => file,
+    path   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+    # Set user:group owners
+    owner  => 'odl',
+    group  => 'odl',
+  }
+
+  $ha_node_count = count($::opendaylight::ha_node_ips)
+  if $::opendaylight::enable_ha and $ha_node_count < 2 {
+    fail("Number of HA nodes less than 2: ${ha_node_count} and HA Enabled")
+  }
+
   # Configuration of ODL NB REST port to listen on
-  augeas {'ODL REST Port':
-    incl    => '/opt/opendaylight/etc/jetty.xml',
-    context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
-    lens    => 'Xml.lns',
-    changes => [
-      "set Call[2]/Arg/New/Set[#attribute[name='port']]/Property/#attribute/default ${opendaylight::odl_rest_port}"]
+  if $opendaylight::enable_tls {
+
+    if $::opendaylight::tls_keystore_password == undef {
+      fail('Enabling TLS requires setting a TLS password for the ODL keystore')
+    }
+
+    if $::opendaylight::tls_key_file or $::opendaylight::tls_cert_file {
+      if $::opendaylight::tls_key_file and $::opendaylight::tls_cert_file {
+        odl_keystore { 'controller':
+          password  => $::opendaylight::tls_keystore_password,
+          cert_file => $::opendaylight::tls_cert_file,
+          key_file  => $::opendaylight::tls_key_file,
+          ca_file   => $::opendaylight::tls_ca_cert_file,
+          require   => File['/opt/opendaylight/configuration/ssl']
+        }
+      } else {
+        fail('Must specify both TLS key file path AND certificate file path')
+      }
+    }
+
+    augeas {'Remove HTTP ODL REST Port':
+      incl    => '/opt/opendaylight/etc/jetty.xml',
+      context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
+      lens    => 'Xml.lns',
+      changes => ["rm Call[2]/Arg/New/Set[#attribute[name='port']]"]
+    }
+
+    augeas {'ODL SSL REST Port':
+      incl    => '/opt/opendaylight/etc/jetty.xml',
+      context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
+      lens    => 'Xml.lns',
+      changes => ["set New[2]/Set[#attribute[name='securePort']]/Property/#attribute/default ${opendaylight::odl_rest_port}"]
+    }
+
+    file_line { 'set pax TLS port':
+      ensure  => present,
+      path    => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+      line    => "org.osgi.service.http.port.secure = ${opendaylight::odl_rest_port}",
+      match   => '^#?org.osgi.service.http.port.secure.*$',
+      require => File['org.ops4j.pax.web.cfg']
+    }
+
+    file_line { 'enable pax TLS':
+      ensure  => present,
+      path    => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+      line    => 'org.osgi.service.http.secure.enabled = true',
+      match   => '^#?org.osgi.service.http.secure.enabled.*$',
+      require => File['org.ops4j.pax.web.cfg']
+    }
+
+    file {'aaa-cert-config.xml':
+      ensure  => file,
+      path    => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-cert-config.xml',
+      owner   => 'odl',
+      group   => 'odl',
+      content => template('opendaylight/aaa-cert-config.xml.erb'),
+    }
+
+    file_line {'set pax TLS keystore location':
+      ensure  => present,
+      path    => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+      line    => 'org.ops4j.pax.web.ssl.keystore = configuration/ssl/ctl.jks',
+      match   => '^#?org.ops4j.pax.web.ssl.keystore.*$',
+      require => File['org.ops4j.pax.web.cfg']
+    }
+    file_line {'set pax TLS keystore integrity password':
+      ensure  => present,
+      path    => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+      line    => "org.ops4j.pax.web.ssl.password = ${opendaylight::tls_keystore_password}",
+      match   => '^#?org.ops4j.pax.web.ssl.password.*$',
+      require => File['org.ops4j.pax.web.cfg']
+    }
+
+    file_line {'set pax TLS keystore password':
+      ensure  => present,
+      path    => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+      line    => "org.ops4j.pax.web.ssl.keypassword = ${opendaylight::tls_keystore_password}",
+      match   => '^#?org.ops4j.pax.web.ssl.keypassword.*$',
+      require => File['org.ops4j.pax.web.cfg']
+    }
+
+    # Enable TLS with OVSDB
+    file { 'org.opendaylight.ovsdb.library.cfg':
+      ensure => file,
+      path   => '/opt/opendaylight/etc/org.opendaylight.ovsdb.library.cfg',
+      owner  => 'odl',
+      group  => 'odl',
+      source => 'puppet:///modules/opendaylight/org.opendaylight.ovsdb.library.cfg',
+    }
+
+    # Configure OpenFlow plugin to use TLS
+    $transport_protocol = 'TLS'
+  } else {
+    $transport_protocol = 'TCP'
+    augeas { 'ODL REST Port':
+      incl    => '/opt/opendaylight/etc/jetty.xml',
+      context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
+      lens    => 'Xml.lns',
+      changes => [
+        "set Call[2]/Arg/New/Set[#attribute[name='port']]/Property/#attribute/default
+          ${opendaylight::odl_rest_port}"]
+    }
+  }
+  # Configure OpenFlow plugin to use TCP/TLS
+  file { 'default-openflow-connection-config.xml':
+    ensure  => file,
+    path    => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/default-openflow-connection-config.xml',
+    # Set user:group owners
+    owner   => 'odl',
+    group   => 'odl',
+    content => template('opendaylight/default-openflow-connection-config.xml.erb'),
   }
   $initial_config_dir = '/opt/opendaylight/configuration/initial'
 
@@ -47,17 +165,11 @@ class opendaylight::config {
         "set Call[2]/Arg/New/Set[#attribute[name='host']]/Property/#attribute/default ${opendaylight::odl_bind_ip}"]
     }
 
-    file { 'org.ops4j.pax.web.cfg':
-      ensure => file,
-      path   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
-      # Set user:group owners
-      owner  => 'odl',
-      group  => 'odl',
-    }
-    -> file_line { 'org.ops4j.pax.web.cfg':
-      ensure => present,
-      path   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
-      line   => "org.ops4j.pax.web.listening.addresses = ${opendaylight::odl_bind_ip}"
+    file_line { 'set pax bind IP':
+      ensure  => present,
+      path    => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+      line    => "org.ops4j.pax.web.listening.addresses = ${opendaylight::odl_bind_ip}",
+      require => File['org.ops4j.pax.web.cfg']
     }
   }
 
@@ -87,40 +199,34 @@ class opendaylight::config {
   }
 
   # Configure ODL HA if enabled
-  $ha_node_count = count($::opendaylight::ha_node_ips)
   if $::opendaylight::enable_ha {
-    if $ha_node_count >= 2 {
-      # Configure ODL OSVDB Clustering
-
-      file {'akka.conf':
-        ensure  => file,
-        path    => "${initial_config_dir}/akka.conf",
-        owner   => 'odl',
-        group   => 'odl',
-        content => template('opendaylight/akka.conf.erb'),
-        require => File[$initial_config_dir]
-      }
+    # Configure ODL OSVDB Clustering
 
-      file {'modules.conf':
-        ensure  => file,
-        path    => "${initial_config_dir}/modules.conf",
-        owner   => 'odl',
-        group   => 'odl',
-        content => template('opendaylight/modules.conf.erb'),
-        require => File[$initial_config_dir]
-      }
+    file {'akka.conf':
+      ensure  => file,
+      path    => "${initial_config_dir}/akka.conf",
+      owner   => 'odl',
+      group   => 'odl',
+      content => template('opendaylight/akka.conf.erb'),
+      require => File[$initial_config_dir]
+    }
 
-      file {'module-shards.conf':
-        ensure  => file,
-        path    => "${initial_config_dir}/module-shards.conf",
-        owner   => 'odl',
-        group   => 'odl',
-        content => template('opendaylight/module-shards.conf.erb'),
-        require => File[$initial_config_dir]
-      }
+    file {'modules.conf':
+      ensure  => file,
+      path    => "${initial_config_dir}/modules.conf",
+      owner   => 'odl',
+      group   => 'odl',
+      content => template('opendaylight/modules.conf.erb'),
+      require => File[$initial_config_dir]
+    }
 
-    } else {
-      fail("Number of HA nodes less than 2: ${ha_node_count} and HA Enabled")
+    file {'module-shards.conf':
+      ensure  => file,
+      path    => "${initial_config_dir}/module-shards.conf",
+      owner   => 'odl',
+      group   => 'odl',
+      content => template('opendaylight/module-shards.conf.erb'),
+      require => File[$initial_config_dir]
     }
   }
 
@@ -130,6 +236,7 @@ class opendaylight::config {
     '/opt/opendaylight/etc/opendaylight/datastore',
     '/opt/opendaylight/etc/opendaylight/datastore/initial',
     '/opt/opendaylight/etc/opendaylight/datastore/initial/config',
+    '/opt/opendaylight/configuration/ssl'
   ]
 
   file { $odl_dirs:
index 25d9ec3a82e07b7245830e8f2cde336b0c85bd15..5eb08bc86e0f16055f10e2b93673de7248f23347 100644 (file)
 #   Maxium number of OpenDaylight log rollovers to keep.
 # [*snat_mechanism*]
 #   Sets the mechanism to be used for SNAT (conntrack, controller)
-#
+# [*enable_tls*]
+#   (Boolean) Enables TLS for REST and OpenFlow/OVSDB with OpenDaylight.
+#   Defaults to false
+# [*tls_keystore_password*]
+#   TLS keystore password.  Required when enabling TLS.
+# [*tls_trusted_certs*]
+#   An array of cert files to be added to OpenDaylight's trusted keystore.
+#   Optional.  Defaults to None.
+# [*tls_key_file*]
+#   Full path to a private key file to be used for OpenDaylight.
+#   Optional.  Defaults to undef.  Requires setting tls_cert_file.
+# [*tls_cert_file*]
+#   Full path to a public certificate file to be used for OpenDaylight.
+#   Optional.  Defaults to undef.  Requires setting tls_key_file.
+# [*tls_ca_cert_file*]
+#   Full path to a public CA authority certificate file which signed
+#   OpenDaylight's certificate.  Not needed if ODL certificate is self-signed.
+#   Optional.  Defaults to undef.
 # === Deprecated Parameters
 #
 # [*ha_node_index*]
 #   Index of ha_node_ips for this node.
 #
 class opendaylight (
-  $default_features    = $::opendaylight::params::default_features,
-  $extra_features      = $::opendaylight::params::extra_features,
-  $odl_rest_port       = $::opendaylight::params::odl_rest_port,
-  $odl_bind_ip         = $::opendaylight::params::odl_bind_ip,
-  $rpm_repo            = $::opendaylight::params::rpm_repo,
-  $deb_repo            = $::opendaylight::params::deb_repo,
-  $log_levels          = $::opendaylight::params::log_levels,
-  $enable_ha           = $::opendaylight::params::enable_ha,
-  $ha_node_ips         = $::opendaylight::params::ha_node_ips,
-  $ha_node_index       = $::opendaylight::params::ha_node_index,
-  $ha_db_modules       = $::opendaylight::params::ha_db_modules,
-  $vpp_routing_node    = $::opendaylight::params::vpp_routing_node,
-  $java_opts           = $::opendaylight::params::java_opts,
-  $manage_repositories = $::opendaylight::params::manage_repositories,
-  $username            = $::opendaylight::params::username,
-  $password            = $::opendaylight::params::password,
-  $log_max_size        = $::opendaylight::params::log_max_size,
-  $log_max_rollover    = $::opendaylight::params::log_max_rollover,
-  $snat_mechanism      = $::opendaylight::params::snat_mechanism
+  $default_features      = $::opendaylight::params::default_features,
+  $extra_features        = $::opendaylight::params::extra_features,
+  $odl_rest_port         = $::opendaylight::params::odl_rest_port,
+  $odl_bind_ip           = $::opendaylight::params::odl_bind_ip,
+  $rpm_repo              = $::opendaylight::params::rpm_repo,
+  $deb_repo              = $::opendaylight::params::deb_repo,
+  $log_levels            = $::opendaylight::params::log_levels,
+  $enable_ha             = $::opendaylight::params::enable_ha,
+  $ha_node_ips           = $::opendaylight::params::ha_node_ips,
+  $ha_node_index         = $::opendaylight::params::ha_node_index,
+  $ha_db_modules         = $::opendaylight::params::ha_db_modules,
+  $vpp_routing_node      = $::opendaylight::params::vpp_routing_node,
+  $java_opts             = $::opendaylight::params::java_opts,
+  $manage_repositories   = $::opendaylight::params::manage_repositories,
+  $username              = $::opendaylight::params::username,
+  $password              = $::opendaylight::params::password,
+  $log_max_size          = $::opendaylight::params::log_max_size,
+  $log_max_rollover      = $::opendaylight::params::log_max_rollover,
+  $snat_mechanism        = $::opendaylight::params::snat_mechanism,
+  $enable_tls            = $::opendaylight::params::enable_tls,
+  $tls_keystore_password = $::opendaylight::params::tls_keystore_password,
+  $tls_trusted_certs     = $::opendaylight::params::tls_trusted_certs,
+  $tls_key_file          = $::opendaylight::params::tls_key_file,
+  $tls_cert_file         = $::opendaylight::params::tls_cert_file,
+  $tls_ca_cert_file      = $::opendaylight::params::tls_ca_cert_file
 ) inherits ::opendaylight::params {
 
   # Validate OS family
@@ -113,5 +136,6 @@ class opendaylight (
   class { '::opendaylight::install': }
   -> class { '::opendaylight::config': }
   ~> class { '::opendaylight::service': }
+  -> class { '::opendaylight::post_config': }
   -> Class['::opendaylight']
 }
index ee9fba4b137409798ab4d7b38df8cb1a670fd39b..9623e891929cd2018e182a501d1b4a2e54be6217 100644 (file)
@@ -27,4 +27,10 @@ class opendaylight::params {
   $log_max_size = '10GB'
   $log_max_rollover = 2
   $snat_mechanism = 'controller'
+  $enable_tls = false
+  $tls_keystore_password = undef
+  $tls_key_file = undef
+  $tls_cert_file = undef
+  $tls_ca_cert_file = undef
+  $tls_trusted_certs = []
 }
diff --git a/manifests/post_config.pp b/manifests/post_config.pp
new file mode 100644 (file)
index 0000000..eecaaec
--- /dev/null
@@ -0,0 +1,34 @@
+# == Class opendaylight::post_config
+#
+# This class handles ODL config changes after ODL has come up.
+# These configuration changes do not require restart of ODL.
+# It's called from the opendaylight class.
+#
+class opendaylight::post_config {
+  # Add trusted certs to ODL keystore
+  $curl_post = "curl -k -X POST -o /dev/null --fail --silent -H 'Content-Type: application/json' -H 'Cache-Control: no-cache'"
+  $cert_rest_url = "https://${opendaylight::odl_bind_ip}:${opendaylight::odl_rest_port}/restconf/operations/aaa-cert-rpc:setNodeCertifcate"
+  if $opendaylight::enable_tls {
+    if !empty($opendaylight::tls_trusted_certs) {
+      $opendaylight::tls_trusted_certs.each |$idx, $cert| {
+        $cert_data = convert_cert_to_string($cert)
+        $rest_data = @("END":json/L)
+          {\
+            "aaa-cert-rpc:input": {\
+            "aaa-cert-rpc:node-alias": "node${idx}",\
+            "aaa-cert-rpc:node-cert": "${cert_data}"\
+            }\
+          }
+          |-END
+
+        exec { "Add trusted cert: ${cert}":
+          command   => "${curl_post} -u ${opendaylight::username}:${
+            opendaylight::password} -d '${rest_data}' ${cert_rest_url}",
+          tries     => 5,
+          try_sleep => 30,
+          path      => '/usr/sbin:/usr/bin:/sbin:/bin',
+        }
+      }
+    }
+  }
+}
index 83e076403b89a9cf75f501d1c90f0d804eafc1f5..a17a66ff0eb17182152dca37158da846aa7cc240 100644 (file)
@@ -1,6 +1,6 @@
 {
     "name": "opendaylight-opendaylight",
-    "version": "6.1.0",
+    "version": "6.2.0",
     "author": "Daniel Farrell",
     "summary": "Puppet module that installs and configures the OpenDaylight SDN controller",
     "license": "BSD-2-Clause",
index 4ec3f60d95b5f0492c68162dbfab7b808842ddd3..e2d8bfc596ca8b2c299e88fc95c5262d68dfc911 100644 (file)
@@ -239,4 +239,13 @@ describe 'opendaylight class' do
       websocket_address_validations(odl_bind_ip: '127.0.0.1')
     end
   end
+
+  describe 'testing enabling TLS' do
+    context 'with self-signed key/cert creation' do
+      install_odl(enable_tls: true, tls_keystore_password: '123456')
+
+      # Call specialized helper fn for TLS config validations
+      tls_validations(enable_tls: true, tls_keystore_password: '123456')
+    end
+  end
 end
index 5b7c866163473b44fa49d9ad0b6bc07aa9f25f5e..1ce50988b41f18b3b79d3c99bd0644727531aa54 100644 (file)
@@ -735,7 +735,7 @@ describe 'opendaylight' do
 
   # SNAT Mechanism tests
   describe 'SNAT mechanism tests' do
-    # Non-OS-type tests assume CentO
+    # Non-OS-type tests assume CentOS 7
     #   See issue #43 for reasoning:
     #   https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
     osfamily = 'RedHat'
@@ -785,7 +785,7 @@ describe 'opendaylight' do
 
   # SFC tests
   describe 'SFC tests' do
-    # Non-OS-type tests assume CentO
+    # Non-OS-type tests assume CentOS 7
     #   See issue #43 for reasoning:
     #   https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
     osfamily = 'RedHat'
@@ -909,7 +909,7 @@ describe 'opendaylight' do
 
   # websocket address tests
   describe 'ODL websocket address tests' do
-    # Non-OS-type tests assume CentO
+    # Non-OS-type tests assume CentOS 7
     #   See issue #43 for reasoning:
     #   https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
     osfamily = 'RedHat'
@@ -953,4 +953,49 @@ describe 'opendaylight' do
       odl_websocket_address_tests(odl_bind_ip: '127.0.0.1')
     end
   end
+
+  # TLS tests
+  describe 'ODL TLS tests' do
+    # Non-OS-type tests assume CentOS 7
+    #   See issue #43 for reasoning:
+    #   https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
+    osfamily = 'RedHat'
+    operatingsystem = 'CentOS'
+    operatingsystemmajrelease = '7'
+    context 'enabling TLS without required keystore password (negative test)' do
+      let(:facts) {{
+        :osfamily => osfamily,
+        :operatingsystem => operatingsystem,
+        :operatingsystemmajrelease => operatingsystemmajrelease,
+      }}
+
+      let(:params) {{
+       :enable_tls => :true
+       }}
+
+      # Run test that specialize in checking TLS
+      # Note that this function is defined in spec_helper
+      odl_tls_tests(enable_tls:true)
+    end
+    context 'enabling TLS with required params' do
+      let(:facts) {{
+        :osfamily => osfamily,
+        :operatingsystem => operatingsystem,
+        :operatingsystemmajrelease => operatingsystemmajrelease,
+      }}
+
+      let(:params) {{
+       :enable_tls => true,
+       :tls_keystore_password => '123456',
+       }}
+
+      # Run shared tests applicable to all supported OSs
+      # Note that this function is defined in spec_helper
+      generic_tests
+
+      # Run test that specialize in checking TLS
+      # Note that this function is defined in spec_helper
+      odl_tls_tests(enable_tls:true, tls_keystore_password:'123456')
+    end
+  end
 end
index 1606d4d8fcf16df6355efa16b118e1c707cec09e..18717c236c1b684591370d1217ceed67ddb08bbe 100644 (file)
@@ -23,6 +23,7 @@ def generic_tests()
   it { should contain_class('opendaylight::params') }
   it { should contain_class('opendaylight::install') }
   it { should contain_class('opendaylight::config') }
+  it { should contain_class('opendaylight::post_config') }
   it { should contain_class('opendaylight::service') }
 
   # Confirm relationships between classes
@@ -31,6 +32,8 @@ def generic_tests()
   it { should contain_class('opendaylight::config').that_notifies('Class[opendaylight::service]') }
   it { should contain_class('opendaylight::service').that_subscribes_to('Class[opendaylight::config]') }
   it { should contain_class('opendaylight::service').that_comes_before('Class[opendaylight]') }
+  it { should contain_class('opendaylight::post_config').that_requires('Class[opendaylight::service]') }
+  it { should contain_class('opendaylight::post_config').that_comes_before('Class[opendaylight]') }
   it { should contain_class('opendaylight').that_requires('Class[opendaylight::service]') }
 
   # Confirm presence of generic resources
@@ -129,9 +132,11 @@ def odl_rest_port_tests(options = {})
   if not odl_bind_ip.eql? '0.0.0.0'
     it {
       should contain_augeas('ODL REST IP')
-      should contain_file_line('org.ops4j.pax.web.cfg').with(
-        'path'  => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
-        'line'  => "org.ops4j.pax.web.listening.addresses = #{odl_bind_ip}",
+      should contain_file_line('set pax bind IP').with(
+        'ensure'  => 'present',
+        'path'    => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+        'line'    => "org.ops4j.pax.web.listening.addresses = #{odl_bind_ip}",
+        'require' => 'File[org.ops4j.pax.web.cfg]'
       )
     }
   else
@@ -412,3 +417,94 @@ def odl_websocket_address_tests(options = {})
     )
   }
 end
+
+def odl_tls_tests(options = {})
+  enable_tls = options.fetch(:enable_tls, false)
+  tls_keystore_password = options.fetch(:tls_keystore_password, nil)
+  tls_trusted_certs = options.fetch(:tls_trusted_certs, [])
+  tls_keystore_password = options.fetch(:tls_keystore_password, nil)
+  tls_key_file = options.fetch(:tls_key_file, nil)
+  tls_cert_file = options.fetch(:tls_cert_file, nil)
+  tls_ca_cert_file = options.fetch(:tls_ca_cert_file, nil)
+  odl_rest_port = options.fetch(:odl_rest_port, 8080)
+
+  if enable_tls
+    if tls_keystore_password.nil?
+      it { expect { should contain_class('opendaylight::config') }.to raise_error(Puppet::PreformattedError) }
+      return
+    end
+
+    if tls_key_file or tls_cert_file
+      if tls_key_file and tls_cert_file
+        it {
+          should contain_odl_keystore('controller')
+        }
+      else
+        it { expect { should contain_class('opendaylight::config') }.to raise_error(Puppet::PreformattedError) }
+      end
+    end
+    it {
+      should contain_augeas('Remove HTTP ODL REST Port')
+      should contain_augeas('ODL SSL REST Port')
+      should contain_file_line('set pax TLS port').with(
+        'path'   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+        'line'   => "org.osgi.service.http.port.secure = #{odl_rest_port}",
+        'match'  => '^#?org.osgi.service.http.port.secure.*$',
+      )
+      should contain_file_line('set pax TLS keystore location').with(
+        'path'   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+        'line'   => 'org.ops4j.pax.web.ssl.keystore = configuration/ssl/ctl.jks',
+        'match'  => '^#?org.ops4j.pax.web.ssl.keystore.*$',
+      )
+      should contain_file_line('set pax TLS keystore integrity password').with(
+        'path'   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+        'line'   => "org.ops4j.pax.web.ssl.password = #{tls_keystore_password}",
+        'match'  => '^#?org.ops4j.pax.web.ssl.password.*$',
+      )
+      should contain_file_line('set pax TLS keystore password').with(
+        'path'   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+        'line'   => "org.ops4j.pax.web.ssl.keypassword = #{tls_keystore_password}",
+        'match'  => '^#?org.ops4j.pax.web.ssl.keypassword.*$',
+      )
+      should contain_file('aaa-cert-config.xml').with(
+        'ensure'  => 'file',
+        'path'    => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-cert-config.xml',
+        'owner'   => 'odl',
+        'group'   => 'odl',
+      )
+      should contain_file('org.opendaylight.ovsdb.library.cfg').with(
+        'ensure' => 'file',
+        'path'   => '/opt/opendaylight/etc/org.opendaylight.ovsdb.library.cfg',
+        'owner'  => 'odl',
+        'group'  => 'odl',
+        'source' => 'puppet:///modules/opendaylight/org.opendaylight.ovsdb.library.cfg'
+      )
+      should contain_file('/opt/opendaylight/configuration/ssl').with(
+        'ensure' => 'directory',
+        'path'   => '/opt/opendaylight/configuration/ssl',
+        'owner'  => 'odl',
+        'group'  => 'odl',
+        'mode'   => '0755'
+      )
+      should contain_file_line('enable pax TLS').with(
+        'ensure' => 'present',
+        'path'   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+        'line'   => 'org.osgi.service.http.secure.enabled = true',
+        'match'  => '^#?org.osgi.service.http.secure.enabled.*$',
+      )
+      should contain_file('org.ops4j.pax.web.cfg').with(
+        'ensure' => 'file',
+        'path'   => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+        'owner'  => 'odl',
+        'group'  => 'odl',
+      )
+      should contain_file('default-openflow-connection-config.xml').with(
+        'ensure'  => 'file',
+        'path'    => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/default-openflow-connection-config.xml',
+        'owner'   => 'odl',
+        'group'   => 'odl',
+        'content' =>  /<transport-protocol>TLS<\/transport-protocol>/
+      )
+    }
+  end
+end
index d757b80a71083fabc1482055b58d9a2bc4f4f1eb..d4607fecc89ed26e5e4c44e426e251424d6acbdc 100644 (file)
@@ -65,6 +65,8 @@ def install_odl(options = {})
   log_max_size = options.fetch(:log_max_size, '10GB')
   log_max_rollover = options.fetch(:log_max_rollover, 2)
   snat_mechanism = options.fetch(:snat_mechanism, 'controller')
+  enable_tls = options.fetch(:enable_tls, false)
+  tls_keystore_password = options.fetch(:tls_keystore_password, 'dummypass')
 
   # Build script for consumption by Puppet apply
   it 'should work idempotently with no errors' do
@@ -86,6 +88,8 @@ def install_odl(options = {})
       log_max_size => '#{log_max_size}',
       log_max_rollover => #{log_max_rollover},
       snat_mechanism => #{snat_mechanism},
+      enable_tls => #{enable_tls},
+      tls_keystore_password => #{tls_keystore_password},
     }
     EOS
 
@@ -435,3 +439,53 @@ def websocket_address_validations(options = {})
     its(:content) { should match /<websocket-address>#{odl_bind_ip}<\/websocket-address>/ }
   end
 end
+
+def tls_validations(options = {})
+  # NB: This param default should match the one used by the opendaylight
+  #   class, which is defined in opendaylight::params
+  # TODO: Remove this possible source of bugs^^
+  tls_keystore_password = options.fetch(:tls_keystore_password)
+  odl_rest_port = options.fetch(:odl_rest_port, 8080)
+
+  describe file('/opt/opendaylight/etc/org.ops4j.pax.web.cfg') do
+    it { should be_file }
+    it { should be_owned_by 'odl' }
+    it { should be_grouped_into 'odl' }
+    its(:content) { should match /org.osgi.service.http.port.secure = #{odl_rest_port}/ }
+    its(:content) { should match /org.ops4j.pax.web.ssl.keystore = configuration\/ssl\/ctl.jks/ }
+    its(:content) { should match /org.ops4j.pax.web.ssl.password = #{tls_keystore_password}/ }
+    its(:content) { should match /org.ops4j.pax.web.ssl.keypassword = #{tls_keystore_password}/ }
+    its(:content) { should match /org.osgi.service.http.secure.enabled = true/ }
+  end
+
+  describe file('/opt/opendaylight/etc/org.opendaylight.ovsdb.library.cfg') do
+    it { should be_file }
+    it { should be_owned_by 'odl' }
+    it { should be_grouped_into 'odl' }
+    its(:content) { should match /use-ssl = true/ }
+  end
+
+  describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/default-openflow-connection-config.xml') do
+    it { should be_file }
+    it { should be_owned_by 'odl' }
+    it { should be_grouped_into 'odl' }
+    its(:content) { should match /<keystore-password>#{tls_keystore_password}<\/keystore-password>/ }
+    its(:content) { should match /<truststore-password>#{tls_keystore_password}<\/truststore-password>/ }
+    its(:content) { should match /<transport-protocol>TLS<\/transport-protocol>/ }
+  end
+
+  describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-cert-config.xml') do
+    it { should be_file }
+    it { should be_owned_by 'odl' }
+    it { should be_grouped_into 'odl' }
+    its(:content) { should match /<store-password>#{tls_keystore_password}<\/store-password>/ }
+    its(:content) { should match /<use-mdsal>false<\/use-mdsal>/ }
+  end
+
+  describe file('/opt/opendaylight/etc/jetty.xml') do
+    it { should be_file }
+    it { should be_owned_by 'odl' }
+    it { should be_grouped_into 'odl' }
+    its(:content) { should match /<Property name="jetty.secure.port" default="#{odl_rest_port}" \/>/ }
+  end
+end
diff --git a/spec/unit/provider/jks_spec.rb b/spec/unit/provider/jks_spec.rb
new file mode 100644 (file)
index 0000000..2650fba
--- /dev/null
@@ -0,0 +1,75 @@
+require 'puppet'
+require 'puppet/provider/odl_keystore/jks'
+require 'spec_helper'
+require 'fileutils'
+
+File.any_instance.stubs(:file?).returns(true)
+
+provider_class = Puppet::Type.type(:odl_keystore).provider(:jks)
+
+describe 'Puppet::Type.type(:odl_keystore).provider(:jks)' do
+
+  before(:all) do
+    FileUtils.stubs(:mkdir_p).returns(true)
+    FileUtils.stubs(:chown)
+    FileUtils.stubs(:rm)
+  end
+
+  let :odl_attrs do
+    {
+      :name      => 'controller',
+      :ensure    => 'present',
+      :password  => 'dummypassword'
+    }
+  end
+
+  let :resource do
+    Puppet::Type::Odl_keystore.new(odl_attrs)
+  end
+
+  let :provider do
+    provider_class.new(resource)
+  end
+
+  describe "when changing cert_file" do
+    it 'should recreate keystore' do
+      File.stubs(:file?).returns(true)
+      provider.expects(:destroy)
+      provider.expects(:create)
+      provider.cert_file = '/tmp/blah.pem'
+    end
+  end
+
+  describe "when changing key_file" do
+    it 'should recreate keystore' do
+      File.stubs(:file?).returns(true)
+      provider.expects(:destroy)
+      provider.expects(:create)
+      provider.key_file = '/tmp/blah.pem'
+    end
+  end
+
+  describe "when adding a CA cert" do
+    it 'should recreate keystore' do
+      provider.expects(:destroy)
+      provider.expects(:create)
+      provider.ca_file = '/tmp/blah.pem'
+    end
+  end
+
+  describe "when keystore path" do
+    it 'should recreate keystore' do
+      provider.expects(:destroy)
+      provider.expects(:create)
+      provider.keystore_path = '/tmp/blah.jks'
+    end
+  end
+
+  describe "when changing password" do
+    it 'should change password' do
+      provider.expects(:destroy)
+      provider.expects(:create)
+      provider.password = 'admin123'
+    end
+  end
+end
diff --git a/spec/unit/type/odl_keystore_spec.rb b/spec/unit/type/odl_keystore_spec.rb
new file mode 100644 (file)
index 0000000..1064684
--- /dev/null
@@ -0,0 +1,26 @@
+require 'puppet'
+require 'puppet/type/odl_keystore'
+require 'spec_helper'
+
+describe 'Puppet::Type.type(:odl_keystore)' do
+
+  it 'should accept name, password, tls options' do
+    File.stubs(:file?).returns(true)
+    Puppet::Type.type(:odl_keystore).new(
+      :name      => 'admin',
+      :password  => 'admin12345',
+      :cert_file => '/tmp/dummy.txt',
+      :key_file  => '/tmp/dummy.txt',
+      :ca_file   => '/tmp/dummy.txt')
+  end
+
+  it 'should fail with password less than 6 chars' do
+    File.stubs(:file?).returns(true)
+    expect{Puppet::Type.type(:odl_keystore).new(
+      :name      => 'admin',
+      :password  => 'admin',
+      :cert_file => '/tmp/dummy.txt',
+      :key_file  => '/tmp/dummy.txt',
+      :ca_file   => '/tmp/dummy.txt')}.to raise_error(Puppet::ResourceError)
+  end
+end
diff --git a/templates/aaa-cert-config.xml.erb b/templates/aaa-cert-config.xml.erb
new file mode 100644 (file)
index 0000000..d6faa89
--- /dev/null
@@ -0,0 +1,23 @@
+<aaa-cert-service-config xmlns="urn:opendaylight:yang:aaa:cert">
+  <use-config>true</use-config>
+  <use-mdsal><%= scope.lookupvar('opendaylight::enable_ha') %></use-mdsal>
+  <bundle-name>opendaylight</bundle-name>
+  <ctlKeystore>
+    <name>ctl.jks</name>
+    <alias>controller</alias>
+    <store-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></store-password>
+    <dname>CN=ODL, OU=Dev, O=LinuxFoundation, L=QC Montreal, C=CA</dname>
+    <validity>365</validity>
+    <key-alg>RSA</key-alg>
+    <sign-alg>SHA1WithRSAEncryption</sign-alg>
+    <keysize>1024</keysize>
+    <tls-protocols />
+    <cipher-suites>
+      <suite-name />
+    </cipher-suites>
+  </ctlKeystore>
+  <trustKeystore>
+    <name>truststore.jks</name>
+    <store-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></store-password>
+  </trustKeystore>
+</aaa-cert-service-config>
diff --git a/templates/default-openflow-connection-config.xml.erb b/templates/default-openflow-connection-config.xml.erb
new file mode 100644 (file)
index 0000000..3736fb5
--- /dev/null
@@ -0,0 +1,16 @@
+<switch-connection-config xmlns="urn:opendaylight:params:xml:ns:yang:openflow:switch:connection:config">
+  <instance-name>openflow-switch-connection-provider-default-impl</instance-name>
+  <port>6653</port>
+  <transport-protocol><%= scope.lookupvar('opendaylight::config::transport_protocol') %></transport-protocol>
+  <tls>
+     <keystore>configuration/ssl/ctl.jks</keystore>
+     <keystore-type>JKS</keystore-type>
+     <keystore-path-type>PATH</keystore-path-type>
+     <keystore-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></keystore-password>
+     <truststore>configuration/ssl/truststore.jks</truststore>
+     <truststore-type>JKS</truststore-type>
+     <truststore-path-type>PATH</truststore-path-type>
+     <truststore-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></truststore-password>
+     <certificate-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></certificate-password>
+  </tls>
+</switch-connection-config>