Restrict nova migration ssh tunnel
authorOliver Walsh <owalsh@redhat.com>
Wed, 19 Apr 2017 13:39:42 +0000 (14:39 +0100)
committerEmilien Macchi <emilien@redhat.com>
Wed, 3 May 2017 20:20:01 +0000 (20:20 +0000)
This change enhances the security of the migration ssh tunnel:
- The ssh authorized_keys file is only writeable by root.
- Creates a new user for migration instead of using root/nova.
- Disables SSH forwarding for this user.
- Optionally restricts the networks that this user can connect from.
- Uses an ssh wrapper command to whitelist the commands that this user can run
  over ssh.

Requires the openstack-nova-migration package from
https://review.rdoproject.org/r/6327

bp tripleo-cold-migration

Change-Id: Idb56acd1e1ecb5a5fd4d942969be428cc9cbe293

manifests/profile/base/nova.pp
releasenotes/notes/cold_migration_security-1543136408c76459.yaml [new file with mode: 0644]
spec/classes/tripleo_profile_base_nova_spec.rb
spec/fixtures/hieradata/default.yaml

index ab9b615..65355d4 100644 (file)
 #   Expects a hash with keys 'private_key' and 'public_key'.
 #   Defaults to {}
 #
+# [*migration_ssh_localaddrs*]
+#   (Optional) Restrict ssh migration to clients connecting via this list of
+#   IPs.
+#   Defaults to [] (no restriction)
+#
 # [*libvirt_tls*]
 #   (Optional) Whether or not libvird TLS service is enabled.
 #   Defaults to false
 
 class tripleo::profile::base::nova (
-  $bootstrap_node          = hiera('bootstrap_nodeid', undef),
-  $libvirt_enabled         = false,
-  $manage_migration        = false,
-  $oslomsg_rpc_proto       = hiera('messaging_rpc_service_name', 'rabbit'),
-  $oslomsg_rpc_hosts       = any2array(hiera('rabbitmq_node_names', undef)),
-  $oslomsg_rpc_password    = hiera('nova::rabbit_password'),
-  $oslomsg_rpc_port        = hiera('nova::rabbit_port', '5672'),
-  $oslomsg_rpc_username    = hiera('nova::rabbit_userid', 'guest'),
-  $oslomsg_notify_proto    = hiera('messaging_notify_service_name', 'rabbit'),
-  $oslomsg_notify_hosts    = any2array(hiera('rabbitmq_node_names', undef)),
-  $oslomsg_notify_password = hiera('nova::rabbit_password'),
-  $oslomsg_notify_port     = hiera('nova::rabbit_port', '5672'),
-  $oslomsg_notify_username = hiera('nova::rabbit_userid', 'guest'),
-  $oslomsg_use_ssl         = hiera('nova::rabbit_use_ssl', '0'),
-  $nova_compute_enabled    = false,
-  $step                    = hiera('step'),
-  $migration_ssh_key       = {},
-  $libvirt_tls             = false
+  $bootstrap_node           = hiera('bootstrap_nodeid', undef),
+  $libvirt_enabled          = false,
+  $manage_migration         = false,
+  $oslomsg_rpc_proto        = hiera('messaging_rpc_service_name', 'rabbit'),
+  $oslomsg_rpc_hosts        = any2array(hiera('rabbitmq_node_names', undef)),
+  $oslomsg_rpc_password     = hiera('nova::rabbit_password'),
+  $oslomsg_rpc_port         = hiera('nova::rabbit_port', '5672'),
+  $oslomsg_rpc_username     = hiera('nova::rabbit_userid', 'guest'),
+  $oslomsg_notify_proto     = hiera('messaging_notify_service_name', 'rabbit'),
+  $oslomsg_notify_hosts     = any2array(hiera('rabbitmq_node_names', undef)),
+  $oslomsg_notify_password  = hiera('nova::rabbit_password'),
+  $oslomsg_notify_port      = hiera('nova::rabbit_port', '5672'),
+  $oslomsg_notify_username  = hiera('nova::rabbit_userid', 'guest'),
+  $oslomsg_use_ssl          = hiera('nova::rabbit_use_ssl', '0'),
+  $nova_compute_enabled     = false,
+  $step                     = hiera('step'),
+  $migration_ssh_key        = {},
+  $migration_ssh_localaddrs = [],
+  $libvirt_tls              = false
 ) {
   if $::hostname == downcase($bootstrap_node) {
     $sync_db = true
@@ -131,10 +137,29 @@ class tripleo::profile::base::nova (
       backend          => 'oslo_cache.memcache_pool',
       memcache_servers => $memcache_servers,
     }
+    class { '::nova':
+      default_transport_url      => os_transport_url({
+        'transport' => $oslomsg_rpc_proto,
+        'hosts'     => $oslomsg_rpc_hosts,
+        'port'      => $oslomsg_rpc_port,
+        'username'  => $oslomsg_rpc_username,
+        'password'  => $oslomsg_rpc_password,
+        'ssl'       => $oslomsg_use_ssl_real,
+      }),
+      notification_transport_url => os_transport_url({
+        'transport' => $oslomsg_notify_proto,
+        'hosts'     => $oslomsg_notify_hosts,
+        'port'      => $oslomsg_notify_port,
+        'username'  => $oslomsg_notify_username,
+        'password'  => $oslomsg_notify_password,
+        'ssl'       => $oslomsg_use_ssl_real,
+      }),
+    }
     include ::nova::placement
+  }
 
-    if $step >= 4 and $manage_migration {
-
+  if $step >= 4 {
+    if $manage_migration {
       # Libvirt setup (live-migration)
       if $libvirt_tls {
         class { '::nova::migration::libvirt':
@@ -148,57 +173,75 @@ class tripleo::profile::base::nova (
           transport          => 'ssh',
           configure_libvirt  => $libvirt_enabled,
           configure_nova     => $nova_compute_enabled,
-          client_user        => 'nova',
-          client_extraparams => {'keyfile' => '/var/lib/nova/.ssh/id_rsa'}
+          client_user        => 'nova_migration',
+          client_extraparams => {'keyfile' => '/etc/nova/migration/identity'}
         }
       }
 
-      if $migration_ssh_key != {} {
+      $services_enabled = hiera('service_names', [])
+      if !empty($migration_ssh_key) and 'sshd' in $services_enabled {
         # Nova SSH tunnel setup (cold-migration)
 
-        #TODO: Remove me when https://review.rdoproject.org/r/#/c/4008 lands
-        user { 'nova':
-          ensure => present,
-          shell  => '/bin/bash',
+        # Server side
+        if !empty($migration_ssh_localaddrs) {
+          $allow_type = sprintf('LocalAddress %s User', join($migration_ssh_localaddrs,','))
+          $deny_type = 'LocalAddress'
+          $deny_name = sprintf('!%s', join($migration_ssh_localaddrs,',!'))
+
+          ssh::server::match_block { 'nova_migration deny':
+            name    => $deny_name,
+            type    => $deny_type,
+            order   => 2,
+            options => {
+              'DenyUsers' => 'nova_migration'
+            },
+            notify  => Service['sshd']
+          }
+        }
+        else {
+          $allow_type = 'User'
+        }
+        $allow_name = 'nova_migration'
+
+        ssh::server::match_block { 'nova_migration allow':
+          name    => $allow_name,
+          type    => $allow_type,
+          order   => 1,
+          options => {
+            'ForceCommand'           => '/bin/nova-migration-wrapper',
+            'PasswordAuthentication' => 'no',
+            'AllowTcpForwarding'     => 'no',
+            'X11Forwarding'          => 'no',
+            'AuthorizedKeysFile'     => '/etc/nova/migration/authorized_keys'
+          },
+          notify  => Service['sshd']
         }
 
-        $private_key_parts = split($migration_ssh_key['public_key'], ' ')
-        $nova_public_key = {
-          type => $private_key_parts[0],
-          key  => $private_key_parts[1]
+        file { '/etc/nova/migration/authorized_keys':
+          content => $migration_ssh_key['public_key'],
+          mode    => '0640',
+          owner   => 'root',
+          group   => 'nova_migration',
+          require => Package['openstack-nova-migration'],
         }
-        $nova_private_key = {
-          type => $private_key_parts[0],
-          key  => $migration_ssh_key['private_key']
+
+        # Client side
+        file { '/etc/nova/migration/identity':
+          content => $migration_ssh_key['private_key'],
+          mode    => '0600',
+          owner   => 'nova',
+          group   => 'nova',
+          require => Package['openstack-nova-migration'],
         }
+        $migration_pkg_ensure = installed
       } else {
-        $nova_public_key = undef
-        $nova_private_key = undef
+        $migration_pkg_ensure = absent
       }
     } else {
-      $nova_public_key = undef
-      $nova_private_key = undef
+      $migration_pkg_ensure = absent
     }
-
-    class { '::nova':
-      default_transport_url      => os_transport_url({
-        'transport' => $oslomsg_rpc_proto,
-        'hosts'     => $oslomsg_rpc_hosts,
-        'port'      => $oslomsg_rpc_port,
-        'username'  => $oslomsg_rpc_username,
-        'password'  => $oslomsg_rpc_password,
-        'ssl'       => $oslomsg_use_ssl_real,
-      }),
-      notification_transport_url => os_transport_url({
-        'transport' => $oslomsg_notify_proto,
-        'hosts'     => $oslomsg_notify_hosts,
-        'port'      => $oslomsg_notify_port,
-        'username'  => $oslomsg_notify_username,
-        'password'  => $oslomsg_notify_password,
-        'ssl'       => $oslomsg_use_ssl_real,
-      }),
-      nova_public_key            => $nova_public_key,
-      nova_private_key           => $nova_private_key,
+    package {'openstack-nova-migration':
+      ensure => $migration_pkg_ensure
     }
   }
 }
diff --git a/releasenotes/notes/cold_migration_security-1543136408c76459.yaml b/releasenotes/notes/cold_migration_security-1543136408c76459.yaml
new file mode 100644 (file)
index 0000000..aaea57e
--- /dev/null
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    Restrict nova migration ssh tunnel
+    * The ssh authorized_keys file is only writeable by root.
+    * Creates a new user for migration instead of using root/nova.
+    * Disables SSH forwarding for this user.
+    * Restricts the networks that this user can connect from.
+    * Uses an ssh wrapper command to whitelist the commands that this user can run over ssh.
+    Adds new parameter "tripleo::profile::base::nova::migration_ssh_localaddrs" to specify which incoming IPs are allow for SSH tunnel connections.
index 8f7bfdc..d77ba1b 100644 (file)
@@ -95,6 +95,9 @@ describe 'tripleo::profile::base::nova' do
         is_expected.to contain_class('nova::cache')
         is_expected.to contain_class('nova::placement')
         is_expected.to_not contain_class('nova::migration::libvirt')
+        is_expected.to contain_package('openstack-nova-migration').with(
+          :ensure => 'absent'
+        )
       }
     end
 
@@ -128,6 +131,9 @@ describe 'tripleo::profile::base::nova' do
           :configure_libvirt => params[:libvirt_enabled],
           :configure_nova    => params[:nova_compute_enabled]
         )
+        is_expected.to contain_package('openstack-nova-migration').with(
+          :ensure => 'absent'
+        )
       }
     end
 
@@ -162,13 +168,22 @@ describe 'tripleo::profile::base::nova' do
           :configure_libvirt => params[:libvirt_enabled],
           :configure_nova    => params[:nova_compute_enabled],
         )
+        is_expected.to contain_package('openstack-nova-migration').with(
+          :ensure => 'absent'
+        )
       }
     end
 
     context 'with step 4 with libvirt and migration ssh key' do
-      let(:pre_condition) {
-        'include ::nova::compute::libvirt::services'
-      }
+      let(:pre_condition) do
+        <<-eof
+        include ::nova::compute::libvirt::services
+        class { '::ssh::server':
+          storeconfigs_enabled => false,
+          options              => {}
+        }
+        eof
+      end
       let(:params) { {
         :step           => 4,
         :libvirt_enabled => true,
@@ -185,8 +200,8 @@ describe 'tripleo::profile::base::nova' do
         is_expected.to contain_class('nova').with(
           :default_transport_url => /.+/,
           :notification_transport_url => /.+/,
-          :nova_public_key  => {'key' => 'bar', 'type' => 'ssh-rsa'},
-          :nova_private_key => {'key' => 'foo', 'type' => 'ssh-rsa'}
+          :nova_public_key  => nil,
+          :nova_private_key => nil,
         )
         is_expected.to contain_class('nova::config')
         is_expected.to contain_class('nova::placement')
@@ -196,13 +211,120 @@ describe 'tripleo::profile::base::nova' do
           :configure_libvirt => params[:libvirt_enabled],
           :configure_nova    => params[:nova_compute_enabled]
         )
+        is_expected.to contain_ssh__server__match_block('nova_migration allow').with(
+          :type  => 'User',
+          :name  => 'nova_migration',
+          :options => {
+            'ForceCommand'           => '/bin/nova-migration-wrapper',
+            'PasswordAuthentication' => 'no',
+            'AllowTcpForwarding'     => 'no',
+            'X11Forwarding'          => 'no',
+            'AuthorizedKeysFile'     => '/etc/nova/migration/authorized_keys'
+          }
+        )
+        is_expected.to_not contain_ssh__server__match_block('nova_migration deny')
+        is_expected.to contain_file('/etc/nova/migration/authorized_keys').with(
+          :content => 'ssh-rsa bar',
+          :mode => '0640',
+          :owner => 'root',
+          :group => 'nova_migration',
+        )
+        is_expected.to contain_file('/etc/nova/migration/identity').with(
+          :content => 'foo',
+          :mode => '0600',
+          :owner => 'nova',
+          :group => 'nova',
+        )
+        is_expected.to contain_package('openstack-nova-migration').with(
+          :ensure => 'installed'
+        )
       }
     end
 
-    context 'with step 4 with libvirt TLS and migration ssh key' do
-      let(:pre_condition) {
-        'include ::nova::compute::libvirt::services'
+    context 'with step 4 with libvirt and migration ssh key and migration_ssh_localaddrs' do
+      let(:pre_condition) do
+        <<-eof
+        include ::nova::compute::libvirt::services
+        class { '::ssh::server':
+          storeconfigs_enabled => false,
+          options              => {}
+        }
+        eof
+      end
+      let(:params) { {
+        :step           => 4,
+        :libvirt_enabled => true,
+        :manage_migration => true,
+        :nova_compute_enabled => true,
+        :bootstrap_node  => 'node.example.com',
+        :oslomsg_rpc_hosts => [ 'localhost' ],
+        :oslomsg_rpc_password => 'foo',
+        :migration_ssh_key => { 'private_key' => 'foo', 'public_key' => 'ssh-rsa bar'},
+        :migration_ssh_localaddrs => ['127.0.0.1', '127.0.0.2']
+      } }
+
+      it {
+        is_expected.to contain_class('tripleo::profile::base::nova')
+        is_expected.to contain_class('nova').with(
+          :default_transport_url => /.+/,
+          :notification_transport_url => /.+/,
+          :nova_public_key  => nil,
+          :nova_private_key => nil,
+        )
+        is_expected.to contain_class('nova::config')
+        is_expected.to contain_class('nova::placement')
+        is_expected.to contain_class('nova::cache')
+        is_expected.to contain_class('nova::migration::libvirt').with(
+          :transport         => 'ssh',
+          :configure_libvirt => params[:libvirt_enabled],
+          :configure_nova    => params[:nova_compute_enabled]
+        )
+        is_expected.to contain_ssh__server__match_block('nova_migration allow').with(
+          :type  => 'LocalAddress 127.0.0.1,127.0.0.2 User',
+          :name  => 'nova_migration',
+          :options => {
+            'ForceCommand'           => '/bin/nova-migration-wrapper',
+            'PasswordAuthentication' => 'no',
+            'AllowTcpForwarding'     => 'no',
+            'X11Forwarding'          => 'no',
+            'AuthorizedKeysFile'     => '/etc/nova/migration/authorized_keys'
+          }
+        )
+        is_expected.to contain_ssh__server__match_block('nova_migration deny').with(
+          :type  => 'LocalAddress',
+          :name  => '!127.0.0.1,!127.0.0.2',
+          :options => {
+            'DenyUsers' => 'nova_migration'
+          }
+        )
+        is_expected.to contain_file('/etc/nova/migration/authorized_keys').with(
+          :content => 'ssh-rsa bar',
+          :mode => '0640',
+          :owner => 'root',
+          :group => 'nova_migration',
+        )
+        is_expected.to contain_file('/etc/nova/migration/identity').with(
+          :content => 'foo',
+          :mode => '0600',
+          :owner => 'nova',
+          :group => 'nova',
+        )
+        is_expected.to contain_package('openstack-nova-migration').with(
+          :ensure => 'installed'
+        )
       }
+    end
+
+    context 'with step 4 with libvirt TLS and migration ssh key' do
+      let(:pre_condition) do
+        <<-eof
+        include ::nova::compute::libvirt::services
+        class { '::ssh::server':
+          storeconfigs_enabled => false,
+          options              => {}
+        }
+        eof
+      end
       let(:params) { {
         :step           => 4,
         :libvirt_enabled => true,
@@ -220,8 +342,8 @@ describe 'tripleo::profile::base::nova' do
         is_expected.to contain_class('nova').with(
           :default_transport_url => /.+/,
           :notification_transport_url => /.+/,
-          :nova_public_key  => {'key' => 'bar', 'type' => 'ssh-rsa'},
-          :nova_private_key => {'key' => 'foo', 'type' => 'ssh-rsa'}
+          :nova_public_key  => nil,
+          :nova_private_key => nil,
         )
         is_expected.to contain_class('nova::config')
         is_expected.to contain_class('nova::placement')
@@ -231,6 +353,33 @@ describe 'tripleo::profile::base::nova' do
           :configure_libvirt => params[:libvirt_enabled],
           :configure_nova    => params[:nova_compute_enabled]
         )
+        is_expected.to contain_ssh__server__match_block('nova_migration allow').with(
+          :type  => 'User',
+          :name  => 'nova_migration',
+          :options => {
+            'ForceCommand'           => '/bin/nova-migration-wrapper',
+            'PasswordAuthentication' => 'no',
+            'AllowTcpForwarding'     => 'no',
+            'X11Forwarding'          => 'no',
+            'AuthorizedKeysFile'     => '/etc/nova/migration/authorized_keys'
+          }
+        )
+        is_expected.to_not contain_ssh__server__match_block('nova_migration deny')
+        is_expected.to contain_file('/etc/nova/migration/authorized_keys').with(
+          :content => 'ssh-rsa bar',
+          :mode => '0640',
+          :owner => 'root',
+          :group => 'nova_migration',
+        )
+        is_expected.to contain_file('/etc/nova/migration/identity').with(
+          :content => 'foo',
+          :mode => '0600',
+          :owner => 'nova',
+          :group => 'nova',
+        )
+        is_expected.to contain_package('openstack-nova-migration').with(
+          :ensure => 'installed'
+        )
       }
     end
 
index 873a49e..3cf2693 100644 (file)
@@ -44,3 +44,4 @@ memcached_node_ips:
 # octavia related items
 octavia::rabbit_password: 'password'
 horizon::secret_key: 'secrete'
+service_names: ['sshd']