diff --git a/bin/glance b/bin/glance index fd44f24886..8aefea5334 100755 --- a/bin/glance +++ b/bin/glance @@ -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): @@ -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 " @@ -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") diff --git a/doc/source/glance.rst b/doc/source/glance.rst index 1a86f4c9e7..e105eb48e6 100644 --- a/doc/source/glance.rst +++ b/doc/source/glance.rst @@ -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. @@ -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``. diff --git a/doc/source/man/glance.rst b/doc/source/man/glance.rst index 16ecdc11bf..ae174f9a36 100644 --- a/doc/source/man/glance.rst +++ b/doc/source/man/glance.rst @@ -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 @@ -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 diff --git a/glance/common/client.py b/glance/common/client.py index 9a3c3bfbef..9e08c15aa5 100644 --- a/glance/common/client.py +++ b/glance/common/client.py @@ -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): """ @@ -170,14 +171,14 @@ class HTTPSClientAuthConnection(httplib.HTTPSConnection): 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, + cert_reqs=ssl.CERT_NONE) + else: self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ca_certs=self.ca_file, cert_reqs=ssl.CERT_REQUIRED) - else: - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, - cert_reqs=ssl.CERT_NONE) class BaseClient(object): @@ -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, @@ -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. @@ -231,6 +238,8 @@ class BaseClient(object): 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 @@ -286,7 +295,15 @@ class BaseClient(object): 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): """ diff --git a/glance/tests/functional/test_ssl.py b/glance/tests/functional/test_ssl.py index b0fdf622d6..4c94d1cf9f 100644 --- a/glance/tests/functional/test_ssl.py +++ b/glance/tests/functional/test_ssl.py @@ -40,6 +40,8 @@ import os 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 @@ -62,6 +64,12 @@ class TestSSL(functional.FunctionalTest): 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" @@ -69,11 +77,17 @@ class TestSSL(functional.FunctionalTest): 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 @@ -1230,3 +1244,94 @@ class TestSSL(functional.FunctionalTest): 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() diff --git a/glance/tests/var/ca.crt b/glance/tests/var/ca.crt new file mode 100644 index 0000000000..9d66ca6270 --- /dev/null +++ b/glance/tests/var/ca.crt @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +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 +WoRMgEwjGJWqzhJZUYpUAQ== +-----END CERTIFICATE----- diff --git a/glance/tests/var/certificate.crt b/glance/tests/var/certificate.crt index 9bb6dd1d0f..3c1aa6363b 100644 --- a/glance/tests/var/certificate.crt +++ b/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----- diff --git a/glance/tests/var/privatekey.key b/glance/tests/var/privatekey.key index 9da0a6c858..b63df3d29d 100644 --- a/glance/tests/var/privatekey.key +++ b/glance/tests/var/privatekey.key @@ -1,27 +1,51 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY-----