In order to renew client certificates via SSH (rather than on the fly via SSL as it was before), we need a new tool which can be called on remote nodes via SSH.
Signed-off-by: Helga Velroyen <[email protected]> --- Makefile.am | 4 +- lib/tools/ssl_update.py | 135 ++++++++++++++++++++++++++++ src/Ganeti/Constants.hs | 3 + test/py/ganeti.tools.ssl_update_unittest.py | 84 +++++++++++++++++ 4 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 lib/tools/ssl_update.py create mode 100755 test/py/ganeti.tools.ssl_update_unittest.py diff --git a/Makefile.am b/Makefile.am index 5eecb8e..b4fdba0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -551,7 +551,8 @@ pytools_PYTHON = \ lib/tools/ensure_dirs.py \ lib/tools/node_cleanup.py \ lib/tools/node_daemon_setup.py \ - lib/tools/prepare_node_join.py + lib/tools/prepare_node_join.py \ + lib/tools/ssl_update.py utils_PYTHON = \ lib/utils/__init__.py \ @@ -2307,6 +2308,7 @@ tools/ensure-dirs: MODULE = ganeti.tools.ensure_dirs tools/node-daemon-setup: MODULE = ganeti.tools.node_daemon_setup tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join tools/node-cleanup: MODULE = ganeti.tools.node_cleanup +tools/ssl-update: MODULE = ganeti.tools.ssl_update $(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst test/hs/%,%,$@) $(PYTHON_BOOTSTRAP) $(gnt_scripts) $(gnt_python_sbin_SCRIPTS): Makefile | stamp-directories diff --git a/lib/tools/ssl_update.py b/lib/tools/ssl_update.py new file mode 100644 index 0000000..36453d2 --- /dev/null +++ b/lib/tools/ssl_update.py @@ -0,0 +1,135 @@ +# +# + +# Copyright (C) 2015 Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Script to recreate and sign the client SSL certificates. + +""" + +import os +import os.path +import optparse +import sys +import logging +import time + +from ganeti import cli +from ganeti import constants +from ganeti import errors +from ganeti import utils +from ganeti import ht +from ganeti import pathutils +from ganeti.tools import common + + +_DATA_CHECK = ht.TStrictDict(False, True, { + constants.NDS_CLUSTER_NAME: ht.TNonEmptyString, + constants.NDS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString, + constants.NDS_NODE_NAME: ht.TNonEmptyString, + }) + + +class SslSetupError(errors.GenericError): + """Local class for reporting errors. + + """ + + +def ParseOptions(): + """Parses the options passed to the program. + + @return: Options and arguments + + """ + parser = optparse.OptionParser(usage="%prog [--dry-run]", + prog=os.path.basename(sys.argv[0])) + parser.add_option(cli.DEBUG_OPT) + parser.add_option(cli.VERBOSE_OPT) + parser.add_option(cli.DRY_RUN_OPT) + + (opts, args) = parser.parse_args() + + return common.VerifyOptions(parser, opts, args) + + +def RegenerateClientCertificate( + data, client_cert=pathutils.NODED_CLIENT_CERT_FILE, + signing_cert=pathutils.NODED_CERT_FILE): + """Regenerates the client certificate of the node. + + @type data: string + @param data: the JSON-formated input data + + """ + if not os.path.exists(signing_cert): + raise SslSetupError("The signing certificate '%s' cannot be found." + % signing_cert) + + # TODO: This sets the serial number to the number of seconds + # since epoch. This is technically not a correct serial number + # (in the way SSL is supposed to be used), but it serves us well + # enough for now, as we don't have any infrastructure for keeping + # track of the number of signed certificates yet. + serial_no = int(time.time()) + + # The hostname of the node is provided with the input data. + hostname = data.get(constants.NDS_NODE_NAME) + + # TODO: make backup of the file before regenerating. + utils.GenerateSignedSslCert(client_cert, serial_no, signing_cert, + common_name=hostname) + + +def Main(): + """Main routine. + + """ + opts = ParseOptions() + + utils.SetupToolLogging(opts.debug, opts.verbose) + + try: + data = common.LoadData(sys.stdin.read(), _DATA_CHECK) + + common.VerifyClusterName(data, SslSetupError) + + # Verifies whether the server certificate of the caller + # is the same as on this node. + common.VerifyCertificate(data, SslSetupError) + + RegenerateClientCertificate(data) + + except Exception, err: # pylint: disable=W0703 + logging.debug("Caught unhandled exception", exc_info=True) + + (retcode, message) = cli.FormatError(err) + logging.error(message) + + return retcode + else: + return constants.EXIT_SUCCESS diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs index 03b32c5..dea59a5 100644 --- a/src/Ganeti/Constants.hs +++ b/src/Ganeti/Constants.hs @@ -4468,6 +4468,9 @@ ndsSsconf = "ssconf" ndsStartNodeDaemon :: String ndsStartNodeDaemon = "start_node_daemon" +ndsNodeName :: String +ndsNodeName = "node_name" + -- * VCluster related constants vClusterEtcHosts :: String diff --git a/test/py/ganeti.tools.ssl_update_unittest.py b/test/py/ganeti.tools.ssl_update_unittest.py new file mode 100755 index 0000000..e4b3ad9 --- /dev/null +++ b/test/py/ganeti.tools.ssl_update_unittest.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# + +# Copyright (C) 2015 Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Script for testing ganeti.tools.ssl_update""" + +import unittest +import shutil +import tempfile +import os.path +import OpenSSL +import time + +from ganeti import errors +from ganeti import constants +from ganeti import serializer +from ganeti import pathutils +from ganeti import compat +from ganeti import utils +from ganeti.tools import ssl_update + +import testutils + + +class TestGenerateClientCert(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + self.client_cert = os.path.join(self.tmpdir, "client.pem") + + self.server_cert = os.path.join(self.tmpdir, "server.pem") + some_serial_no = int(time.time()) + utils.GenerateSelfSignedSslCert(self.server_cert, some_serial_no) + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def testRegnerateClientCertificate(self): + my_node_name = "mynode.example.com" + data = {constants.NDS_CLUSTER_NAME: "winnie_poohs_cluster", + constants.NDS_NODE_DAEMON_CERTIFICATE: "some_cert", + constants.NDS_NODE_NAME: my_node_name} + + ssl_update.RegenerateClientCertificate(data, client_cert=self.client_cert, + signing_cert=self.server_cert) + + client_cert_pem = utils.ReadFile(self.client_cert) + server_cert_pem = utils.ReadFile(self.server_cert) + client_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + client_cert_pem) + signing_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + server_cert_pem) + self.assertEqual(client_cert.get_issuer().CN, signing_cert.get_subject().CN) + self.assertEqual(client_cert.get_subject().CN, my_node_name) + + +if __name__ == "__main__": + testutils.GanetiTestProgram() -- 2.4.3.573.g4eafbef
