Skip to content

Commit 747304d

Browse files
committed
Merge pull request #250 from dotcloud/momer-tls
Support for TLS auth
2 parents ed2b458 + 7b8e0cd commit 747304d

File tree

7 files changed

+188
-3
lines changed

7 files changed

+188
-3
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,62 @@ c.start(container_id, binds={
342342
}
343343
})
344344
```
345+
346+
Connection to daemon using HTTPS
347+
================================
348+
349+
*These instructions are docker-py specific. Please refer to
350+
http://docs.docker.com/articles/https/ first.*
351+
352+
* Authenticate server based on public/default CA pool
353+
354+
```python
355+
client = docker.Client(base_url='<https_url>', tls=True)
356+
```
357+
358+
Equivalent CLI options: `docker --tls ...`
359+
360+
If you want to use TLS but don't want to verify the server certificate
361+
(for example when testing with a self-signed certificate):
362+
363+
```python
364+
tls_config = docker.tls.TLSConfig(verify=False)
365+
client = docker.Client(base_url='<https_url>', tls=tls_config)
366+
```
367+
368+
* Authenticate server based on given CA
369+
370+
```python
371+
tls_config = docker.tls.TLSConfig(ca_cert='/path/to/ca.pem')
372+
client = docker.Client(base_url='<https_url>', tls=tls_config)
373+
```
374+
375+
Equivalent CLI options: `docker --tlsverify --tlscacert /path/to/ca.pem ...`
376+
377+
* Authenticate with client certificate, do not authenticate server
378+
based on given CA
379+
380+
```python
381+
tls_config = docker.tls.TLSConfig(
382+
client_cert=('/path/to/client-cert.pem', '/path/to/client-key.pem')
383+
)
384+
client = docker.Client(base_url='<https_url>', tls=tls_config)
385+
```
386+
387+
Equivalent CLI options:
388+
`docker --tls --tlscert /path/to/client-cert.pem
389+
--tlskey /path/to/client-key.pem ...`
390+
391+
* Authenticate with client certificate, authenticate server based on given CA
392+
393+
```python
394+
tls_config = docker.tls.TLSConfig(
395+
client_cert=('/path/to/client-cert.pem', '/path/to/client-key.pem'),
396+
ca_cert='/path/to/ca.pem'
397+
)
398+
client = docker.Client(base_url='<https_url>', tls=tls_config)
399+
```
400+
401+
Equivalent CLI options:
402+
`docker --tlsverify --tlscert /path/to/client-cert.pem
403+
--tlskey /path/to/client-key.pem --tlscacert /path/to/ca.pem ...`

docker/client.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424

2525
from .auth import auth
2626
from .unixconn import unixconn
27+
from .ssladapter import ssladapter
2728
from .utils import utils
2829
from . import errors
30+
from .tls import TLSConfig
2931

3032
if not six.PY3:
3133
import websocket
@@ -37,17 +39,27 @@
3739

3840
class Client(requests.Session):
3941
def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
40-
timeout=DEFAULT_TIMEOUT_SECONDS):
42+
timeout=DEFAULT_TIMEOUT_SECONDS, tls=False):
4143
super(Client, self).__init__()
4244
base_url = utils.parse_host(base_url)
4345
if 'http+unix:///' in base_url:
4446
base_url = base_url.replace('unix:/', 'unix:')
47+
if tls and not base_url.startswith('https://'):
48+
raise errors.TLSParameterError(
49+
'If using TLS, the base_url argument must begin with '
50+
'"https://".')
4551
self.base_url = base_url
4652
self._version = version
4753
self._timeout = timeout
4854
self._auth_configs = auth.load_config()
4955

50-
self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout))
56+
# Use SSLAdapter for the ability to specify SSL version
57+
if isinstance(tls, TLSConfig):
58+
tls.configure_client(self)
59+
elif tls:
60+
self.mount('https://', ssladapter.SSLAdapter())
61+
else:
62+
self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout))
5163

5264
def _set_request_timeout(self, kwargs):
5365
"""Prepare the kwargs for an HTTP request by inserting the timeout

docker/errors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,14 @@ class InvalidConfigFile(DockerException):
6363

6464
class DeprecatedMethod(DockerException):
6565
pass
66+
67+
68+
class TLSParameterError(DockerException):
69+
def __init__(self, msg):
70+
self.msg = msg
71+
72+
def __str__(self):
73+
return self.msg + (". TLS configurations should map the Docker CLI "
74+
"client configurations. See "
75+
"http://docs.docker.com/examples/https/ for "
76+
"API details.")

docker/ssladapter/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .ssladapter import SSLAdapter # flake8: noqa

docker/ssladapter/ssladapter.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
""" Resolves OpenSSL issues in some servers:
2+
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
3+
https://github.com/kennethreitz/requests/pull/799
4+
"""
5+
from distutils.version import StrictVersion
6+
from requests.adapters import HTTPAdapter
7+
try:
8+
import requests.packages.urllib3 as urllib3
9+
except ImportError:
10+
import urllib3
11+
12+
13+
PoolManager = urllib3.poolmanager.PoolManager
14+
15+
16+
class SSLAdapter(HTTPAdapter):
17+
'''An HTTPS Transport Adapter that uses an arbitrary SSL version.'''
18+
def __init__(self, ssl_version=None, **kwargs):
19+
self.ssl_version = ssl_version
20+
super(SSLAdapter, self).__init__(**kwargs)
21+
22+
def init_poolmanager(self, connections, maxsize, block=False):
23+
urllib_ver = urllib3.__version__.split('-')[0]
24+
if urllib3 and urllib_ver != 'dev' and \
25+
StrictVersion(urllib_ver) <= StrictVersion('1.5'):
26+
self.poolmanager = PoolManager(num_pools=connections,
27+
maxsize=maxsize,
28+
block=block)
29+
else:
30+
self.poolmanager = PoolManager(num_pools=connections,
31+
maxsize=maxsize,
32+
block=block,
33+
ssl_version=self.ssl_version)

docker/tls.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import os
2+
3+
from . import errors
4+
from .ssladapter import ssladapter
5+
6+
7+
class TLSConfig(object):
8+
cert = None
9+
verify = None
10+
ssl_version = None
11+
12+
def __init__(self, client_cert=None, ca_cert=None, verify=None,
13+
ssl_version=None):
14+
# Argument compatibility/mapping with
15+
# http://docs.docker.com/examples/https/
16+
# This diverges from the Docker CLI in that users can specify 'tls'
17+
# here, but also disable any public/default CA pool verification by
18+
# leaving tls_verify=False
19+
20+
# urllib3 sets a default ssl_version if ssl_version is None
21+
# http://tinyurl.com/kxga8hb
22+
self.ssl_version = ssl_version
23+
24+
# "tls" and "tls_verify" must have both or neither cert/key files
25+
# In either case, Alert the user when both are expected, but any are
26+
# missing.
27+
28+
if client_cert:
29+
try:
30+
tls_cert, tls_key = client_cert
31+
except ValueError:
32+
raise errors.TLSParameterError(
33+
'client_config must be a tuple of'
34+
' (client certificate, key file)'
35+
)
36+
37+
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
38+
not os.path.isfile(tls_key)):
39+
raise errors.TLSParameterError(
40+
'Path to a certificate and key files must be provided'
41+
' through the client_config param'
42+
)
43+
self.cert = (tls_cert, tls_key)
44+
45+
# Either set verify to True (public/default CA checks) or to the
46+
# path of a CA Cert file.
47+
if verify is not None:
48+
if not ca_cert:
49+
self.verify = verify
50+
elif os.path.isfile(ca_cert):
51+
if not verify:
52+
raise errors.TLSParameterError(
53+
'verify can not be False when a CA cert is'
54+
' provided.'
55+
)
56+
self.verify = ca_cert
57+
else:
58+
raise errors.TLSParameterError(
59+
'Invalid CA certificate provided for `tls_ca_cert`.'
60+
)
61+
62+
def configure_client(self, client):
63+
client.ssl_version = self.ssl_version
64+
if self.verify is not None:
65+
client.verify = self.verify
66+
if self.cert:
67+
client.cert = self.cert
68+
client.mount('https://', ssladapter.SSLAdapter(self.ssl_version))

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
name="docker-py",
2323
version=version,
2424
description="Python client for Docker.",
25-
packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils'],
25+
packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils',
26+
'docker.ssladapter'],
2627
install_requires=requirements + test_requirements,
2728
zip_safe=False,
2829
test_suite='tests',

0 commit comments

Comments
 (0)