Skip to content

Commit

Permalink
New -k/--insecure command line option
Browse files Browse the repository at this point in the history
Fix for bug 929591.

Change glance to require server certificate validation
by default when using https. The standard system
CA file will be used if available (and an alternative was not
provided).

The --insecure option can be used by clients to skip server
certificate validation if appropriate.

* This change will impact Nova clients accessing glance over https.
  If the standard CA file is not suitable they will need to provide
  a CA file or else create an 'insecure' glance client.
* Accesses to a https registry server must now perform server
  certificate validation.
* If the package which provides the standard
  system CA file is installed then that file will be used by default.
  It probably makes sense for the glance package to have a
  dependency on whichever package provides the default CA bundle.
  (In Ubuntu this is 'ca-certificates')

Change-Id: I7c83361ba0881559ec77d4baf10dfeb5b8e32185
  • Loading branch information
Stuart McLaren committed Feb 13, 2012
1 parent 56efd27 commit 0f0fe2b
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 57 deletions.
10 changes: 8 additions & 2 deletions bin/glance
Expand Up @@ -754,7 +754,7 @@ def get_client(options):
use_ssl=use_ssl,
auth_tok=options.auth_token or \
os.getenv('OS_TOKEN'),
creds=creds)
creds=creds, insecure=options.insecure)


def create_options(parser):
Expand All @@ -781,6 +781,12 @@ def create_options(parser):
"(http/https) of the glance server, for example "
"-U https://localhost:" + str(DEFAULT_PORT) +
"/v1 Default: None")
parser.add_option('-k', '--insecure', dest="insecure",
default=False, action="store_true",
help="Explicitly allow glance to perform \"insecure\" "
"SSL (https) requests. The server's certificate will "
"not be verified against any certificate authorities. "
"This option should be used with caution.")
parser.add_option('-A', '--auth_token', dest="auth_token",
metavar="TOKEN", default=None,
help="Authentication token to use to identify the "
Expand Down Expand Up @@ -810,7 +816,7 @@ def create_options(parser):
help="Sort results by this image attribute.")
parser.add_option('--sort_dir', dest="sort_dir", metavar="[desc|asc]",
help="Sort results in this direction.")
parser.add_option('-f', '--force', dest="force", metavar="FORCE",
parser.add_option('-f', '--force', dest="force",
default=False, action="store_true",
help="Prevent select actions from requesting "
"user confirmation")
Expand Down
6 changes: 5 additions & 1 deletion doc/source/glance.rst
Expand Up @@ -100,6 +100,10 @@ a brief help message, like so::
specify the hostname, port and protocol (http/https)
of the glance server, for example -U
https://localhost:9292/v1 Default: None
-k, --insecure Explicitly allow glance to perform insecure SSL
requests. The server certificate will not be
verified against any certificate authorities.
This option should be used with caution.
--limit=LIMIT Page size to use while requesting image metadata
--marker=MARKER Image index after which to begin pagination
--sort_key=KEY Sort results by this image attribute.
Expand Down Expand Up @@ -153,7 +157,7 @@ Important Information about Uploading Images
Before we go over the commands for adding an image to Glance, it is
important to understand that Glance **does not currently inspect** the image
files you add to it. In other words, **Glance only understands what you tell it,
via attributes and custom properties**.
via attributes and custom properties**.

If the file extension of the file you upload to Glance ends in '.vhd', Glance
**does not** know that the image you are uploading has a disk format of ``vhd``.
Expand Down
13 changes: 9 additions & 4 deletions doc/source/man/glance.rst
Expand Up @@ -78,10 +78,10 @@ OPTIONS

**-v, --verbose**
Print more verbose output

**-d, --debug**
Print more verbose output

**-H ADDRESS, --host=ADDRESS**
Address of Glance API host. Default: 0.0.0.0

Expand All @@ -90,10 +90,15 @@ OPTIONS

**-U URL, --url=URL**
URL of Glance service. This option can be used to specify the hostname,
port and protocol (http/https) of the glance server, for example
-U https://localhost:9292/v1
port and protocol (http/https) of the glance server, for example
-U https://localhost:9292/v1
Default: None

**-k, --insecure**
Explicitly allow glance to perform insecure SSL (https) requests.
The server certificate will not be verified against any certificate
authorities. This option should be used with caution.

**-A TOKEN, --auth_token=TOKEN**
Authentication token to use to identify the client to the glance server

Expand Down
33 changes: 25 additions & 8 deletions glance/common/client.py
Expand Up @@ -148,13 +148,14 @@ class HTTPSClientAuthConnection(httplib.HTTPSConnection):
"""

def __init__(self, host, port, key_file, cert_file,
ca_file, timeout=None):
ca_file, timeout=None, insecure=False):
httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
self.timeout = timeout
self.insecure = insecure

def connect(self):
"""
Expand All @@ -170,14 +171,14 @@ def connect(self):
if self._tunnel_host:
self.sock = sock
self._tunnel()
# If there's no CA File, don't force Server Certificate Check
if self.ca_file:
# Check CA file unless 'insecure' is specificed
if self.insecure is True:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ca_certs=self.ca_file,
cert_reqs=ssl.CERT_REQUIRED)
cert_reqs=ssl.CERT_NONE)
else:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
cert_reqs=ssl.CERT_NONE)
ca_certs=self.ca_file,
cert_reqs=ssl.CERT_REQUIRED)


