Add cluster domain secret
authorMichael Hanselmann <hansmi@google.com>
Tue, 16 Mar 2010 13:49:26 +0000 (14:49 +0100)
committerMichael Hanselmann <hansmi@google.com>
Wed, 17 Mar 2010 15:18:21 +0000 (16:18 +0100)
Information exchanged between different clusters via untrusted
third parties (e.g. for remote instance import/export) must be
signed with a secret shared between all involved clusters to
ensure the third party doesn't modify the information.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

lib/bootstrap.py
lib/cli.py
lib/constants.py
man/gnt-cluster.sgml
qa/qa_cluster.py
scripts/gnt-cluster
tools/cfgupgrade

index ebfad81..0125b09 100644 (file)
@@ -77,7 +77,7 @@ def GenerateHmacKey(file_name):
 
 
 def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key,
-                          rapi_cert_pem=None):
+                          new_cds, rapi_cert_pem=None, cds=None):
   """Updates the cluster certificates, keys and secrets.
 
   @type new_cluster_cert: bool
@@ -86,8 +86,12 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key,
   @param new_rapi_cert: Whether to generate a new RAPI certificate
   @type new_confd_hmac_key: bool
   @param new_confd_hmac_key: Whether to generate a new HMAC key
+  @type new_cds: bool
+  @param new_cds: Whether to generate a new cluster domain secret
   @type rapi_cert_pem: string
   @param rapi_cert_pem: New RAPI certificate in PEM format
+  @type cds: string
+  @param cds: New cluster domain secret
 
   """
   # noded SSL certificate
@@ -122,6 +126,18 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key,
                   constants.RAPI_CERT_FILE)
     utils.GenerateSelfSignedSslCert(constants.RAPI_CERT_FILE)
 
+  # Cluster domain secret
+  if cds:
+    logging.debug("Writing cluster domain secret to %s",
+                  constants.CLUSTER_DOMAIN_SECRET_FILE)
+    utils.WriteFile(constants.CLUSTER_DOMAIN_SECRET_FILE,
+                    data=cds, backup=True)
+
+  elif new_cds or not os.path.exists(constants.CLUSTER_DOMAIN_SECRET_FILE):
+    logging.debug("Generating new cluster domain secret at %s",
+                  constants.CLUSTER_DOMAIN_SECRET_FILE)
+    GenerateHmacKey(constants.CLUSTER_DOMAIN_SECRET_FILE)
+
 
 def _InitGanetiServerSetup(master_name):
   """Setup the necessary configuration for the initial node daemon.
@@ -131,7 +147,7 @@ def _InitGanetiServerSetup(master_name):
 
   """
   # Generate cluster secrets
-  GenerateClusterCrypto(True, False, False)
+  GenerateClusterCrypto(True, False, False, False)
 
   result = utils.RunCmd([constants.DAEMON_UTIL, "start", constants.NODED])
   if result.failed:
@@ -415,6 +431,7 @@ def SetupNodeDaemon(cluster_name, node, ssh_key_check):
   # and then connect with ssh to set password and start ganeti-noded
   # note that all the below variables are sanitized at this point,
   # either by being constants or by the checks above
