Skip to content

Commit

Permalink
Authn-k8s:websockets: api endpoints with path
Browse files Browse the repository at this point in the history
The Kubernetes API endpoint can include a path, as is the case with the Rancher API, the websocket implementation should take the URL as is instead of taking only host and port.
The commit originally included support for server name indication (SNI) on the the websocket client, which enables the client to be specific about the host it is trying to reach in the first step of the TLS handshake, preventing common name mismatch errors.
However, that commit was removed in favour of the more comprehensive work to support SNI at #2432.
  • Loading branch information
doodlesbykumbi authored Feb 17, 2022
1 parent 89454a8 commit ac946be
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 22 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 22 additions & 16 deletions app/domain/authentication/authn_k8s/execute_command_in_container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/domain/authentication/authn_k8s/web_socket_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit ac946be

Please sign in to comment.