diff --git a/README.md b/README.md index cf0d18e..bff5532 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,15 @@ docker = DockerEngineRuby::Client.new( ) ``` +By default, peer verification is enabled. You can disable it explicitly: + +```ruby +docker = DockerEngineRuby::Client.new( + base_url: "https://localhost:2376", + tls_verify_peer: false +) +``` + You can also configure these through environment variables: - `DOCKER_TLS_CA_CERT_PATH` diff --git a/lib/docker_engine_ruby/client.rb b/lib/docker_engine_ruby/client.rb index 967de63..1623d77 100644 --- a/lib/docker_engine_ruby/client.rb +++ b/lib/docker_engine_ruby/client.rb @@ -15,6 +15,9 @@ class Client < DockerEngineRuby::Internal::Transport::BaseClient # Default max retry delay in seconds. DEFAULT_MAX_RETRY_DELAY = 8.0 + # Whether to verify server TLS certificate chain. + DEFAULT_TLS_VERIFY_PEER = true + # rubocop:disable Style/MutableConstant # @type [Hash{Symbol=>String}] ENVIRONMENTS = {production: "http://localhost:2375", production_tls: "https://localhost:2376"} @@ -106,6 +109,8 @@ class Client < DockerEngineRuby::Internal::Transport::BaseClient # @param initial_retry_delay [Float] # # @param max_retry_delay [Float] + # + # @param tls_verify_peer [Boolean] Whether to verify server TLS certificate chain. def initialize( tls_ca_cert_path: ENV["DOCKER_TLS_CA_CERT_PATH"], tls_client_cert_path: ENV["DOCKER_TLS_CLIENT_CERT_PATH"], @@ -115,10 +120,12 @@ def initialize( max_retries: self.class::DEFAULT_MAX_RETRIES, timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS, initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY, - max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY + max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY, + tls_verify_peer: self.class::DEFAULT_TLS_VERIFY_PEER ) base_url ||= DockerEngineRuby::Client::ENVIRONMENTS.fetch(environment&.to_sym || :production) do - message = "environment must be one of #{DockerEngineRuby::Client::ENVIRONMENTS.keys}, got #{environment}" + message = "environment must be one of " \ + "#{DockerEngineRuby::Client::ENVIRONMENTS.keys}, got #{environment}" raise ArgumentError.new(message) end @@ -132,6 +139,7 @@ def initialize( max_retries: max_retries, initial_retry_delay: initial_retry_delay, max_retry_delay: max_retry_delay, + tls_verify_peer: tls_verify_peer, tls_ca_cert_path: @tls_ca_cert_path, tls_client_cert_path: @tls_client_cert_path, tls_client_key_path: @tls_client_key_path diff --git a/lib/docker_engine_ruby/internal/transport/base_client.rb b/lib/docker_engine_ruby/internal/transport/base_client.rb index 40ffd09..d683727 100644 --- a/lib/docker_engine_ruby/internal/transport/base_client.rb +++ b/lib/docker_engine_ruby/internal/transport/base_client.rb @@ -197,6 +197,7 @@ def reap_connection!(status, stream:) # @param max_retry_delay [Float] # @param headers [Hash{String=>String, Integer, Array, nil}] # @param idempotency_header [String, nil] + # @param tls_verify_peer [Boolean] # @param tls_ca_cert_path [String, nil] # @param tls_client_cert_path [String, nil] # @param tls_client_key_path [String, nil] @@ -208,6 +209,7 @@ def initialize( max_retry_delay: 0.0, headers: {}, idempotency_header: nil, + tls_verify_peer: true, tls_ca_cert_path: nil, tls_client_cert_path: nil, tls_client_key_path: nil @@ -219,6 +221,7 @@ def initialize( end @requester = DockerEngineRuby::Internal::Transport::PooledNetRequester.new( unix_socket_path: @unix_socket_path, + tls_verify_peer: tls_verify_peer, tls_ca_cert_path: tls_ca_cert_path, tls_client_cert_path: tls_client_cert_path, tls_client_key_path: tls_client_key_path diff --git a/lib/docker_engine_ruby/internal/transport/pooled_net_requester.rb b/lib/docker_engine_ruby/internal/transport/pooled_net_requester.rb index 2e56f3b..784f50a 100644 --- a/lib/docker_engine_ruby/internal/transport/pooled_net_requester.rb +++ b/lib/docker_engine_ruby/internal/transport/pooled_net_requester.rb @@ -37,10 +37,11 @@ class << self # @param tls_cert [OpenSSL::X509::Certificate, nil] # @param tls_key [OpenSSL::PKey::PKey, nil] # @param unix_socket_path [String, nil] + # @param tls_verify_peer [Boolean] # @param url [URI::Generic] # # @return [Net::HTTP] - def connect(cert_store:, tls_cert:, tls_key:, unix_socket_path:, url:) + def connect(cert_store:, tls_cert:, tls_key:, unix_socket_path:, tls_verify_peer:, url:) if unix_socket_path return UnixSocketHTTP.new(unix_socket_path).tap do _1.use_ssl = false @@ -65,6 +66,7 @@ def connect(cert_store:, tls_cert:, tls_key:, unix_socket_path:, url:) _1.cert_store = cert_store _1.cert = tls_cert if tls_cert _1.key = tls_key if tls_key + _1.verify_mode = tls_verify_peer ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE end end end @@ -141,6 +143,7 @@ def build_request(request, &blk) tls_cert: @tls_cert, tls_key: @tls_key, unix_socket_path: unix_socket_path, + tls_verify_peer: @tls_verify_peer, url: url ) end @@ -236,9 +239,11 @@ def execute(request) # @param tls_client_cert_path [String, nil] # @param tls_client_key_path [String, nil] # @param unix_socket_path [String, nil] + # @param tls_verify_peer [Boolean] def initialize( size: self.class::DEFAULT_MAX_CONNECTIONS, unix_socket_path: nil, + tls_verify_peer: true, tls_ca_cert_path: nil, tls_client_cert_path: nil, tls_client_key_path: nil @@ -246,12 +251,15 @@ def initialize( @mutex = Mutex.new @size = size @default_unix_socket_path = unix_socket_path + @tls_verify_peer = tls_verify_peer @cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths) @cert_store.add_file(tls_ca_cert_path) if tls_ca_cert_path if tls_client_cert_path || tls_client_key_path if tls_client_cert_path.nil? || tls_client_key_path.nil? - raise ArgumentError.new("Both tls_client_cert_path and tls_client_key_path must be provided together.") + raise ArgumentError.new( + "Both tls_client_cert_path and tls_client_key_path must be provided together." + ) end @tls_cert = OpenSSL::X509::Certificate.new(File.read(tls_client_cert_path)) diff --git a/rbi/docker_engine_ruby/client.rbi b/rbi/docker_engine_ruby/client.rbi index 1b11887..0193725 100644 --- a/rbi/docker_engine_ruby/client.rbi +++ b/rbi/docker_engine_ruby/client.rbi @@ -10,6 +10,8 @@ module DockerEngineRuby DEFAULT_MAX_RETRY_DELAY = T.let(8.0, Float) + DEFAULT_TLS_VERIFY_PEER = T.let(T.unsafe(nil), T::Boolean) + ENVIRONMENTS = T.let( { @@ -88,7 +90,8 @@ module DockerEngineRuby max_retries: Integer, timeout: Float, initial_retry_delay: Float, - max_retry_delay: Float + max_retry_delay: Float, + tls_verify_peer: T::Boolean ).returns(T.attached_class) end def self.new( @@ -115,7 +118,8 @@ module DockerEngineRuby max_retries: DockerEngineRuby::Client::DEFAULT_MAX_RETRIES, timeout: DockerEngineRuby::Client::DEFAULT_TIMEOUT_IN_SECONDS, initial_retry_delay: DockerEngineRuby::Client::DEFAULT_INITIAL_RETRY_DELAY, - max_retry_delay: DockerEngineRuby::Client::DEFAULT_MAX_RETRY_DELAY + max_retry_delay: DockerEngineRuby::Client::DEFAULT_MAX_RETRY_DELAY, + tls_verify_peer: DockerEngineRuby::Client::DEFAULT_TLS_VERIFY_PEER ) end end diff --git a/rbi/docker_engine_ruby/internal/transport/base_client.rbi b/rbi/docker_engine_ruby/internal/transport/base_client.rbi index 90f2210..4c17f53 100644 --- a/rbi/docker_engine_ruby/internal/transport/base_client.rbi +++ b/rbi/docker_engine_ruby/internal/transport/base_client.rbi @@ -166,6 +166,7 @@ module DockerEngineRuby ) ], idempotency_header: T.nilable(String), + tls_verify_peer: T::Boolean, tls_ca_cert_path: T.nilable(String), tls_client_cert_path: T.nilable(String), tls_client_key_path: T.nilable(String) @@ -179,6 +180,7 @@ module DockerEngineRuby max_retry_delay: 0.0, headers: {}, idempotency_header: nil, + tls_verify_peer: true, tls_ca_cert_path: nil, tls_client_cert_path: nil, tls_client_key_path: nil diff --git a/rbi/docker_engine_ruby/internal/transport/pooled_net_requester.rbi b/rbi/docker_engine_ruby/internal/transport/pooled_net_requester.rbi index ebe6815..bf8d352 100644 --- a/rbi/docker_engine_ruby/internal/transport/pooled_net_requester.rbi +++ b/rbi/docker_engine_ruby/internal/transport/pooled_net_requester.rbi @@ -31,12 +31,13 @@ module DockerEngineRuby cert_store: OpenSSL::X509::Store, tls_cert: T.nilable(OpenSSL::X509::Certificate), tls_key: T.nilable(OpenSSL::PKey::PKey), + tls_verify_peer: T::Boolean, url: URI::Generic ).returns( Net::HTTP ) end - def connect(cert_store:, tls_cert:, tls_key:, url:) + def connect(cert_store:, tls_cert:, tls_key:, tls_verify_peer:, url:) end # @api private @@ -81,6 +82,7 @@ module DockerEngineRuby sig do params( size: Integer, + tls_verify_peer: T::Boolean, tls_ca_cert_path: T.nilable(String), tls_client_cert_path: T.nilable(String), tls_client_key_path: T.nilable(String) @@ -88,6 +90,7 @@ module DockerEngineRuby end def self.new( size: DockerEngineRuby::Internal::Transport::PooledNetRequester::DEFAULT_MAX_CONNECTIONS, + tls_verify_peer: true, tls_ca_cert_path: nil, tls_client_cert_path: nil, tls_client_key_path: nil diff --git a/sig/docker_engine_ruby/client.rbs b/sig/docker_engine_ruby/client.rbs index def98db..e65bbbb 100644 --- a/sig/docker_engine_ruby/client.rbs +++ b/sig/docker_engine_ruby/client.rbs @@ -8,6 +8,8 @@ module DockerEngineRuby DEFAULT_MAX_RETRY_DELAY: Float + DEFAULT_TLS_VERIFY_PEER: bool + ENVIRONMENTS: { production: "http://localhost:2375", production_tls: "https://localhost:2376" @@ -58,7 +60,8 @@ module DockerEngineRuby ?max_retries: Integer, ?timeout: Float, ?initial_retry_delay: Float, - ?max_retry_delay: Float + ?max_retry_delay: Float, + ?tls_verify_peer: bool ) -> void end end diff --git a/sig/docker_engine_ruby/internal/transport/base_client.rbs b/sig/docker_engine_ruby/internal/transport/base_client.rbs index 8a03fa6..424f6a8 100644 --- a/sig/docker_engine_ruby/internal/transport/base_client.rbs +++ b/sig/docker_engine_ruby/internal/transport/base_client.rbs @@ -83,6 +83,7 @@ module DockerEngineRuby | Integer | ::Array[(String | Integer)?])?], ?idempotency_header: String?, + ?tls_verify_peer: bool, ?tls_ca_cert_path: String?, ?tls_client_cert_path: String?, ?tls_client_key_path: String? diff --git a/sig/docker_engine_ruby/internal/transport/pooled_net_requester.rbs b/sig/docker_engine_ruby/internal/transport/pooled_net_requester.rbs index 1fc042a..f911376 100644 --- a/sig/docker_engine_ruby/internal/transport/pooled_net_requester.rbs +++ b/sig/docker_engine_ruby/internal/transport/pooled_net_requester.rbs @@ -21,6 +21,7 @@ module DockerEngineRuby cert_store: OpenSSL::X509::Store, tls_cert: OpenSSL::X509::Certificate?, tls_key: OpenSSL::PKey::PKey?, + tls_verify_peer: bool, url: URI::Generic ) -> top @@ -45,6 +46,7 @@ module DockerEngineRuby def initialize: ( ?size: Integer, + ?tls_verify_peer: bool, ?tls_ca_cert_path: String?, ?tls_client_cert_path: String?, ?tls_client_key_path: String? diff --git a/test/docker_engine_ruby/client_test.rb b/test/docker_engine_ruby/client_test.rb index ef77793..fbab27c 100644 --- a/test/docker_engine_ruby/client_test.rb +++ b/test/docker_engine_ruby/client_test.rb @@ -35,6 +35,18 @@ def test_raises_on_unknown_environment assert_match(/environment must be one of/, e.message) end + def test_client_enables_tls_verify_peer_by_default + docker = DockerEngineRuby::Client.new(base_url: "http://localhost") + + assert_equal(true, docker.requester.instance_variable_get(:@tls_verify_peer)) + end + + def test_client_accepts_tls_verify_peer_option + docker = DockerEngineRuby::Client.new(base_url: "http://localhost", tls_verify_peer: false) + + assert_equal(false, docker.requester.instance_variable_get(:@tls_verify_peer)) + end + def test_client_accepts_tls_paths with_tls_files do |ca_path:, cert_path:, key_path:| docker = @@ -54,7 +66,10 @@ def test_client_accepts_tls_paths def test_client_raises_when_only_one_client_tls_file_is_provided e = assert_raises(ArgumentError) do - DockerEngineRuby::Client.new(base_url: "https://localhost:2376", tls_client_cert_path: "/tmp/cert.pem") + DockerEngineRuby::Client.new( + base_url: "https://localhost:2376", + tls_client_cert_path: "/tmp/cert.pem" + ) end assert_match(/must be provided together/, e.message)