Project

General

Profile

« Previous | Next » 

Revision 358ec5a3

Added by Dominic Cleal over 11 years ago

  • ID 358ec5a3a1b59c098b5c14fcd7a90ca1a6a5dccd

fixes #2121, #2069 - restrict importers and ENC to puppetmasters and users

CVE-2013-0171: report and fact importers parse YAML directly from the remote
host without authentication. Untrusted YAML can instantiate objects and be
used to exploit Foreman.

CVE-2013-0174: external nodes (ENC) output is available to any source and
could contain sensitive information, e.g. root password.

The restrict_registered_puppetmasters setting (default: on) now only permits
access to the three routes if the remote host has a smart proxy registered
with the Puppet feature.

The require_ssl_puppetmasters setting (default: on) requires a client SSL
certificate on HTTPS requests. The CN is checked against known smart proxies
as above. :require_ssl in settings.yaml is recommended to disable HTTP.

Ensure ENC (node.rb) and report (foreman.rb) scripts are updated to supply
client SSL certificates.

View differences:

app/controllers/fact_values_controller.rb
require 'foreman/controller/smart_proxy_auth'
class FactValuesController < ApplicationController
include Foreman::Controller::AutoCompleteSearch
include Foreman::Controller::SmartProxyAuth
skip_before_filter :require_ssl, :only => :create
skip_before_filter :require_login, :only => :create
skip_before_filter :authorize, :only => :create
skip_before_filter :verify_authenticity_token, :only => :create
skip_before_filter :set_taxonomy, :only => :create
skip_before_filter :session_expiry, :update_activity_time, :only => :create
before_filter :set_admin_user, :only => :create
add_puppetmaster_filters :create
before_filter :setup_search_options, :only => :index
def index
app/controllers/hosts_controller.rb
require 'foreman/controller/host_details'
require 'foreman/controller/smart_proxy_auth'
class HostsController < ApplicationController
include Foreman::Controller::HostDetails
include Foreman::Controller::AutoCompleteSearch
include Foreman::Controller::SmartProxyAuth
# actions which don't require authentication and are always treated as the admin user
ANONYMOUS_ACTIONS=[ :externalNodes, :lookup ]
PUPPETMASTER_ACTIONS=[ :externalNodes, :lookup ]
SEARCHABLE_ACTIONS= %w[index active errors out_of_sync pending disabled ]
AJAX_REQUESTS=%w{compute_resource_selected hostgroup_or_environment_selected current_parameters}
skip_before_filter :require_login, :only => ANONYMOUS_ACTIONS
skip_before_filter :require_ssl, :only => ANONYMOUS_ACTIONS
skip_before_filter :authorize, :only => ANONYMOUS_ACTIONS
skip_before_filter :set_taxonomy, :only => ANONYMOUS_ACTIONS
skip_before_filter :session_expiry, :update_activity_time, :only => ANONYMOUS_ACTIONS
before_filter :set_admin_user, :only => ANONYMOUS_ACTIONS
add_puppetmaster_filters PUPPETMASTER_ACTIONS
before_filter :ajax_request, :only => AJAX_REQUESTS
before_filter :find_multiple, :only => [:update_multiple_parameters, :multiple_build,
:select_multiple_hostgroup, :select_multiple_environment, :multiple_parameters, :multiple_destroy,
app/controllers/reports_controller.rb
require 'foreman/controller/smart_proxy_auth'
class ReportsController < ApplicationController
include Foreman::Controller::AutoCompleteSearch
include Foreman::Controller::SmartProxyAuth
skip_before_filter :require_login, :only => :create
skip_before_filter :require_ssl, :only => :create
skip_before_filter :authorize, :only => :create
skip_before_filter :verify_authenticity_token, :only => :create
skip_before_filter :set_taxonomy, :only => :create
skip_before_filter :session_expiry, :update_activity_time, :only => :create
before_filter :set_admin_user, :only => :create
add_puppetmaster_filters :create
before_filter :setup_search_options, :only => :index
def index
lib/foreman/access_permissions.rb
end
map.security_block :hosts do |map|
map.permission :view_hosts, {:hosts => [:index, :show, :errors, :active, :out_of_sync, :disabled], :dashboard => [:OutOfSync, :errors, :active]}
map.permission :view_hosts, {:hosts => [:index, :show, :errors, :active, :out_of_sync, :disabled, :externalNodes], :dashboard => [:OutOfSync, :errors, :active]}
map.permission :create_hosts, {:hosts => [:new, :create, :clone]}
map.permission :edit_hosts, {:hosts => [:edit, :update, :multiple_actions, :reset_multiple,
:select_multiple_hostgroup, :select_multiple_environment, :submit_multiple_disable,
lib/foreman/controller/smart_proxy_auth.rb
require 'resolv'
require 'uri'
module Foreman::Controller::SmartProxyAuth
module ClassMethods
def add_puppetmaster_filters(actions)
skip_before_filter :require_login, :only => actions
skip_before_filter :require_ssl, :only => actions
skip_before_filter :authorize, :only => actions
skip_before_filter :verify_authenticity_token, :only => actions
skip_before_filter :set_taxonomy, :only => actions
skip_before_filter :session_expiry, :update_activity_time, :only => actions
before_filter :require_puppetmaster_or_login, :only => actions
end
end
def self.included(base)
base.extend(ClassMethods)
end
# Permits registered puppetmasters or a user with permission
def require_puppetmaster_or_login
if !Setting[:restrict_registered_puppetmasters] or auth_smart_proxy(SmartProxy.puppet_proxies, Setting[:require_ssl_puppetmasters])
set_admin_user
return true
end
require_login
unless User.current
render_403
return false
end
authorize
end
# Filter requests to only permit from hosts with a registered smart proxy
# Uses rDNS of the request to match proxy hostnames
def auth_smart_proxy(proxies = SmartProxy.all, require_cert = true)
request_hosts = nil
if request.ssl?
if cn = request.env[Setting[:ssl_client_cn_env]]
if request.env[Setting[:ssl_client_verify_env]] == 'SUCCESS'
request_hosts = [cn]
else
logger.warn "SSL cert for #{cn} has not been verified - request from #{request.ip}"
end
elsif require_cert
logger.warn "No SSL cert with CN supplied - request from #{request.ip}"
else
request_hosts = Resolv.new.getnames(request.ip)
end
elsif SETTINGS[:require_ssl]
logger.warn "SSL is required - request from #{request.ip}"
else
request_hosts = Resolv.new.getnames(request.ip)
end
return false unless request_hosts
logger.debug("Verifying request from #{request_hosts} against #{proxies.map {|p| URI.parse(p.url).host}.inspect}")
unless proxies.detect { |p| request_hosts.include? URI.parse(p.url).host }
logger.warn "No smart proxy server found on #{request_hosts.inspect}"
return false
end
true
end
end
lib/foreman/default_settings/loader.rb
[ set('oauth_active', "Should foreman use OAuth for authorization in API", false),
set('oauth_consumer_key', "OAuth consumer key", 'katello'),
set('oauth_consumer_secret', "OAuth consumer secret", 'shhhh'),
set('oauth_map_users', "Should foreman map users by username in request-header", true)
set('oauth_map_users', "Should foreman map users by username in request-header", true),
set('restrict_registered_puppetmasters', 'Only known Smart Proxies with the Puppet feature can access fact/report importers and ENC output', true),
set('require_ssl_puppetmasters', 'Client SSL certificates are used to identify Smart Proxies accessing fact/report importers and ENC output over HTTPS (:require_ssl should also be enabled)', true),
set('ssl_client_cn_env', 'Environment variable containing the subject CN from a client SSL certificate', 'SSL_CLIENT_S_DN_CN'),
set('ssl_client_verify_env', 'Environment variable containing the verification status of a client SSL certificate', 'SSL_CLIENT_VERIFY')
].compact.each { |s| create s.update(:category => "Auth")}
end
true
test/fixtures/settings.yml
category: Provisioning
default: 0
description: "Time in minutes installation tokens should be valid for, 0 to disable"
attribute27:
name: restrict_registered_puppetmasters
category: Auth
default: true
description: "Only known Smart Proxies with the Puppet feature can access fact/report importers and ENC output"
attribute28:
name: require_ssl_puppetmasters
category: Auth
default: true
description: "Client SSL certificates are used to identify Smart Proxies accessing fact/report importers and ENC output over HTTPS (:require_ssl should also be enabled)"
attribute29:
name: ssl_client_cn_env
category: Auth
default: "SSL_CLIENT_S_DN_CN"
description: "Environment variable containing the subject CN from a client SSL certificate"
attribute30:
name: ssl_client_verify_env
category: Auth
default: "SSL_CLIENT_VERIFY"
description: "Environment variable containing the verification status of a client SSL certificate"
test/functional/fact_values_controller_test.rb
Pathname.new("#{Rails.root}/test/fixtures/brslc022.facts.yaml").read
end
def setup
User.current = nil
end
fixtures
def test_index
......
def test_create_invalid
User.current = nil
post :create, {:facts => fact_fixture[1..-1], :format => "yml"}
post :create, {:facts => fact_fixture[1..-1], :format => "yml"}, set_session_user
assert_response :bad_request
end
def test_create_valid_puppet_node_facts_object
User.current = nil
post :create, {:facts => fact_fixture, :format => "yml"}
post :create, {:facts => fact_fixture, :format => "yml"}, set_session_user
assert_response :success
end
......
User.current = nil
facts = Facter.to_hash
assert_instance_of Hash, facts
post :create, {:facts => facts.to_yaml, :format => "yml"}
post :create, {:facts => facts.to_yaml, :format => "yml"}, set_session_user
assert_response :success
end
......
assert_response :success
end
test 'when ":restrict_registered_puppetmasters" is false, HTTP requests should be able to import facts' do
Setting[:restrict_registered_puppetmasters] = false
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:facts => fact_fixture, :format => "yml"}
assert_response :success
end
test 'hosts with a registered smart proxy on should import facts successfully' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:facts => fact_fixture, :format => "yml"}
assert_response :success
end
test 'hosts without a registered smart proxy on should not be able to import facts' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = false
Resolv.any_instance.stubs(:getnames).returns(['another.host'])
post :create, {:facts => fact_fixture, :format => "yml"}
assert_equal 403, @response.status
end
test 'hosts with a registered smart proxy and SSL cert should import facts successfully' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'else.where'
@request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
post :create, {:facts => fact_fixture, :format => "yml"}
assert_response :success
end
test 'hosts without a registered smart proxy but with an SSL cert should not be able to import facts' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'another.host'
@request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
post :create, {:facts => fact_fixture, :format => "yml"}
assert_equal 403, @response.status
end
test 'hosts with an unverified SSL cert should not be able to import facts' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'secure.host'
@request.env['SSL_CLIENT_VERIFY'] = 'FAILED'
post :create, {:facts => fact_fixture, :format => "yml"}
assert_equal 403, @response.status
end
test 'when "require_ssl_puppetmasters" and "require_ssl" are true, HTTP requests should not be able to import facts' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = true
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:facts => fact_fixture, :format => "yml"}
assert_equal 403, @response.status
end
test 'when "require_ssl_puppetmasters" is true and "require_ssl" is false, HTTP requests should be able to import facts' do
# since require_ssl_puppetmasters is only applicable to HTTPS connections, both should be set
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:facts => fact_fixture, :format => "yml"}
assert_response :success
end
end
test/functional/hosts_controller_test.rb
end
test "externalNodes should render correctly when format text/html is given" do
get :externalNodes, {:name => @host.name}
get :externalNodes, {:name => @host.name}, set_session_user
assert_response :success
assert_template :text => @host.info.to_yaml.gsub("\n","<br/>")
end
......
assert flash[:notice] == "Foreman now no longer manages the build cycle for #{@host.name}"
end
test 'when ":restrict_registered_puppetmasters" is false, HTTP requests should be able to get externalNodes' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = false
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_response :success
end
test 'hosts with a registered smart proxy on should get externalNodes successfully' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_response :success
end
test 'hosts without a registered smart proxy on should not be able to get externalNodes' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = false
Resolv.any_instance.stubs(:getnames).returns(['another.host'])
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_equal 403, @response.status
end
test 'hosts with a registered smart proxy and SSL cert should get externalNodes successfully' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'else.where'
@request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_response :success
end
test 'hosts without a registered smart proxy but with an SSL cert should not be able to get externalNodes' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'another.host'
@request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_equal 403, @response.status
end
test 'hosts with an unverified SSL cert should not be able to get externalNodes' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'else.where'
@request.env['SSL_CLIENT_VERIFY'] = 'FAILURE'
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_equal 403, @response.status
end
test 'when "require_ssl_puppetmasters" and "require_ssl" are true, HTTP requests should not be able to get externalNodes' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = true
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_equal 403, @response.status
end
test 'when "require_ssl_puppetmasters" is true and "require_ssl" is false, HTTP requests should be able to get externalNodes' do
User.current = nil
# since require_ssl_puppetmasters is only applicable to HTTPS connections, both should be set
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
get :externalNodes, {:name => @host.name, :format => "yml"}
assert_response :success
end
test 'authenticated users over HTTP should be able to get externalNodes' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['users.host'])
get :externalNodes, {:name => @host.name, :format => "yml"}, set_session_user
assert_response :success
end
test 'authenticated users over HTTPS should be able to get externalNodes' do
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['users.host'])
@request.env['HTTPS'] = 'on'
get :externalNodes, {:name => @host.name, :format => "yml"}, set_session_user
assert_response :success
end
private
def initialize_host
User.current = users(:admin)
test/functional/reports_controller_test.rb
def test_create_duplicate
create_a_puppet_transaction_report
User.current = nil
post :create, {:report => @log, :format => "yml"}
post :create, {:report => @log, :format => "yml"}, set_session_user
assert_response :success
post :create, {:report => @log, :format => "yml"}
post :create, {:report => @log, :format => "yml"}, set_session_user
assert_response :error
end
def test_create_valid
create_a_puppet_transaction_report
User.current = nil
post :create, {:report => @log, :format => "yml"}
post :create, {:report => @log, :format => "yml"}, set_session_user
assert_response :success
end
......
get :index, {}, set_session_user
assert_response :success
end
test 'when ":restrict_registered_puppetmasters" is false, HTTP requests should be able to create a report' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = false
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_response :success
end
test 'hosts with a registered smart proxy on should create a report successfully' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_response :success
end
test 'hosts without a registered smart proxy on should not be able to create a report' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = false
Resolv.any_instance.stubs(:getnames).returns(['another.host'])
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_equal 403, @response.status
end
test 'hosts with a registered smart proxy and SSL cert should create a report successfully' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'else.where'
@request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_response :success
end
test 'hosts without a registered smart proxy but with an SSL cert should not be able to create a report' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'another.host'
@request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_equal 403, @response.status
end
test 'hosts with an unverified SSL cert should not be able to create a report' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
@request.env['HTTPS'] = 'on'
@request.env['SSL_CLIENT_S_DN_CN'] = 'else.where'
@request.env['SSL_CLIENT_VERIFY'] = 'FAILED'
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_equal 403, @response.status
end
test 'when "require_ssl_puppetmasters" and "require_ssl" are true, HTTP requests should not be able to create a report' do
User.current = nil
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = true
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_equal 403, @response.status
end
test 'when "require_ssl_puppetmasters" is true and "require_ssl" is false, HTTP requests should be able to create reports' do
User.current = nil
# since require_ssl_puppetmasters is only applicable to HTTPS connections, both should be set
Setting[:restrict_registered_puppetmasters] = true
Setting[:require_ssl_puppetmasters] = true
SETTINGS[:require_ssl] = false
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
post :create, {:report => create_a_puppet_transaction_report, :format => "yml"}
assert_response :success
end
end

Also available in: Unified diff