Commit 3db3eb2a authored by Michael Hanselmann's avatar Michael Hanselmann

Add cluster domain secret

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: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent c89e6fdf
......@@ -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"
......
......@@ -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.
......
......@@ -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"
......
......@@ -715,6 +715,9 @@
<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>
......@@ -726,14 +729,24 @@
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>
......
......@@ -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)
......
......@@ -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"),
}
......
......@@ -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"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment