From 0b69c078acacfb07aeb232d15534238060d2516e Mon Sep 17 00:00:00 2001 From: Philipp Thun Date: Fri, 6 Mar 2026 14:29:21 +0100 Subject: [PATCH] Add support for configurable SSH algorithms in diego-sshd Add configuration schema and implementation to allow operators to configure SSH algorithms (ciphers, host key algorithms, key exchanges, and MACs) for diego-sshd running in app containers. The diego.sshd configuration accepts comma-separated strings for each algorithm type. When configured, these values are passed as command-line flags to diego-sshd (using -allowedCiphers, -allowedHostKeyAlgorithms, -allowedKeyExchanges, -allowedMACs). Empty strings result in no flags being passed, allowing diego-sshd to use its defaults. Both the sshd section and individual algorithm options are optional in all schemas (api, worker, clock, and deployment_updater), allowing operators to configure only the algorithms they need to restrict. --- .../config_schemas/api_schema.rb | 8 +- .../config_schemas/clock_schema.rb | 8 +- .../deployment_updater_schema.rb | 8 +- .../config_schemas/worker_schema.rb | 8 +- .../diego/main_lrp_action_builder.rb | 27 ++++-- .../diego/main_lrp_action_builder_spec.rb | 93 +++++++++++++++++++ 6 files changed, 141 insertions(+), 11 deletions(-) diff --git a/lib/cloud_controller/config_schemas/api_schema.rb b/lib/cloud_controller/config_schemas/api_schema.rb index 26cc02896e..c9b5db365f 100644 --- a/lib/cloud_controller/config_schemas/api_schema.rb +++ b/lib/cloud_controller/config_schemas/api_schema.rb @@ -160,7 +160,13 @@ class ApiSchema < VCAP::Config insecure_docker_registry_list: [String], docker_staging_stack: String, optional(:temporary_oci_buildpack_mode) => enum('oci-phase-1', NilClass), - enable_declarative_asset_downloads: bool + enable_declarative_asset_downloads: bool, + optional(:sshd) => { + optional(:allowed_ciphers) => String, + optional(:allowed_host_key_algorithms) => String, + optional(:allowed_key_exchanges) => String, + optional(:allowed_macs) => String + } }, app_log_revision: bool, diff --git a/lib/cloud_controller/config_schemas/clock_schema.rb b/lib/cloud_controller/config_schemas/clock_schema.rb index 975d832d7d..a822b57ca3 100644 --- a/lib/cloud_controller/config_schemas/clock_schema.rb +++ b/lib/cloud_controller/config_schemas/clock_schema.rb @@ -96,7 +96,13 @@ class ClockSchema < VCAP::Config use_privileged_containers_for_running: bool, use_privileged_containers_for_staging: bool, optional(:temporary_oci_buildpack_mode) => enum('oci-phase-1', NilClass), - enable_declarative_asset_downloads: bool + enable_declarative_asset_downloads: bool, + optional(:sshd) => { + optional(:allowed_ciphers) => String, + optional(:allowed_host_key_algorithms) => String, + optional(:allowed_key_exchanges) => String, + optional(:allowed_macs) => String + } }, app_log_revision: bool, diff --git a/lib/cloud_controller/config_schemas/deployment_updater_schema.rb b/lib/cloud_controller/config_schemas/deployment_updater_schema.rb index 51b30dae78..aed8877c8c 100644 --- a/lib/cloud_controller/config_schemas/deployment_updater_schema.rb +++ b/lib/cloud_controller/config_schemas/deployment_updater_schema.rb @@ -107,7 +107,13 @@ class DeploymentUpdaterSchema < VCAP::Config use_privileged_containers_for_running: bool, use_privileged_containers_for_staging: bool, optional(:temporary_oci_buildpack_mode) => enum('oci-phase-1', NilClass), - enable_declarative_asset_downloads: bool + enable_declarative_asset_downloads: bool, + optional(:sshd) => { + optional(:allowed_ciphers) => String, + optional(:allowed_host_key_algorithms) => String, + optional(:allowed_key_exchanges) => String, + optional(:allowed_macs) => String + } }, app_log_revision: bool, diff --git a/lib/cloud_controller/config_schemas/worker_schema.rb b/lib/cloud_controller/config_schemas/worker_schema.rb index 054fccf4c3..4970d23863 100644 --- a/lib/cloud_controller/config_schemas/worker_schema.rb +++ b/lib/cloud_controller/config_schemas/worker_schema.rb @@ -88,7 +88,13 @@ class WorkerSchema < VCAP::Config use_privileged_containers_for_running: bool, use_privileged_containers_for_staging: bool, optional(:temporary_oci_buildpack_mode) => enum('oci-phase-1', NilClass), - enable_declarative_asset_downloads: bool + enable_declarative_asset_downloads: bool, + optional(:sshd) => { + optional(:allowed_ciphers) => String, + optional(:allowed_host_key_algorithms) => String, + optional(:allowed_key_exchanges) => String, + optional(:allowed_macs) => String + } }, app_log_revision: bool, diff --git a/lib/cloud_controller/diego/main_lrp_action_builder.rb b/lib/cloud_controller/diego/main_lrp_action_builder.rb index 95a4a8122d..d7cb92ff85 100644 --- a/lib/cloud_controller/diego/main_lrp_action_builder.rb +++ b/lib/cloud_controller/diego/main_lrp_action_builder.rb @@ -100,21 +100,34 @@ def generate_sidecar_actions(user) end def generate_ssh_action(user, environment_variables) + sshd_args = [ + "-address=#{sprintf('0.0.0.0:%d', port: DEFAULT_SSH_PORT)}", + "-hostKey=#{ssh_key.private_key}", + "-authorizedKey=#{ssh_key.authorized_key}", + '-inheritDaemonEnv', + '-logLevel=fatal' + ] + add_allowed_ssh_algorithms(sshd_args) + action(::Diego::Bbs::Models::RunAction.new( user: user, path: '/tmp/lifecycle/diego-sshd', - args: [ - "-address=#{sprintf('0.0.0.0:%d', port: DEFAULT_SSH_PORT)}", - "-hostKey=#{ssh_key.private_key}", - "-authorizedKey=#{ssh_key.authorized_key}", - '-inheritDaemonEnv', - '-logLevel=fatal' - ], + args: sshd_args, env: environment_variables, resource_limits: ::Diego::Bbs::Models::ResourceLimits.new(nofile: process.file_descriptors), log_source: SSHD_LOG_SOURCE )) end + + def add_allowed_ssh_algorithms(args) + sshd_config = VCAP::CloudController::Config.config.get(:diego, :sshd) + return if sshd_config.nil? + + args << "-allowedCiphers=#{sshd_config[:allowed_ciphers]}" if sshd_config[:allowed_ciphers].present? + args << "-allowedHostKeyAlgorithms=#{sshd_config[:allowed_host_key_algorithms]}" if sshd_config[:allowed_host_key_algorithms].present? + args << "-allowedKeyExchanges=#{sshd_config[:allowed_key_exchanges]}" if sshd_config[:allowed_key_exchanges].present? + args << "-allowedMACs=#{sshd_config[:allowed_macs]}" if sshd_config[:allowed_macs].present? + end end end end diff --git a/spec/unit/lib/cloud_controller/diego/main_lrp_action_builder_spec.rb b/spec/unit/lib/cloud_controller/diego/main_lrp_action_builder_spec.rb index f47e2995b9..cc8340438a 100644 --- a/spec/unit/lib/cloud_controller/diego/main_lrp_action_builder_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/main_lrp_action_builder_spec.rb @@ -247,6 +247,99 @@ module Diego ) ) end + + context 'when SSH algorithm configuration is provided' do + before do + TestConfig.override( + diego: { + sshd: { + allowed_ciphers: 'cipher-1,cipher-2', + allowed_host_key_algorithms: 'hostkeyalg-1,hostkeyalg-2', + allowed_key_exchanges: 'kex-1,kex-2', + allowed_macs: 'mac-1,mac-2' + } + } + ) + end + + it 'includes SSH algorithm flags in the diego-sshd arguments' do + actions = MainLRPActionBuilder.build(process, lrp_builder, ssh_key).codependent_action.actions.map(&:run_action) + ssh_action = actions.find { |action| action.path == '/tmp/lifecycle/diego-sshd' } + + expect(ssh_action.args).to include('-allowedCiphers=cipher-1,cipher-2') + expect(ssh_action.args).to include('-allowedHostKeyAlgorithms=hostkeyalg-1,hostkeyalg-2') + expect(ssh_action.args).to include('-allowedKeyExchanges=kex-1,kex-2') + expect(ssh_action.args).to include('-allowedMACs=mac-1,mac-2') + end + end + + context 'when SSH algorithm configuration has empty strings' do + before do + TestConfig.override( + diego: { + sshd: { + allowed_ciphers: '', + allowed_host_key_algorithms: '', + allowed_key_exchanges: '', + allowed_macs: '' + } + } + ) + end + + it 'does not include SSH algorithm flags in the arguments' do + actions = MainLRPActionBuilder.build(process, lrp_builder, ssh_key).codependent_action.actions.map(&:run_action) + ssh_action = actions.find { |action| action.path == '/tmp/lifecycle/diego-sshd' } + + expect(ssh_action.args).not_to include(match(/-allowedCiphers=/)) + expect(ssh_action.args).not_to include(match(/-allowedHostKeyAlgorithms=/)) + expect(ssh_action.args).not_to include(match(/-allowedKeyExchanges=/)) + expect(ssh_action.args).not_to include(match(/-allowedMACs=/)) + end + end + + context 'when SSH algorithm configuration is partially provided' do + before do + TestConfig.override( + diego: { + sshd: { + allowed_ciphers: 'cipher-1', + allowed_host_key_algorithms: '', + } + } + ) + end + + it 'includes only the configured algorithm flags' do + actions = MainLRPActionBuilder.build(process, lrp_builder, ssh_key).codependent_action.actions.map(&:run_action) + ssh_action = actions.find { |action| action.path == '/tmp/lifecycle/diego-sshd' } + + expect(ssh_action.args).to include('-allowedCiphers=cipher-1') + expect(ssh_action.args).not_to include(match(/-allowedHostKeyAlgorithms=/)) + expect(ssh_action.args).not_to include(match(/-allowedKeyExchanges=/)) + expect(ssh_action.args).not_to include(match(/-allowedMACs=/)) + end + end + + context 'when diego sshd configuration is missing' do + before do + TestConfig.override(diego: {}) + end + + it 'does not raise an error and excludes algorithm flags' do + expect do + MainLRPActionBuilder.build(process, lrp_builder, ssh_key) + end.not_to raise_error + + actions = MainLRPActionBuilder.build(process, lrp_builder, ssh_key).codependent_action.actions.map(&:run_action) + ssh_action = actions.find { |action| action.path == '/tmp/lifecycle/diego-sshd' } + + expect(ssh_action.args).not_to include(match(/-allowedCiphers=/)) + expect(ssh_action.args).not_to include(match(/-allowedHostKeyAlgorithms=/)) + expect(ssh_action.args).not_to include(match(/-allowedKeyExchanges=/)) + expect(ssh_action.args).not_to include(match(/-allowedMACs=/)) + end + end end describe 'VCAP_PLATFORM_OPTIONS' do