diff --git a/bin/keystone b/bin/keystone index f1985fbe61..55af1e4fcd 100755 --- a/bin/keystone +++ b/bin/keystone @@ -70,19 +70,35 @@ if __name__ == '__main__': print "Using config file:", config_file # Load Service API server - server = wsgi.Server() - server.start(app, int(conf['service_port']), conf['service_host']) + if conf['service_ssl'] == 'True': + server = wsgi.SslServer() + server.start(app, int(conf['service_port']), conf['service_host'], + certfile=conf['certfile'], keyfile=conf['keyfile'], + ca_certs=conf['ca_certs'], + cert_required=conf['cert_required']) + else: + server = wsgi.Server() + server.start(app, int(conf['service_port']), conf['service_host']) + - print "Service API listening on %s:%s" % ( - conf['service_host'], conf['service_port']) + print "Service API (ssl=%s) listening on %s:%s" % ( + conf['service_ssl'], conf['service_host'], conf['service_port']) # Load Admin API server - admin_server = wsgi.Server() - admin_server.start(admin_app, - int(conf['admin_port']), conf['admin_host']) + if conf['admin_ssl'] == 'True': + admin_server = wsgi.SslServer() + admin_server.start(admin_app, + int(conf['admin_port']), conf['admin_host'], + certfile=conf['certfile'], keyfile=conf['keyfile'], + ca_certs=conf['ca_certs'], + cert_required=conf['cert_required']) + else: + admin_server = wsgi.Server() + admin_server.start(admin_app, + int(conf['admin_port']), conf['admin_host']) - print "Admin API listening on %s:%s" % ( - conf['admin_host'], conf['admin_port']) + print "Admin API (ssl=%s) listening on %s:%s" % ( + conf['admin_ssl'], conf['admin_host'], conf['admin_port']) # Wait until done server.wait() diff --git a/doc/source/ssl.rst b/doc/source/ssl.rst new file mode 100644 index 0000000000..32800ab4fa --- /dev/null +++ b/doc/source/ssl.rst @@ -0,0 +1,86 @@ +.. + Copyright 2011 OpenStack, LLC + All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +===================================================== +Instructions for Keystone x.509 client authentication +===================================================== + +Purpose +------- +Allows the Keystone middleware to authenticate itself with the Keystone server +via an x.509 client certificate. Both Service API and Admin API may be secured +with this feature. + +Certificates +------------ +The following types of certificates are required. A set of certficates is provided +in the examples/ssl directory with the Keystone distribution for testing. Here +is the description of each of them and their purpose: + +1. ca.pem : Certificate Authority chain to validate against. +2. keystone.pem : Public certificate for Keystone server. +3. middleware-key.pem: Public and private certificate for Keystone middleware. +4. cakey.pem : Private key for the CA. +5. keystonekey.pem : Private key for the Keystone server. + +Note that you may choose whatever names you want for these certificates, or combine +the public/private keys in the same file if you wish. These certificates are just +provided as an example. + +Keystone server +--------------- +By default, the Keystone server does not use SSL. To enable SSL with client authentication, +modify the etc/keystone.conf file accordingly: + +1. To enable SSL for Service API: + service_ssl = True +2. To enable SSL for Admin API: + admin_ssl = True +3. To enable SSL client authentication: + cert_required = True +4. Set the location of the Keystone certificate file (example): + certfile = /etc/keystone/ca/certs/keystone.pem +5. Set the location of the Keystone private file (example): + keyfile = /etc/keystone/ca/private/keystonekey.pem +6. Set the location of the CA chain: + ca_certs = /etc/keystone/ca/certs/ca.pem + +Middleware +---------- +Add the following to your middleware configuration to support x.509 client authentication. +If cert_required is set to False on the keystone server, the certfile and keyfile parameters +in steps 3) and 4) may be commented out. + +1. Specify 'https' as the auth_protocol: + auth_protocol = https +2. Modify the protocol in 'auth_uri' to be 'https' as well, if the service API is configured + for SSL: + auth_uri = https://localhost:5000/ +3. Set the location of the middleware certificate file (example): + certfile = /etc/keystone/ca/certs/middleware-key.pem +4. Set the location of the Keystone private file (example): + keyfile = /etc/keystone/ca/certs/middleware-key.pem + +For an example, take a look at the 'echo.ini' middleware configuration for the 'echo' example +service in the examples/echo directory. + +Testing +------- +You can test out how it works by using the 'echo' example service in the examples/echo directory +and the certficates included in the examples/ssl directory. Invoke the echo_client.py with +the path to the client certificate: + + python echo_client.py -s diff --git a/etc/keystone.conf b/etc/keystone.conf index e3ce48b7fa..4921c93d4d 100644 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -32,12 +32,34 @@ service_host = 0.0.0.0 # Port the bind the API server to service_port = 5000 +# SSL for API server +service_ssl = False + # Address to bind the Admin API server admin_host = 0.0.0.0 # Port the bind the Admin API server to admin_port = 35357 +# SSL for API Admin server +admin_ssl = False + +# Keystone certificate file (modify as needed) +# Only required if *_ssl is set to True +certfile = /etc/keystone/ssl/certs/keystone.pem + +# Keystone private key file (modify as needed) +# Only required if *_ssl is set to True +keyfile = /etc/keystone/ssl/private/keystonekey.pem + +# Keystone trusted CA certificates (modify as needed) +# Only required if *_ssl is set to True +ca_certs = /etc/keystone/ssl/certs/ca.pem + +# Client certificate required +# Only relevant if *_ssl is set to True +cert_required = True + #Role that allows to perform admin operations. keystone-admin-role = Admin diff --git a/examples/echo/echo/echo.ini b/examples/echo/echo/echo.ini index 75d64db79f..59f05fe404 100644 --- a/examples/echo/echo/echo.ini +++ b/examples/echo/echo/echo.ini @@ -10,6 +10,9 @@ service_port = 35357 ;used to verify this component with the OpenStack service (or PAPIAuth) service_pass = dTpw +;where to find x.509 client certificates +certfile = ../../ssl/certs/middleware-key.pem +keyfile = ../../ssl/certs/middleware-key.pem [app:echo] paste.app_factory = echo:app_factory @@ -26,6 +29,10 @@ auth_host = 127.0.0.1 auth_port = 35357 auth_protocol = http auth_uri = http://localhost:5000/ +;Uncomment the following out for SSL connections +;auth_protocol = https +;auth_uri = https://localhost:5000/ + ;how to authenticate to the auth service for priviledged operations ;like validate token admin_token = 999888777666 diff --git a/examples/echo/echo_client.py b/examples/echo/echo_client.py index e928cee301..8070f0bd0a 100755 --- a/examples/echo/echo_client.py +++ b/examples/echo/echo_client.py @@ -20,15 +20,29 @@ Implement a client for Echo service using Identity service import httplib import json +import sys + + +def keystone_conn(): + """ Get a connection. If it is SSL, needs the '-s' option, optionally + followed by the location of the cert_file with private key. """ + if '-s' in sys.argv: + cert_file = None + if len(sys.argv) > sys.argv.index('-s') + 1: + cert_file = sys.argv[sys.argv.index('-s') + 1] + conn = httplib.HTTPSConnection("localhost:5000", cert_file=cert_file) + else: + conn = httplib.HTTPConnection("localhost:5000") + return conn def get_auth_token(username, password, tenant): headers = {"Content-type": "application/json", "Accept": "application/json"} - params = {"passwordCredentials": {"username": username, - "password": password, - "tenantId": tenant}} - conn = httplib.HTTPConnection("localhost:5000") + params = {"auth": {"passwordCredentials": + {"username": username, "password": password}, + "tenantName": tenant}} + conn = keystone_conn() conn.request("POST", "/v2.0/tokens", json.dumps(params), headers=headers) response = conn.getresponse() data = response.read() @@ -72,7 +86,7 @@ if __name__ == '__main__': print "\033[91mTrying with valid test credentials...\033[0m" auth = get_auth_token("joeuser", "secrete", "customer-x") obj = json.loads(auth) - token = obj["auth"]["token"]["id"] + token = obj["access"]["token"]["id"] print "Token obtained:", token # Use that token to call an OpenStack service (echo) @@ -94,5 +108,5 @@ if __name__ == '__main__': #Supply bad credentials print "\033[91mTrying with bad credentials...\033[0m" - auth = get_auth_token("joeuser", "wrongpass", "1") + auth = get_auth_token("joeuser", "wrongpass", "customer-x") print "Response:", auth diff --git a/examples/ssl/certs/ca.pem b/examples/ssl/certs/ca.pem new file mode 100644 index 0000000000..07ae29a0a2 --- /dev/null +++ b/examples/ssl/certs/ca.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmTCCAwKgAwIBAgIJALMGu1g0q5GjMA0GCSqGSIb3DQEBBQUAMIGQMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVJvc2V2aWxsZTESMBAGA1UE +ChMJT3BlbnN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTESMBAGA1UEAxMJbG9jYWxo +b3N0MSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnMB4XDTEx +MTAyMDE2MDQ0MloXDTIxMTAxNzE2MDQ0MlowgZAxCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJDQTESMBAGA1UEBxMJUm9zZXZpbGxlMRIwEAYDVQQKEwlPcGVuc3RhY2sx +ETAPBgNVBAsTCEtleXN0b25lMRIwEAYDVQQDEwlsb2NhbGhvc3QxJTAjBgkqhkiG +9w0BCQEWFmtleXN0b25lQG9wZW5zdGFjay5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBAMfYcS0Fs7DRqdGSMVyrLk91vdzs+K6a6NOgppxhETqrOMAjW5yL +ajE2Ly48qfO/BRZR0kgTGSpnv7oiFzWLCvPf63nUnCalkE+uBpksY7BpphnTCJ8F +IsZ6aggAGKto9mmADpiKxt1uSQ6DDpPm8quXbMdSZTFOOVQNPYhwPMYvAgMBAAGj +gfgwgfUwHQYDVR0OBBYEFGA/MhYYUnjIdH9FWFVVo/YODkZBMIHFBgNVHSMEgb0w +gbqAFGA/MhYYUnjIdH9FWFVVo/YODkZBoYGWpIGTMIGQMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCQ0ExEjAQBgNVBAcTCVJvc2V2aWxsZTESMBAGA1UEChMJT3BlbnN0 +YWNrMREwDwYDVQQLEwhLZXlzdG9uZTESMBAGA1UEAxMJbG9jYWxob3N0MSUwIwYJ +KoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnggkAswa7WDSrkaMwDAYD +VR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQBoeuR/pRznAtStj4Axe8Xq1ivL +jXFt2G9Pj+MwLs2wokcUBYz6/rJdSTjW21s4/FQCHiw9K7HA63c4mbjkRRgtJlXo +F5PiQqv4F1KqZmWeIDGxOGStQbgc77unsYYXILI27pSqQLKc9xlli77LekY+BzTK +tr5JYtKMaby4lJTg3A== +-----END CERTIFICATE----- diff --git a/examples/ssl/certs/keystone.pem b/examples/ssl/certs/keystone.pem new file mode 100644 index 0000000000..6460d32a75 --- /dev/null +++ b/examples/ssl/certs/keystone.pem @@ -0,0 +1,62 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=CA, L=Roseville, O=Openstack, OU=Keystone, CN=localhost/emailAddress=keystone@openstack.org + Validity + Not Before: Oct 20 16:34:17 2011 GMT + Not After : Oct 19 16:34:17 2012 GMT + Subject: C=US, ST=CA, L=Roseville, O=Openstack, OU=Keystone, CN=localhost/emailAddress=keystone@openstack.org + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:9e:5a:5c:be:dc:20:d4:af:36:5c:33:6d:72:44: + 94:59:c6:a9:24:ed:fa:8b:2c:53:ab:24:7d:79:46: + cc:a6:45:05:b0:57:b4:0d:d6:8f:f4:d9:a5:11:64: + e4:78:b1:26:30:de:fb:4a:72:c8:97:e7:31:4f:55: + bb:5b:16:d7:22:1b:13:ca:fc:6b:04:bd:15:9c:09: + 51:d6:f9:14:51:67:a3:42:4a:81:ce:98:0f:6e:5c: + ac:7f:36:be:0f:79:ad:07:81:75:a2:21:a8:5f:e5: + 9c:22:71:4c:db:63:b6:44:29:65:22:76:6e:07:98: + de:be:58:3f:b2:fe:cd:27:f7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + C1:E3:A1:36:45:3F:B5:3B:11:A1:23:A4:7E:3A:A0:F9:BC:F6:93:A3 + X509v3 Authority Key Identifier: + keyid:60:3F:32:16:18:52:78:C8:74:7F:45:58:55:55:A3:F6:0E:0E:46:41 + + Signature Algorithm: sha1WithRSAEncryption + 06:86:d7:5d:93:11:94:ce:23:ae:74:b2:16:09:99:32:63:3d: + d9:be:8f:99:87:43:7c:0d:27:25:5c:08:c2:d6:18:37:3c:4e: + b9:06:51:53:a9:d7:93:da:14:a1:25:96:2b:eb:8d:81:9d:68: + 8d:ec:b8:1f:9e:09:80:25:fb:be:f8:20:5b:fc:ca:6c:3d:38: + c7:09:36:aa:dd:f8:0c:01:35:3e:c5:c5:3b:60:24:8c:5f:c5: + 44:e7:7f:9b:ce:b6:d5:85:b7:93:e4:8a:a5:a9:90:ff:2d:09: + 56:8c:e6:17:1f:07:33:0a:46:73:b1:65:13:d8:6f:39:76:3a: + 93:87 +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQUFADCBkDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlSb3NldmlsbGUxEjAQBgNVBAoTCU9wZW5z +dGFjazERMA8GA1UECxMIS2V5c3RvbmUxEjAQBgNVBAMTCWxvY2FsaG9zdDElMCMG +CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzAeFw0xMTEwMjAxNjM0 +MTdaFw0xMjEwMTkxNjM0MTdaMIGQMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +EjAQBgNVBAcTCVJvc2V2aWxsZTESMBAGA1UEChMJT3BlbnN0YWNrMREwDwYDVQQL +EwhLZXlzdG9uZTESMBAGA1UEAxMJbG9jYWxob3N0MSUwIwYJKoZIhvcNAQkBFhZr +ZXlzdG9uZUBvcGVuc3RhY2sub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQCeWly+3CDUrzZcM21yRJRZxqkk7fqLLFOrJH15RsymRQWwV7QN1o/02aURZOR4 +sSYw3vtKcsiX5zFPVbtbFtciGxPK/GsEvRWcCVHW+RRRZ6NCSoHOmA9uXKx/Nr4P +ea0HgXWiIahf5ZwicUzbY7ZEKWUidm4HmN6+WD+y/s0n9wIDAQABo3sweTAJBgNV +HRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZp +Y2F0ZTAdBgNVHQ4EFgQUweOhNkU/tTsRoSOkfjqg+bz2k6MwHwYDVR0jBBgwFoAU +YD8yFhhSeMh0f0VYVVWj9g4ORkEwDQYJKoZIhvcNAQEFBQADgYEABobXXZMRlM4j +rnSyFgmZMmM92b6PmYdDfA0nJVwIwtYYNzxOuQZRU6nXk9oUoSWWK+uNgZ1ojey4 +H54JgCX7vvggW/zKbD04xwk2qt34DAE1PsXFO2AkjF/FROd/m8621YW3k+SKpamQ +/y0JVozmFx8HMwpGc7FlE9hvOXY6k4c= +-----END CERTIFICATE----- diff --git a/examples/ssl/certs/middleware-key.pem b/examples/ssl/certs/middleware-key.pem new file mode 100644 index 0000000000..780de11086 --- /dev/null +++ b/examples/ssl/certs/middleware-key.pem @@ -0,0 +1,77 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=CA, L=Roseville, O=Openstack, OU=Keystone, CN=localhost/emailAddress=keystone@openstack.org + Validity + Not Before: Oct 20 17:22:02 2011 GMT + Not After : Oct 19 17:22:02 2012 GMT + Subject: C=US, ST=CA, O=Openstack, OU=Middleware, CN=localhost/emailAddress=middleware@openstack.org + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:cb:8d:ff:0a:f8:1f:da:0b:65:d9:15:86:e7:4a: + 89:07:81:26:7a:2e:ef:67:30:bb:5b:88:3e:73:31: + 0e:c9:d9:eb:84:55:7c:57:1b:07:8a:29:7f:41:ed: + 1a:47:b2:c4:74:3c:dc:52:81:81:ba:6c:43:b8:44: + bd:83:20:28:4a:82:03:34:f2:1e:88:89:1c:f3:d6: + ef:02:27:9f:7b:4b:dc:ed:50:91:7a:13:a0:8f:5f: + 44:10:a6:17:01:6f:7d:7a:3a:a2:1a:28:4e:6e:c5: + b6:06:0b:ba:5c:c9:e9:15:39:95:54:63:bb:40:90: + 5d:5d:76:f6:ae:ed:ee:ed:85 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 5A:34:DE:19:11:FF:77:19:2E:E5:6C:36:FA:42:17:6B:46:AF:6A:61 + X509v3 Authority Key Identifier: + keyid:60:3F:32:16:18:52:78:C8:74:7F:45:58:55:55:A3:F6:0E:0E:46:41 + + Signature Algorithm: sha1WithRSAEncryption + a2:1b:e0:d3:e5:c5:35:ad:18:cb:79:a4:fc:f3:d6:7b:53:1e: + dd:28:95:e0:6c:b0:db:fe:aa:30:04:19:c8:99:7a:eb:cb:ed: + dd:74:29:ad:f8:89:6a:ed:d0:10:35:b3:62:36:a2:b0:cc:9f: + 86:e8:96:fd:d7:1b:5e:2c:64:b5:5d:f3:bf:1a:1a:07:8b:01: + 1f:5f:09:c3:e1:62:cd:30:35:1a:08:e1:cd:71:be:8c:87:de: + f6:7d:40:1b:c6:5f:f0:80:a0:68:55:01:00:74:86:08:52:7e: + c7:fd:62:f9:e3:d0:f8:0b:b0:64:d9:20:70:80:ec:95:11:74: + fb:0b +-----BEGIN CERTIFICATE----- +MIIDAzCCAmygAwIBAgIBATANBgkqhkiG9w0BAQUFADCBkDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlSb3NldmlsbGUxEjAQBgNVBAoTCU9wZW5z +dGFjazERMA8GA1UECxMIS2V5c3RvbmUxEjAQBgNVBAMTCWxvY2FsaG9zdDElMCMG +CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzAeFw0xMTEwMjAxNzIy +MDJaFw0xMjEwMTkxNzIyMDJaMIGAMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +EjAQBgNVBAoTCU9wZW5zdGFjazETMBEGA1UECxMKTWlkZGxld2FyZTESMBAGA1UE +AxMJbG9jYWxob3N0MScwJQYJKoZIhvcNAQkBFhhtaWRkbGV3YXJlQG9wZW5zdGFj +ay5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMuN/wr4H9oLZdkVhudK +iQeBJnou72cwu1uIPnMxDsnZ64RVfFcbB4opf0HtGkeyxHQ83FKBgbpsQ7hEvYMg +KEqCAzTyHoiJHPPW7wInn3tL3O1QkXoToI9fRBCmFwFvfXo6ohooTm7FtgYLulzJ +6RU5lVRju0CQXV129q7t7u2FAgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBRa +NN4ZEf93GS7lbDb6QhdrRq9qYTAfBgNVHSMEGDAWgBRgPzIWGFJ4yHR/RVhVVaP2 +Dg5GQTANBgkqhkiG9w0BAQUFAAOBgQCiG+DT5cU1rRjLeaT889Z7Ux7dKJXgbLDb +/qowBBnImXrry+3ddCmt+Ilq7dAQNbNiNqKwzJ+G6Jb91xteLGS1XfO/GhoHiwEf +XwnD4WLNMDUaCOHNcb6Mh972fUAbxl/wgKBoVQEAdIYIUn7H/WL549D4C7Bk2SBw +gOyVEXT7Cw== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDLjf8K+B/aC2XZFYbnSokHgSZ6Lu9nMLtbiD5zMQ7J2euEVXxX +GweKKX9B7RpHssR0PNxSgYG6bEO4RL2DIChKggM08h6IiRzz1u8CJ597S9ztUJF6 +E6CPX0QQphcBb316OqIaKE5uxbYGC7pcyekVOZVUY7tAkF1ddvau7e7thQIDAQAB +AoGAITSpzV1KvOQtGiuz1RlIn0vHPhlX/opplfX00g/HrM/65pyXaxJCuZwpYVTP +e7DC8X9YJbFwuzucFHxKOhDN4YbnW145bgfHbI9KLXtZiDvXvHg2MGKjpL/S3Lp3 +zzWBo8gknmFGLK41WbYCCWKcvikEb3/KowcooznY5X5BjWECQQD6NC9Bi2EUUyPR +B2ZT3C3h2Hj53yqLkJzP0PaxTC+j7rsycy5r7UiOK8+8aC1T9EsaJrmEKlYBmlbd +lVdhohpNAkEA0EUphaVGURlNmXZgYdSZ1rrpJTvKbFtXmUCowi7Ml2h/oTuHDFHf +i4P8//79YB1uJ4Ll9edjJsZqtAErUTnMGQJBAJcKp7hutqU5Z3bJe8mGMqCTOLzH +LvzfyPpfkH0Jm/zfolxbUhvPO6yv4BFB5pM295uK4xVZJWCEVoofnIeQ/0UCQHuK +ex3esv5KTyCX+oYtkW+xgbjnZaSu7iBnHXPKROwPPZ4LbIlfS4Y7rejAfdX0vzHK +0NP0BHmsuwC5rNNKwIkCQBZqTnLVcisz1FRM2g/OKfWMx+lhVf5fIng+jUEJCdNE +fGjCUu4BRs+nXq6EzoijLvtrmRmFL7VYAKdabSVeLRc= +-----END RSA PRIVATE KEY----- diff --git a/examples/ssl/private/cakey.pem b/examples/ssl/private/cakey.pem new file mode 100644 index 0000000000..36e38e090c --- /dev/null +++ b/examples/ssl/private/cakey.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,116D8984CC1AC50A + +PnDGqu3+5ITsGtOwCucdQBs7UmPpJKk3x+UuBdpJuygMEgGM70P+eN+RLTH/vaAl +GFWV9wvlL7j+azrEYlbiKhHn4+6SmDSWjjSVM5wGclzH/UYhyhVe/GZsJ8axW278 +6EdwzmrbvIjuPTN/dJjyXdeOlFFpoCST8TI03+qYo9T0L86560Y3SjTr/hHhlVyL +PgwfcN3wdarhPloJvFoV10kNH3MBpgGeclNQcNVRH7+Z2DwzHgV3bW28w1h4dOI7 +RrPpa1YaAi0lTltuiZYLUtTBI/+xEDf3kFkeSNSdl3sLp9faHUoosVObdFfLCmV1 ++66MdqgesPFipkfGPlTGuUX9CmYMooCn+hs7+tVZUqCl/fcErFWeW8iS5+nrat7f +HBiAsOTZ96AEvy/FksYPymrdaK085aODgPqSfR2pvMuF66iKS1xRZiTpMnDApTVN +A6BOZdJgTqGX4yny7ORxQ90xkv39oZYS9cc10Hqec1DG1LWy9dfvavEPk7/GejiT +Z5SMbIHHiNe5tNTomGqtgLIhjfoRXH14zbPGbJ5bI0REJ+sdUM3ItH75tTHYQUIb +S8UQBkHzU+ExK4q5E3BvKR7UH0KD5z6B6QhAyCB6mQp+63nsIP5cImXuAY9u0s1a +3tOmvUpXWDpqJLeAShb3DAPz5+FMx4mbT5oZq1Y8q5RDqMSSrB7XilAroCqasUIb +LoLNMri7WKcrCT1dKjN4y17ucwU8wLPo7Lpo+x5/XWqQA5qSB83YG9nh6nuzYNyo +aUsLH4cfAj3vCPU+KQux5jJfpcma9fyxVfCfa55dmakmGM8ww6ZXXQ== +-----END RSA PRIVATE KEY----- diff --git a/examples/ssl/private/keystonekey.pem b/examples/ssl/private/keystonekey.pem new file mode 100644 index 0000000000..563c65f02f --- /dev/null +++ b/examples/ssl/private/keystonekey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCeWly+3CDUrzZcM21yRJRZxqkk7fqLLFOrJH15RsymRQWwV7QN +1o/02aURZOR4sSYw3vtKcsiX5zFPVbtbFtciGxPK/GsEvRWcCVHW+RRRZ6NCSoHO +mA9uXKx/Nr4Pea0HgXWiIahf5ZwicUzbY7ZEKWUidm4HmN6+WD+y/s0n9wIDAQAB +AoGAf6eY3MPYM5yL1ggfUt62OSlNcdfnAgrZ6D2iaQIKOH+r9ly9aepuYpSR3VPY +WvN0NjGLopil3M8jkTEruGLRSgin8+v+qlcRFsoXamegc3NV4XtxJhSmSIocKIIK +14w5YxcDz1QGqoati4LxQ1D6V5eNhiO65YhdcUDarGnlcAECQQDK4vcBGLY7H91f +lGT/oFJ0crqF4V+bLxMO28NhtS0G+GoM0MKrPfIu+nZDlKQzzHUlEZMNXSLz1T+T +po92UVe3AkEAx87ZKDK4xZZRNz0dAe29a3gQ6PmVkav1+NIxr0MP7Ff4tH6K/uoz +96OZpZg+TxdaoxSeNltuUelt3/xPs9AxwQJBAI01t1FuD7fLD9ssf7djsMAX8jao +jFCITS10S+K/pR1K3RUaX8OsE9oavSGAXWEoFwi72KvefStU6zErJoLlTrUCQCG5 +wmHMne+L/c1rHVhT/qMDMyd/6UUbV3tWT1ib4zYraylcKq34bikgjjCrT+kdsgjQ +1BustyRQWGF0PyfEvoECQGhVOY2byAOEau+GeTC0c3LIDoErx6WaW3d9ty3Tmx3G +Y81XHlbO4Lw2q8fWZ8Ah2ptjv2IpKj0GAGRiJ5NnPTM= +-----END RSA PRIVATE KEY----- diff --git a/keystone/client.py b/keystone/client.py index e845c08e4b..e36104529a 100644 --- a/keystone/client.py +++ b/keystone/client.py @@ -30,7 +30,7 @@ class ServiceClient(object): _default_port = 5000 - def __init__(self, host, port=None): + def __init__(self, host, port=None, is_ssl=False, cert_file=None): """Initialize client. :param host: The hostname or IP of the Keystone service to use @@ -39,6 +39,8 @@ class ServiceClient(object): """ self.host = host self.port = port or self._default_port + self.is_ssl = is_ssl + self.cert_file = cert_file def _http_request(self, verb, path, body=None, headers=None): """Perform an HTTP request and return the HTTP response. @@ -50,7 +52,11 @@ class ServiceClient(object): :returns: httplib.HTTPResponse object """ - connection = httplib.HTTPConnection(self.auth_address) + if (self.is_ssl): + connection = httplib.HTTPSConnection(self.auth_address, + cert_file=self.cert_file) + else: + connection = httplib.HTTPConnection(self.auth_address) connection.request(verb, path, body=body, headers=headers) response = connection.getresponse() @@ -109,7 +115,8 @@ class AdminClient(ServiceClient): _default_admin_name = "admin" _default_admin_pass = "password" - def __init__(self, host, port=None, admin_name=None, admin_pass=None): + def __init__(self, host, port=None, is_ssl=False, cert_file=None, + admin_name=None, admin_pass=None): """Initialize client. :param host: The hostname or IP of the Keystone service to use @@ -118,7 +125,8 @@ class AdminClient(ServiceClient): :param admin_pass: The password to use for the admin account """ - super(AdminClient, self).__init__(host, port=port) + super(AdminClient, self).__init__(host, port=port, is_ssl=is_ssl, + cert_file=cert_file) self.admin_name = admin_name or self._default_admin_name self.admin_pass = admin_pass or self._default_admin_pass self._admin_token = None diff --git a/keystone/common/bufferedhttp.py b/keystone/common/bufferedhttp.py index fdb35ee657..1b1b691e8f 100644 --- a/keystone/common/bufferedhttp.py +++ b/keystone/common/bufferedhttp.py @@ -101,7 +101,8 @@ class BufferedHTTPConnection(HTTPConnection): def http_connect(ipaddr, port, device, partition, method, path, - headers=None, query_string=None, ssl=False): + headers=None, query_string=None, ssl=False, key_file=None, + cert_file=None): """ Helper function to create an HTTPConnection object. If ssl is set True, HTTPSConnection will be used. However, if ssl=False, BufferedHTTPConnection @@ -116,26 +117,18 @@ def http_connect(ipaddr, port, device, partition, method, path, :param headers: dictionary of headers :param query_string: request query string :param ssl: set True if SSL should be used (default: False) + :param key_file Private key file (not needed if cert_file has private key) + :param cert_file Certificate file (Keystore) :returns: HTTPConnection object """ - if ssl: - conn = HTTPSConnection('%s:%s' % (ipaddr, port)) - else: - conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port)) path = quote('/' + device + '/' + str(partition) + path) - if query_string: - path += '?' + query_string - conn.path = path - conn.putrequest(method, path) - if headers: - for header, value in headers.iteritems(): - conn.putheader(header, value) - conn.endheaders() - return conn + return http_connect_raw(ipaddr, port, device, partition, method, path, + headers, query_string, ssl, key_file, cert_file) def http_connect_raw(ipaddr, port, method, path, headers=None, - query_string=None, ssl=False): + query_string=None, ssl=False, key_file=None, + cert_file=None): """ Helper function to create an HTTPConnection object. If ssl is set True, HTTPSConnection will be used. However, if ssl=False, BufferedHTTPConnection @@ -148,10 +141,13 @@ def http_connect_raw(ipaddr, port, method, path, headers=None, :param headers: dictionary of headers :param query_string: request query string :param ssl: set True if SSL should be used (default: False) + :param key_file Private key file (not needed if cert_file has private key) + :param cert_file Certificate file (Keystore) :returns: HTTPConnection object """ if ssl: - conn = HTTPSConnection('%s:%s' % (ipaddr, port)) + conn = HTTPSConnection('%s:%s' % (ipaddr, port), key_file=key_file, + cert_file=cert_file) else: conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port)) if query_string: diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index fa83b7d155..7edcff1d2d 100755 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -25,6 +25,7 @@ import json import logging import sys import datetime +import ssl import eventlet.wsgi eventlet.patcher.monkey_patch(all=False, socket=True) @@ -111,6 +112,24 @@ class Server(object): log=WritableLogger(logger, logging.root.level)) +class SslServer(Server): + """SSL Server class to manage multiple WSGI sockets and applications.""" + def start(self, application, port, host='0.0.0.0', backlog=128, + certfile=None, keyfile=None, ca_certs=None, + cert_required='True'): + """Run a 2-way SSL WSGI server with the given application.""" + socket = eventlet.listen((host, port), backlog=backlog) + if cert_required == 'True': + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + sslsocket = eventlet.wrap_ssl(socket, certfile=certfile, + keyfile=keyfile, + server_side=True, cert_reqs=cert_reqs, + ca_certs=ca_certs) + self.pool.spawn_n(self._run, application, sslsocket) + + class Middleware(object): """ Base WSGI middleware wrapper. These classes require an application to be diff --git a/keystone/middleware/auth_token.py b/keystone/middleware/auth_token.py index 05c7b3deb9..3430a58818 100755 --- a/keystone/middleware/auth_token.py +++ b/keystone/middleware/auth_token.py @@ -127,6 +127,10 @@ class AuthProtocol(object): # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = conf.get('admin_token') + # Certificate file and key file used to authenticate with Keystone + # server + self.cert_file = conf.get('certfile', None) + self.key_file = conf.get('keyfile', None) def __init__(self, app, conf): """ Common initialization code """ @@ -204,26 +208,6 @@ class AuthProtocol(object): #Send request downstream return self._forward_request(env, start_response, proxy_headers) - # NOTE(todd): unused - def get_admin_auth_token(self, username, password): - """ - This function gets an admin auth token to be used by this service to - validate a user's token. Validate_token is a priviledged call so - it needs to be authenticated by a service that is calling it - """ - headers = {"Content-type": "application/json", - "Accept": "application/json"} - params = {"passwordCredentials": {"username": username, - "password": password, - "tenantId": "1"}} - conn = httplib.HTTPConnection("%s:%s" \ - % (self.auth_host, self.auth_port)) - conn.request("POST", "/v2.0/tokens", json.dumps(params), \ - headers=headers) - response = conn.getresponse() - data = response.read() - return data - def _get_claims(self, env): """Get claims from request""" claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) @@ -262,8 +246,10 @@ class AuthProtocol(object): #Khaled's version uses creds to get a token # "X-Auth-Token": admin_token} # we're using a test token from the ini file for now - conn = http_connect(self.auth_host, self.auth_port, 'GET', - '/v2.0/tokens/%s' % claims, headers=headers) + conn = http_connect(self.auth_host, self.auth_port, 'HEAD', + '/v2.0/tokens/%s' % claims, headers=headers, + ssl=(self.auth_protocol == 'https'), + key_file=self.key_file, cert_file=self.cert_file) resp = conn.getresponse() # data = resp.read() conn.close() @@ -289,7 +275,9 @@ class AuthProtocol(object): # "X-Auth-Token": admin_token} # we're using a test token from the ini file for now conn = http_connect(self.auth_host, self.auth_port, 'GET', - '/v2.0/tokens/%s' % claims, headers=headers) + '/v2.0/tokens/%s' % claims, headers=headers, + ssl=(self.auth_protocol == 'https'), + key_file=self.key_file, cert_file=self.cert_file) resp = conn.getresponse() data = resp.read() conn.close() diff --git a/keystone/test/__init__.py b/keystone/test/__init__.py index e701a93174..9133432d2c 100644 --- a/keystone/test/__init__.py +++ b/keystone/test/__init__.py @@ -5,9 +5,12 @@ import tempfile import time import unittest2 as unittest +from functional.common import HttpTestCase + TEST_DIR = os.path.abspath(os.path.dirname(__file__)) BASE_DIR = os.path.abspath(os.path.join(TEST_DIR, '..', '..')) +TEST_CERT = os.path.join(BASE_DIR, 'examples/ssl/certs/middleware-key.pem') def execute(cmd, raise_error=True): @@ -26,7 +29,6 @@ def execute(cmd, raise_error=True): # Make sure that we use the programs in the # current source directory's bin/ directory. env['PATH'] = os.path.join(BASE_DIR, 'bin') + ':' + env['PATH'] - process = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, @@ -58,7 +60,8 @@ class KeystoneTest(object): end of test execution from the temporary space used to run these tests). """ - CONF_PARAMS = {'test_dir': TEST_DIR} + CONF_PARAMS = {'test_dir': TEST_DIR, 'base_dir': BASE_DIR} + isSsl = False def clear_database(self): """Remove any test databases or files generated by previous tests.""" @@ -81,6 +84,10 @@ class KeystoneTest(object): self.clear_database() self.construct_temp_conf_file() + # Set client certificate for test client + if (self.isSsl == True): + os.environ['cert_file'] = TEST_CERT + # run the keystone server print "Starting the keystone server..." self.server = subprocess.Popen( @@ -115,7 +122,6 @@ class KeystoneTest(object): execute('coverage run %s discover -t %s -s %s' % ('/usr/bin/unit2', BASE_DIR, TEST_DIR)) else: - execute('unit2 discover -f -t %s -s %s' % - (BASE_DIR, TEST_DIR)) + execute('unit2 discover -f -t %s -s %s' % (BASE_DIR, TEST_DIR)) finally: self.tearDown() diff --git a/keystone/test/etc/ldap.conf.template b/keystone/test/etc/ldap.conf.template index ff7ab7b9e7..354e47fcb5 100755 --- a/keystone/test/etc/ldap.conf.template +++ b/keystone/test/etc/ldap.conf.template @@ -10,8 +10,10 @@ service-header-mappings = { 'cdn' : 'X-CDN-Manageent-Url'} service_host = 0.0.0.0 service_port = 5000 +service_ssl = False admin_host = 0.0.0.0 admin_port = 35357 +admin_ssl = False keystone-admin-role = Admin keystone-service-admin-role = KeystoneServiceAdmin hash-password = True diff --git a/keystone/test/etc/memcache.conf.template b/keystone/test/etc/memcache.conf.template index e785ae6069..c861ead516 100644 --- a/keystone/test/etc/memcache.conf.template +++ b/keystone/test/etc/memcache.conf.template @@ -10,8 +10,10 @@ service-header-mappings = { 'cdn' : 'X-CDN-Manageent-Url'} service_host = 0.0.0.0 service_port = 5000 +service_ssl = False admin_host = 0.0.0.0 admin_port = 35357 +admin_ssl = False keystone-admin-role = Admin keystone-service-admin-role = KeystoneServiceAdmin diff --git a/keystone/test/etc/sql.conf.template b/keystone/test/etc/sql.conf.template index a2e97f3998..0013b6f5ce 100644 --- a/keystone/test/etc/sql.conf.template +++ b/keystone/test/etc/sql.conf.template @@ -10,8 +10,10 @@ service-header-mappings = { 'cdn' : 'X-CDN-Manageent-Url'} service_host = 0.0.0.0 service_port = 5000 +service_ssl = False admin_host = 0.0.0.0 admin_port = 35357 +admin_ssl = False keystone-admin-role = Admin keystone-service-admin-role = KeystoneServiceAdmin hash-password = True diff --git a/keystone/test/etc/ssl.conf.template b/keystone/test/etc/ssl.conf.template new file mode 100644 index 0000000000..bbdab3e1d3 --- /dev/null +++ b/keystone/test/etc/ssl.conf.template @@ -0,0 +1,55 @@ +[DEFAULT] +verbose = False +debug = False +default_store = sqlite +log_file = %(test_dir)s/keystone.ssl.log +backends = keystone.backends.sqlalchemy +service-header-mappings = { + 'nova' : 'X-Server-Management-Url', + 'swift' : 'X-Storage-Url', + 'cdn' : 'X-CDN-Manageent-Url'} +service_host = 0.0.0.0 +service_port = 5000 +service_ssl = True +admin_host = 0.0.0.0 +admin_port = 35357 +admin_ssl = True +keystone-admin-role = Admin +keystone-service-admin-role = KeystoneServiceAdmin +hash-password = True +certfile = %(base_dir)s/examples/ssl/certs/keystone.pem +keyfile = %(base_dir)s/examples/ssl/private/keystonekey.pem +ca_certs = %(base_dir)s/examples/ssl/certs/ca.pem +cert_required = True + +[keystone.backends.sqlalchemy] +sql_connection = for_testing_only +sql_idle_timeout = 30 +backend_entities = ['Endpoints', 'Credentials', 'EndpointTemplates', 'Tenant', 'User', 'UserRoleAssociation', 'Role', 'Token', 'Service'] + +[pipeline:admin] +pipeline = + urlrewritefilter + admin_api + +[pipeline:keystone-legacy-auth] +pipeline = + urlrewritefilter + legacy_auth + RAX-KEY-extension + service_api + +[app:service_api] +paste.app_factory = keystone.server:service_app_factory + +[app:admin_api] +paste.app_factory = keystone.server:admin_app_factory + +[filter:urlrewritefilter] +paste.filter_factory = keystone.middleware.url:filter_factory + +[filter:legacy_auth] +paste.filter_factory = keystone.frontends.legacy_token_auth:filter_factory + +[filter:RAX-KEY-extension] +paste.filter_factory = keystone.contrib.extensions.service.raxkey.frontend:filter_factory \ No newline at end of file diff --git a/keystone/test/functional/common.py b/keystone/test/functional/common.py index 335a43891f..b759c2ec95 100644 --- a/keystone/test/functional/common.py +++ b/keystone/test/functional/common.py @@ -2,9 +2,17 @@ import unittest2 as unittest import httplib import uuid import json +import os from xml.etree import ElementTree +def isSsl(): + """ See if we are testing with SSL. If cert is non-empty, we are! """ + if 'cert_file' in os.environ: + return os.environ['cert_file'] + return None + + class HttpTestCase(unittest.TestCase): """Performs generic HTTP request testing. @@ -23,7 +31,13 @@ class HttpTestCase(unittest.TestCase): headers = {} if not headers else headers # Initialize a connection - connection = httplib.HTTPConnection(host, port, timeout=20) + cert_file = isSsl() + if (cert_file != None): + connection = httplib.HTTPSConnection(host, port, + cert_file=cert_file, + timeout=20) + else: + connection = httplib.HTTPConnection(host, port, timeout=20) # Perform the request connection.request(method, path, body, headers) @@ -906,8 +920,7 @@ class FunctionalTestCase(ApiTestCase): "global": is_global, "versionId": version_id, "versionInfo": version_info, - "versionList": version_list - }} + "versionList": version_list}} return self.post_endpoint_template(as_json=data, **kwargs) def remove_endpoint_template(self, endpoint_template_id=None, **kwargs): diff --git a/keystone/test/functional/test_client.py b/keystone/test/functional/test_client.py index 3942884506..62946fa9b5 100644 --- a/keystone/test/functional/test_client.py +++ b/keystone/test/functional/test_client.py @@ -2,6 +2,7 @@ import unittest import keystone.common.exception import keystone.client +from common import isSsl class TestAdminClient(unittest.TestCase): @@ -13,7 +14,10 @@ class TestAdminClient(unittest.TestCase): """ Run before each test. """ + cert_file = isSsl() self.client = keystone.client.AdminClient("127.0.0.1", + is_ssl=(cert_file != None), + cert_file=cert_file, admin_name="admin", admin_pass="secrete") @@ -71,7 +75,10 @@ class TestServiceClient(unittest.TestCase): """ Run before each test. """ - self.client = keystone.client.ServiceClient("127.0.0.1") + cert_file = isSsl() + self.client = keystone.client.ServiceClient("127.0.0.1", + is_ssl=(cert_file != None), + cert_file=cert_file) def test_admin_get_token(self): """ diff --git a/run_tests.py b/run_tests.py index d799a30c3d..80c181d328 100755 --- a/run_tests.py +++ b/run_tests.py @@ -10,6 +10,12 @@ class SQLTest(KeystoneTest): test_files = ('keystone.db',) +class SSLTest(KeystoneTest): + config_name = 'ssl.conf.template' + test_files = ('keystone.db',) + isSsl = True + + class MemcacheTest(KeystoneTest): """Test defined using only SQLAlchemy and Memcache back-end""" config_name = 'memcache.conf.template' @@ -25,6 +31,7 @@ TESTS = [ SQLTest, # currently failing, and has yet to pass in jenkins: MemcacheTest, LDAPTest, + SSLTest, ] if __name__ == '__main__': diff --git a/run_tests.sh b/run_tests.sh index 8d04e9e25a..ed47a33ccd 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -42,11 +42,11 @@ addlargs= wrapper="" just_pep8=0 just_pylint=0 -RUNTESTS="python run_tests.py $addlargs" for arg in "$@"; do process_option $arg done +RUNTESTS="python run_tests.py $addlargs" function run_tests { # Just run the test suites in current environment