diff --git a/README.md b/README.md index 398cf63bbe..fd07a0113f 100644 --- a/README.md +++ b/README.md @@ -342,3 +342,62 @@ c.start(container_id, binds={ } }) ``` + +Connection to daemon using HTTPS +================================ + +*These instructions are docker-py specific. Please refer to +http://docs.docker.com/articles/https/ first.* + +* Authenticate server based on public/default CA pool + +```python +client = docker.Client(base_url='', tls=True) +``` + +Equivalent CLI options: `docker --tls ...` + +If you want to use TLS but don't want to verify the server certificate +(for example when testing with a self-signed certificate): + +```python +tls_config = docker.tls.TLSConfig(verify=False) +client = docker.Client(base_url='', tls=tls_config) +``` + +* Authenticate server based on given CA + +```python +tls_config = docker.tls.TLSConfig(ca_cert='/path/to/ca.pem') +client = docker.Client(base_url='', tls=tls_config) +``` + +Equivalent CLI options: `docker --tlsverify --tlscacert /path/to/ca.pem ...` + +* Authenticate with client certificate, do not authenticate server + based on given CA + +```python +tls_config = docker.tls.TLSConfig( + client_cert=('/path/to/client-cert.pem', '/path/to/client-key.pem') +) +client = docker.Client(base_url='', tls=tls_config) +``` + +Equivalent CLI options: +`docker --tls --tlscert /path/to/client-cert.pem +--tlskey /path/to/client-key.pem ...` + +* Authenticate with client certificate, authenticate server based on given CA + +```python +tls_config = docker.tls.TLSConfig( + client_cert=('/path/to/client-cert.pem', '/path/to/client-key.pem'), + ca_cert='/path/to/ca.pem' +) +client = docker.Client(base_url='', tls=tls_config) +``` + +Equivalent CLI options: +`docker --tlsverify --tlscert /path/to/client-cert.pem +--tlskey /path/to/client-key.pem --tlscacert /path/to/ca.pem ...` diff --git a/docker/client.py b/docker/client.py index cd275d4201..0a12b66588 100644 --- a/docker/client.py +++ b/docker/client.py @@ -24,8 +24,10 @@ from .auth import auth from .unixconn import unixconn +from .ssladapter import ssladapter from .utils import utils from . import errors +from .tls import TLSConfig if not six.PY3: import websocket @@ -37,17 +39,27 @@ class Client(requests.Session): def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, - timeout=DEFAULT_TIMEOUT_SECONDS): + timeout=DEFAULT_TIMEOUT_SECONDS, tls=False): super(Client, self).__init__() base_url = utils.parse_host(base_url) if 'http+unix:///' in base_url: base_url = base_url.replace('unix:/', 'unix:') + if tls and not base_url.startswith('https://'): + raise errors.TLSParameterError( + 'If using TLS, the base_url argument must begin with ' + '"https://".') self.base_url = base_url self._version = version self._timeout = timeout self._auth_configs = auth.load_config() - self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) + # Use SSLAdapter for the ability to specify SSL version + if isinstance(tls, TLSConfig): + tls.configure_client(self) + elif tls: + self.mount('https://', ssladapter.SSLAdapter()) + else: + self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) def _set_request_timeout(self, kwargs): """Prepare the kwargs for an HTTP request by inserting the timeout diff --git a/docker/errors.py b/docker/errors.py index 85a6d45262..749facff94 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -63,3 +63,14 @@ class InvalidConfigFile(DockerException): class DeprecatedMethod(DockerException): pass + + +class TLSParameterError(DockerException): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return self.msg + (". TLS configurations should map the Docker CLI " + "client configurations. See " + "http://docs.docker.com/examples/https/ for " + "API details.") diff --git a/docker/ssladapter/__init__.py b/docker/ssladapter/__init__.py new file mode 100644 index 0000000000..1a5e1bb6d4 --- /dev/null +++ b/docker/ssladapter/__init__.py @@ -0,0 +1 @@ +from .ssladapter import SSLAdapter # flake8: noqa diff --git a/docker/ssladapter/ssladapter.py b/docker/ssladapter/ssladapter.py new file mode 100644 index 0000000000..99dc36a6cd --- /dev/null +++ b/docker/ssladapter/ssladapter.py @@ -0,0 +1,33 @@ +""" Resolves OpenSSL issues in some servers: + https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/ + https://github.com/kennethreitz/requests/pull/799 +""" +from distutils.version import StrictVersion +from requests.adapters import HTTPAdapter +try: + import requests.packages.urllib3 as urllib3 +except ImportError: + import urllib3 + + +PoolManager = urllib3.poolmanager.PoolManager + + +class SSLAdapter(HTTPAdapter): + '''An HTTPS Transport Adapter that uses an arbitrary SSL version.''' + def __init__(self, ssl_version=None, **kwargs): + self.ssl_version = ssl_version + super(SSLAdapter, self).__init__(**kwargs) + + def init_poolmanager(self, connections, maxsize, block=False): + urllib_ver = urllib3.__version__.split('-')[0] + if urllib3 and urllib_ver != 'dev' and \ + StrictVersion(urllib_ver) <= StrictVersion('1.5'): + self.poolmanager = PoolManager(num_pools=connections, + maxsize=maxsize, + block=block) + else: + self.poolmanager = PoolManager(num_pools=connections, + maxsize=maxsize, + block=block, + ssl_version=self.ssl_version) diff --git a/docker/tls.py b/docker/tls.py new file mode 100644 index 0000000000..531f4d681b --- /dev/null +++ b/docker/tls.py @@ -0,0 +1,68 @@ +import os + +from . import errors +from .ssladapter import ssladapter + + +class TLSConfig(object): + cert = None + verify = None + ssl_version = None + + def __init__(self, client_cert=None, ca_cert=None, verify=None, + ssl_version=None): + # Argument compatibility/mapping with + # http://docs.docker.com/examples/https/ + # This diverges from the Docker CLI in that users can specify 'tls' + # here, but also disable any public/default CA pool verification by + # leaving tls_verify=False + + # urllib3 sets a default ssl_version if ssl_version is None + # http://tinyurl.com/kxga8hb + self.ssl_version = ssl_version + + # "tls" and "tls_verify" must have both or neither cert/key files + # In either case, Alert the user when both are expected, but any are + # missing. + + if client_cert: + try: + tls_cert, tls_key = client_cert + except ValueError: + raise errors.TLSParameterError( + 'client_config must be a tuple of' + ' (client certificate, key file)' + ) + + if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or + not os.path.isfile(tls_key)): + raise errors.TLSParameterError( + 'Path to a certificate and key files must be provided' + ' through the client_config param' + ) + self.cert = (tls_cert, tls_key) + + # Either set verify to True (public/default CA checks) or to the + # path of a CA Cert file. + if verify is not None: + if not ca_cert: + self.verify = verify + elif os.path.isfile(ca_cert): + if not verify: + raise errors.TLSParameterError( + 'verify can not be False when a CA cert is' + ' provided.' + ) + self.verify = ca_cert + else: + raise errors.TLSParameterError( + 'Invalid CA certificate provided for `tls_ca_cert`.' + ) + + def configure_client(self, client): + client.ssl_version = self.ssl_version + if self.verify is not None: + client.verify = self.verify + if self.cert: + client.cert = self.cert + client.mount('https://', ssladapter.SSLAdapter(self.ssl_version)) diff --git a/setup.py b/setup.py index d94aa156a2..0055cd6cd2 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,8 @@ name="docker-py", version=version, description="Python client for Docker.", - packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils'], + packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils', + 'docker.ssladapter'], install_requires=requirements + test_requirements, zip_safe=False, test_suite='tests',