+  # TODO: Could this command exceed a shell's maximum command length?
   mycommand = ("umask 077 && "
                "cat > '%s' << '!EOF.' && \n"
                "%s!EOF.\n"
index bb49098..c178bb6 100644 (file)
@@ -50,6 +50,7 @@ __all__ = [
   "AUTO_REPLACE_OPT",
   "BACKEND_OPT",
   "CLEANUP_OPT",
+  "CLUSTER_DOMAIN_SECRET_OPT",
   "CONFIRM_OPT",
   "CP_SIZE_OPT",
   "DEBUG_OPT",
@@ -81,6 +82,7 @@ __all__ = [
   "MC_OPT",
   "NET_OPT",
   "NEW_CLUSTER_CERT_OPT",
+  "NEW_CLUSTER_DOMAIN_SECRET_OPT",
   "NEW_CONFD_HMAC_KEY_OPT",
   "NEW_RAPI_CERT_OPT",
   "NEW_SECONDARY_OPT",
@@ -824,7 +826,6 @@ MASTER_NETDEV_OPT = cli_option("--master-netdev", dest="master_netdev",
                                metavar="NETDEV",
                                default=constants.DEFAULT_BRIDGE)
 
-
 GLOBAL_FILEDIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
                                 help="Specify the default directory (cluster-"
                                 "wide) for storing the file-based disks [%s]" %
@@ -898,6 +899,18 @@ NEW_CONFD_HMAC_KEY_OPT = cli_option("--new-confd-hmac-key",
                                     help=("Create a new HMAC key for %s" %
                                           constants.CONFD))
 
+CLUSTER_DOMAIN_SECRET_OPT = cli_option("--cluster-domain-secret",
+                                       dest="cluster_domain_secret",
+                                       default=None,
+                                       help=("Load new new cluster domain"
+                                             " secret from file"))
+
+NEW_CLUSTER_DOMAIN_SECRET_OPT = cli_option("--new-cluster-domain-secret",
+                                           dest="new_cluster_domain_secret",
+                                           default=False, action="store_true",
+                                           help=("Create a new cluster domain"
+                                                 " secret"))
+
 
 def _ParseArgs(argv, commands, aliases):
   """Parser for the command line arguments.
index 3edbd6d..f8b68c4 100644 (file)
@@ -100,6 +100,7 @@ CLUSTER_CONF_FILE = DATA_DIR + "/config.data"
 NODED_CERT_FILE = DATA_DIR + "/server.pem"
 RAPI_CERT_FILE = DATA_DIR + "/rapi.pem"
 CONFD_HMAC_KEY = DATA_DIR + "/hmac.key"
+CLUSTER_DOMAIN_SECRET_FILE = DATA_DIR + "/cluster-domain-secret"
 WATCHER_STATEFILE = DATA_DIR + "/watcher.data"
 WATCHER_PAUSEFILE = DATA_DIR + "/watcher.pause"
 INSTANCE_UPFILE = RUN_GANETI_DIR + "/instance-status"
index 450f404..6ee98c2 100644 (file)
         <sbr>
         <arg choice="opt">--new-rapi-certificate</arg>
         <arg choice="opt">--rapi-certificate <replaceable>rapi-cert</replaceable></arg>
+        <sbr>
+        <arg choice="opt">--new-cluster-domain-secret</arg>
+        <arg choice="opt">--cluster-domain-secret <replaceable>filename</replaceable></arg>
       </cmdsynopsis>
 
       <para>
         cluster-internal SSL certificate respective the HMAC key used by
         <citerefentry>
         <refentrytitle>ganeti-confd</refentrytitle><manvolnum>8</manvolnum>
-        </citerefentry>. To generate a new self-signed RAPI certificate (used
-        by <citerefentry>
+        </citerefentry>.
+      </para>
+
+      <para>
+        To generate a new self-signed RAPI certificate (used by <citerefentry>
         <refentrytitle>ganeti-rapi</refentrytitle><manvolnum>8</manvolnum>
         </citerefentry>) specify <option>--new-rapi-certificate</option>. If
         you want to use your own certificate, e.g. one signed by a certificate
         authority (CA), pass its filename to
         <option>--rapi-certificate</option>.
       </para>
+
+      <para>
+        <option>--new-cluster-domain-secret</option> generates a new, random
+        cluster domain secret. <option>--cluster-domain-secret</option> reads
+        the secret from a file. The cluster domain secret is used to sign
+        information exchanged between separate clusters via a third party.
+      </para>
     </refsect2>
 
     <refsect2>
index 5ce6252..65c9751 100644 (file)
@@ -151,10 +151,14 @@ def TestClusterRenewCrypto():
 
   # Conflicting options
   cmd = ["gnt-cluster", "renew-crypto", "--force",
-         "--new-cluster-certificate", "--new-confd-hmac-key",
-         "--new-rapi-certificate", "--rapi-certificate=/dev/null"]
-  AssertNotEqual(StartSSH(master["primary"],
-                          utils.ShellQuoteArgs(cmd)).wait(), 0)
+         "--new-cluster-certificate", "--new-confd-hmac-key"]
+  conflicting = [
+    ["--new-rapi-certificate", "--rapi-certificate=/dev/null"],
+    ["--new-cluster-domain-secret", "--cluster-domain-secret=/dev/null"],
+    ]
+  for i in conflicting:
+    AssertNotEqual(StartSSH(master["primary"],
+                            utils.ShellQuoteArgs(cmd + i)).wait(), 0)
 
   # Invalid RAPI certificate
   cmd = ["gnt-cluster", "renew-crypto", "--force",
@@ -181,10 +185,27 @@ def TestClusterRenewCrypto():
     AssertEqual(StartSSH(master["primary"],
                          utils.ShellQuoteArgs(cmd)).wait(), 0)
 
+  # Custom cluster domain secret
+  cds_fh = tempfile.NamedTemporaryFile()
+  cds_fh.write(utils.GenerateSecret())
+  cds_fh.write("\n")
+  cds_fh.flush()
+
+  tmpcds = qa_utils.UploadFile(master["primary"], cds_fh.name)
+  try:
+    cmd = ["gnt-cluster", "renew-crypto", "--force",
+           "--cluster-domain-secret=%s" % tmpcds]
+    AssertEqual(StartSSH(master["primary"],
+                         utils.ShellQuoteArgs(cmd)).wait(), 0)
+  finally:
+    cmd = ["rm", "-f", tmpcds]
+    AssertEqual(StartSSH(master["primary"],
+                         utils.ShellQuoteArgs(cmd)).wait(), 0)
+
   # Normal case
   cmd = ["gnt-cluster", "renew-crypto", "--force",
          "--new-cluster-certificate", "--new-confd-hmac-key",
-         "--new-rapi-certificate"]
+         "--new-rapi-certificate", "--new-cluster-domain-secret"]
   AssertEqual(StartSSH(master["primary"],
                        utils.ShellQuoteArgs(cmd)).wait(), 0)
 
index 50e68fe..c51b73d 100755 (executable)
@@ -495,7 +495,8 @@ def SearchTags(opts, args):
 
 
 def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
-                 new_confd_hmac_key, force):
+                 new_confd_hmac_key, new_cds, cds_filename,
+                 force):
   """Renews cluster certificates, keys and secrets.
 
   @type new_cluster_cert: bool
@@ -506,6 +507,10 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
   @param rapi_cert_filename: Path to file containing new RAPI certificate
   @type new_confd_hmac_key: bool
   @param new_confd_hmac_key: Whether to generate a new HMAC key
+  @type new_cds: bool
+  @param new_cds: Whether to generate a new cluster domain secret
+  @type cds_filename: string
+  @param cds_filename: Path to file containing new cluster domain secret
   @type force: bool
   @param force: Whether to ask user for confirmation
 
@@ -515,6 +520,12 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
              " options can be specified at the same time.")
     return 1
 
+  if new_cds and cds_filename:
+    ToStderr("Only one of the --new-cluster-domain-secret and"
+             " --cluster-domain-secret options can be specified at"
+             " the same time.")
+    return 1
+
   if rapi_cert_filename:
     # Read and verify new certificate
     try:
@@ -537,6 +548,16 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
   else:
     rapi_cert_pem = None
 
+  if cds_filename:
+    try:
+      cds = utils.ReadFile(cds_filename)
+    except Exception, err: # pylint: disable-msg=W0703
+      ToStderr("Can't load new cluster domain secret from %s: %s" %
+               (cds_filename, str(err)))
+      return 1
+  else:
+    cds = None
+
   if not force:
     usertext = ("This requires all daemons on all nodes to be restarted and"
                 " may take some time. Continue?")
@@ -547,7 +568,9 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
     ctx.feedback_fn("Updating certificates and keys")
     bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert,
                                     new_confd_hmac_key,
-                                    rapi_cert_pem=rapi_cert_pem)
+                                    new_cds,
+                                    rapi_cert_pem=rapi_cert_pem,
+                                    cds=cds)
 
     files_to_copy = []
 