class BaseClient(object):
Expand All @@ -186,6 +187,12 @@ class BaseClient(object):

DEFAULT_PORT = 80
DEFAULT_DOC_ROOT = None
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD
DEFAULT_CA_FILE_PATH = '/etc/ssl/certs/ca-certificates.crt:'\
'/etc/pki/tls/certs/ca-bundle.crt:'\
'/etc/ssl/ca-bundle.pem:'\
'/etc/ssl/cert.pem'

OK_RESPONSE_CODES = (
httplib.OK,
Expand All @@ -203,8 +210,8 @@ class BaseClient(object):
)

def __init__(self, host, port=None, use_ssl=False, auth_tok=None,
creds=None, doc_root=None,
key_file=None, cert_file=None, ca_file=None):
creds=None, doc_root=None, key_file=None,
cert_file=None, ca_file=None, insecure=False):
"""
Creates a new client to some service.
Expand All @@ -231,6 +238,8 @@ def __init__(self, host, port=None, use_ssl=False, auth_tok=None,
If use_ssl is True, and this param is None (the
default), then an environ variable
GLANCE_CLIENT_CA_FILE is looked for.
:param insecure: Optional. If set then the server's certificate
will not be verified.
"""
self.host = host
self.port = port or self.DEFAULT_PORT
Expand Down Expand Up @@ -286,7 +295,15 @@ def __init__(self, host, port=None, use_ssl=False, auth_tok=None,
msg = _("The CA file you specified %s does not "
"exist") % ca_file
raise exception.ClientConnectionError(msg)

if ca_file is None:
for ca in self.DEFAULT_CA_FILE_PATH.split(":"):
if os.path.exists(ca):
ca_file = ca
break

self.connect_kwargs['ca_file'] = ca_file
self.connect_kwargs['insecure'] = insecure

def set_auth_token(self, auth_tok):
"""
Expand Down
107 changes: 106 additions & 1 deletion glance/tests/functional/test_ssl.py
Expand Up @@ -40,6 +40,8 @@
import tempfile
import unittest

from glance import client as glance_client
from glance.common import exception
from glance.common import utils
from glance.store.location import get_location_from_uri
from glance.tests import functional
Expand All @@ -62,18 +64,30 @@ def setUp(self):
self.inited = False
self.disabled = True

# Test key/cert/CA file created as per:
# http://blog.didierstevens.com/2008/12/30/
# howto-make-your-own-cert-with-openssl/
# Note that for these tests certificate.crt had to
# be created with 'Common Name' set to 0.0.0.0

self.key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
if not os.path.exists(self.key_file):
self.disabled_message = "Could not find private key file"
self.inited = True
return

self.cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
if not os.path.exists(self.key_file):
if not os.path.exists(self.cert_file):
self.disabled_message = "Could not find certificate file"
self.inited = True
return

self.ca_file = os.path.join(TEST_VAR_DIR, 'ca.crt')
if not os.path.exists(self.ca_file):
self.disabled_message = "Could not find CA file"
self.inited = True
return

self.inited = True
self.disabled = False

Expand Down Expand Up @@ -1230,3 +1244,94 @@ def test_delete_not_existing(self):
self.assertEqual(response.status, 404)

self.stop_servers()

@skip_if_disabled
def test_certificate_validation(self):
"""
Check SSL client cerificate verification
"""
self.cleanup()
self.start_servers(**self.__dict__.copy())

# 0. GET /images
# Verify no public images
path = "https://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
https = httplib2.Http(disable_ssl_certificate_validation=True)
response, content = https.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, '{"images": []}')

# 1. POST /images with public image named Image1
headers = {'Content-Type': 'application/octet-stream',
'X-Image-Meta-Name': 'Image1',
'X-Image-Meta-Status': 'active',
'X-Image-Meta-Container-Format': 'ovf',
'X-Image-Meta-Disk-Format': 'vdi',
'X-Image-Meta-Size': '19',
'X-Image-Meta-Is-Public': 'True'}
path = "https://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
https = httplib2.Http(disable_ssl_certificate_validation=True)
response, content = https.request(path, 'POST', headers=headers)
self.assertEqual(response.status, 201)
data = json.loads(content)

image_id = data['image']['id']

# 2. Attempt to delete the image *without* CA file
path = "https://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
secure_cli = glance_client.Client(host="0.0.0.0", port=self.api_port,
use_ssl=True, insecure=False)
try:
secure_cli.delete_image(image_id)
self.fail("Client with no CA file deleted image %s" % image_id)
except exception.ClientConnectionError, e:
pass

# 3. Delete the image with a secure client *with* CA file
secure_cli2 = glance_client.Client(host="0.0.0.0", port=self.api_port,
use_ssl=True, ca_file=self.ca_file,
insecure=False)
try:
secure_cli2.delete_image(image_id)
except exception.ClientConnectionError, e:
self.fail("Secure client failed to delete image %s" % image_id)

# Verify image is deleted
path = "https://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
https = httplib2.Http(disable_ssl_certificate_validation=True)
response, content = https.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, '{"images": []}')

# 4. POST another image
headers = {'Content-Type': 'application/octet-stream',
'X-Image-Meta-Name': 'Image1',
'X-Image-Meta-Status': 'active',
'X-Image-Meta-Container-Format': 'ovf',
'X-Image-Meta-Disk-Format': 'vdi',
'X-Image-Meta-Size': '19',
'X-Image-Meta-Is-Public': 'True'}
path = "https://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
https = httplib2.Http(disable_ssl_certificate_validation=True)
response, content = https.request(path, 'POST', headers=headers)
self.assertEqual(response.status, 201)
data = json.loads(content)

image_id = data['image']['id']

# 5. Delete the image with an insecure client
insecure_cli = glance_client.Client(host="0.0.0.0", port=self.api_port,
use_ssl=True, insecure=True)
try:
insecure_cli.delete_image(image_id)
except exception.ClientConnectionError, e:
self.fail("Insecure client failed to delete image")

# Verify image is deleted
path = "https://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
https = httplib2.Http(disable_ssl_certificate_validation=True)
response, content = https.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, '{"images": []}')

self.stop_servers()
35 changes: 35 additions & 0 deletions glance/tests/var/ca.crt
@@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGDDCCA/SgAwIBAgIJAPSvwQYk4qI4MA0GCSqGSIb3DQEBBQUAMGExCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRUwEwYDVQQKEwxPcGVuc3RhY2sg
Q0ExEjAQBgNVBAsTCUdsYW5jZSBDQTESMBAGA1UEAxMJR2xhbmNlIENBMB4XDTEy
MDIwOTE3MTAwMloXDTIyMDIwNjE3MTAwMlowYTELMAkGA1UEBhMCQVUxEzARBgNV
BAgTClNvbWUtU3RhdGUxFTATBgNVBAoTDE9wZW5zdGFjayBDQTESMBAGA1UECxMJ
R2xhbmNlIENBMRIwEAYDVQQDEwlHbGFuY2UgQ0EwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQDmf+fapWfzy1Uylus0KGalw4X/5xZ+ltPVOr+IdCPbstvi
RTC5g+O+TvXeOP32V/cnSY4ho/+f2q730za+ZA/cgWO252rcm3Q7KTJn3PoqzJvX
/l3EXe3/TCrbzgZ7lW3QLTCTEE2eEzwYG3wfDTOyoBq+F6ct6ADh+86gmpbIRfYI
N+ixB0hVyz9427PTof97fL7qxxkjAayB28OfwHrkEBl7iblNhUC0RoH+/H9r5GEl
GnWiebxfNrONEHug6PHgiaGq7/Dj+u9bwr7J3/NoS84I08ajMnhlPZxZ8bS/O8If
ceWGZv7clPozyhABT/otDfgVcNH1UdZ4zLlQwc1MuPYN7CwxrElxc8Quf94ttGjb
tfGTl4RTXkDofYdG1qBWW962PsGl2tWmbYDXV0q5JhV/IwbrE1X9f+OksJQne1/+
dZDxMhdf2Q1V0P9hZZICu4+YhmTMs5Mc9myKVnzp4NYdX5fXoB/uNYph+G7xG5IK
WLSODKhr1wFGTTcuaa8LhOH5UREVenGDJuc6DdgX9a9PzyJGIi2ngQ03TJIkCiU/
4J/r/vsm81ezDiYZSp2j5JbME+ixW0GBLTUWpOIxUSHgUFwH5f7lQwbXWBOgwXQk
BwpZTmdQx09MfalhBtWeu4/6BnOCOj7e/4+4J0eVxXST0AmVyv8YjJ2nz1F9oQID
AQABo4HGMIHDMB0GA1UdDgQWBBTk7Krj4bEsTjHXaWEtI2GZ5ACQyTCBkwYDVR0j
BIGLMIGIgBTk7Krj4bEsTjHXaWEtI2GZ5ACQyaFlpGMwYTELMAkGA1UEBhMCQVUx
EzARBgNVBAgTClNvbWUtU3RhdGUxFTATBgNVBAoTDE9wZW5zdGFjayBDQTESMBAG
A1UECxMJR2xhbmNlIENBMRIwEAYDVQQDEwlHbGFuY2UgQ0GCCQD0r8EGJOKiODAM
BgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQA8Zrss/MiwFHGmDlercE0h
UvzA54n/EvKP9nP3jHM2qW/VPfKdnFw99nEPFLhb+lN553vdjOpCYFm+sW0Z5Mi4
qsFkk4AmXIIEFOPt6zKxMioLYDQ9Sw/BUv6EZGeANWr/bhmaE+dMcKJt5le/0jJm
2ahsVB9fbFu9jBFeYb7Ba/x2aLkEGMxaDLla+6EQhj148fTnS1wjmX9G2cNzJvj/
+C2EfKJIuDJDqw2oS2FGVpP37FA2Bz2vga0QatNneLkGKCFI3ZTenBznoN+fmurX
TL3eJE4IFNrANCcdfMpdyLAtXz4KpjcehqpZMu70er3d30zbi1l0Ajz4dU+WKz/a
NQES+vMkT2wqjXHVTjrNwodxw3oLK/EuTgwoxIHJuplx5E5Wrdx9g7Gl1PBIJL8V
xiOYS5N7CakyALvdhP7cPubA2+TPAjNInxiAcmhdASS/Vrmpvrkat6XhGn8h9liv
ysDOpMQmYQkmgZBpW8yBKK7JABGGsJADJ3E6J5MMWBX2RR4kFoqVGAzdOU3oyaTy
I0kz5sfuahaWpdYJVlkO+esc0CRXw8fLDYivabK2tOgUEWeZsZGZ9uK6aV1VxTAY
9Guu3BJ4Rv/KP/hk7mP8rIeCwotV66/2H8nq72ImQhzSVyWcxbFf2rJiFQJ3BFwA
WoRMgEwjGJWqzhJZUYpUAQ==
-----END CERTIFICATE-----
44 changes: 28 additions & 16 deletions glance/tests/var/certificate.crt
@@ -1,18 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIC4DCCAcigAwIBAgIBATANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDEwhNeVRl
c3RDQTAeFw0xMTA3MjExNTA1NDZaFw0xMjA3MjAxNTA1NDZaMCMxEDAOBgNVBAMT
B2FobWFkcGMxDzANBgNVBAoTBnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAO9zpczf+W4DoK2z8oFbsZfbvz1y/yQOnrQYvb1zv1IieT+QA+Ti
N64N/sgR/cR7YEIXDnhij8yE1JTWMk1W6g4m7TGacUMXD/WAcsTM7kRol/FVksdn
F51qxCYqWUPQ3xiTfBg2SJWvJCUGowvz06xh8JeOEXLbALC5xrzrM3hclpdbrKYE
oe8kikI/K0TKpu52VJJrTBGPHMsw+eIqL2Ix5pWHh7DPfjBiiG7khsJxN7xSqLbX
LrhDi24nTM9pndaqABkmPYQ9qd11SoAUB82QAAGj8A7iR/DnAzAfJl1usvQp+Me6
sR3TPY27zifBbD04tiROi1swM/1xRH7qOpkCAwEAAaMvMC0wCQYDVR0TBAIwADAL
BgNVHQ8EBAMCBSAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQAD
ggEBAIJvnQjkEDFvLT7NiyFrO938BuxdQH2mX2N7Fz86myZLcGpr5NCdLvT9tD9f
6KqrR8e839pYVPZY80cBpGTmRmzW3xLsmGCFHPHt4p1tkqSP1R5iLzKDe8jawHhD
sch8P9URRhW9ZgBzA4xiv9FnIxZ70uDr04uX/sR/j41HGBS8YW6dJvr9Y2SpGqSS
rR2btnNZ945dau6CPLRNd9Fls3Qjx03PnsmZ5ikSuV0pT1sPQmhhw7rBYV/b2ff+
z/4cRtZrR00NVc74IEXLoujIjUUpFC83in10PKQmAvKYTeTdXns48eC4Cwqe8eaM
N0YtxqQvSTsUo6vPM28NR99Fbow=
MIIFLjCCAxYCAQEwDQYJKoZIhvcNAQEFBQAwYTELMAkGA1UEBhMCQVUxEzARBgNV
BAgTClNvbWUtU3RhdGUxFTATBgNVBAoTDE9wZW5zdGFjayBDQTESMBAGA1UECxMJ
R2xhbmNlIENBMRIwEAYDVQQDEwlHbGFuY2UgQ0EwHhcNMTIwMjA5MTcxMDUzWhcN
MjIwMjA2MTcxMDUzWjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0
ZTESMBAGA1UEChMJT3BlbnN0YWNrMQ8wDQYDVQQLEwZHbGFuY2UxEDAOBgNVBAMT
BzAuMC4wLjAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXpUkQN6pu
avo+gz3o1K4krVdPl1m7NjNJDyD/+ZH0EGNcEN7iag1qPE7JsjqGPNZsQK1dMoXb
Sz+OSi9qvNeJnBcfwUx5qTAtwyAb9AxGkwuMafIU+lWbsclo+dPGsja01ywbXTCZ
bF32iqnpOMYhfxWUdoQYiBkhxxhW9eMPKLS/KkP8/bx+Vaa2XJiAebqkd9nrksAA
BeGc9mlafYBEmiChPdJEPw+1ePA4QVq9aPepDsqAKtGN8JLpmoC3BdxQQTbbwL3Q
8fTXK4tCNUaVk4AbDy/McFq6y0ocQoBPJjihOY35mWG/OLtcI99yPOpWGnps/5aG
/64DDJ2D67Fnaj6gKHV+6TXFO8KZxlnxtgtiZDJBZkneTBt9ArSOv+l6NBsumRz0
iEJ4o4H1S2TSMnprAvX7WnGtc6Xi9gXahYcDHEelwwYzqAiTBv6hxSp4MZ2dNXa+
KzOitC7ZbV2qsg0au0wjfE/oSQ3NvsvUr8nOmfutJTvHRAwbC1v4G/tuAsO7O0w2
0u2B3u+pG06m5+rnEqp+rB9hmukRYTfgEFRRsVIvpFl/cwvPXKRcX03UIMx+lLr9
Ft+ep7YooBhY3wY2kwCxD4lRYNmbwsCIVywZt40f/4ad98TkufR9NhsfycxGeqbr
mTMFlZ8TTlmP82iohekKCOvoyEuTIWL2+wIDAQABMA0GCSqGSIb3DQEBBQUAA4IC
AQBMUBgV0R+Qltf4Du7u/8IFmGAoKR/mktB7R1gRRAqsvecUt7kIwBexGdavGg1y
0pU0+lgUZjJ20N1SlPD8gkNHfXE1fL6fmMjWz4dtYJjzRVhpufHPeBW4tl8DgHPN
rBGAYQ+drDSXaEjiPQifuzKx8WS+DGA3ki4co5mPjVnVH1xvLIdFsk89z3b3YD1k
yCJ/a9K36x6Z/c67JK7s6MWtrdRF9+MVnRKJ2PK4xznd1kBz16V+RA466wBDdARY
vFbtkafbEqOb96QTonIZB7+fAldKDPZYnwPqasreLmaGOaM8sxtlPYAJ5bjDONbc
AaXG8BMRQyO4FyH237otDKlxPyHOFV66BaffF5S8OlwIMiZoIvq+IcTZOdtDUSW2
KHNLfe5QEDZdKjWCBrfqAfvNuG13m03WqfmcMHl3o/KiPJlx8l9Z4QEzZ9xcyQGL
cncgeHM9wJtzi2cD/rTDNFsx/gxvoyutRmno7I3NRbKmpsXF4StZioU3USRspB07
hYXOVnG3pS+PjVby7ThT3gvFHSocguOsxClx1epdUJAmJUbmM7NmOp5WVBVtMtC2
Su4NG/xJciXitKzw+btb7C7RjO6OEqv/1X/oBDzKBWQAwxUC+lqmnM7W6oqWJFEM
YfTLnrjs7Hj6ThMGcEnfvc46dWK3dz0RjsQzUxugPuEkLA==
-----END CERTIFICATE-----

0 comments on commit 0f0fe2b

Please sign in to comment.