diff --git a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs index 09f42bc78a2..c1f17721b0d 100644 --- a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Threading; #if DOTNETCORE @@ -143,6 +145,12 @@ private static void DefaultRequestDataCreated(RequestData response) { } private Action _onRequestDataCreated = DefaultRequestDataCreated; Action IConnectionConfigurationValues.OnRequestDataCreated => _onRequestDataCreated; + private Func _serverCertificateValidationCallback; + Func IConnectionConfigurationValues.ServerCertificateValidationCallback => _serverCertificateValidationCallback; + + private X509CertificateCollection _clientCertificates; + X509CertificateCollection IConnectionConfigurationValues.ClientCertificates => _clientCertificates; + /// /// The default predicate for implementations that return true for /// in which case master only nodes are excluded from API calls. @@ -412,6 +420,35 @@ public T EnableDebugMode(Action onRequestCompleted = null) return (T)this; } + /// + /// Register a ServerCertificateValidationCallback, this is called per endpoint until it returns true. + /// After this callback returns true that endpoint is validated for the lifetime of the ServiceEndpoint + /// for that host. + /// + public T ServerCertificateValidationCallback(Func callback) => + Assign(a => a._serverCertificateValidationCallback = callback); + + /// + /// Use the following certificates to authenticate all HTTP requests. You can also set them on individual + /// request using + /// + public T ClientCertificates(X509CertificateCollection certificates) => + Assign(a => a._clientCertificates = certificates); + + /// + /// Use the following certificate to authenticate all HTTP requests. You can also set them on individual + /// request using + /// + public T ClientCertificate(X509Certificate certificate) => + Assign(a => a._clientCertificates = new X509Certificate2Collection { certificate }); + + /// + /// Use the following certificate to authenticate all HTTP requests. You can also set them on individual + /// request using + /// + public T ClientCertificate(string certificatePath) => + Assign(a => a._clientCertificates = new X509Certificate2Collection { new X509Certificate(certificatePath) }); + void IDisposable.Dispose() => this.DisposeManagedResources(); protected virtual void DisposeManagedResources() diff --git a/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs b/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs index 13d9afbb639..9322de98ba3 100644 --- a/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs +++ b/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Specialized; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Threading; namespace Elasticsearch.Net @@ -172,11 +174,22 @@ public interface IConnectionConfigurationValues : IDisposable /// TimeSpan? KeepAliveInterval { get; } + /// + /// Register a ServerCertificateValidationCallback per request + /// + Func ServerCertificateValidationCallback { get; } + /// /// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this predicate and always execute on all nodes. /// When using an implementation that supports reseeding of nodes, this will default to omitting master only node from regular API calls. /// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken verbatim. /// Func NodePredicate { get; } + + /// + /// Use the following certificates to authenticate all HTTP requests. You can also set them on individual + /// request using + /// + X509CertificateCollection ClientCertificates { get; } } } diff --git a/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs b/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs index 62d848d996c..b589784e29d 100644 --- a/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Threading; namespace Elasticsearch.Net @@ -74,6 +76,11 @@ public interface IRequestConfiguration ///
https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
 		/// 
 		string RunAs { get; set; }
+
+		/// 
+		/// Use the following client certificates to authenticate this single request
+		/// 
+		X509CertificateCollection ClientCertificates { get; set; }
 	}
 
 	public class RequestConfiguration : IRequestConfiguration
@@ -96,6 +103,8 @@ public class RequestConfiguration : IRequestConfiguration
 		/// https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
 		/// 
 		public string RunAs { get; set; }
+
+		public X509CertificateCollection ClientCertificates { get; set; }
 	}
 
 	public class RequestConfigurationDescriptor : IRequestConfiguration
@@ -115,6 +124,7 @@ public class RequestConfigurationDescriptor : IRequestConfiguration
 		BasicAuthenticationCredentials IRequestConfiguration.BasicAuthenticationCredentials { get; set; }
 		bool IRequestConfiguration.EnableHttpPipelining { get; set; } = true;
 		string IRequestConfiguration.RunAs { get; set; }
+		X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; }
 
 		public RequestConfigurationDescriptor(IRequestConfiguration config)
 		{
@@ -131,6 +141,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
 			Self.BasicAuthenticationCredentials = config?.BasicAuthenticationCredentials;
 			Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true;
 			Self.RunAs = config?.RunAs;
+			Self.ClientCertificates = config?.ClientCertificates;
 		}
 
 		/// 
@@ -202,6 +213,7 @@ public RequestConfigurationDescriptor ForceNode(Uri uri)
 			Self.ForceNode = uri;
 			return this;
 		}
+
 		public RequestConfigurationDescriptor MaxRetries(int retry)
 		{
 			Self.MaxRetries = retry;
@@ -222,5 +234,20 @@ public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true)
 			Self.EnableHttpPipelining = enable;
 			return this;
 		}
+
+		///  Use the following client certificates to authenticate this request to Elasticsearch 
+		public RequestConfigurationDescriptor ClientCertificates(X509CertificateCollection certificates)
+		{
+			Self.ClientCertificates = certificates;
+			return this;
+		}
+
+		///  Use the following client certificate to authenticate this request to Elasticsearch 
+		public RequestConfigurationDescriptor ClientCertificate(X509Certificate certificate) =>
+			this.ClientCertificates(new X509Certificate2Collection { certificate });
+
+		///  Use the following client certificate to authenticate this request to Elasticsearch 
+		public RequestConfigurationDescriptor ClientCertificate(string certificatePath) =>
+			this.ClientCertificates(new X509Certificate2Collection {new X509Certificate(certificatePath)});
 	}
 }
