diff --git a/MANIFEST.in b/MANIFEST.in index c9b076cdf4..011f0e5d6a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,5 +15,6 @@ graft bin graft doc graft tests graft tools -recursive-include keystone *.json *.xml *.cfg README +graft examples +recursive-include keystone *.json *.xml *.cfg *.pem README global-exclude *.pyc *.sdx *.log *.db *.swp diff --git a/bin/keystone-all b/bin/keystone-all index bc4ab4b8e1..be9f3e93f2 100755 --- a/bin/keystone-all +++ b/bin/keystone-all @@ -28,7 +28,11 @@ CONF = config.CONF def create_server(conf, name, host, port): app = deploy.loadapp('config:%s' % conf, name=name) - return wsgi.Server(app, host=host, port=port) + server = wsgi.Server(app, host=host, port=port) + if CONF.ssl.enable: + server.set_ssl(CONF.ssl.certfile, CONF.ssl.keyfile, + CONF.ssl.ca_certs, CONF.ssl.cert_required) + return server def serve(*servers): diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 9095b7c07f..f6fb02396c 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -60,6 +60,7 @@ values are organized into the following sections: * ``[catalog]`` - service catalog driver configuration * ``[token]`` - token driver configuration * ``[policy]`` - policy system driver configuration for RBAC +* ``[ssl]`` - SSL configuration The Keystone configuration file is expected to be named ``keystone.conf``. When starting keystone, you can specify a different configuration file to @@ -149,6 +150,58 @@ choosing the output levels and formats. .. _Paste: http://pythonpaste.org/ .. _`python logging module`: http://docs.python.org/library/logging.html +SSL +--- + +Keystone may be configured to support 2-way SSL out-of-the-box. The x509 +certificates used by Keystone must be obtained externally and configured for use +with Keystone as described in this section. However, a set of sample 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: + +Types of certificates +^^^^^^^^^^^^^^^^^^^^^ + +ca.pem + Certificate Authority chain to validate against. + +keystone.pem + Public certificate for Keystone server. + +middleware.pem + Public and private certificate for Keystone middleware/client. + +cakey.pem + Private key for the CA. + +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. + +Configuration +^^^^^^^^^^^^^ + +To enable SSL with client authentication, modify the etc/keystone.conf file accordingly +under the [ssl] section. SSL configuration example using the included sample +certificates:: + + [ssl] + enable = True + certfile = + keyfile = + ca_certs = + cert_required = True + +* ``enable``: True enables SSL. Defaults to False. +* ``certfile``: Path to Keystone public certificate file. +* ``keyfile``: Path to Keystone private certificate file. If the private key is included in the certfile, the keyfile maybe omitted. +* ``ca_certs``: Path to CA trust chain. +* ``cert_required``: Requires client certificate. Defaults to False. + + Sample Configuration Files -------------------------- diff --git a/doc/source/middlewarearchitecture.rst b/doc/source/middlewarearchitecture.rst index 9216719bf8..dc0b1d5397 100644 --- a/doc/source/middlewarearchitecture.rst +++ b/doc/source/middlewarearchitecture.rst @@ -133,6 +133,9 @@ a WSGI component. Example for the auth_token middleware:: admin_tenant_name = service ;Uncomment next line and check ip:port to use memcached to cache tokens ;memcache_servers = 127.0.0.1:11211 + ;Uncomment next 2 lines if Keystone server is validating client cert + certfile = + keyfile = Configuration Options --------------------- @@ -153,6 +156,9 @@ Configuration Options * ``auth_port``: (optional, default `35357`) the port used to validate tokens * ``auth_protocol``: (optional, default `https`) * ``auth_uri``: (optional, defaults to `auth_protocol`://`auth_host`:`auth_port`) +* ``certfile``: (required, if Keystone server requires client cert) +* ``keyfile``: (required, if Keystone server requires client cert) This can be + the same as the certfile if the certfile includes the private key. Caching for improved response ----------------------------- diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 03a6b753f8..11961b18ad 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -81,6 +81,14 @@ [ec2] # driver = keystone.contrib.ec2.backends.kvs.Ec2 +[ssl] +#enable = True +#certfile = /etc/keystone/ssl/certs/keystone.pem +#keyfile = /etc/keystone/ssl/private/keystonekey.pem +#ca_certs = /etc/keystone/ssl/certs/ca.pem +#cert_required = True + + [ldap] # url = ldap://localhost # user = dc=Manager,dc=example,dc=com 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.pem b/examples/ssl/certs/middleware.pem new file mode 100644 index 0000000000..780de11086 --- /dev/null +++ b/examples/ssl/certs/middleware.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/common/bufferedhttp.py b/keystone/common/bufferedhttp.py index 967161d97e..ecc19b3315 100644 --- a/keystone/common/bufferedhttp.py +++ b/keystone/common/bufferedhttp.py @@ -107,7 +107,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 @@ -122,26 +123,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 @@ -154,10 +147,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 34bbbd6993..a70e0c427e 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -23,12 +23,10 @@ import json import sys -import eventlet import eventlet.wsgi eventlet.patcher.monkey_patch(all=False, socket=True, time=True) -import routes import routes.middleware -import webob +import ssl import webob.dec import webob.exc @@ -61,6 +59,8 @@ class Server(object): self.pool = eventlet.GreenPool(threads) self.socket_info = {} self.greenthread = None + self.do_ssl = False + self.cert_required = False def start(self, key=None, backlog=128): """Run a WSGI server with the given application.""" @@ -69,9 +69,30 @@ class Server(object): 'host': self.host, 'port': self.port}) socket = eventlet.listen((self.host, self.port), backlog=backlog) - self.greenthread = self.pool.spawn(self._run, self.application, socket) if key: self.socket_info[key] = socket.getsockname() + # SSL is enabled + if self.do_ssl: + if self.cert_required: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + sslsocket = eventlet.wrap_ssl(socket, certfile=self.certfile, + keyfile=self.keyfile, + server_side=True, + cert_reqs=cert_reqs, + ca_certs=self.ca_certs) + socket = sslsocket + + self.greenthread = self.pool.spawn(self._run, self.application, socket) + + def set_ssl(self, certfile, keyfile=None, ca_certs=None, + cert_required=True): + self.certfile = certfile + self.keyfile = keyfile + self.ca_certs = ca_certs + self.cert_required = cert_required + self.do_ssl = True def kill(self): if self.greenthread: diff --git a/keystone/config.py b/keystone/config.py index 4b50f41148..5c3dff28d0 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -145,6 +145,12 @@ register_str('admin_port', default=35357) register_str('public_port', default=5000) register_str('onready') +#ssl options +register_bool('enable', group='ssl', default=False) +register_str('certfile', group='ssl', default=None) +register_str('keyfile', group='ssl', default=None) +register_str('ca_certs', group='ssl', default=None) +register_bool('cert_required', group='ssl', default=False) # sql options register_str('connection', group='sql', default='sqlite:///keystone.db') diff --git a/keystone/middleware/auth_token.py b/keystone/middleware/auth_token.py index 92c889d508..dd91fa48d9 100644 --- a/keystone/middleware/auth_token.py +++ b/keystone/middleware/auth_token.py @@ -125,17 +125,21 @@ class AuthProtocol(object): # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') self.auth_port = int(conf.get('auth_port', 35357)) - auth_protocol = conf.get('auth_protocol', 'https') - if auth_protocol == 'http': + self.auth_protocol = conf.get('auth_protocol', 'https') + if self.auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection else: self.http_client_class = httplib.HTTPSConnection - default_auth_uri = '%s://%s:%s' % (auth_protocol, + default_auth_uri = '%s://%s:%s' % (self.auth_protocol, self.auth_host, self.auth_port) self.auth_uri = conf.get('auth_uri', default_auth_uri) + # SSL + self.cert_file = conf.get('certfile') + self.key_file = conf.get('keyfile') + # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = conf.get('admin_token') @@ -252,7 +256,11 @@ class AuthProtocol(object): return self.admin_token def _get_http_connection(self): - return self.http_client_class(self.auth_host, self.auth_port) + if self.auth_protocol == 'http': + return self.http_client_class(self.auth_host, self.auth_port) + else: + return self.http_client_class(self.auth_host, self.auth_port, + self.key_file, self.cert_file) def _json_request(self, method, path, body=None, additional_headers=None): """HTTP request helper used to make json requests. diff --git a/keystone/middleware/s3_token.py b/keystone/middleware/s3_token.py index 19953acda0..a4f1f09f46 100644 --- a/keystone/middleware/s3_token.py +++ b/keystone/middleware/s3_token.py @@ -60,11 +60,14 @@ class S3Token(object): # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') self.auth_port = int(conf.get('auth_port', 35357)) - auth_protocol = conf.get('auth_protocol', 'https') - if auth_protocol == 'http': + self.auth_protocol = conf.get('auth_protocol', 'https') + if self.auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection else: self.http_client_class = httplib.HTTPSConnection + # SSL + self.cert_file = conf.get('certfile') + self.key_file = conf.get('keyfile') def deny_request(self, code): error_table = { @@ -86,7 +89,11 @@ class S3Token(object): headers = {'Content-Type': 'application/json'} try: - conn = self.http_client_class(self.auth_host, self.auth_port) + if self.auth_protocol == 'http': + conn = self.http_client_class(self.auth_host, self.auth_port) + else: + conn = self.http_client_class(self.auth_host, self.auth_port, + self.key_file, self.cert_file) conn.request('POST', '/v2.0/s3tokens', body=creds_json, headers=headers) diff --git a/keystone/test.py b/keystone/test.py index 1e733ed215..686de58320 100644 --- a/keystone/test.py +++ b/keystone/test.py @@ -32,7 +32,7 @@ from keystone.common import wsgi LOG = logging.getLogger(__name__) -ROOTDIR = os.path.dirname(os.path.dirname(__file__)) +ROOTDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) VENDOR = os.path.join(ROOTDIR, 'vendor') TESTSDIR = os.path.join(ROOTDIR, 'tests') ETCDIR = os.path.join(ROOTDIR, 'etc') @@ -236,9 +236,13 @@ class TestCase(unittest.TestCase): def appconfig(self, config): return deploy.appconfig(self._paste_config(config)) - def serveapp(self, config, name=None): + def serveapp(self, config, name=None, cert=None, key=None, ca=None, + cert_required=None): app = self.loadapp(config, name=name) server = wsgi.Server(app, host="127.0.0.1", port=0) + if cert is not None and ca is not None and key is not None: + server.set_ssl(certfile=cert, keyfile=key, ca_certs=ca, + cert_required=cert_required) server.start(key='socket') # Service catalog tests need to know the port we ran on. diff --git a/tests/test_ssl.py b/tests/test_ssl.py new file mode 100644 index 0000000000..7e36b4d669 --- /dev/null +++ b/tests/test_ssl.py @@ -0,0 +1,103 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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. + +import os +import httplib +import ssl + +from keystone import test +from keystone import config + + +CONF = config.CONF + +CERTDIR = test.rootdir("examples/ssl/certs") +KEYDIR = test.rootdir("examples/ssl/private") +CERT = os.path.join(CERTDIR, 'keystone.pem') +KEY = os.path.join(KEYDIR, 'keystonekey.pem') +CA = os.path.join(CERTDIR, 'ca.pem') +CLIENT = os.path.join(CERTDIR, 'middleware.pem') + + +class SSLTestCase(test.TestCase): + def setUp(self): + super(SSLTestCase, self).setUp() + self.load_backends() + + def test_1way_ssl_ok(self): + """ + Make sure both public and admin API work with 1-way SSL. + """ + self.public_server = self.serveapp('keystone', name='main', + cert=CERT, key=KEY, ca=CA) + self.admin_server = self.serveapp('keystone', name='admin', + cert=CERT, key=KEY, ca=CA) + # Verify Admin + conn = httplib.HTTPSConnection('127.0.0.1', CONF.admin_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(resp.status, 300) + # Verify Public + conn = httplib.HTTPSConnection('127.0.0.1', CONF.public_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(resp.status, 300) + + def test_2way_ssl_ok(self): + """ + Make sure both public and admin API work with 2-way SSL. Requires + client certificate. + """ + self.public_server = self.serveapp('keystone', name='main', + cert=CERT, key=KEY, ca=CA, cert_required=True) + self.admin_server = self.serveapp('keystone', name='admin', + cert=CERT, key=KEY, ca=CA, cert_required=True) + # Verify Admin + conn = httplib.HTTPSConnection( + '127.0.0.1', CONF.admin_port, CLIENT, CLIENT) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(resp.status, 300) + # Verify Public + conn = httplib.HTTPSConnection( + '127.0.0.1', CONF.public_port, CLIENT, CLIENT) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(resp.status, 300) + + def test_2way_ssl_fail(self): + """ + Expect to fail when client does not present proper certificate. + """ + self.public_server = self.serveapp('keystone', name='main', + cert=CERT, key=KEY, ca=CA, cert_required=True) + self.admin_server = self.serveapp('keystone', name='admin', + cert=CERT, key=KEY, ca=CA, cert_required=True) + # Verify Admin + conn = httplib.HTTPSConnection('127.0.0.1', CONF.admin_port) + try: + conn.request('GET', '/') + self.fail('Admin API shoulda failed with SSL handshake!') + except ssl.SSLError: + pass + # Verify Public + conn = httplib.HTTPSConnection('127.0.0.1', CONF.public_port) + try: + conn.request('GET', '/') + self.fail('Public API shoulda failed with SSL handshake!') + except ssl.SSLError: + pass