diff --git a/app/models/runtime/process_model.rb b/app/models/runtime/process_model.rb index e83b4460bc..b31700b07f 100644 --- a/app/models/runtime/process_model.rb +++ b/app/models/runtime/process_model.rb @@ -576,8 +576,6 @@ def permitted_users end def docker_run_action_user - return AppModel::DEFAULT_CONTAINER_USER unless docker? - desired_droplet&.docker_user.presence || AppModel::DEFAULT_DOCKER_CONTAINER_USER end diff --git a/config/cloud_controller.yml b/config/cloud_controller.yml index d82de9db8e..c4d48202b9 100644 --- a/config/cloud_controller.yml +++ b/config/cloud_controller.yml @@ -68,6 +68,7 @@ max_retained_deployments_per_app: 100 max_retained_builds_per_app: 100 max_retained_revisions_per_app: 100 additional_allowed_process_users: ['ContainerUser', 'TestUser'] +allow_docker_root_user: true broker_client_default_async_poll_interval_seconds: 60 broker_client_max_async_poll_duration_minutes: 10080 diff --git a/lib/cloud_controller/config_schemas/api_schema.rb b/lib/cloud_controller/config_schemas/api_schema.rb index 8eb4be2152..5825ee4b58 100644 --- a/lib/cloud_controller/config_schemas/api_schema.rb +++ b/lib/cloud_controller/config_schemas/api_schema.rb @@ -45,6 +45,7 @@ class ApiSchema < VCAP::Config default_health_check_timeout: Integer, maximum_health_check_timeout: Integer, additional_allowed_process_users: Array, + allow_docker_root_user: bool, instance_file_descriptor_limit: Integer, diff --git a/lib/cloud_controller/config_schemas/clock_schema.rb b/lib/cloud_controller/config_schemas/clock_schema.rb index c2499cd23e..bc6ee50288 100644 --- a/lib/cloud_controller/config_schemas/clock_schema.rb +++ b/lib/cloud_controller/config_schemas/clock_schema.rb @@ -188,6 +188,7 @@ class ClockSchema < VCAP::Config max_retained_builds_per_app: Integer, max_retained_revisions_per_app: Integer, additional_allowed_process_users: Array, + allow_docker_root_user: bool, diego_sync: { frequency_in_seconds: Integer }, diff --git a/lib/cloud_controller/config_schemas/deployment_updater_schema.rb b/lib/cloud_controller/config_schemas/deployment_updater_schema.rb index d0b18faeb6..8585d622c1 100644 --- a/lib/cloud_controller/config_schemas/deployment_updater_schema.rb +++ b/lib/cloud_controller/config_schemas/deployment_updater_schema.rb @@ -58,6 +58,7 @@ class DeploymentUpdaterSchema < VCAP::Config maximum_app_disk_in_mb: Integer, instance_file_descriptor_limit: Integer, additional_allowed_process_users: Array, + allow_docker_root_user: bool, deployment_updater: { update_frequency_in_seconds: Integer, diff --git a/lib/cloud_controller/config_schemas/worker_schema.rb b/lib/cloud_controller/config_schemas/worker_schema.rb index 86053bbff6..4b718eef11 100644 --- a/lib/cloud_controller/config_schemas/worker_schema.rb +++ b/lib/cloud_controller/config_schemas/worker_schema.rb @@ -198,6 +198,7 @@ class WorkerSchema < VCAP::Config default_app_log_rate_limit_in_bytes_per_second: Integer, default_app_ssh_access: bool, additional_allowed_process_users: Array, + allow_docker_root_user: bool, jobs: { global: { diff --git a/lib/cloud_controller/diego/desire_app_handler.rb b/lib/cloud_controller/diego/desire_app_handler.rb index 055c464fd1..498d6eca64 100644 --- a/lib/cloud_controller/diego/desire_app_handler.rb +++ b/lib/cloud_controller/diego/desire_app_handler.rb @@ -12,6 +12,8 @@ def create_or_update_app(process, client) if e.name == 'RunnerError' && e.message['the requested resource already exists'] existing_lrp = client.get_app(process) client.update_app(process, existing_lrp) + elsif e.name == 'UnprocessableEntity' + raise end end end diff --git a/lib/cloud_controller/diego/docker/lifecycle_protocol.rb b/lib/cloud_controller/diego/docker/lifecycle_protocol.rb index a2b4b30287..240764e679 100644 --- a/lib/cloud_controller/diego/docker/lifecycle_protocol.rb +++ b/lib/cloud_controller/diego/docker/lifecycle_protocol.rb @@ -8,6 +8,8 @@ module CloudController module Diego module Docker class LifecycleProtocol + ROOT_USERS = %w[root 0].freeze + def lifecycle_data(staging_details) lifecycle_data = Diego::Docker::LifecycleData.new lifecycle_data.docker_image = staging_details.package.image @@ -22,10 +24,18 @@ def staging_action_builder(config, staging_details) end def task_action_builder(config, task) + if task.docker? && !docker_run_action_user_permitted?(task.run_action_user) + raise ::CloudController::Errors::ApiError.new_from_details('UnprocessableEntity', 'Attempting to run task as root user, which is not permitted.') + end + TaskActionBuilder.new(config, task, { droplet_path: task.droplet.docker_receipt_image }) end def desired_lrp_builder(config, process) + if process.docker? && !docker_run_action_user_permitted?(process.run_action_user) + raise ::CloudController::Errors::ApiError.new_from_details('UnprocessableEntity', 'Attempting to run process as root user, which is not permitted.') + end + DesiredLrpBuilder.new(config, builder_opts(process)) end @@ -46,6 +56,10 @@ def container_env_vars_for_process(process) additional_env = [] additional_env + WindowsEnvironmentSage.ponder(process.app) end + + def docker_run_action_user_permitted?(run_action_user) + Config.config.get(:allow_docker_root_user) || ROOT_USERS.exclude?(run_action_user) + end end end end diff --git a/spec/unit/lib/cloud_controller/diego/desire_app_handler_spec.rb b/spec/unit/lib/cloud_controller/diego/desire_app_handler_spec.rb index 0eb79e0d8c..6db219ac2c 100644 --- a/spec/unit/lib/cloud_controller/diego/desire_app_handler_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/desire_app_handler_spec.rb @@ -51,6 +51,18 @@ module Diego expect(client).to have_received(:get_app).exactly(2).times end end + + context 'when root user is not permitted' do + it 'raises a CloudController::Errors::ApiError' do + allow(client).to receive(:desire_app).and_raise(CloudController::Errors::ApiError.new_from_details('UnprocessableEntity', + 'Attempting to run process as root user, which is not permitted.')) + expect { DesireAppHandler.create_or_update_app(process, client) }.to(raise_error do |error| + expect(error).to be_a(CloudController::Errors::ApiError) + expect(error.name).to eq('UnprocessableEntity') + expect(error.message).to include('Attempting to run process as root user, which is not permitted') + end) + end + end end end end diff --git a/spec/unit/lib/cloud_controller/diego/docker/lifecycle_protocol_spec.rb b/spec/unit/lib/cloud_controller/diego/docker/lifecycle_protocol_spec.rb index 62681d83d0..2a6dca3736 100644 --- a/spec/unit/lib/cloud_controller/diego/docker/lifecycle_protocol_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/docker/lifecycle_protocol_spec.rb @@ -118,6 +118,162 @@ module Docker end end end + + context 'when root user is allowed' do + let(:app) { AppModel.make(:docker, { droplet: }) } + + before do + TestConfig.override(allow_docker_root_user: true, additional_allowed_process_users: %w[root 0]) + end + + context 'and the process sets the root user' do + let(:process) { ProcessModel.make(:docker, { app: app, user: 'root' }) } + + it 'creates a diego DesiredLrpBuilder' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.not_to raise_error + end + end + + context 'and the process does not set a user' do + let(:process) { ProcessModel.make(:docker, { app: }) } + + context 'and the droplet docker execution metadata sets the root user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"root"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'creates a diego DesiredLRPBuilder' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.not_to raise_error + end + end + + context 'and the droplet docker execution metadata sets the 0 user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"0"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'creates a diego TaskActionBuilder' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.not_to raise_error + end + end + + context 'and the droplet docker execution metadata does not set a user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"]}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'creates a diego TaskActionBuilder' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.not_to raise_error + end + end + end + end + + context 'when root user IS NOT allowed' do + let(:app) { AppModel.make(:docker, { droplet: }) } + + before do + TestConfig.override(allow_docker_root_user: false, additional_allowed_process_users: %w[root 0]) + end + + context 'and the process does not set a user' do + let(:process) { ProcessModel.make(:docker, { app: }) } + + context 'and the droplet docker execution metadata sets the root user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"root"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'raises an error' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run process as root user, which is not permitted/) + end + end + + context 'and the droplet docker execution metadata sets the 0 user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"0"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'raises an error' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run process as root user, which is not permitted/) + end + end + + context 'and the droplet docker execution metadata does not set a user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"]}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'raises an error' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run process as root user, which is not permitted/) + end + end + end + + context 'and the process sets the root user' do + let(:process) { ProcessModel.make(:docker, { app: app, user: 'root' }) } + + it 'raises an error' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run process as root user, which is not permitted/) + end + end + + context 'and the process sets the 0 user' do + let(:process) { ProcessModel.make(:docker, { app: app, user: 0 }) } + + it 'raises an error' do + expect do + lifecycle_protocol.desired_lrp_builder(config, process) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run process as root user, which is not permitted/) + end + end + end end describe '#task_action_builder' do @@ -138,6 +294,135 @@ module Docker ) lifecycle_protocol.task_action_builder(config, task) end + + context 'when root user is allowed' do + before do + TestConfig.override(allow_docker_root_user: true, additional_allowed_process_users: %w[root 0]) + end + + context 'and the task does not set a user' do + let(:app) { AppModel.make(:docker, { droplet: }) } + let(:task) { TaskModel.make(:docker, { droplet:, app: }) } + + context 'and the droplet docker execution metadata sets the root user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"root"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'creates a diego TaskActionBuilder' do + expect do + lifecycle_protocol.task_action_builder(config, task) + end.not_to raise_error + end + end + + context 'and the droplet docker execution metadata sets the 0 user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"0"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'creates a diego TaskActionBuilder' do + expect do + lifecycle_protocol.task_action_builder(config, task) + end.not_to raise_error + end + end + + context 'and the droplet docker execution metadata does not set a user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"]}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'creates a diego TaskActionBuilder' do + expect do + lifecycle_protocol.task_action_builder(config, task) + end.not_to raise_error + end + end + end + end + + context 'when root user IS NOT allowed' do + before do + TestConfig.override(allow_docker_root_user: false, additional_allowed_process_users: %w[root 0]) + end + + context 'and the task does not set a user' do + let(:app) { AppModel.make(:docker, { droplet: }) } + let(:task) { TaskModel.make(:docker, { droplet:, app: }) } + + context 'and the droplet docker execution metadata sets the root user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"root"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'raises an error' do + expect do + lifecycle_protocol.task_action_builder(config, task) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run task as root user, which is not permitted/) + end + end + + context 'and the droplet docker execution metadata sets the 0 user' do + let(:droplet_execution_metadata) { '{"entrypoint":["/image-entrypoint.sh"],"user":"0"}' } + let(:droplet) do + DropletModel.make(:docker, { + state: DropletModel::STAGED_STATE, + docker_receipt_image: 'the-image', + execution_metadata: droplet_execution_metadata + }) + end + + it 'raises an error' do + expect do + lifecycle_protocol.task_action_builder(config, task) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run task as root user, which is not permitted/) + end + end + end + + context 'and the task sets the root user' do + let(:app) { AppModel.make(:docker, { droplet: }) } + let(:task) { TaskModel.make(:docker, { droplet: droplet, app: app, user: 'root' }) } + + it 'raises an error' do + expect do + lifecycle_protocol.task_action_builder(config, task) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run task as root user, which is not permitted/) + end + end + + context 'and the task sets the 0 user' do + let(:app) { AppModel.make(:docker, { droplet: }) } + let(:task) { TaskModel.make(:docker, { droplet: droplet, app: app, user: '0' }) } + + it 'raises an error' do + expect do + lifecycle_protocol.task_action_builder(config, task) + end.to raise_error(::CloudController::Errors::ApiError, /Attempting to run task as root user, which is not permitted/) + end + end + end end end end