diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 2c61e38..fea7ca5 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -70,3 +70,49 @@ Where /tmp/one-node contains:: "cpu": "1" } ] + +---------------------------------------------------------- +Generating keys and certificates for use with Keystone PKI +---------------------------------------------------------- + +The generate-keystone-pki line utility generates keys and certificates +which Keystone uses for signing authentication tokens. + +- Keys and certificates can be generated into separate files:: + + generate-keystone-pki /tmp/certificates + + That creates four files with signing and CA keys and certificates in + /tmp/certificates directory. + +- Key and certificates can be generated into heat environment file:: + + generate-keystone-pki -j overcloud-env.json + + That adds following values into overcloud-env.json file:: + + { + "parameters": { + "KeystoneSigningKey": "some_key", + "KeystoneSigningCertificate": "some_cert", + "KeystoneCACertificate": "some_cert" + } + } + + CA key is not added because this file is not needed by Keystone PKI. + +- Key and certificates can be generated into os-apply-config metadata file:: + + generate-keystone-pki -s -j local.json + + This adds following values into local.json file:: + + { + "keystone": { + "signing_certificate": "some_cert", + "signing_key": "some_key", + "ca_certificate": "some_cert" + } + } + + CA key is not added because this file is not needed by Keystone PKI. diff --git a/os_cloud_config/cmd/generate_keystone_pki.py b/os_cloud_config/cmd/generate_keystone_pki.py index b0789be..4aab127 100644 --- a/os_cloud_config/cmd/generate_keystone_pki.py +++ b/os_cloud_config/cmd/generate_keystone_pki.py @@ -28,6 +28,9 @@ def parse_args(): signing_cert.pem - certificate for verifying token validity, the certificate itself is verifiable by ca_cert.pem + Alternatively write generated key/certs into JSON file + or into an os-apply-config metadata file for use without Heat. + ca_key.pem doesn't have to (shouldn't) be uploaded to Keystone nodes. """) @@ -35,14 +38,24 @@ def parse_args(): description=description, formatter_class=argparse.RawDescriptionHelpFormatter, ) - parser.add_argument( - 'directory', - metavar='', - help='directory where keys/certs will be generated', - ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('-d', '--directory', dest='directory', + help='directory where keys/certs will be generated') + group.add_argument('-j', '--heatenv', dest='heatenv', + help='write signing key/cert and CA cert into JSON ' + 'Heat environment file, CA key is omitted') + parser.add_argument('-s', '--seed', action='store_true', + help='JSON file for seed machine has different ' + 'structure (for seed machine we update directly ' + 'heat metadata file injected into image). ' + 'Different key/certs names and different ' + 'parent node are used (default: false)') return parser.parse_args() def main(): args = parse_args() - keystone_pki.create_and_write_ca_and_signing_pairs(args.directory) + if args.heatenv: + keystone_pki.generate_certs_into_json(args.heatenv, args.seed) + else: + keystone_pki.create_and_write_ca_and_signing_pairs(args.directory) diff --git a/os_cloud_config/keystone_pki.py b/os_cloud_config/keystone_pki.py index 9ead3fb..6f1d74b 100644 --- a/os_cloud_config/keystone_pki.py +++ b/os_cloud_config/keystone_pki.py @@ -15,6 +15,7 @@ import logging import os from os import path +import simplejson import stat from OpenSSL import crypto @@ -136,6 +137,57 @@ def create_and_write_ca_and_signing_pairs(directory): _write_pki_file(path.join(directory, 'signing_cert.pem'), signing_cert_pem) +def generate_certs_into_json(jsonfile, seed): + """Create and write out CA certificate and signing certificate/key. + + Generate CA certificate, signing certificate and signing key and + add them into a JSON file. If key/certs already exist in JSON file, no + change is done. + + :param jsonfile: JSON file where certs and key will be written + :type jsonfile: string + :param seed: JSON file for seed machine has different structure. Different + key/certs names and different parent node are used + :type seed: boolean + """ + if os.path.isfile(jsonfile): + with open(jsonfile) as json_fd: + all_data = simplejson.load(json_fd) + else: + all_data = {} + + if seed: + parent = 'keystone' + ca_cert_name = 'ca_certificate' + signing_key_name = 'signing_key' + signing_cert_name = 'signing_certificate' + else: + parent = 'parameters' + ca_cert_name = 'KeystoneCACertificate' + signing_key_name = 'KeystoneSigningKey' + signing_cert_name = 'KeystoneSigningCertificate' + + if parent not in all_data: + all_data[parent] = {} + parent_node = all_data[parent] + + if not (ca_cert_name in parent_node and + signing_key_name in parent_node and + signing_cert_name in parent_node): + ca_key_pem, ca_cert_pem = create_ca_pair() + signing_key_pem, signing_cert_pem = create_signing_pair(ca_key_pem, + ca_cert_pem) + parent_node.update({ca_cert_name: ca_cert_pem, + signing_key_name: signing_key_pem, + signing_cert_name: signing_cert_pem}) + with open(jsonfile, 'w') as json_fd: + simplejson.dump(all_data, json_fd, sort_keys=True) + LOG.debug("Wrote key/certs into '%s'.", path.abspath(jsonfile)) + else: + LOG.info("Key/certs are already present in '%s', skipping.", + path.abspath(jsonfile)) + + def _write_pki_file(file_path, contents): with open(file_path, 'w') as f: f.write(contents) diff --git a/os_cloud_config/tests/test_keystone_pki.py b/os_cloud_config/tests/test_keystone_pki.py index d533e50..314586e 100644 --- a/os_cloud_config/tests/test_keystone_pki.py +++ b/os_cloud_config/tests/test_keystone_pki.py @@ -98,3 +98,43 @@ class KeystonePKITest(base.TestCase): mock.call('mock_signing_key'), mock.call('mock_signing_cert'), ]) + + @mock.patch('os_cloud_config.keystone_pki.path.isfile', create=True) + @mock.patch('os_cloud_config.keystone_pki.create_ca_pair') + @mock.patch('os_cloud_config.keystone_pki.create_signing_pair') + @mock.patch('os_cloud_config.keystone_pki.open', create=True) + @mock.patch('os_cloud_config.keystone_pki.simplejson.dump') + def test_generate_certs_into_json( + self, mock_json, open_, create_signing, create_ca, isfile): + create_ca.return_value = ('mock_ca_key', 'mock_ca_cert') + create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') + isfile.return_value = False + + keystone_pki.generate_certs_into_json('/jsonfile', False) + + params = mock_json.call_args[0][0]['parameters'] + self.assertEqual(params['KeystoneCACertificate'], 'mock_ca_cert') + self.assertEqual(params['KeystoneSigningKey'], 'mock_signing_key') + self.assertEqual(params['KeystoneSigningCertificate'], + 'mock_signing_cert') + + @mock.patch('os_cloud_config.keystone_pki.path.isfile', create=True) + @mock.patch('os_cloud_config.keystone_pki.create_ca_pair') + @mock.patch('os_cloud_config.keystone_pki.create_signing_pair') + @mock.patch('os_cloud_config.keystone_pki.open', create=True) + @mock.patch('os_cloud_config.keystone_pki.simplejson.load') + @mock.patch('os_cloud_config.keystone_pki.simplejson.dump') + def test_generate_certs_into_json_with_existing_certs( + self, mock_json_dump, mock_json_load, open_, create_signing, + create_ca, isfile): + create_ca.return_value = ('mock_ca_key', 'mock_ca_cert') + create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') + isfile.return_value = True + mock_json_load.return_value = { + 'KeystoneCACertificate': 'mock_ca_cert', + 'KeystoneSigningKey': 'mock_signing_key', + 'KeystoneSigningCertificate': 'mock_signing_cert' + } + + keystone_pki.generate_certs_into_json('/jsonfile', False) + mock_json_dump.assert_not_called()