diff --git a/manifests/profile/base/nova.pp b/manifests/profile/base/nova.pp index b6c19103a..885714c11 100644 --- a/manifests/profile/base/nova.pp +++ b/manifests/profile/base/nova.pp @@ -51,20 +51,26 @@ # 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, - $nova_compute_enabled = false, - $step = hiera('step'), - $rabbit_hosts = hiera('rabbitmq_node_ips', undef), - $rabbit_port = hiera('nova::rabbit_port', 5672), - $migration_ssh_key = {}, - $libvirt_tls = false + $bootstrap_node = hiera('bootstrap_nodeid', undef), + $libvirt_enabled = false, + $manage_migration = false, + $nova_compute_enabled = false, + $step = hiera('step'), + $rabbit_hosts = hiera('rabbitmq_node_ips', undef), + $rabbit_port = hiera('nova::rabbit_port', 5672), + $migration_ssh_key = {}, + $migration_ssh_localaddrs = [], + $libvirt_tls = false ) { if $::hostname == downcase($bootstrap_node) { $sync_db = true @@ -80,15 +86,19 @@ class tripleo::profile::base::nova ( if $step >= 4 or ($step >= 3 and $sync_db) { $rabbit_endpoints = suffix(any2array(normalize_ip_for_uri($rabbit_hosts)), ":${rabbit_port}") + class { '::nova' : + rabbit_hosts => $rabbit_endpoints, + } include ::nova::config class { '::nova::cache': enabled => true, backend => 'oslo_cache.memcache_pool', memcache_servers => $memcache_servers, } + } - if $step >= 4 and $manage_migration { - + if $step >= 4 { + if $manage_migration { # Libvirt setup (live-migration) if $libvirt_tls { class { '::nova::migration::libvirt': @@ -102,44 +112,77 @@ class tripleo::profile::base::nova ( transport => 'ssh', configure_libvirt => $libvirt_enabled, configure_nova => $nova_compute_enabled, - client_user => 'nova', + client_user => 'nova_migration', client_extraparams => { - 'keyfile' => '/var/lib/nova/.ssh/id_rsa' + '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' : - rabbit_hosts => $rabbit_endpoints, - 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 index 000000000..aaea57e99 --- /dev/null +++ b/releasenotes/notes/cold_migration_security-1543136408c76459.yaml @@ -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. diff --git a/spec/classes/tripleo_profile_base_nova_spec.rb b/spec/classes/tripleo_profile_base_nova_spec.rb index 92511fb1c..c1d82e6be 100644 --- a/spec/classes/tripleo_profile_base_nova_spec.rb +++ b/spec/classes/tripleo_profile_base_nova_spec.rb @@ -22,7 +22,7 @@ describe 'tripleo::profile::base::nova' do context 'with step less than 3' do let(:params) { { :step => 1, - :rabbit_hosts => [ '127.0.0.1' ], + :rabbit_hosts => [ '127.0.0.1' ], } } it { @@ -37,13 +37,13 @@ describe 'tripleo::profile::base::nova' do let(:params) { { :step => 3, :bootstrap_node => 'node.example.com', - :rabbit_hosts => [ '127.0.0.1' ], + :rabbit_hosts => [ '127.0.0.1' ], } } it { is_expected.to contain_class('tripleo::profile::base::nova') is_expected.to contain_class('nova').with( - :rabbit_hosts => ['127.0.0.1:5672'] + :rabbit_hosts => ['127.0.0.1:5672'] ) is_expected.to contain_class('nova::config') @@ -59,7 +59,7 @@ describe 'tripleo::profile::base::nova' do let(:params) { { :step => 3, :bootstrap_node => 'other.example.com', - :rabbit_hosts => [ '127.0.0.1' ], + :rabbit_hosts => [ '127.0.0.1' ], } } it { @@ -74,7 +74,7 @@ describe 'tripleo::profile::base::nova' do let(:params) { { :step => 4, :bootstrap_node => 'other.example.com', - :rabbit_hosts => [ '127.0.0.1' ], + :rabbit_hosts => [ '127.0.0.1' ], } } it { @@ -87,6 +87,9 @@ describe 'tripleo::profile::base::nova' do is_expected.to contain_class('nova::config') is_expected.to contain_class('nova::cache') is_expected.to_not contain_class('nova::migration::libvirt') + is_expected.to contain_package('openstack-nova-migration').with( + :ensure => 'absent' + ) } end @@ -100,7 +103,7 @@ describe 'tripleo::profile::base::nova' do :manage_migration => true, :nova_compute_enabled => true, :bootstrap_node => 'node.example.com', - :rabbit_hosts => [ '127.0.0.1' ], + :rabbit_hosts => [ '127.0.0.1' ], } } it { @@ -117,6 +120,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 @@ -148,13 +154,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, @@ -169,8 +184,8 @@ describe 'tripleo::profile::base::nova' do is_expected.to contain_class('tripleo::profile::base::nova') is_expected.to contain_class('nova').with( :rabbit_hosts => /.+/, - :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::cache') @@ -179,13 +194,117 @@ 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 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', + :rabbit_hosts => [ '127.0.0.1' ], + :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( + :rabbit_hosts => /.+/, + :nova_public_key => nil, + :nova_private_key => nil, + ) + is_expected.to contain_class('nova::config') + 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) { - '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, @@ -201,9 +320,8 @@ describe 'tripleo::profile::base::nova' do is_expected.to contain_class('tripleo::profile::base::nova') is_expected.to contain_class('nova').with( :rabbit_hosts => /.+/, - :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::cache') @@ -212,6 +330,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 diff --git a/spec/fixtures/hieradata/default.yaml b/spec/fixtures/hieradata/default.yaml index 534942502..6ba85a165 100644 --- a/spec/fixtures/hieradata/default.yaml +++ b/spec/fixtures/hieradata/default.yaml @@ -7,3 +7,4 @@ memcached_node_ips_v6: - '::1' memcached_node_ips: - '127.0.0.1' +service_names: ['sshd']