diff --git a/src/Elasticsearch.Net/Connection/CertificateValidations.cs b/src/Elasticsearch.Net/Connection/CertificateValidations.cs
new file mode 100644
index 00000000000..0a6038dcbd3
--- /dev/null
+++ b/src/Elasticsearch.Net/Connection/CertificateValidations.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Concurrent;
+using System.Net;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Elasticsearch.Net
+{
+	/// 
+	/// A collection of handy baked in server certificate validation callbacks
+	/// 
+	public static class CertificateValidations
+	{
+		/// 
+		/// DANGEROUS, never use this in production validates ALL certificates to true.
+		/// 
+		/// Always true, allowing ALL certificates
+		public static bool AllowAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => true;
+
+		/// 
+		/// Always false, in effect blocking ALL certificates
+		/// 
+		/// Always false, always blocking ALL certificates
+		public static bool DenyAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => false;
+
+		/// 
+		/// Helper to create a certificate validation callback based on the certificate authority certificate that we used to
+		/// generate the nodes certificates with. This callback expects the CA to be part of the chain as intermediate CA.
+		/// 
+		/// The ca certificate used to generate the nodes certificate 
+		/// Custom CA are never trusted by default unless they are in the machines trusted store, set this to true
+		/// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted.
+		/// 
+		/// By default we do not check revocation, it is however recommended to check this (either offline or online).
+		public static Func AuthorityPartOfChain(
+			X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck) =>
+			(sender, cert, chain, errors) =>
+				errors == SslPolicyErrors.None
+				|| ValidIntermediateCa(caCertificate, cert, chain, trustRoot, revocationMode);
+
+		/// 
+		/// Helper to create a certificate validation callback based on the certificate authority certificate that we used to
+		/// generate the nodes certificates with. This callback does NOT expect the CA to be part of the chain presented by the server.
+		/// Including the root certificate in the chain increases the SSL handshake size and Elasticsearch's certgen by default does not include
+		/// the CA in the certificate chain.
+		/// 
+		/// The ca certificate used to generate the nodes certificate 
+		/// Custom CA are never trusted by default unless they are in the machines trusted store, set this to true
+		/// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted.
+		/// 
+		/// By default we do not check revocation, it is however recommended to check this (either offline or online).
+		public static Func AuthorityIsRoot(
+			X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck) =>
+			(sender, cert, chain, errors) =>
+				errors == SslPolicyErrors.None
+				|| ValidRootCa(caCertificate, cert, chain, trustRoot, revocationMode);
+
+		private static X509Certificate2 to2(X509Certificate certificate)
+		{
+			#if DOTNETCORE
+				return new X509Certificate2(certificate.Export(X509ContentType.Cert));
+			#else
+				return new X509Certificate2(certificate);
+			#endif
+		}
+
+		private static bool ValidRootCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot,
+			X509RevocationMode revocationMode)
+		{
+			var ca = to2(caCertificate);
+			var privateChain = new X509Chain {ChainPolicy = {RevocationMode = revocationMode}};
+			privateChain.ChainPolicy.ExtraStore.Add(ca);
+			privateChain.Build(to2(certificate));
+
+			//lets validate the our chain status
+			foreach (var chainStatus in privateChain.ChainStatus)
+			{
+				//custom CA's that are not in the machine trusted store will always have this status
+				//by setting trustRoot = true (default) we skip this error
+				if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue;
+				//trustRoot is false so we expected our CA to be in the machines trusted store
+				if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false;
+				//otherwise if the chain has any error of any sort return false
+				if (chainStatus.Status != X509ChainStatusFlags.NoError) return false;
+			}
+			return true;
+
+		}
+
+		private static bool ValidIntermediateCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot,
+			X509RevocationMode revocationMode)
+		{
+			var ca = to2(caCertificate);
+			var privateChain = new X509Chain {ChainPolicy = {RevocationMode = revocationMode}};
+			privateChain.ChainPolicy.ExtraStore.Add(ca);
+			privateChain.Build(to2(certificate));
+
+			//Assert our chain has the same number of elements as the certifcate presented by the server
+			if (chain.ChainElements.Count != privateChain.ChainElements.Count) return false;
+
+			//lets validate the our chain status
+			foreach (var chainStatus in privateChain.ChainStatus)
+			{
+				//custom CA's that are not in the machine trusted store will always have this status
+				//by setting trustRoot = true (default) we skip this error
+				if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue;
+				//trustRoot is false so we expected our CA to be in the machines trusted store
+				if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false;
+				//otherwise if the chain has any error of any sort return false
+				if (chainStatus.Status != X509ChainStatusFlags.NoError) return false;
+			}
+
+			var found = false;
+			//We are going to walk both chains and make sure the thumbprints align
+			//while making sure one of the chains certificates presented by the server has our expected CA thumbprint
+			for (var i = 0; i < chain.ChainElements.Count; i++)
+			{
+				var c = chain.ChainElements[i].Certificate.Thumbprint;
+				var cPrivate = privateChain.ChainElements[i].Certificate.Thumbprint;
+				if (c == ca.Thumbprint) found = true;
+
+				//mis aligned certificate chain, return false so we do not accept this certificate
+				if (c != cPrivate) return false;
+				i++;
+			}
+			return found;
+		}
+	}
+}
diff --git a/src/Elasticsearch.Net/Connection/ClientCertificate.cs b/src/Elasticsearch.Net/Connection/ClientCertificate.cs
new file mode 100644
index 00000000000..18085ff3139
--- /dev/null
+++ b/src/Elasticsearch.Net/Connection/ClientCertificate.cs
@@ -0,0 +1,148 @@
+// this code contains a refactored version of DecodeRSAPrivateKey() found here http://www.jensign.com/opensslkey/opensslkey.cs
+// Its license permits redistribution, the license is included here for reference.
+
+/*
+//
+//OpenSSLKey
+// .NET 2.0  OpenSSL Public & Private Key Parser
+//
+/*
+Copyright (c) 2000  JavaScience Consulting,  Michel Gallant
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Elasticsearch.Net
+{
+//.NET removed the setter for PrivateKey for X509Certificate, you'll have to manually convert to pfx/p12 or add the key to the machine store
+#if !DOTNETCORE
+
+	public class ClientCertificate
+	{
+		//https://tls.mbed.org/kb/cryptography/asn1-key-structures-in-der-and-pem
+		private static RSACryptoServiceProvider DecodeRsaPrivateKey(byte[] privkey)
+		{
+			using (var mem = new MemoryStream(privkey))
+			using (var binr = new BinaryReader(mem))
+			{
+				var twobytes = binr.ReadUInt16();
+				switch (twobytes)
+				{
+					case 0x8130:
+						binr.ReadByte(); //advance 1 byte
+						break;
+					case 0x8230:
+						binr.ReadInt16(); //advance 2 bytes
+						break;
+					default:
+						return null;
+				}
+
+				twobytes = binr.ReadUInt16();
+				if (twobytes != 0x0102) return null; //version number
+				var bt = binr.ReadByte();
+				if (bt != 0x00) return null;
+
+				// We make sure the provider typeString is compatible with RSA
+				// ----
+				// https://msdn.microsoft.com/en-us/library/system.security.cryptography.cspparameters.providertype(v=vs.110).aspx
+				// https://msdn.microsoft.com/en-us/subscriptions/aa387431.aspx
+				// https://blogs.msdn.microsoft.com/alejacma/2009/04/30/default-provider-typeString-for-cspparameters-has-changed/
+
+				var serviceProvider = new RSACryptoServiceProvider(new CspParameters
+				{
+					Flags = CspProviderFlags.NoFlags,
+					KeyContainerName = Guid.NewGuid().ToString(),
+					ProviderType = 1
+				});
+				serviceProvider.ImportParameters(new RSAParameters
+				{
+					Modulus = ReadNext(binr),
+					Exponent = ReadNext(binr),
+					D = ReadNext(binr),
+					P = ReadNext(binr),
+					Q = ReadNext(binr),
+					DP = ReadNext(binr),
+					DQ = ReadNext(binr),
+					InverseQ = ReadNext(binr)
+				});
+				return serviceProvider;
+			}
+		}
+
+		private static byte[] ReadNext(BinaryReader br) => br.ReadBytes(GetSizeOfIntegerToReadNext(br));
+
+		private static int GetSizeOfIntegerToReadNext(BinaryReader br)
+		{
+			var bt = br.ReadByte();
+			if (bt != 0x02) return 0; //expect integer
+
+			var count = 0;
+			bt = br.ReadByte();
+			switch (bt)
+			{
+				case 0x81:
+					count = br.ReadByte(); // data size in next byte
+					break;
+				case 0x82:
+					var highbyte = br.ReadByte();
+					var lowbyte = br.ReadByte();
+					byte[] modint = {lowbyte, highbyte, 0x00, 0x00};
+					count = BitConverter.ToInt32(modint, 0);
+					break;
+				default:
+					count = bt; // we already have the data size
+					break;
+			}
+			while (br.ReadByte() == 0x00) //remove high order zeros in data
+				count -= 1;
+			br.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte
+			return count;
+		}
+
+		private static byte[] ReadBytesFromPemFile(string fileContents, string typeString)
+		{
+			var header = $"-----BEGIN {typeString}-----";
+			var footer = $"-----END {typeString}-----";
+			var start = fileContents.IndexOf(header, StringComparison.Ordinal) + header.Length;
+			var end = fileContents.IndexOf(footer, start, StringComparison.Ordinal) - start;
+			var base64Der = fileContents.Substring(start, end);
+			return Convert.FromBase64String(base64Der);
+		}
+
+		public static X509Certificate2 LoadWithPrivateKey(string publicCertificatePath, string privateKeyPath, string password)
+		{
+			var publicCert = File.ReadAllText(publicCertificatePath);
+			var privateKey = File.ReadAllText(privateKeyPath);
+			var certBuffer = ReadBytesFromPemFile(publicCert, "CERTIFICATE");
+			var keyBuffer = ReadBytesFromPemFile(privateKey, "RSA PRIVATE KEY");
+			var certificate = !string.IsNullOrEmpty(password) ? new X509Certificate2(certBuffer, password) : new X509Certificate2(certBuffer);
+			var prov = DecodeRsaPrivateKey(keyBuffer);
+			certificate.PrivateKey = prov;
+			return certificate;
+		}
+	}
+#endif
+}
diff --git a/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs b/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs
index a9011b0379b..b6766900526 100644
--- a/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs
+++ b/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs
@@ -110,7 +110,7 @@ public virtual async Task> RequestAsync(
 			return await builder.ToResponseAsync().ConfigureAwait(false);
 		}
 
-		private static string MissingConnectionLimitMethodError =
+		private static readonly string MissingConnectionLimitMethodError =
 			$"Your target platform does not support {nameof(ConnectionConfiguration.ConnectionLimit)}"
 			+ $" please set {nameof(ConnectionConfiguration.ConnectionLimit)} to -1 on your connection configuration."
 			+ $" this will cause the {nameof(HttpClientHandler.MaxConnectionsPerServer)} not to be set on {nameof(HttpClientHandler)}";
@@ -147,6 +147,17 @@ protected virtual HttpClientHandler CreateHttpClientHandler(RequestData requestD
 			if (requestData.DisableAutomaticProxyDetection)
 				handler.Proxy = null;
 
+			var callback = requestData?.ConnectionSettings?.ServerCertificateValidationCallback;
+			if (callback != null && handler.ServerCertificateCustomValidationCallback == null)
+				handler.ServerCertificateCustomValidationCallback = callback;
+
+
+			if (requestData.ClientCertificates != null)
+			{
+				handler.ClientCertificateOptions = ClientCertificateOption.Automatic;
+				handler.ClientCertificates.AddRange(requestData.ClientCertificates);
+			}
+
 			return handler;
 		}
 
diff --git a/src/Elasticsearch.Net/Connection/HttpConnection.cs b/src/Elasticsearch.Net/Connection/HttpConnection.cs
index 69ff9e5995f..1f0ca89c464 100644
--- a/src/Elasticsearch.Net/Connection/HttpConnection.cs
+++ b/src/Elasticsearch.Net/Connection/HttpConnection.cs
@@ -3,6 +3,7 @@
 using System.IO.Compression;
 using System.Linq;
 using System.Net;
+using System.Net.Security;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -13,16 +14,12 @@ namespace Elasticsearch.Net
 {
 	public class HttpConnection : IConnection
 	{
+		internal static bool IsMono { get; } = Type.GetType("Mono.Runtime") != null;
+
 		static HttpConnection()
 		{
-			//ServicePointManager.SetTcpKeepAlive(true, 2000, 2000);
-
-			//WebException's GetResponse is limitted to 65kb by default.
-			//Elasticsearch can be alot more chatty then that when dumping exceptions
-			//On error responses, so lets up the ante.
-
 			//Not available under mono
-			if (Type.GetType("Mono.Runtime") == null)
+			if (!IsMono)
 				HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
 		}
 
@@ -31,10 +28,31 @@ protected virtual HttpWebRequest CreateHttpWebRequest(RequestData requestData)
 			var request = this.CreateWebRequest(requestData);
 			this.SetBasicAuthenticationIfNeeded(request, requestData);
 			this.SetProxyIfNeeded(request, requestData);
+			this.SetServerCertificateValidationCallBackIfNeeded(request, requestData);
+			this.SetClientCertificates(request, requestData);
 			this.AlterServicePoint(request.ServicePoint, requestData);
 			return request;
 		}
 
+		protected virtual void SetClientCertificates(HttpWebRequest request, RequestData requestData)
+		{
+			if (requestData.ClientCertificates != null)
+				request.ClientCertificates.AddRange(requestData.ClientCertificates);
+		}
+
+		protected virtual void SetServerCertificateValidationCallBackIfNeeded(HttpWebRequest request, RequestData requestData)
+		{
+			var callback = requestData?.ConnectionSettings?.ServerCertificateValidationCallback;
+			#if !__MonoCS__
+			//Only assign if one is defined on connection settings and a subclass has not already set one
+			if (callback != null && request.ServerCertificateValidationCallback == null)
+				request.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(callback);
+			#else
+				if (callback != null)
+					throw new Exception("Mono misses ServerCertificateValidationCallback on HttpWebRequest");
+			#endif
+		}
+
 		protected virtual HttpWebRequest CreateWebRequest(RequestData requestData)
 		{
 			var request = (HttpWebRequest)WebRequest.Create(requestData.Uri);
@@ -83,6 +101,7 @@ protected virtual void AlterServicePoint(ServicePoint requestServicePoint, Reque
 			//this method only sets internal values and wont actually cause timers and such to be reset
 			//So it should be idempotent if called with the same parameters
 			requestServicePoint.SetTcpKeepAlive(true, requestData.KeepAliveTime, requestData.KeepAliveInterval);
+
 		}
 
 		protected virtual void SetProxyIfNeeded(HttpWebRequest request, RequestData requestData)
diff --git a/src/Elasticsearch.Net/Elasticsearch.Net.csproj b/src/Elasticsearch.Net/Elasticsearch.Net.csproj
index 05eecd78a27..1a41c1971fb 100644
--- a/src/Elasticsearch.Net/Elasticsearch.Net.csproj
+++ b/src/Elasticsearch.Net/Elasticsearch.Net.csproj
@@ -67,11 +67,13 @@
     
     
     
+    
     
     
     
     
     
+    
     
     
     
diff --git a/src/Elasticsearch.Net/Extensions/X509CertificateExtensions.cs b/src/Elasticsearch.Net/Extensions/X509CertificateExtensions.cs
new file mode 100644
index 00000000000..2a25c4c0d91
--- /dev/null
+++ b/src/Elasticsearch.Net/Extensions/X509CertificateExtensions.cs
@@ -0,0 +1,43 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace Elasticsearch.Net
+{
+	internal static class X509CertificateExtensions
+	{
+#if DOTNETCORE
+
+		// https://referencesource.microsoft.com/#mscorlib/system/security/cryptography/x509certificates/x509certificate.cs,318
+		internal static string GetCertHashString(this X509Certificate certificate)
+		{
+			var bytes = certificate.GetCertHash();
+			return EncodeHexString(bytes);
+
+		}
+		// https://referencesource.microsoft.com/#mscorlib/system/security/util/hex.cs,1bfe838f662feef3
+
+		// converts number to hex digit. Does not do any range checks.
+		private static char HexDigit(int num) {
+			return (char)((num < 10) ? (num + '0') : (num + ('A' - 10)));
+		}
+
+		private static string EncodeHexString(byte[] sArray)
+		{
+			string result = null;
+
+			if (sArray == null) return result;
+			var hexOrder = new char[sArray.Length * 2];
+
+			for(int i = 0, j = 0; i < sArray.Length; i++) {
+				var digit = (sArray[i] & 0xf0) >> 4;
+				hexOrder[j++] = HexDigit(digit);
+				digit = sArray[i] & 0x0f;
+				hexOrder[j++] = HexDigit(digit);
+			}
+			result = new string(hexOrder);
+			return result;
+		}
+
+#endif
+
+	}
+}
diff --git a/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs b/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs
index 3a80a0fccdb..0c7d427f1d1 100644
--- a/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs
+++ b/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs
@@ -3,6 +3,8 @@
 using System.Collections.Specialized;
 using System.IO;
 using System.Linq;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
 using Purify;
 
 namespace Elasticsearch.Net
@@ -45,6 +47,8 @@ public class RequestData
 		public IConnectionConfigurationValues ConnectionSettings { get; }
 		public IMemoryStreamFactory MemoryStreamFactory { get; }
 
+		public X509CertificateCollection ClientCertificates { get; set; }
+
 		public RequestData(HttpMethod method, string path, PostData data, IConnectionConfigurationValues global, IRequestParameters local, IMemoryStreamFactory memoryStreamFactory)
 			: this(method, path, data, global, local?.RequestConfiguration, memoryStreamFactory)
 		{
@@ -92,6 +96,7 @@ private RequestData(
 			this.DisableAutomaticProxyDetection = global.DisableAutomaticProxyDetection;
 			this.BasicAuthorizationCredentials = local?.BasicAuthenticationCredentials ?? global.BasicAuthenticationCredentials;
 			this.AllowedStatusCodes = local?.AllowedStatusCodes ?? Enumerable.Empty();
+			this.ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates;
 		}
 
 		private string CreatePathWithQueryStrings(string path, IConnectionConfigurationValues global, IRequestParameters request = null)
diff --git a/src/Tests/App.config b/src/Tests/App.config
new file mode 100644
index 00000000000..b6a695548bd
--- /dev/null
+++ b/src/Tests/App.config
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Tests/ClientConcepts/Certificates/SslAndKpiXPackCluster.cs b/src/Tests/ClientConcepts/Certificates/SslAndKpiXPackCluster.cs
new file mode 100644
index 00000000000..a1da8c81eef
--- /dev/null
+++ b/src/Tests/ClientConcepts/Certificates/SslAndKpiXPackCluster.cs
@@ -0,0 +1,111 @@
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using Nest;
+using Tests.Framework;
+using Tests.Framework.ManagedElasticsearch.Clusters;
+using Tests.Framework.ManagedElasticsearch.Nodes;
+using Tests.Framework.ManagedElasticsearch.Plugins;
+using Tests.Framework.ManagedElasticsearch.Tasks.InstallationTasks;
+
+namespace Tests.ClientConcepts.Certificates
+{
+	[IntegrationOnly, RequiresPlugin(ElasticsearchPlugin.XPack)]
+	public abstract class SslAndKpiXPackCluster : XPackCluster
+	{
+		public override bool EnableSsl { get; } = true;
+		/// 
+		/// Skipping bootstrap validation because they call out to elasticsearch and would force
+		/// The ServerCertificateValidationCallback to return true. Since i
+		/// 
+		public override bool SkipValidation { get; } = true;
+
+		protected override InstallationTaskBase[] AdditionalInstallationTasks => new [] { new EnableSslAndKpiOnCluster() };
+
+		protected override string[] AdditionalServerSettings => new []
+		{
+			$"xpack.ssl.key={this.Node.FileSystem.NodePrivateKey}",
+			$"xpack.ssl.certificate={this.Node.FileSystem.NodeCertificate}",
+			$"xpack.ssl.certificate_authorities={this.Node.FileSystem.CaCertificate}",
+			"xpack.security.transport.ssl.enabled=true",
+			"xpack.security.http.ssl.enabled=true",
+		};
+
+		private static int _port = 9200;
+		private int? _desiredPort;
+		public override int DesiredPort
+		{
+			get
+			{
+				if (!this._desiredPort.HasValue)
+					this._desiredPort = ++_port;
+				return this._desiredPort.Value;
+			}
+		}
+
+		public override ConnectionSettings ClusterConnectionSettings(ConnectionSettings s) =>
+			this.ConnectionSettings(Authenticate(s));
+
+		public virtual ConnectionSettings Authenticate(ConnectionSettings s) =>
+			s.BasicAuthentication("es_admin", "es_admin");
+
+		protected abstract ConnectionSettings ConnectionSettings(ConnectionSettings s);
+	}
+
+	public class EnableSslAndKpiOnCluster : InstallationTaskBase
+	{
+		public override void Run(NodeConfiguration config, NodeFileSystem fileSystem)
+		{
+			//due to a bug in certgen this file needs to live in two places
+			var silentModeConfigFile  = Path.Combine(fileSystem.ElasticsearchHome, "certgen") + ".yml";
+			var silentModeConfigFileDuplicate  = Path.Combine(fileSystem.ConfigPath, "x-pack", "certgen") + ".yml";
+			foreach(var file in new []{silentModeConfigFile, silentModeConfigFileDuplicate})
+                if (!File.Exists(file)) File.WriteAllLines(file, new []
+                {
+                    "instances:",
+                    $"    - name : \"{fileSystem.CertificateNodeName}\"",
+                    $"    - name : \"{fileSystem.ClientCertificateName}\"",
+                    $"      filename : \"{fileSystem.ClientCertificateFilename}\"",
+                });
+
+			this.GenerateCertificates(fileSystem, silentModeConfigFile);
+			this.GenerateUnusedCertificates(fileSystem, silentModeConfigFile);
+			this.AddClientCertificateUser(fileSystem);
+		}
+
+		private void AddClientCertificateUser(NodeFileSystem fileSystem)
+		{
+			var file = Path.Combine(fileSystem.ConfigPath, "x-pack", "role_mapping") + ".yml";
+			var name = fileSystem.ClientCertificateName;
+            if (!File.Exists(file) || !File.ReadAllLines(file).Any(f=>f.Contains(name))) File.WriteAllLines(file, new []
+            {
+             	"admin:",
+                $"    - \"{name}\""
+            });
+		}
+		private void GenerateCertificates(NodeFileSystem fileSystem, string silentModeConfigFile)
+		{
+			var name = fileSystem.CertificateFolderName;
+			if (!File.Exists(fileSystem.CaCertificate))
+				this.ExecuteBinary(fileSystem.CertGenBinary, "generating ssl certificates for this session",
+					"-in", silentModeConfigFile, "-out", $"{name}.zip");
+
+			if (Directory.Exists(fileSystem.CertificatesPath)) return;
+			Directory.CreateDirectory(fileSystem.CertificatesPath);
+			var zipLocation = Path.Combine(fileSystem.ConfigPath, "x-pack", name) + ".zip";
+			ZipFile.ExtractToDirectory(zipLocation, fileSystem.CertificatesPath);
+		}
+		private void GenerateUnusedCertificates(NodeFileSystem fileSystem, string silentModeConfigFile)
+		{
+			var name = fileSystem.UnusedCertificateFolderName;
+			if (!File.Exists(fileSystem.UnusedCaCertificate))
+				this.ExecuteBinary(fileSystem.CertGenBinary, "generating ssl certificates for this session",
+					"-in", silentModeConfigFile, "-out", $"{name}.zip");
+
+			if (Directory.Exists(fileSystem.UnusedCertificatesPath)) return;
+			Directory.CreateDirectory(fileSystem.UnusedCertificatesPath);
+			var zipLocation = Path.Combine(fileSystem.ConfigPath, "x-pack", name) + ".zip";
+			ZipFile.ExtractToDirectory(zipLocation, fileSystem.UnusedCertificatesPath);
+		}
+	}
+}
diff --git a/src/Tests/ClientConcepts/Certificates/WorkingWithCertificates.doc.cs b/src/Tests/ClientConcepts/Certificates/WorkingWithCertificates.doc.cs
new file mode 100644
index 00000000000..7f6f67612f7
--- /dev/null
+++ b/src/Tests/ClientConcepts/Certificates/WorkingWithCertificates.doc.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using Elasticsearch.Net;
+using FluentAssertions;
+using Nest;
+using Tests.Framework;
+using Tests.Framework.Integration;
+
+namespace Tests.ClientConcepts.Certificates
+{
+	/**== Working with certificates
+	 *
+	 * === Server Certificates
+	 *
+	 * If you've enabled SSL on elasticsearch with x-pack or through a proxy in front of elasticsearch and the Certificate Authority (CA)
+	 * That generated the certificate is trusted by the machine running the client code there should be nothing you'll have to do to to talk
+	 * to over https with the client. If you are using your own CA which is not trusted .NET won't allow you to make https calls to that endpoint.
+	 *
+	 * .NET allows you to preempt this though through a custom validation through the the global static `ServicePointManager.ServerCertificateValidationCallback`.
+	 * Most examples you will find on the .NET will simply return `true` from this delegate and call it quits. This is not advisable as this will allow any HTTPS
+	 * traffic in the current AppDomain and not run any validations. Imagine you deploy a web app that talks to Elasticsearch over HTTPS but also some third party
+	 * SOAP/WSDL endpoint setting `ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true;` will skip validation of BOTH
+	 * Elasticsearch and that external web service.
+
+	 * .NET also allows you to set that callback per service endpoint and Elasticsearch.NET/NEST exposes this through connection settings.
+	 * You can do your own validation in that handler or simply assign baked in handler that we ship with out of the box on the static
+	 * class `CertificateValidations`.
+	 *
+	 * The two most basic ones are `AllowAll` and `DenyAll` which does accept or deny any ssl trafic to our nodes`:
+	 *
+	 */
+	public class DenyAllCertificatesCluster : SslAndKpiXPackCluster
+	{
+		protected override ConnectionSettings ConnectionSettings(ConnectionSettings s) => s
+			.ServerCertificateValidationCallback((o, certificate, chain, errors) => false)
+			.ServerCertificateValidationCallback(CertificateValidations.DenyAll);
+	}
+	//hide
+	[IntegrationOnly]
+	public class DenyAllSslCertificatesApiTests : ConnectionErrorTestBase
+	{
+		public DenyAllSslCertificatesApiTests(DenyAllCertificatesCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+		[I] public async Task UsedHttps() => await AssertOnAllResponses(r => r.ApiCall.Uri.Scheme.Should().Be("https"));
+
+		protected override void AssertException(WebException e) =>
+			e.Message.Should().Contain("Could not establish trust relationship for the SSL/TLS secure channel.");
+
+		protected override void AssertException(HttpRequestException e) { }
+	}
+
+	public class AllowAllCertificatesCluster : SslAndKpiXPackCluster
+	{
+		protected override ConnectionSettings ConnectionSettings(ConnectionSettings s) => s
+			.ServerCertificateValidationCallback((o, certificate, chain, errors) => true)
+			.ServerCertificateValidationCallback(CertificateValidations.AllowAll);
+	}
+	//hide
+	[IntegrationOnly]
+	public class AllowAllSllCertificatesApiTests : CanConnectTestBase
+	{
+		public AllowAllSllCertificatesApiTests(AllowAllCertificatesCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+		[I] public async Task UsedHttps() => await AssertOnAllResponses(r => r.ApiCall.Uri.Scheme.Should().Be("https"));
+	}
+
+	/**
+	 * If your client application however has access to the public CA certificate locally Elasticsearch.NET/NEST ships with handy helpers that assert
+	 * that the certificate that the server presented was one that came from our local CA certificate. If you use x-pack's `certgen` tool to
+	 * [generate SSL certificates](https://www.elastic.co/guide/en/x-pack/current/ssl-tls.html) the generated node certificate does not include the CA in the
+	 * certificate chain. This to cut back on SSL handshake size. In those case you can use `CertificateValidations.AuthorityIsRoot` and pass it your local copy
+	 * of the CA public key to assert that the certificate the server presented was generated off that.
+	 */
+
+	public class CertgenCaCluster : SslAndKpiXPackCluster
+	{
+		protected override ConnectionSettings ConnectionSettings(ConnectionSettings s) => s
+			.ServerCertificateValidationCallback(
+				CertificateValidations.AuthorityIsRoot(new X509Certificate(this.Node.FileSystem.CaCertificate))
+			);
+	}
+
+	//hide
+	[IntegrationOnly]
+	public class CertgenCaApiTests : CanConnectTestBase
+	{
+		public CertgenCaApiTests(CertgenCaCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+		[I] public async Task UsedHttps() => await AssertOnAllResponses(r => r.ApiCall.Uri.Scheme.Should().Be("https"));
+	}
+
+	/**
+	 * If your local copy does not match the servers CA Elasticsearch.NET/NEST will fail to connect
+	 */
+	public class BadCertgenCaCluster : SslAndKpiXPackCluster
+	{
+		protected override ConnectionSettings ConnectionSettings(ConnectionSettings s) => s
+			.ServerCertificateValidationCallback(
+				CertificateValidations.AuthorityPartOfChain(new X509Certificate(this.Node.FileSystem.UnusedCaCertificate))
+			);
+	}
+
+	//hide
+	[IntegrationOnly]
+	public class BadCertgenCaApiTests : ConnectionErrorTestBase
+	{
+		public BadCertgenCaApiTests(BadCertgenCaCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+		[I] public async Task UsedHttps() => await AssertOnAllResponses(r => r.ApiCall.Uri.Scheme.Should().Be("https"));
+
+		protected override void AssertException(WebException e) =>
+			e.Message.Should().Contain("Could not establish trust relationship for the SSL/TLS secure channel.");
+
+		protected override void AssertException(HttpRequestException e) { }
+
+	}
+	/**
+	 * If you go for a vendor generated SSL certificate its common practice for them to include the CA and any intermediary CA's in the certificate chain
+	 * in those case use `CertificateValidations.AuthorityPartOfChain` which validates that the local CA certificate is part of that chain and was used to
+	 * generate the servers key.
+	 */
+
+#if !DOTNETCORE
+	/**
+	 * === Client Certificates
+	 * X-Pack also allows you to configure a [PKI realm](https://www.elastic.co/guide/en/x-pack/current/pki-realm.html) to enable user authentication
+	 * through client certificates. The `certgen` tool included with X-Pack allows you to
+	 * [generate client certificates as well](https://www.elastic.co/guide/en/x-pack/current/ssl-tls.html#CO13-4) and assign the distinguished name (DN) of the
+	 * certificate as a user with a certain role.
+	 *
+	 * certgen by default only generates a public certificate (`.cer`) and a private key `.key`. To authenticate with client certificates you need to present both
+	 * as one certificate. The easiest way to do this is to generate a `pfx` or `p12` file from the two and present that to `new X509Certificate(pathToPfx)`.
+
+	 * If you do not have a way to run `openssl` or `Pvk2Pfx` to do so as part of your deployments the clients ships with a handy helper to generate one
+	 * on the fly in code based of `.cer`  and `.key` files that `certgen` outputs. Sadly this is not available on .NET core because we can no longer set `PublicKey`
+	 * crypto service provider.
+
+	 * You can set Client Certificates to use on all connections on `ConnectionSettings`
+
+	 */
+	public class PkiCluster : CertgenCaCluster
+	{
+		public override ConnectionSettings Authenticate(ConnectionSettings s) => s
+			//.ClientCertificate(this.Node.FileSystem.ClientCertificate);
+			.ClientCertificate(
+				ClientCertificate.LoadWithPrivateKey(this.Node.FileSystem.ClientCertificate, this.Node.FileSystem.ClientPrivateKey, "")
+			);
+
+		protected override string[] AdditionalServerSettings => base.AdditionalServerSettings.Concat(new []
+		{
+			"xpack.security.authc.realms.file1.enabled=false",
+			"xpack.security.http.ssl.client_authentication=required"
+		}).ToArray();
+	}
+	//hide
+	[IntegrationOnly]
+	public class PkiApiTests : CanConnectTestBase
+	{
+		public PkiApiTests(PkiCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+		[I] public async Task UsedHttps() => await AssertOnAllResponses(r => r.ApiCall.Uri.Scheme.Should().Be("https"));
+	}
+
+	/**
+	 * Or per request on `RequestConfiguration` which will take precedence over the ones defined on `ConnectionConfiguration`
+	 */
+	public class BadPkiCluster : PkiCluster {}
+	[IntegrationOnly]
+	public class BadCustomCertificatePerRequestWinsApiTests : ConnectionErrorTestBase
+	{
+		public BadCustomCertificatePerRequestWinsApiTests(BadPkiCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+		[I] public async Task UsedHttps() => await AssertOnAllResponses(r => r.ApiCall.Uri.Scheme.Should().Be("https"));
+
+		private string BadCertificate => this.Cluster.Node.FileSystem.ClientCertificate;
+
+		protected override RootNodeInfoRequest Initializer => new RootNodeInfoRequest
+		{
+			RequestConfiguration = new RequestConfiguration
+			{
+				ClientCertificates = new X509Certificate2Collection { new X509Certificate2(this.BadCertificate) }
+			}
+		};
+
+		protected override Func Fluent => s => s
+			.RequestConfiguration(r => r
+				.ClientCertificate(this.BadCertificate)
+
+			);
+
+		protected override void AssertException(WebException e) =>
+			e.Message.Should().Contain("Could not create SSL/TLS secure channel");
+
+		protected override void AssertException(HttpRequestException e) { }
+
+	}
+#endif
+
+}
diff --git a/src/Tests/Framework/EndpointTests/ApiIntegrationAgainstNewIndexTestBase.cs b/src/Tests/Framework/EndpointTests/ApiIntegrationAgainstNewIndexTestBase.cs
new file mode 100644
index 00000000000..a91c41dc06f
--- /dev/null
+++ b/src/Tests/Framework/EndpointTests/ApiIntegrationAgainstNewIndexTestBase.cs
@@ -0,0 +1,29 @@
+using System.Linq;
+using Elasticsearch.Net;
+using Nest;
+using Tests.Framework.Integration;
+using Tests.Framework.ManagedElasticsearch.Clusters;
+
+namespace Tests.Framework
+{
+	public abstract class ApiIntegrationAgainstNewIndexTestBase
+		: ApiIntegrationTestBase
+		where TCluster : ClusterBase, new()
+		where TResponse : class, IResponse
+		where TDescriptor : class, TInterface
+		where TInitializer : class, TInterface
+		where TInterface : class
+	{
+		protected ApiIntegrationAgainstNewIndexTestBase(ClusterBase cluster, EndpointUsage usage) : base(cluster, usage) { }
+
+		protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values)
+		{
+			foreach (var index in values.Values) client.CreateIndex(index, this.CreateIndexSettings).ShouldBeValid();
+			var indices = Infer.Indices(values.Values.Select(i => (IndexName)i));
+			client.ClusterHealth(f => f.WaitForStatus(WaitForStatus.Yellow).Index(indices))
+				.ShouldBeValid();
+		}
+
+		protected virtual ICreateIndexRequest CreateIndexSettings(CreateIndexDescriptor create) => create;
+	}
+}
\ No newline at end of file
diff --git a/src/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs b/src/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs
index 7b6872fe628..a09b7554eea 100644
--- a/src/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs
+++ b/src/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs
@@ -1,9 +1,7 @@
 using System;
-using System.Linq;
 using System.Net;
 using System.Runtime.ExceptionServices;
 using System.Threading.Tasks;
-using Elasticsearch.Net;
 using FluentAssertions;
 using Nest;
 using Tests.Framework.Integration;
@@ -29,16 +27,13 @@ protected ApiIntegrationTestBase(ClusterBase cluster, EndpointUsage usage) : bas
 		protected override IElasticClient Client => this.Cluster.Client;
 		protected override TInitializer Initializer => Activator.CreateInstance();
 
-		[I]
-		protected async Task HandlesStatusCode() =>
+		[I] public async Task HandlesStatusCode() =>
 			await this.AssertOnAllResponses(r => r.ApiCall.HttpStatusCode.Should().Be(this.ExpectStatusCode));
 
-		[I]
-		protected async Task ReturnsExpectedIsValid() =>
+		[I] public async Task ReturnsExpectedIsValid() =>
 			await this.AssertOnAllResponses(r => r.ShouldHaveExpectedIsValid(this.ExpectIsValid));
 
-		[I]
-		protected async Task ReturnsExpectedResponse() => await this.AssertOnAllResponses(ExpectResponse);
+		[I] public async Task ReturnsExpectedResponse() => await this.AssertOnAllResponses(ExpectResponse);
 
 		protected override Task AssertOnAllResponses(Action assert)
 		{
@@ -66,25 +61,4 @@ private static bool IsNotRequestExceptionType(Type exceptionType)
 #endif
 		}
 	}
-
-	public abstract class ApiIntegrationAgainstNewIndexTestBase
-		: ApiIntegrationTestBase
-		where TCluster : ClusterBase, new()
-		where TResponse : class, IResponse
-		where TDescriptor : class, TInterface
-		where TInitializer : class, TInterface
-		where TInterface : class
-	{
-		protected ApiIntegrationAgainstNewIndexTestBase(ClusterBase cluster, EndpointUsage usage) : base(cluster, usage) { }
-
-		protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values)
-		{
-			foreach (var index in values.Values) client.CreateIndex(index, this.CreateIndexSettings).ShouldBeValid();
-			var indices = Infer.Indices(values.Values.Select(i => (IndexName)i));
-			client.ClusterHealth(f => f.WaitForStatus(WaitForStatus.Yellow).Index(indices))
-				.ShouldBeValid();
-		}
-
-		protected virtual ICreateIndexRequest CreateIndexSettings(CreateIndexDescriptor create) => create;
-	}
 }
diff --git a/src/Tests/Framework/EndpointTests/CanConnectTestBase.cs b/src/Tests/Framework/EndpointTests/CanConnectTestBase.cs
new file mode 100644
index 00000000000..01127dd4926
--- /dev/null
+++ b/src/Tests/Framework/EndpointTests/CanConnectTestBase.cs
@@ -0,0 +1,34 @@
+using Elasticsearch.Net;
+using FluentAssertions;
+using Nest;
+using Tests.Framework.Integration;
+using Tests.Framework.ManagedElasticsearch.Clusters;
+
+namespace Tests.Framework
+{
+	public abstract class CanConnectTestBase :
+		ApiIntegrationTestBase
+		where TCluster : ClusterBase, new()
+	{
+		protected CanConnectTestBase(TCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+		protected override LazyResponses ClientUsage() => Calls(
+			fluent: (client, f) => client.RootNodeInfo(),
+			fluentAsync: (client, f) => client.RootNodeInfoAsync(),
+			request: (client, r) => client.RootNodeInfo(r),
+			requestAsync: (client, r) => client.RootNodeInfoAsync(r)
+		);
+
+		protected override bool ExpectIsValid => true;
+		protected override int ExpectStatusCode => 200;
+		protected override HttpMethod HttpMethod => HttpMethod.GET;
+		protected override string UrlPath => "/";
+
+		protected override void ExpectResponse(IRootNodeInfoResponse response)
+		{
+			response.Version.Should().NotBeNull();
+			response.Version.LuceneVersion.Should().NotBeNullOrWhiteSpace();
+			response.Tagline.Should().NotBeNullOrWhiteSpace();
+			response.Name.Should().NotBeNullOrWhiteSpace();
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/Tests/Framework/EndpointTests/ConnectionErrorTestBase.cs b/src/Tests/Framework/EndpointTests/ConnectionErrorTestBase.cs
new file mode 100644
index 00000000000..c5aae575e57
--- /dev/null
+++ b/src/Tests/Framework/EndpointTests/ConnectionErrorTestBase.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Elasticsearch.Net;
+using FluentAssertions;
+using Nest;
+using Tests.Framework.Integration;
+using Tests.Framework.ManagedElasticsearch.Clusters;
+
+namespace Tests.Framework
+{
+	public abstract class ConnectionErrorTestBase
+		: ApiTestBase
+		where TCluster : ClusterBase, new()
+	{
+		protected ConnectionErrorTestBase(ClusterBase cluster, EndpointUsage usage) : base(cluster, usage) { }
+
+		protected override LazyResponses ClientUsage() => Calls(
+			fluent: (client, f) => client.RootNodeInfo(f),
+			fluentAsync: (client, f) => client.RootNodeInfoAsync(f),
+			request: (client, r) => client.RootNodeInfo(r),
+			requestAsync: (client, r) => client.RootNodeInfoAsync(r)
+		);
+
+		protected override IElasticClient Client => this.Cluster.Client;
+		protected override RootNodeInfoRequest Initializer => new RootNodeInfoRequest();
+
+		protected override string UrlPath => "";
+		protected override HttpMethod HttpMethod => HttpMethod.GET;
+
+		[I] public async Task IsValidIsFalse() => await this.AssertOnAllResponses(r => r.ShouldHaveExpectedIsValid(false));
+
+		[I] public async Task AssertException() => await this.AssertOnAllResponses(r =>
+		{
+			var e = r.OriginalException;
+			e.Should().NotBeNull();
+			if (e is WebException) this.AssertException((WebException) e);
+			else if (e is System.Net.Http.HttpRequestException)
+				this.AssertException((System.Net.Http.HttpRequestException) e);
+			else throw new Exception("Response orginal exception is not one of the expected connection exception but" + e.GetType().FullName);
+		});
+
+		protected abstract void AssertException(WebException e);
+		protected abstract void AssertException(System.Net.Http.HttpRequestException e);
+
+	}
+}
\ No newline at end of file
diff --git a/src/Tests/Framework/ManagedElasticsearch/Clusters/ClusterBase.cs b/src/Tests/Framework/ManagedElasticsearch/Clusters/ClusterBase.cs
index f6018c41c05..3ff0baf4256 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Clusters/ClusterBase.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Clusters/ClusterBase.cs
@@ -25,15 +25,26 @@ protected ClusterBase()
 		public virtual int MaxConcurrency => 0;
 		protected virtual string[] AdditionalServerSettings { get; } = { };
 		protected virtual InstallationTaskBase[] AdditionalInstallationTasks { get; } = { };
+
+		public virtual bool EnableSsl { get; }
+		public virtual bool SkipValidation { get; }
+
+		public virtual int DesiredPort { get; } = 9200;
+
+		public virtual ConnectionSettings ClusterConnectionSettings(ConnectionSettings s) => s;
+
 		protected virtual void SeedNode() { }
 
 		public void Start()
 		{
-			this.TaskRunner.Install();
+			this.TaskRunner.Install(this.AdditionalInstallationTasks);
 			var nodeSettings = this.NodeConfiguration.CreateSettings(this.AdditionalServerSettings);
 			this.TaskRunner.OnBeforeStart(nodeSettings);
 			this.Node.Start(nodeSettings);
-			this.TaskRunner.ValidateAfterStart(this.Node.Client);
+			if (!this.SkipValidation)
+				this.TaskRunner.ValidateAfterStart(this.Node.Client);
+			if (this.NodeConfiguration.RunIntegrationTests && this.Node.Port != this.DesiredPort)
+				throw new Exception($"The cluster that was started runs on {this.Node.Port} but this cluster wants {this.DesiredPort}");
 			this.SeedNode();
 		}
 
diff --git a/src/Tests/Framework/ManagedElasticsearch/Clusters/XPackCluster.cs b/src/Tests/Framework/ManagedElasticsearch/Clusters/XPackCluster.cs
index a8c46def75e..a32b6249660 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Clusters/XPackCluster.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Clusters/XPackCluster.cs
@@ -1,8 +1,13 @@
-using Tests.Framework.Integration;
+using Nest;
+using Tests.Framework.Integration;
 using Tests.Framework.ManagedElasticsearch.Plugins;
 
 namespace Tests.Framework.ManagedElasticsearch.Clusters
 {
 	[RequiresPlugin(ElasticsearchPlugin.XPack)]
-	public class XPackCluster : ClusterBase { }
+	public class XPackCluster : ClusterBase
+	{
+		public override ConnectionSettings ClusterConnectionSettings(ConnectionSettings s) =>
+			s.BasicAuthentication("es_admin", "es_admin");
+	}
 }
diff --git a/src/Tests/Framework/ManagedElasticsearch/Nodes/ElasticsearchNode.cs b/src/Tests/Framework/ManagedElasticsearch/Nodes/ElasticsearchNode.cs
index ab3c0507507..7ac0ea1a044 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Nodes/ElasticsearchNode.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Nodes/ElasticsearchNode.cs
@@ -57,7 +57,7 @@ public IElasticClient Client
 					if (this._client != null) return this._client;
 
 					var port = this.Started ? this.Port : 9200;
-					this._client = TestClient.GetClient(ComposeSettings, port);
+					this._client = TestClient.GetClient(ComposeSettings, port, forceSsl: this._config.EnableSsl);
 					return this.Client;
 				}
 			}
@@ -133,21 +133,22 @@ private void HandleConsoleMessage(ElasticsearchConsoleOut consoleOut, XplatManua
 		}
 
 		private ConnectionSettings ClusterSettings(ConnectionSettings s, Func settings) =>
-			AddBasicAuthentication(AppendClusterNameToHttpHeaders(settings(s)));
+			AddClusterSpecificConnectionSettings(AppendClusterNameToHttpHeaders(settings(s)));
 
 		private IElasticClient GetPrivateClient(Func settings, bool forceInMemory, int port)
 		{
 			settings = settings ?? (s => s);
 			var client = forceInMemory
 				? TestClient.GetInMemoryClient(s => ClusterSettings(s, settings), port)
-				: TestClient.GetClient(s => ClusterSettings(s, settings), port);
+				: TestClient.GetClient(s => ClusterSettings(s, settings), port, forceSsl: this._config.EnableSsl);
 			return client;
 		}
 
-		private ConnectionSettings ComposeSettings(ConnectionSettings s) => AddBasicAuthentication(AppendClusterNameToHttpHeaders(s));
+		private ConnectionSettings ComposeSettings(ConnectionSettings s) =>
+			AddClusterSpecificConnectionSettings(AppendClusterNameToHttpHeaders(s));
 
-		private ConnectionSettings AddBasicAuthentication(ConnectionSettings settings) =>
-			!this._config.XPackEnabled ? settings : settings.BasicAuthentication("es_admin", "es_admin");
+		private ConnectionSettings AddClusterSpecificConnectionSettings(ConnectionSettings settings) =>
+			this._config.ClusterConnectionSettings(settings);
 
 		private ConnectionSettings AppendClusterNameToHttpHeaders(ConnectionSettings settings)
 		{
diff --git a/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeConfiguration.cs b/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeConfiguration.cs
index 04bf58202ef..5524278ee04 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeConfiguration.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeConfiguration.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Nest;
 using Tests.Framework.Configuration;
 using Tests.Framework.ManagedElasticsearch.Clusters;
 using Tests.Framework.ManagedElasticsearch.Plugins;
@@ -20,10 +21,13 @@ public class NodeConfiguration : ITestConfiguration
 		public string ClusterFilter { get; }
 		public string TestFilter { get; }
 		public NodeFileSystem FileSystem { get; }
+		public int DesiredPort { get; }
 
 		public ElasticsearchPlugin[] RequiredPlugins { get; } = { };
 
 		public bool XPackEnabled => this.RequiredPlugins.Contains(ElasticsearchPlugin.XPack);
+		public bool EnableSsl { get; }
+		public ConnectionSettings ClusterConnectionSettings(ConnectionSettings s) => _cluster.ClusterConnectionSettings(s);
 
 		private readonly string _uniqueSuffix = Guid.NewGuid().ToString("N").Substring(0, 6);
 		public string ClusterMoniker => this._cluster.GetType().Name.Replace("Cluster", "").ToLowerInvariant();
@@ -35,6 +39,7 @@ public class NodeConfiguration : ITestConfiguration
 		public NodeConfiguration(ITestConfiguration configuration, ClusterBase cluster)
 		{
 			this._cluster = cluster;
+			this.EnableSsl = cluster.SkipValidation;
 
 			this.RequiredPlugins = ClusterRequiredPlugins(cluster);
 			this.Mode = configuration.Mode;
@@ -48,23 +53,28 @@ public NodeConfiguration(ITestConfiguration configuration, ClusterBase cluster)
 			this.ClusterFilter = configuration.ClusterFilter;
 			this.TestFilter = configuration.TestFilter;
 			this.FileSystem = new NodeFileSystem(configuration.ElasticsearchVersion, this.ClusterName, this.NodeName);
+			this.DesiredPort = cluster.DesiredPort;
 
 			var attr = v.Major >= 5 ? "attr." : "";
 			var indexedOrStored = v > new ElasticsearchVersion("5.0.0-alpha1") ? "stored" : "indexed";
 			var shieldOrSecurity = v > new ElasticsearchVersion("5.0.0-alpha1") ? "xpack.security" : "shield";
 			var es = v > new ElasticsearchVersion("5.0.0-alpha2") ? "" : "es.";
 			var b = this.XPackEnabled.ToString().ToLowerInvariant();
+			var sslEnabled = this.EnableSsl.ToString().ToLowerInvariant();
 			this.DefaultNodeSettings = new List
 			{
 				$"{es}cluster.name={this.ClusterName}",
 				$"{es}node.name={this.NodeName}",
 				$"{es}path.repo={this.FileSystem.RepositoryPath}",
 				$"{es}path.data={this.FileSystem.DataPath}",
+				$"{es}http.port={this.DesiredPort}",
 				$"{es}script.inline=true",
 				$"{es}script.max_compilations_per_minute=10000",
 				$"{es}script.{indexedOrStored}=true",
 				$"{es}node.{attr}testingcluster=true",
-				$"{es}{shieldOrSecurity}.enabled={b}"
+				$"{es}{shieldOrSecurity}.enabled={b}",
+				$"{es}{shieldOrSecurity}.http.ssl.enabled={sslEnabled}",
+				$"{es}{shieldOrSecurity}.authc.realms.pki1.enabled={sslEnabled}",
 			};
 		}
 
diff --git a/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeFileSystem.cs b/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeFileSystem.cs
index 1b8404efcff..86e2fc5c8d3 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeFileSystem.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Nodes/NodeFileSystem.cs
@@ -26,6 +26,26 @@ public class NodeFileSystem
 		public string DownloadZipLocation => Path.Combine(this.RoamingFolder, this._version.Zip);
 		public string TaskRunnerFile => Path.Combine(this.RoamingFolder, "taskrunner.log");
 
+
+		//certificates
+		public string CertGenBinary => Path.Combine(this.ElasticsearchHome, "bin", "x-pack", "certgen") + ".bat";
+
+		public string CertificateFolderName => "node-certificates";
+		public string CertificateNodeName => "node01";
+		public string ClientCertificateName => "cn=John Doe,ou=example,o=com";
+		public string ClientCertificateFilename => "john_doe";
+		public string CertificatesPath => Path.Combine(this.ConfigPath, this.CertificateFolderName);
+		public string CaCertificate => Path.Combine(this.CertificatesPath, "ca", "ca") + ".crt";
+		public string NodePrivateKey => Path.Combine(this.CertificatesPath, this.CertificateNodeName, this.CertificateNodeName) + ".key";
+		public string NodeCertificate => Path.Combine(this.CertificatesPath, this.CertificateNodeName, this.CertificateNodeName) + ".crt";
+		public string ClientCertificate => Path.Combine(this.CertificatesPath, this.ClientCertificateFilename, this.ClientCertificateFilename) + ".crt";
+		public string ClientPrivateKey => Path.Combine(this.CertificatesPath, this.ClientCertificateFilename, this.ClientCertificateFilename) + ".key";
+
+		public string UnusedCertificateFolderName => $"unused-{CertificateFolderName}";
+		public string UnusedCertificatesPath => Path.Combine(this.ConfigPath, this.UnusedCertificateFolderName);
+		public string UnusedCaCertificate => Path.Combine(this.UnusedCertificatesPath, "ca", "ca") + ".crt";
+		public string UnusedClientCertificate => Path.Combine(this.UnusedCertificatesPath, this.ClientCertificateFilename, this.ClientCertificateFilename) + ".crt";
+
 		public NodeFileSystem(ElasticsearchVersion version, string clusterName, string nodeName)
 		{
 			this._version = version;
diff --git a/src/Tests/Framework/ManagedElasticsearch/Tasks/InstallationTasks/EnsureSecurityRealms.cs b/src/Tests/Framework/ManagedElasticsearch/Tasks/InstallationTasks/EnsureSecurityRealms.cs
new file mode 100644
index 00000000000..7358ff5a2b2
--- /dev/null
+++ b/src/Tests/Framework/ManagedElasticsearch/Tasks/InstallationTasks/EnsureSecurityRealms.cs
@@ -0,0 +1,56 @@
+using System.IO;
+using System.Linq;
+using Tests.Framework.Configuration;
+using Tests.Framework.Integration;
+using Tests.Framework.ManagedElasticsearch.Nodes;
+
+namespace Tests.Framework.ManagedElasticsearch.Tasks.InstallationTasks
+{
+	public class EnsureSecurityRealms : InstallationTaskBase
+	{
+		public override void Run(NodeConfiguration config, NodeFileSystem fileSystem)
+		{
+			var configFile = Path.Combine(fileSystem.ElasticsearchHome, "config", "elasticsearch.yml");
+			var lines = File.ReadAllLines(configFile).ToList();
+			var saveFile = false;
+
+			// set up for Watcher HipChat action
+			if (!lines.Any(line => line.Contains("file1")))
+			{
+				lines.AddRange(new[]
+				{
+					string.Empty,
+					"xpack:",
+					"  security:",
+					"    authc:",
+					"      realms:",
+					"        file1:",
+					"          type: file",
+					"          order: 0",
+					string.Empty
+				});
+				saveFile = true;
+			}
+
+			// set up for Watcher HipChat action
+			if (!lines.Any(line => line.Contains("pki1")))
+			{
+				lines.AddRange(new[]
+				{
+					string.Empty,
+					"xpack:",
+					"  security:",
+					"    authc:",
+					"      realms:",
+					"        pki1:",
+					"          type: pki",
+					"          order: 1",
+					string.Empty
+				});
+				saveFile = true;
+			}
+
+			if (saveFile) File.WriteAllLines(configFile, lines);
+		}
+	}
+}
diff --git a/src/Tests/Framework/ManagedElasticsearch/Tasks/NodeTaskRunner.cs b/src/Tests/Framework/ManagedElasticsearch/Tasks/NodeTaskRunner.cs
index 4399ab7b6ba..d5c3a4d7d87 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Tasks/NodeTaskRunner.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Tasks/NodeTaskRunner.cs
@@ -35,6 +35,7 @@ public NodeTaskRunner(NodeConfiguration nodeConfiguration)
 			new EnsureSecurityRolesFileExists(),
 			new EnsureWatcherActionConfigurationInElasticsearchYaml(),
 			new EnsureSecurityRolesFileExists(),
+			new EnsureSecurityRealms(),
 			new EnsureSecurityUsersInDefaultRealmAreAdded(),
 		};
 		private static IEnumerable BeforeStart { get; } = new List
@@ -54,8 +55,11 @@ public NodeTaskRunner(NodeConfiguration nodeConfiguration)
 			new ValidateClusterStateTask()
 		};
 
-		public void Install()=>
-			Itterate(InstallationTasks, (t, n,  fs) => t.Run(n, fs));
+		public void Install(InstallationTaskBase[] additionalInstallationTasks)=>
+			Itterate(
+				InstallationTasks.Concat(additionalInstallationTasks ?? Enumerable.Empty()),
+				(t, n,  fs) => t.Run(n, fs)
+			);
 
 		public void Dispose() =>
 			Itterate(NodeStoppedTasks, (t, n,  fs) => t.Run(n, fs));
diff --git a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateClusterStateTask.cs b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateClusterStateTask.cs
index 17764a5221e..80446e50ebe 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateClusterStateTask.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateClusterStateTask.cs
@@ -21,4 +21,4 @@ public override void Validate(IElasticClient client, NodeConfiguration configura
 				throw new Exception($"Did not see a healhty cluster before calling onNodeStarted handler." + healthyCluster.DebugInformation);
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateLicenseTask.cs b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateLicenseTask.cs
index 581d1ca3d93..ea9878abf94 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateLicenseTask.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateLicenseTask.cs
@@ -1,4 +1,5 @@
 using System;
+using Elasticsearch.Net;
 using Nest;
 using Tests.Framework.Configuration;
 using Tests.Framework.Integration;
@@ -11,6 +12,7 @@ public class ValidateLicenseTask : NodeValidationTaskBase
 		public override void Validate(IElasticClient client, NodeConfiguration configuration)
 		{
 			if (!configuration.XPackEnabled) return;
+
 			var license = client.GetLicense();
 			if (license.IsValid && license.License.Status == LicenseStatus.Active) return;
 
@@ -22,7 +24,10 @@ public override void Validate(IElasticClient client, NodeConfiguration configura
 #endif
 			if (!string.IsNullOrWhiteSpace(licenseFile))
 			{
-				var putLicense = client.PostLicense(new PostLicenseRequest {License = License.LoadFromDisk(licenseFile)});
+				var putLicense = client.PostLicense(new PostLicenseRequest
+				{
+					License = License.LoadFromDisk(licenseFile)
+				});
 				if (!putLicense.IsValid)
 					throw new Exception("Server has invalid license and the ES_LICENSE_FILE failed to register\r\n" + putLicense.DebugInformation);
 
@@ -51,4 +56,4 @@ public override void Validate(IElasticClient client, NodeConfiguration configura
 				throw exception;
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidatePluginsTask.cs b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidatePluginsTask.cs
index 9db28bceb42..8ac84a8402b 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidatePluginsTask.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidatePluginsTask.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using Elasticsearch.Net;
 using Nest;
 using Tests.Framework.Configuration;
 using Tests.Framework.Integration;
@@ -37,4 +38,4 @@ public override void Validate(IElasticClient client, NodeConfiguration configura
 			throw new Exception($"Already running elasticsearch missed the following plugin(s): {pluginsString}.");
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateRunningVersion.cs b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateRunningVersion.cs
index 359f90d02e2..dea19e38ba7 100644
--- a/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateRunningVersion.cs
+++ b/src/Tests/Framework/ManagedElasticsearch/Tasks/ValidationTasks/ValidateRunningVersion.cs
@@ -1,4 +1,5 @@
 using System;
+using Elasticsearch.Net;
 using Nest;
 using Tests.Framework.Configuration;
 using Tests.Framework.Integration;
diff --git a/src/Tests/Framework/TestClient.cs b/src/Tests/Framework/TestClient.cs
index 3d64b5c7cdb..b85bf4fda3f 100644
--- a/src/Tests/Framework/TestClient.cs
+++ b/src/Tests/Framework/TestClient.cs
@@ -24,7 +24,8 @@ public static class TestClient
 		public static IElasticClient Default = new ElasticClient(GlobalDefaultSettings);
 		public static IElasticClient DefaultInMemoryClient = GetInMemoryClient();
 
-		public static Uri CreateUri(int port = 9200) => new UriBuilder("http", Host, port).Uri;
+		public static Uri CreateUri(int port = 9200, bool forceSsl = false) =>
+			new UriBuilder(forceSsl ? "https" : "http", Host, port).Uri;
 
 		public static string Host => (RunningFiddler) ? "ipv4.fiddler" : "localhost";
 
@@ -110,13 +111,14 @@ public static ConnectionSettings CreateSettings(
 			Func modifySettings = null,
 			int port = 9200,
 			bool forceInMemory = false,
+			bool forceSsl = false,
 			Func createPool = null,
 			Func serializerFactory = null
 		)
 		{
 			createPool = createPool ?? (u => new SingleNodeConnectionPool(u));
 #pragma warning disable CS0618 // Type or member is obsolete
-			var defaultSettings = DefaultSettings(new ConnectionSettings(createPool(CreateUri(port)),
+			var defaultSettings = DefaultSettings(new ConnectionSettings(createPool(CreateUri(port, forceSsl)),
 				CreateConnection(forceInMemory: forceInMemory), serializerFactory));
 #pragma warning restore CS0618 // Type or member is obsolete
 			var settings = modifySettings != null ? modifySettings(defaultSettings) : defaultSettings;
@@ -131,8 +133,16 @@ public static IElasticClient GetInMemoryClientWithSerializerFactory(
 			Func serializerFactory) =>
 			new ElasticClient(CreateSettings(modifySettings, forceInMemory: true, serializerFactory: serializerFactory));
 
-		public static IElasticClient GetClient(Func modifySettings = null,
-			int port = 9200, Func createPool = null) =>
+		public static IElasticClient GetClient(
+			Func modifySettings = null,
+			int port = 9200,
+			bool forceSsl = false) =>
+			new ElasticClient(CreateSettings(modifySettings, port, forceInMemory: false, forceSsl: forceSsl));
+
+		public static IElasticClient GetClient(
+			Func createPool,
+			Func modifySettings = null,
+			int port = 9200) =>
 			new ElasticClient(CreateSettings(modifySettings, port, forceInMemory: false, createPool: createPool));
 
 		public static IConnection CreateConnection(ConnectionSettings settings = null, bool forceInMemory = false) =>
diff --git a/src/Tests/Framework/XUnitPlumbing/IntegrationOnly.cs b/src/Tests/Framework/XUnitPlumbing/IntegrationOnly.cs
new file mode 100644
index 00000000000..91233e12ebd
--- /dev/null
+++ b/src/Tests/Framework/XUnitPlumbing/IntegrationOnly.cs
@@ -0,0 +1,6 @@
+using System;
+
+namespace Tests.Framework
+{
+	public class IntegrationOnly : Attribute { }
+}
\ No newline at end of file
diff --git a/src/Tests/Framework/XUnitPlumbing/RequiresPluginAttribute.cs b/src/Tests/Framework/XUnitPlumbing/RequiresPluginAttribute.cs
index 5954b5fa6e2..ba105d3603a 100644
--- a/src/Tests/Framework/XUnitPlumbing/RequiresPluginAttribute.cs
+++ b/src/Tests/Framework/XUnitPlumbing/RequiresPluginAttribute.cs
@@ -19,5 +19,4 @@ public RequiresPluginAttribute(params ElasticsearchPlugin[] plugins)
 			this.Plugins = plugins.ToList();
 		}
 	}
-
 }
diff --git a/src/Tests/Framework/XUnitPlumbing/UnitTestDiscoverer.cs b/src/Tests/Framework/XUnitPlumbing/UnitTestDiscoverer.cs
index 54627a98ec8..5e0b770527a 100644
--- a/src/Tests/Framework/XUnitPlumbing/UnitTestDiscoverer.cs
+++ b/src/Tests/Framework/XUnitPlumbing/UnitTestDiscoverer.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using Xunit;
 using Xunit.Abstractions;
 
@@ -7,9 +8,20 @@ namespace Tests.Framework
 	public class UnitTestDiscoverer : NestTestDiscoverer
 	{
 		public UnitTestDiscoverer(IMessageSink diagnosticMessageSink)
-			: base(diagnosticMessageSink, TestClient.Configuration.RunUnitTests) { }
+			: base(diagnosticMessageSink, TestClient.Configuration.RunUnitTests)
+		{
+		}
 
-		protected override bool SkipMethod(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) =>
-			!TestClient.Configuration.RunUnitTests;
+		protected override bool SkipMethod(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
+		{
+			var classOfMethod = Type.GetType(testMethod.TestClass.Class.Name, true, true);
+			return !TestClient.Configuration.RunUnitTests || ClassIsIntegrationOnly(classOfMethod);
+		}
+
+		private static bool ClassIsIntegrationOnly(Type classOfMethod)
+		{
+			var attributes = classOfMethod.GetAttributes();
+			return (attributes.Any());
+		}
 	}
 }
diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj
index 9c988a4c44c..b6f4cb07415 100644
--- a/src/Tests/Tests.csproj
+++ b/src/Tests/Tests.csproj
@@ -231,6 +231,8 @@
     
     
     
+    
+    
     
     
     
@@ -261,8 +263,11 @@
     
     
     
+    
     
     
+    
+    
     
     
     
@@ -290,6 +295,7 @@
     
     
     
+    
     
     
     
@@ -354,6 +360,7 @@
     
     
     
+    
     
     
     
@@ -842,6 +849,7 @@
     
   
   
+