diff --git a/CHANGELOG.md b/CHANGELOG.md index af9d13da49..e5cf6a1b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - IAM Authn bug fix - Take rexml gem to production configuration [#2493](https://github.com/cyberark/conjur/pull/2493) +- Use entirety of configured Kubernetes endpoint URL in Kubernetes authenticator's + web socket client, instead of only host and port + [#2479](https://github.com/cyberark/conjur/pull/2479) ### Security - Updated Rails to 6.1.4.4 to resolve CVE-2021-44528 (Medium, Not Vulnerable) diff --git a/app/domain/authentication/authn_k8s/execute_command_in_container.rb b/app/domain/authentication/authn_k8s/execute_command_in_container.rb index b517c64a07..6228a226cc 100644 --- a/app/domain/authentication/authn_k8s/execute_command_in_container.rb +++ b/app/domain/authentication/authn_k8s/execute_command_in_container.rb @@ -44,7 +44,7 @@ def init_ws_client def ws_client @ws_client ||= @websocket_client.connect( - server_url, + ws_exec_url, { headers: headers, verify_mode: OpenSSL::SSL::VERIFY_PEER, @@ -54,7 +54,7 @@ def ws_client end def ws_client_event_handler - @close_event_queue = Queue.new + @close_event_queue = Queue.new @ws_client_event_handler ||= @ws_client_event_handler_class.new( close_event_queue: @close_event_queue, ws_client: ws_client, @@ -72,14 +72,14 @@ def add_websocket_event_handlers # the curly brackets it will look for such a member in the websocket_client # class ws_client_event_handler = @ws_client_event_handler - main_thread_tags = @logger.formatter.current_tags - logger = @logger + main_thread_tags = @logger.formatter.current_tags + logger = @logger ws_client.on(:open) do # Add log tags (origin, thread id, etc.) to sub-thread as they are not # passed automatically. We append the sub-thread id to the main one so # we can easily know the flow from the logs and connect between the threads - tid = syscall(186) + tid = syscall(186) sub_thread_tags = main_thread_tags.map do |x| x.start_with?("tid=") ? "#{x}=>#{tid}" : x end @@ -112,19 +112,25 @@ def verify_error_stream_is_empty ) end - def server_url - api_uri = kube_client.api_endpoint - base_url = "wss://#{api_uri.host}:#{api_uri.port}" - path = "/api/v1/namespaces/#{@pod_namespace}/pods/#{@pod_name}/exec" + def ws_exec_url + api_uri = kube_client.api_endpoint.clone # contains /api path prefix + api_uri.scheme = "wss" - base_query_string_parts = %W[container=#{CGI.escape(@container)} stderr=true stdout=true] - stdin_part = @stdin ? ['stdin=true'] : [] - cmds_part = @cmds.map { |cmd| "command=#{CGI.escape(cmd)}" } - query_string = ( - base_query_string_parts + stdin_part + cmds_part - ).join("&") + # append pod exec path + api_uri.path += "/v1/namespaces/#{@pod_namespace}/pods/#{@pod_name}/exec" - "#{base_url}#{path}?#{query_string}" + # populate query params + api_uri.query = ws_exec_query_params(String(api_uri.query)) + + api_uri.to_s + end + + def ws_exec_query_params query + base_query_string_parts = [["container", @container], ["stderr", "true"], ["stdout", "true"]] + stdin_part = @stdin ? [['stdin', 'true']] : [] + cmds_part = @cmds.map { |cmd| ["command", cmd] } + query_ar = URI.decode_www_form(query) + base_query_string_parts + stdin_part + cmds_part + URI.encode_www_form(query_ar) end def headers diff --git a/app/domain/authentication/authn_k8s/web_socket_client.rb b/app/domain/authentication/authn_k8s/web_socket_client.rb index 29468e4a5d..17b39ea9c7 100644 --- a/app/domain/authentication/authn_k8s/web_socket_client.rb +++ b/app/domain/authentication/authn_k8s/web_socket_client.rb @@ -37,6 +37,7 @@ def connect(url, options = {}) ctx.cert_store = cert_store @socket = ::OpenSSL::SSL::SSLSocket.new(@socket, ctx) + # support SNI, see https://www.cloudflare.com/en-gb/learning/ssl/what-is-sni/ if ssl_version != 'SSLv23' @socket.hostname = options[:hostname] || uri.host end diff --git a/spec/app/domain/authentication/authn_k8s/execute_command_in_container_spec.rb b/spec/app/domain/authentication/authn_k8s/execute_command_in_container_spec.rb index 999e91568f..5cbe32bc43 100644 --- a/spec/app/domain/authentication/authn_k8s/execute_command_in_container_spec.rb +++ b/spec/app/domain/authentication/authn_k8s/execute_command_in_container_spec.rb @@ -89,12 +89,7 @@ def trigger_error(err) end let(:kube_client_api_endpoint) do - double('kube_client_api_endpoint').tap do |kube_client_api_endpoint| - allow(kube_client_api_endpoint).to receive(:host) - .and_return("host") - allow(kube_client_api_endpoint).to receive(:port) - .and_return("port") - end + URI.parse("https://path/to/api/endpoint") end let(:kube_client) do @@ -169,6 +164,65 @@ def subject_in_thread(ws_client:, timeout:, body:, stdin:) end context "Calling ExecuteCommandInContainer" do + context "converts endpoint for websocket client" do + let(:kube_client_api_endpoint) do + raise "@kube_client_api_endpoint not defined" unless @kube_client_api_endpoint + + URI.parse(@kube_client_api_endpoint) + end + + let(:ws_client) do + ws_client = WsClientMock.new(handshake_error: nil) + + thread = subject_in_thread( + ws_client: ws_client, + timeout: 1, + body: body, + stdin: true + ) + + ws_client.trigger_open + ws_client.trigger_message(message) + ws_client.trigger_close + thread.join + thread[:output] + + ws_client + end + + it "retains subpath" do + @kube_client_api_endpoint = "https://path/to" + + expect(ws_client.connect_args[0]).to eq( + "wss://path/to/v1/namespaces/PodNamespace/pods/PodName/exec?container=Container&stderr=true&stdout=true&stdin=true&command=command1&command=command2" + ) + end + + it "retains port" do + @kube_client_api_endpoint = "https://path/to:5432" + + expect(ws_client.connect_args[0]).to eq( + "wss://path/to:5432/v1/namespaces/PodNamespace/pods/PodName/exec?container=Container&stderr=true&stdout=true&stdin=true&command=command1&command=command2" + ) + end + + it "retains query params" do + @kube_client_api_endpoint = "https://path/to?meow=moo" + + expect(ws_client.connect_args[0]).to eq( + "wss://path/to/v1/namespaces/PodNamespace/pods/PodName/exec?meow=moo&container=Container&stderr=true&stdout=true&stdin=true&command=command1&command=command2" + ) + end + + it "retains everything" do + @kube_client_api_endpoint = "https://path/to:5342?meow=moo" + + expect(ws_client.connect_args[0]).to eq( + "wss://path/to:5342/v1/namespaces/PodNamespace/pods/PodName/exec?meow=moo&container=Container&stderr=true&stdout=true&stdin=true&command=command1&command=command2" + ) + end + end + context "when the ws_client has no handshake error" do context "with stdin" do ws_client = WsClientMock.new(handshake_error: nil)