@@ -560,6 +583,9 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
     if new_confd_hmac_key:
       files_to_copy.append(constants.CONFD_HMAC_KEY)
 
+    if new_cds or cds:
+      files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)
+
     if files_to_copy:
       for node_name in ctx.nonmaster_nodes:
         ctx.feedback_fn("Copying %s to %s" %
@@ -583,6 +609,8 @@ def RenewCrypto(opts, args):
                       opts.new_rapi_cert,
                       opts.rapi_cert,
                       opts.new_confd_hmac_key,
+                      opts.new_cluster_domain_secret,
+                      opts.cluster_domain_secret,
                       opts.force)
 
 
@@ -789,7 +817,8 @@ commands = {
   "renew-crypto": (
     RenewCrypto, ARGS_NONE,
     [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
-     NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT],
+     NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT,
+     NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT],
     "[opts...]",
     "Renews cluster certificates, keys and secrets"),
   }
index fa6a819..c9db1ce 100755 (executable)
@@ -174,7 +174,7 @@ def main():
                     backup=True)
 
     if not options.dry_run:
-      bootstrap.GenerateClusterCrypto(False, False, False)
+      bootstrap.GenerateClusterCrypto(False, False, False, False)
 
   except:
     logging.critical("Writing configuration failed. It is probably in an"