From 8764aa9eea8646e6aaff86e9633ecf7456936e77 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sat, 19 Aug 2023 15:34:31 -0700 Subject: [PATCH] Add cluster config settings --- salt/channel/server.py | 38 +- salt/cli/daemons.py | 25 +- salt/config/__init__.py | 24 + salt/crypt.py | 64 +- salt/daemons/masterapi.py | 6 +- salt/key.py | 70 +- salt/master.py | 13 +- salt/utils/minions.py | 14 +- salt/utils/verify.py | 33 +- .../unit/channel/test_request_channel.py | 604 ++++++++++-------- tests/pytests/unit/test_crypt.py | 42 ++ 11 files changed, 572 insertions(+), 361 deletions(-) diff --git a/salt/channel/server.py b/salt/channel/server.py index d1f3a7ac1c1..9df2f7a0776 100644 --- a/salt/channel/server.py +++ b/salt/channel/server.py @@ -55,7 +55,10 @@ class ReqServerChannel: def __init__(self, opts, transport): self.opts = opts self.transport = transport - self.event = None + self.event = salt.utils.event.get_master_event( + self.opts, self.opts["sock_dir"], listen=False + ) + self.master_key = salt.crypt.MasterKeys(self.opts) def pre_fork(self, process_manager): """ @@ -187,7 +190,10 @@ class ReqServerChannel: The server equivalent of ReqChannel.crypted_transfer_decode_dictentry """ # encrypt with a specific AES key - pubfn = os.path.join(self.opts["pki_dir"], "minions", target) + if self.master_key.cluster_key: + pubfn = os.path.join(self.opts["cluster_pki_dir"], "minions", target) + else: + pubfn = os.path.join(self.opts["pki_dir"], "minions", target) key = salt.crypt.Crypticle.generate_key_string() pcrypt = salt.crypt.Crypticle(self.opts, key) try: @@ -212,10 +218,9 @@ class ReqServerChannel: tosign = salt.payload.dumps( {"key": pret["key"], "pillar": ret, "nonce": nonce} ) - master_pem_path = os.path.join(self.opts["pki_dir"], "master.pem") signed_msg = { "data": tosign, - "sig": salt.crypt.sign_message(master_pem_path, tosign), + "sig": salt.crypt.sign_message(self.master_key.rsa_path, tosign), } pret[dictkey] = pcrypt.dumps(signed_msg) else: @@ -223,12 +228,11 @@ class ReqServerChannel: return pret def _clear_signed(self, load): - master_pem_path = os.path.join(self.opts["pki_dir"], "master.pem") tosign = salt.payload.dumps(load) return { "enc": "clear", "load": tosign, - "sig": salt.crypt.sign_message(master_pem_path, tosign), + "sig": salt.crypt.sign_message(self.master_key.rsa_path, tosign), } def _update_aes(self): @@ -326,18 +330,21 @@ class ReqServerChannel: else: return {"enc": "clear", "load": {"ret": "full"}} + pki_dir = self.opts["pki_dir"] + if self.opts["cluster_id"]: + if self.opts["cluster_pki_dir"]: + pki_dir = self.opts["cluster_pki_dir"] + # Check if key is configured to be auto-rejected/signed auto_reject = self.auto_key.check_autoreject(load["id"]) auto_sign = self.auto_key.check_autosign( load["id"], load.get("autosign_grains", None) ) - pubfn = os.path.join(self.opts["pki_dir"], "minions", load["id"]) - pubfn_pend = os.path.join(self.opts["pki_dir"], "minions_pre", load["id"]) - pubfn_rejected = os.path.join( - self.opts["pki_dir"], "minions_rejected", load["id"] - ) - pubfn_denied = os.path.join(self.opts["pki_dir"], "minions_denied", load["id"]) + pubfn = os.path.join(pki_dir, "minions", load["id"]) + pubfn_pend = os.path.join(pki_dir, "minions_pre", load["id"]) + pubfn_rejected = os.path.join(pki_dir, "minions_rejected", load["id"]) + pubfn_denied = os.path.join(pki_dir, "minions_denied", load["id"]) if self.opts["open_mode"]: # open mode is turned on, nuts to checks and overwrite whatever # is there @@ -740,6 +747,7 @@ class PubServerChannel: self.event = salt.utils.event.get_event("master", opts=self.opts, listen=False) self.ckminions = salt.utils.minions.CkMinions(self.opts) self.present = {} + self.master_key = salt.crypt.MasterKeys(self.opts) def close(self): self.transport.close() @@ -771,6 +779,7 @@ class PubServerChannel: secrets = kwargs.get("secrets", None) if secrets is not None: salt.master.SMaster.secrets = secrets + self.master_key = salt.crypt.MasterKeys(self.opts) self.transport.publish_daemon( self.publish_payload, self.presence_callback, self.remove_presence_callback ) @@ -861,9 +870,10 @@ class PubServerChannel: ) payload["load"] = crypticle.dumps(load) if self.opts["sign_pub_messages"]: - master_pem_path = os.path.join(self.opts["pki_dir"], "master.pem") log.debug("Signing data packet") - payload["sig"] = salt.crypt.sign_message(master_pem_path, payload["load"]) + payload["sig"] = salt.crypt.sign_message( + self.master_key.rsa_path, payload["load"] + ) int_payload = {"payload": salt.payload.dumps(payload)} # If topics are upported, target matching has to happen master side diff --git a/salt/cli/daemons.py b/salt/cli/daemons.py index ecc05c919ef..5caabd46d53 100644 --- a/salt/cli/daemons.py +++ b/salt/cli/daemons.py @@ -153,12 +153,35 @@ class Master( self.config["syndic_dir"], self.config["sqlite_queue_dir"], ] + pki_dir = self.config["pki_dir"] + if ( + self.config["cluster_pki_dir"] + and self.config["cluster_pki_dir"] != self.config["pki_dir"] + ): + v_dirs.extend( + [ + self.config["cluster_pki_dir"], + os.path.join(self.config["cluster_pki_dir"], "minions"), + os.path.join(self.config["cluster_pki_dir"], "minions_pre"), + os.path.join( + self.config["cluster_pki_dir"], "minions_denied" + ), + os.path.join( + self.config["cluster_pki_dir"], "minions_autosign" + ), + os.path.join( + self.config["cluster_pki_dir"], "minions_rejected" + ), + ] + ) + pki_dir = [self.config["pki_dir"], self.config["cluster_pki_dir"]] + verify_env( v_dirs, self.config["user"], permissive=self.config["permissive_pki_access"], root_dir=self.config["root_dir"], - pki_dir=self.config["pki_dir"], + pki_dir=pki_dir, ) # Clear out syndics from cachedir for syndic_file in os.listdir(self.config["syndic_dir"]): diff --git a/salt/config/__init__.py b/salt/config/__init__.py index c9120dd1ddb..815209ad8e9 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -185,6 +185,14 @@ VALID_OPTS = immutabletypes.freeze( "pki_dir": str, # A unique identifier for this daemon "id": str, + # When defined we operate this master as a part of a cluster. + "cluster_id": str, + # Defines the other masters in the cluster. + "cluster_peers": list, + # Use this location instead of pki dir for cluster. This allows users + # to define where minion keys and the cluster private key will be + # stored. + "cluster_pki_dir": str, # Use a module function to determine the unique identifier. If this is # set and 'id' is not set, it will allow invocation of a module function # to determine the value of 'id'. For simple invocations without function @@ -409,6 +417,8 @@ VALID_OPTS = immutabletypes.freeze( "permissive_pki_access": bool, # The passphrase of the master's private key "key_pass": (type(None), str), + # The passphrase of the master cluster's private key + "cluster_key_pass": (type(None), str), # The passphrase of the master's private signing key "signing_key_pass": (type(None), str), # The path to a directory to pull in configuration file includes @@ -1544,6 +1554,7 @@ DEFAULT_MASTER_OPTS = immutabletypes.freeze( "verify_env": True, "permissive_pki_access": False, "key_pass": None, + "cluster_key_pass": None, "signing_key_pass": None, "default_include": "master.d/*.conf", "winrepo_dir": os.path.join(salt.syspaths.BASE_FILE_ROOTS_DIR, "win", "repo"), @@ -4034,6 +4045,19 @@ def apply_master_config(overrides=None, defaults=None): prepend_root_dir(opts, prepend_root_dirs) + # When a cluster id is defined, make sure the other nessicery bits a + # defined. + if "cluster_id" not in opts: + opts["cluster_id"] = None + if opts["cluster_id"] is not None: + if not opts.get("cluster_peers", None): + opts["cluster_peers"] = [] + if not opts.get("cluster_pki_dir", None): + opts["cluster_pki_dir"] = opts["pki_dir"] + else: + opts["cluster_peers"] = [] + opts["cluster_pki_dir"] = None + # Enabling open mode requires that the value be set to True, and # nothing else! opts["open_mode"] = opts["open_mode"] is True diff --git a/salt/crypt.py b/salt/crypt.py index 0003b024d9f..c3bdeae6a9d 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -371,11 +371,27 @@ class MasterKeys(dict): def __init__(self, opts): super().__init__() self.opts = opts - self.pub_path = os.path.join(self.opts["pki_dir"], "master.pub") - self.rsa_path = os.path.join(self.opts["pki_dir"], "master.pem") - + self.master_pub_path = os.path.join(self.opts["pki_dir"], "master.pub") + self.master_rsa_path = os.path.join(self.opts["pki_dir"], "master.pem") key_pass = salt.utils.sdb.sdb_get(self.opts["key_pass"], self.opts) - self.key = self.__get_keys(passphrase=key_pass) + self.master_key = self.__get_keys(passphrase=key_pass) + + self.cluster_pub_path = None + self.cluster_rsa_path = None + self.cluster_key = None + if self.opts["cluster_id"]: + self.cluster_pub_path = os.path.join( + self.opts["cluster_pki_dir"], "cluster.pub" + ) + self.cluster_rsa_path = os.path.join( + self.opts["cluster_pki_dir"], "cluster.pem" + ) + key_pass = salt.utils.sdb.sdb_get(self.opts["cluster_key_pass"], self.opts) + self.cluster_key = self.__get_keys( + name="cluster", + passphrase=key_pass, + pki_dir=self.opts["cluster_pki_dir"], + ) self.pub_signature = None @@ -433,15 +449,35 @@ class MasterKeys(dict): def __getstate__(self): return {"opts": self.opts} - def __get_keys(self, name="master", passphrase=None): + @property + def key(self): + if self.cluster_key: + return self.cluster_key + return self.master_key + + @property + def pub_path(self): + if self.cluster_pub_path: + return self.cluster_pub_path + return self.master_pub_path + + @property + def rsa_path(self): + if self.cluster_rsa_path: + return self.cluster_rsa_path + return self.master_rsa_path + + def __get_keys(self, name="master", passphrase=None, pki_dir=None): """ Returns a key object for a key in the pki-dir """ - path = os.path.join(self.opts["pki_dir"], name + ".pem") + if pki_dir is None: + pki_dir = self.opts["pki_dir"] + path = os.path.join(pki_dir, name + ".pem") if not os.path.exists(path): - log.info("Generating %s keys: %s", name, self.opts["pki_dir"]) + log.info("Generating %s keys: %s", name, pki_dir) gen_keys( - self.opts["pki_dir"], + pki_dir, name, self.opts["keysize"], self.opts.get("user"), @@ -465,7 +501,14 @@ class MasterKeys(dict): Return the string representation of a public key in the pki-directory """ - path = os.path.join(self.opts["pki_dir"], name + ".pub") + if self.cluster_pub_path: + path = self.cluster_pub_path + else: + path = self.master_pub_path + # XXX We should always have a key present when this is called, if not + # it's an error. + # if not os.path.isfile(path): + # raise RuntimeError(f"The key {path} does not exist.") if not os.path.isfile(path): key = self.__get_keys() if HAS_M2: @@ -476,6 +519,9 @@ class MasterKeys(dict): with salt.utils.files.fopen(path) as rfh: return rfh.read() + def get_ckey_paths(self): + return self.cluster_pub_path, self.cluster_rsa_path + def get_mkey_paths(self): return self.pub_path, self.rsa_path diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index da5f5ea317a..777eae136e7 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -332,7 +332,11 @@ class AutoKey: """ Check a keyid for membership in a autosign directory. """ - autosign_dir = os.path.join(self.opts["pki_dir"], "minions_autosign") + if self.opts["cluster_id"]: + pki_dir = self.opts["cluster_pki_dir"] + else: + pki_dir = self.opts["pki_dir"] + autosign_dir = os.path.join(pki_dir, "minions_autosign") # cleanup expired files expire_minutes = self.opts.get("autosign_timeout", 120) diff --git a/salt/key.py b/salt/key.py index 14ccf450f3c..66fd42ce073 100644 --- a/salt/key.py +++ b/salt/key.py @@ -309,6 +309,9 @@ class Key: def __init__(self, opts, io_loop=None): self.opts = opts + self.pki_dir = self.opts["pki_dir"] + if self.opts["cluster_id"]: + self.pki_dir = self.opts["cluster_pki_dir"] kind = self.opts.get("__role", "") # application kind if kind not in salt.utils.kinds.APPL_KINDS: emsg = f"Invalid application kind = '{kind}'." @@ -330,11 +333,11 @@ class Key: """ Return the minion keys directory paths """ - minions_accepted = os.path.join(self.opts["pki_dir"], self.ACC) - minions_pre = os.path.join(self.opts["pki_dir"], self.PEND) - minions_rejected = os.path.join(self.opts["pki_dir"], self.REJ) + minions_accepted = os.path.join(self.pki_dir, self.ACC) + minions_pre = os.path.join(self.pki_dir, self.PEND) + minions_rejected = os.path.join(self.pki_dir, self.REJ) - minions_denied = os.path.join(self.opts["pki_dir"], self.DEN) + minions_denied = os.path.join(self.pki_dir, self.DEN) return minions_accepted, minions_pre, minions_rejected, minions_denied def _get_key_attrs(self, keydir, keyname, keysize, user): @@ -342,10 +345,10 @@ class Key: if "gen_keys_dir" in self.opts: keydir = self.opts["gen_keys_dir"] else: - keydir = self.opts["pki_dir"] + keydir = self.pki_dir if not keyname: if "gen_keys" in self.opts: - keyname = self.opts["gen_keys"] + keyname = self.pki_dir else: keyname = "minion" if not keysize: @@ -380,7 +383,7 @@ class Key: return f"Public-key {pub} does not exist" # default to master.pub else: - mpub = self.opts["pki_dir"] + "/" + "master.pub" + mpub = self.pki_dir + "/" + "master.pub" if os.path.isfile(mpub): pub = mpub @@ -390,7 +393,7 @@ class Key: return f"Private-key {priv} does not exist" # default to master_sign.pem else: - mpriv = self.opts["pki_dir"] + "/" + "master_sign.pem" + mpriv = self.pki_dir + "/" + "master_sign.pem" if os.path.isfile(mpriv): priv = mpriv @@ -399,22 +402,17 @@ class Key: log.debug( "Generating new signing key-pair .%s.* in %s", self.opts["master_sign_key_name"], - self.opts["pki_dir"], + self.pki_dir, ) salt.crypt.gen_keys( - self.opts["pki_dir"], + self.pki_dir, self.opts["master_sign_key_name"], keysize or self.opts["keysize"], self.opts.get("user"), self.passphrase, ) - priv = ( - self.opts["pki_dir"] - + "/" - + self.opts["master_sign_key_name"] - + ".pem" - ) + priv = self.pki_dir + "/" + self.opts["master_sign_key_name"] + ".pem" else: return "No usable private-key found" @@ -428,7 +426,7 @@ class Key: if not os.path.isdir(signature_path): log.debug("target directory %s does not exist", signature_path) else: - signature_path = self.opts["pki_dir"] + signature_path = self.pki_dir sign_path = signature_path + "/" + self.opts["master_pubkey_signature"] @@ -525,9 +523,9 @@ class Key: Return a dict of local keys """ ret = {"local": []} - for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(self.opts["pki_dir"])): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(self.pki_dir)): if fn_.endswith(".pub") or fn_.endswith(".pem"): - path = os.path.join(self.opts["pki_dir"], fn_) + path = os.path.join(self.pki_dir, fn_) ret["local"].append(fn_) return ret @@ -600,7 +598,7 @@ class Key: for status, keys in self.name_match(match).items(): ret[status] = {} for key in salt.utils.data.sorted_ignorecase(keys): - path = os.path.join(self.opts["pki_dir"], status, key) + path = os.path.join(self.pki_dir, status, key) with salt.utils.files.fopen(path, "r") as fp_: ret[status][key] = salt.utils.stringutils.to_unicode(fp_.read()) return ret @@ -613,7 +611,7 @@ class Key: for status, keys in self.list_keys().items(): ret[status] = {} for key in salt.utils.data.sorted_ignorecase(keys): - path = os.path.join(self.opts["pki_dir"], status, key) + path = os.path.join(self.pki_dir, status, key) with salt.utils.files.fopen(path, "r") as fp_: ret[status][key] = salt.utils.stringutils.to_unicode(fp_.read()) return ret @@ -639,7 +637,7 @@ class Key: invalid_keys = [] for keydir in keydirs: for key in matches.get(keydir, []): - key_path = os.path.join(self.opts["pki_dir"], keydir, key) + key_path = os.path.join(self.pki_dir, keydir, key) try: salt.crypt.get_rsa_pub_key(key_path) except salt.exceptions.InvalidKeyError: @@ -649,7 +647,7 @@ class Key: try: shutil.move( key_path, - os.path.join(self.opts["pki_dir"], self.ACC, key), + os.path.join(self.pki_dir, self.ACC, key), ) eload = {"result": True, "act": "accept", "id": key} self.event.fire_event(eload, salt.utils.event.tagify(prefix="key")) @@ -668,8 +666,8 @@ class Key: for key in keys[self.PEND]: try: shutil.move( - os.path.join(self.opts["pki_dir"], self.PEND, key), - os.path.join(self.opts["pki_dir"], self.ACC, key), + os.path.join(self.pki_dir, self.PEND, key), + os.path.join(self.pki_dir, self.ACC, key), ) eload = {"result": True, "act": "accept", "id": key} self.event.fire_event(eload, salt.utils.event.tagify(prefix="key")) @@ -713,7 +711,7 @@ class Key: "master AES key is rotated or auth is revoked " "with 'saltutil.revoke_auth'.".format(key) ) - os.remove(os.path.join(self.opts["pki_dir"], status, key)) + os.remove(os.path.join(self.pki_dir, status, key)) eload = {"result": True, "act": "delete", "id": key} self.event.fire_event( eload, salt.utils.event.tagify(prefix="key") @@ -738,7 +736,7 @@ class Key: for status, keys in self.list_keys().items(): for key in keys[self.DEN]: try: - os.remove(os.path.join(self.opts["pki_dir"], status, key)) + os.remove(os.path.join(self.pki_dir, status, key)) eload = {"result": True, "act": "delete", "id": key} self.event.fire_event(eload, salt.utils.event.tagify(prefix="key")) except OSError: @@ -753,7 +751,7 @@ class Key: for status, keys in self.list_keys().items(): for key in keys: try: - os.remove(os.path.join(self.opts["pki_dir"], status, key)) + os.remove(os.path.join(self.pki_dir, status, key)) eload = {"result": True, "act": "delete", "id": key} self.event.fire_event(eload, salt.utils.event.tagify(prefix="key")) except OSError: @@ -787,8 +785,8 @@ class Key: for key in matches.get(keydir, []): try: shutil.move( - os.path.join(self.opts["pki_dir"], keydir, key), - os.path.join(self.opts["pki_dir"], self.REJ, key), + os.path.join(self.pki_dir, keydir, key), + os.path.join(self.pki_dir, self.REJ, key), ) eload = {"result": True, "act": "reject", "id": key} self.event.fire_event(eload, salt.utils.event.tagify(prefix="key")) @@ -809,8 +807,8 @@ class Key: for key in keys[self.PEND]: try: shutil.move( - os.path.join(self.opts["pki_dir"], self.PEND, key), - os.path.join(self.opts["pki_dir"], self.REJ, key), + os.path.join(self.pki_dir, self.PEND, key), + os.path.join(self.pki_dir, self.REJ, key), ) eload = {"result": True, "act": "reject", "id": key} self.event.fire_event(eload, salt.utils.event.tagify(prefix="key")) @@ -836,9 +834,9 @@ class Key: ret[status] = {} for key in keys: if status == "local": - path = os.path.join(self.opts["pki_dir"], key) + path = os.path.join(self.pki_dir, key) else: - path = os.path.join(self.opts["pki_dir"], status, key) + path = os.path.join(self.pki_dir, status, key) ret[status][key] = salt.utils.crypt.pem_finger(path, sum_type=hash_type) return ret @@ -854,9 +852,9 @@ class Key: ret[status] = {} for key in keys: if status == "local": - path = os.path.join(self.opts["pki_dir"], key) + path = os.path.join(self.pki_dir, key) else: - path = os.path.join(self.opts["pki_dir"], status, key) + path = os.path.join(self.pki_dir, status, key) ret[status][key] = salt.utils.crypt.pem_finger(path, sum_type=hash_type) return ret diff --git a/salt/master.py b/salt/master.py index 546582e5af6..784fdf6279c 100644 --- a/salt/master.py +++ b/salt/master.py @@ -289,13 +289,13 @@ class Maintenance(salt.utils.process.SignalHandlingProcess): else: acc = "accepted" - for fn_ in os.listdir(os.path.join(self.opts["pki_dir"], acc)): + for fn_ in os.listdir(os.path.join(self.pki_dir, acc)): if not fn_.startswith("."): keys.append(fn_) log.debug("Writing master key cache") # Write a temporary file securely with salt.utils.atomicfile.atomic_open( - os.path.join(self.opts["pki_dir"], acc, ".key_cache"), mode="wb" + os.path.join(self.pki_dir, acc, ".key_cache"), mode="wb" ) as cache_file: salt.payload.dump(keys, cache_file) @@ -1309,6 +1309,10 @@ class AESFuncs(TransportMethods): ) self.__setup_fileserver() self.masterapi = salt.daemons.masterapi.RemoteFuncs(opts) + if "cluster_id" in self.opts and self.opts["cluster_id"]: + self.pki_dir = self.opts["cluster_pki_dir"] + else: + self.pki_dir = self.opts.get("pki_dir", "") def __setup_fileserver(self): """ @@ -1341,8 +1345,7 @@ class AESFuncs(TransportMethods): """ if not salt.utils.verify.valid_id(self.opts, id_): return False - pub_path = os.path.join(self.opts["pki_dir"], "minions", id_) - + pub_path = os.path.join(self.pki_dir, "minions", id_) try: pub = salt.crypt.get_rsa_pub_key(pub_path) except OSError: @@ -1764,7 +1767,7 @@ class AESFuncs(TransportMethods): log.trace("Verifying signed event publish from minion") sig = load.pop("sig") this_minion_pubkey = os.path.join( - self.opts["pki_dir"], "minions/{}".format(load["id"]) + self.pki_dir, "minions/{}".format(load["id"]) ) serialized_load = salt.serializers.msgpack.serialize(load) if not salt.crypt.verify_signature( diff --git a/salt/utils/minions.py b/salt/utils/minions.py index 71b3d2f0fc0..afab2f0316e 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -216,6 +216,10 @@ class CkMinions: self.acc = "minions" else: self.acc = "accepted" + if self.opts.get("cluster_id", None) is not None: + self.pki_dir = self.opts.get("cluster_pki_dir", "") + else: + self.pki_dir = self.opts.get("pki_dir", "") def _check_nodegroup_minions(self, expr, greedy): # pylint: disable=unused-argument """ @@ -261,7 +265,7 @@ class CkMinions: Respects cache if configured """ minions = [] - pki_cache_fn = os.path.join(self.opts["pki_dir"], self.acc, ".key_cache") + pki_cache_fn = os.path.join(self.pki_dir, self.acc, ".key_cache") try: os.makedirs(os.path.dirname(pki_cache_fn)) except OSError: @@ -273,7 +277,7 @@ class CkMinions: return salt.payload.load(fn_) else: for fn_ in salt.utils.data.sorted_ignorecase( - os.listdir(os.path.join(self.opts["pki_dir"], self.acc)) + os.listdir(os.path.join(self.pki_dir, self.acc)) ): if not fn_.startswith("."): minions.append(fn_) @@ -301,7 +305,7 @@ class CkMinions: if greedy: minions = [] for fn_ in salt.utils.data.sorted_ignorecase( - os.listdir(os.path.join(self.opts["pki_dir"], self.acc)) + os.listdir(os.path.join(self.pki_dir, self.acc)) ): if not fn_.startswith("."): minions.append(fn_) @@ -447,7 +451,7 @@ class CkMinions: if greedy: mlist = [] for fn_ in salt.utils.data.sorted_ignorecase( - os.listdir(os.path.join(self.opts["pki_dir"], self.acc)) + os.listdir(os.path.join(self.pki_dir, self.acc)) ): if not fn_.startswith("."): mlist.append(fn_) @@ -677,7 +681,7 @@ class CkMinions: """ mlist = [] for fn_ in salt.utils.data.sorted_ignorecase( - os.listdir(os.path.join(self.opts["pki_dir"], self.acc)) + os.listdir(os.path.join(self.pki_dir, self.acc)) ): if not fn_.startswith("."): mlist.append(fn_) diff --git a/salt/utils/verify.py b/salt/utils/verify.py index 879128f2312..f28093492b7 100644 --- a/salt/utils/verify.py +++ b/salt/utils/verify.py @@ -298,15 +298,26 @@ def verify_env( # If acls are enabled, the pki_dir needs to remain readable, this # is still secure because the private keys are still only readable # by the user running the master - if dir_ == pki_dir: - smode = stat.S_IMODE(mode.st_mode) - if smode != 448 and smode != 488: - if os.access(dir_, os.W_OK): - os.chmod(dir_, 448) - else: - log.critical( - 'Unable to securely set the permissions of "%s".', dir_ - ) + if isinstance(pki_dir, str): + if dir_ == pki_dir: + smode = stat.S_IMODE(mode.st_mode) + if smode != 448 and smode != 488: + if os.access(dir_, os.W_OK): + os.chmod(dir_, 448) + else: + log.critical( + 'Unable to securely set the permissions of "%s".', dir_ + ) + else: + if dir_ in pki_dir: + smode = stat.S_IMODE(mode.st_mode) + if smode != 448 and smode != 488: + if os.access(dir_, os.W_OK): + os.chmod(dir_, 448) + else: + log.critical( + 'Unable to securely set the permissions of "%s".', dir_ + ) if skip_extra is False: # Run the extra verification checks @@ -539,6 +550,10 @@ def valid_id(opts, id_): try: if any(x in id_ for x in ("/", "\\", "\0")): return False + if opts.get("cluster_id", None) is not None: + pki_dir = opts["cluster_pki_dir"] + else: + pki_dir = opts["pki_dir"] return bool(clean_path(opts["pki_dir"], id_)) except (AttributeError, KeyError, TypeError, UnicodeDecodeError): return False diff --git a/tests/pytests/unit/channel/test_request_channel.py b/tests/pytests/unit/channel/test_request_channel.py index b2245dc148e..86b58cbd2e5 100644 --- a/tests/pytests/unit/channel/test_request_channel.py +++ b/tests/pytests/unit/channel/test_request_channel.py @@ -478,23 +478,25 @@ def test_serverside_exception(temp_salt_minion, temp_salt_master): assert ret == "Server-side exception handling payload" -def test_req_server_chan_encrypt_v2(pki_dir): +def test_req_server_chan_encrypt_v2(master_opts, pki_dir): loop = tornado.ioloop.IOLoop.current() - opts = { - "worker_threads": 1, - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "zmq_monitor": False, - "mworker_queue_niceness": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("master")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - } - server = salt.channel.server.ReqServerChannel.factory(opts) + master_opts.update( + { + "worker_threads": 1, + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "zmq_monitor": False, + "mworker_queue_niceness": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("master")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + } + ) + server = salt.channel.server.ReqServerChannel.factory(master_opts) dictkey = "pillar" nonce = "abcdefg" pillar_data = {"pillar1": "meh"} @@ -511,7 +513,7 @@ def test_req_server_chan_encrypt_v2(pki_dir): else: cipher = PKCS1_OAEP.new(key) aes = cipher.decrypt(ret["key"]) - pcrypt = salt.crypt.Crypticle(opts, aes) + pcrypt = salt.crypt.Crypticle(master_opts, aes) signed_msg = pcrypt.loads(ret[dictkey]) assert "sig" in signed_msg @@ -527,23 +529,25 @@ def test_req_server_chan_encrypt_v2(pki_dir): server.close() -def test_req_server_chan_encrypt_v1(pki_dir): +def test_req_server_chan_encrypt_v1(master_opts, pki_dir): loop = tornado.ioloop.IOLoop.current() - opts = { - "worker_threads": 1, - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "zmq_monitor": False, - "mworker_queue_niceness": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("master")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - } - server = salt.channel.server.ReqServerChannel.factory(opts) + master_opts.update( + { + "worker_threads": 1, + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "zmq_monitor": False, + "mworker_queue_niceness": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("master")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + } + ) + server = salt.channel.server.ReqServerChannel.factory(master_opts) dictkey = "pillar" nonce = "abcdefg" pillar_data = {"pillar1": "meh"} @@ -563,31 +567,33 @@ def test_req_server_chan_encrypt_v1(pki_dir): else: cipher = PKCS1_OAEP.new(key) aes = cipher.decrypt(ret["key"]) - pcrypt = salt.crypt.Crypticle(opts, aes) + pcrypt = salt.crypt.Crypticle(master_opts, aes) data = pcrypt.loads(ret[dictkey]) assert data == pillar_data finally: server.close() -def test_req_chan_decode_data_dict_entry_v1(pki_dir): +def test_req_chan_decode_data_dict_entry_v1(minion_opts, master_opts, pki_dir): mockloop = MagicMock() - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) server = salt.channel.server.ReqServerChannel.factory(master_opts) - client = salt.channel.client.ReqChannel.factory(opts, io_loop=mockloop) + client = salt.channel.client.ReqChannel.factory(minion_opts, io_loop=mockloop) try: dictkey = "pillar" target = "minion" @@ -607,24 +613,26 @@ def test_req_chan_decode_data_dict_entry_v1(pki_dir): server.close() -async def test_req_chan_decode_data_dict_entry_v2(pki_dir): +async def test_req_chan_decode_data_dict_entry_v2(minion_opts, master_opts, pki_dir): mockloop = MagicMock() - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) server = salt.channel.server.ReqServerChannel.factory(master_opts) - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=mockloop) + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=mockloop) dictkey = "pillar" target = "minion" @@ -632,7 +640,7 @@ async def test_req_chan_decode_data_dict_entry_v2(pki_dir): # Mock auth and message client. auth = client.auth - auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) + auth._crypticle = salt.crypt.Crypticle(minion_opts, AES_KEY) client.auth = MagicMock() client.auth.mpub = auth.mpub client.auth.authenticated = True @@ -679,24 +687,28 @@ async def test_req_chan_decode_data_dict_entry_v2(pki_dir): server.close() -async def test_req_chan_decode_data_dict_entry_v2_bad_nonce(pki_dir): +async def test_req_chan_decode_data_dict_entry_v2_bad_nonce( + minion_opts, master_opts, pki_dir +): mockloop = MagicMock() - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) server = salt.channel.server.ReqServerChannel.factory(master_opts) - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=mockloop) + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=mockloop) dictkey = "pillar" badnonce = "abcdefg" @@ -705,7 +717,7 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_nonce(pki_dir): # Mock auth and message client. auth = client.auth - auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) + auth._crypticle = salt.crypt.Crypticle(minion_opts, AES_KEY) client.auth = MagicMock() client.auth.mpub = auth.mpub client.auth.authenticated = True @@ -751,24 +763,28 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_nonce(pki_dir): server.close() -async def test_req_chan_decode_data_dict_entry_v2_bad_signature(pki_dir): +async def test_req_chan_decode_data_dict_entry_v2_bad_signature( + minion_opts, master_opts, pki_dir +): mockloop = MagicMock() - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) server = salt.channel.server.ReqServerChannel.factory(master_opts) - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=mockloop) + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=mockloop) dictkey = "pillar" badnonce = "abcdefg" @@ -777,7 +793,7 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_signature(pki_dir): # Mock auth and message client. auth = client.auth - auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) + auth._crypticle = salt.crypt.Crypticle(minion_opts, AES_KEY) client.auth = MagicMock() client.auth.mpub = auth.mpub client.auth.authenticated = True @@ -839,24 +855,28 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_signature(pki_dir): server.close() -async def test_req_chan_decode_data_dict_entry_v2_bad_key(pki_dir): +async def test_req_chan_decode_data_dict_entry_v2_bad_key( + minion_opts, master_opts, pki_dir +): mockloop = MagicMock() - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) server = salt.channel.server.ReqServerChannel.factory(master_opts) - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=mockloop) + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=mockloop) dictkey = "pillar" badnonce = "abcdefg" @@ -865,7 +885,7 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_key(pki_dir): # Mock auth and message client. auth = client.auth - auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY) + auth._crypticle = salt.crypt.Crypticle(minion_opts, AES_KEY) client.auth = MagicMock() client.auth.mpub = auth.mpub client.auth.authenticated = True @@ -895,7 +915,7 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_key(pki_dir): # Now encrypt with a different key key = salt.crypt.Crypticle.generate_key_string() - pcrypt = salt.crypt.Crypticle(opts, key) + pcrypt = salt.crypt.Crypticle(minion_opts, key) pubfn = os.path.join(master_opts["pki_dir"], "minions", "minion") pub = salt.crypt.get_rsa_pub_key(pubfn) ret[dictkey] = pcrypt.dumps(signed_msg) @@ -934,25 +954,27 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_key(pki_dir): server.close() -async def test_req_serv_auth_v1(pki_dir): - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "max_minions": 0, - "auto_accept": False, - "open_mode": False, - "key_pass": None, - "master_sign_pubkey": False, - "publish_port": 4505, - "auth_mode": 1, - } +async def test_req_serv_auth_v1(minion_opts, master_opts, pki_dir): + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "max_minions": 0, + "auto_accept": False, + "open_mode": False, + "key_pass": None, + "master_sign_pubkey": False, + "publish_port": 4505, + "auth_mode": 1, + } + ) SMaster.secrets["aes"] = { "secret": multiprocessing.Array( ctypes.c_char, @@ -960,7 +982,7 @@ async def test_req_serv_auth_v1(pki_dir): ), "reload": salt.crypt.Crypticle.generate_key_string, } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) server = salt.channel.server.ReqServerChannel.factory(master_opts) server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) server.cache_cli = False @@ -990,25 +1012,27 @@ async def test_req_serv_auth_v1(pki_dir): server.close() -async def test_req_serv_auth_v2(pki_dir): - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "max_minions": 0, - "auto_accept": False, - "open_mode": False, - "key_pass": None, - "master_sign_pubkey": False, - "publish_port": 4505, - "auth_mode": 1, - } +async def test_req_serv_auth_v2(minion_opts, master_opts, pki_dir): + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "max_minions": 0, + "auto_accept": False, + "open_mode": False, + "key_pass": None, + "master_sign_pubkey": False, + "publish_port": 4505, + "auth_mode": 1, + } + ) SMaster.secrets["aes"] = { "secret": multiprocessing.Array( ctypes.c_char, @@ -1016,7 +1040,7 @@ async def test_req_serv_auth_v2(pki_dir): ), "reload": salt.crypt.Crypticle.generate_key_string, } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) server = salt.channel.server.ReqServerChannel.factory(master_opts) server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) server.cache_cli = False @@ -1048,26 +1072,28 @@ async def test_req_serv_auth_v2(pki_dir): server.close() -async def test_req_chan_auth_v2(pki_dir, io_loop): - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "max_minions": 0, - "auto_accept": False, - "open_mode": False, - "key_pass": None, - "publish_port": 4505, - "auth_mode": 1, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } +async def test_req_chan_auth_v2(minion_opts, master_opts, pki_dir, io_loop): + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "max_minions": 0, + "auto_accept": False, + "open_mode": False, + "key_pass": None, + "publish_port": 4505, + "auth_mode": 1, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) SMaster.secrets["aes"] = { "secret": multiprocessing.Array( ctypes.c_char, @@ -1075,15 +1101,15 @@ async def test_req_chan_auth_v2(pki_dir, io_loop): ), "reload": salt.crypt.Crypticle.generate_key_string, } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) master_opts["master_sign_pubkey"] = False server = salt.channel.server.ReqServerChannel.factory(master_opts) server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) server.cache_cli = False server.master_key = salt.crypt.MasterKeys(server.opts) - opts["verify_master_pubkey_sign"] = False - opts["always_verify_signature"] = False - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=io_loop) + minion_opts["verify_master_pubkey_sign"] = False + minion_opts["always_verify_signature"] = False + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=io_loop) signin_payload = client.auth.minion_sign_in_payload() pload = client._package_load(signin_payload) try: @@ -1101,26 +1127,30 @@ async def test_req_chan_auth_v2(pki_dir, io_loop): server.close() -async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop): - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "max_minions": 0, - "auto_accept": False, - "open_mode": False, - "key_pass": None, - "publish_port": 4505, - "auth_mode": 1, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } +async def test_req_chan_auth_v2_with_master_signing( + minion_opts, master_opts, pki_dir, io_loop +): + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "max_minions": 0, + "auto_accept": False, + "open_mode": False, + "key_pass": None, + "publish_port": 4505, + "auth_mode": 1, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) SMaster.secrets["aes"] = { "secret": multiprocessing.Array( ctypes.c_char, @@ -1128,7 +1158,7 @@ async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop): ), "reload": salt.crypt.Crypticle.generate_key_string, } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) master_opts["master_sign_pubkey"] = True master_opts["master_use_pubkey_signature"] = False master_opts["signing_key_pass"] = True @@ -1137,17 +1167,17 @@ async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop): server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) server.cache_cli = False server.master_key = salt.crypt.MasterKeys(server.opts) - opts["verify_master_pubkey_sign"] = True - opts["always_verify_signature"] = True - opts["master_sign_key_name"] = "master_sign" - opts["master"] = "master" + minion_opts["verify_master_pubkey_sign"] = True + minion_opts["always_verify_signature"] = True + minion_opts["master_sign_key_name"] = "master_sign" + minion_opts["master"] = "master" assert ( pki_dir.joinpath("minion", "minion_master.pub").read_text() == pki_dir.joinpath("master", "master.pub").read_text() ) - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=io_loop) + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=io_loop) signin_payload = client.auth.minion_sign_in_payload() pload = client._package_load(signin_payload) assert "version" in pload @@ -1196,28 +1226,32 @@ async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop): server.close() -async def test_req_chan_auth_v2_new_minion_with_master_pub(pki_dir, io_loop): +async def test_req_chan_auth_v2_new_minion_with_master_pub( + minion_opts, master_opts, pki_dir, io_loop +): pki_dir.joinpath("master", "minions", "minion").unlink() - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "max_minions": 0, - "auto_accept": False, - "open_mode": False, - "key_pass": None, - "publish_port": 4505, - "auth_mode": 1, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "max_minions": 0, + "auto_accept": False, + "open_mode": False, + "key_pass": None, + "publish_port": 4505, + "auth_mode": 1, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) SMaster.secrets["aes"] = { "secret": multiprocessing.Array( ctypes.c_char, @@ -1225,15 +1259,15 @@ async def test_req_chan_auth_v2_new_minion_with_master_pub(pki_dir, io_loop): ), "reload": salt.crypt.Crypticle.generate_key_string, } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) master_opts["master_sign_pubkey"] = False server = salt.channel.server.ReqServerChannel.factory(master_opts) server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) server.cache_cli = False server.master_key = salt.crypt.MasterKeys(server.opts) - opts["verify_master_pubkey_sign"] = False - opts["always_verify_signature"] = False - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=io_loop) + minion_opts["verify_master_pubkey_sign"] = False + minion_opts["always_verify_signature"] = False + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=io_loop) signin_payload = client.auth.minion_sign_in_payload() pload = client._package_load(signin_payload) try: @@ -1249,7 +1283,9 @@ async def test_req_chan_auth_v2_new_minion_with_master_pub(pki_dir, io_loop): server.close() -async def test_req_chan_auth_v2_new_minion_with_master_pub_bad_sig(pki_dir, io_loop): +async def test_req_chan_auth_v2_new_minion_with_master_pub_bad_sig( + minion_opts, master_opts, pki_dir, io_loop +): pki_dir.joinpath("master", "minions", "minion").unlink() @@ -1261,25 +1297,27 @@ async def test_req_chan_auth_v2_new_minion_with_master_pub_bad_sig(pki_dir, io_l mapub.unlink() mapub.write_text(MASTER2_PUB_KEY.strip()) - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "max_minions": 0, - "auto_accept": False, - "open_mode": False, - "key_pass": None, - "publish_port": 4505, - "auth_mode": 1, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "max_minions": 0, + "auto_accept": False, + "open_mode": False, + "key_pass": None, + "publish_port": 4505, + "auth_mode": 1, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) SMaster.secrets["aes"] = { "secret": multiprocessing.Array( ctypes.c_char, @@ -1287,15 +1325,15 @@ async def test_req_chan_auth_v2_new_minion_with_master_pub_bad_sig(pki_dir, io_l ), "reload": salt.crypt.Crypticle.generate_key_string, } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) master_opts["master_sign_pubkey"] = False server = salt.channel.server.ReqServerChannel.factory(master_opts) server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) server.cache_cli = False server.master_key = salt.crypt.MasterKeys(server.opts) - opts["verify_master_pubkey_sign"] = False - opts["always_verify_signature"] = False - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=io_loop) + minion_opts["verify_master_pubkey_sign"] = False + minion_opts["always_verify_signature"] = False + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=io_loop) signin_payload = client.auth.minion_sign_in_payload() pload = client._package_load(signin_payload) try: @@ -1311,29 +1349,33 @@ async def test_req_chan_auth_v2_new_minion_with_master_pub_bad_sig(pki_dir, io_l server.close() -async def test_req_chan_auth_v2_new_minion_without_master_pub(pki_dir, io_loop): +async def test_req_chan_auth_v2_new_minion_without_master_pub( + minion_opts, master_opts, pki_dir, io_loop +): pki_dir.joinpath("master", "minions", "minion").unlink() pki_dir.joinpath("minion", "minion_master.pub").unlink() - opts = { - "master_uri": "tcp://127.0.0.1:4506", - "interface": "127.0.0.1", - "ret_port": 4506, - "ipv6": False, - "sock_dir": ".", - "pki_dir": str(pki_dir.joinpath("minion")), - "id": "minion", - "__role": "minion", - "keysize": 4096, - "max_minions": 0, - "auto_accept": False, - "open_mode": False, - "key_pass": None, - "publish_port": 4505, - "auth_mode": 1, - "acceptance_wait_time": 3, - "acceptance_wait_time_max": 3, - } + minion_opts.update( + { + "master_uri": "tcp://127.0.0.1:4506", + "interface": "127.0.0.1", + "ret_port": 4506, + "ipv6": False, + "sock_dir": ".", + "pki_dir": str(pki_dir.joinpath("minion")), + "id": "minion", + "__role": "minion", + "keysize": 4096, + "max_minions": 0, + "auto_accept": False, + "open_mode": False, + "key_pass": None, + "publish_port": 4505, + "auth_mode": 1, + "acceptance_wait_time": 3, + "acceptance_wait_time_max": 3, + } + ) SMaster.secrets["aes"] = { "secret": multiprocessing.Array( ctypes.c_char, @@ -1341,15 +1383,15 @@ async def test_req_chan_auth_v2_new_minion_without_master_pub(pki_dir, io_loop): ), "reload": salt.crypt.Crypticle.generate_key_string, } - master_opts = dict(opts, pki_dir=str(pki_dir.joinpath("master"))) + master_opts.update(pki_dir=str(pki_dir.joinpath("master"))) master_opts["master_sign_pubkey"] = False server = salt.channel.server.ReqServerChannel.factory(master_opts) server.auto_key = salt.daemons.masterapi.AutoKey(server.opts) server.cache_cli = False server.master_key = salt.crypt.MasterKeys(server.opts) - opts["verify_master_pubkey_sign"] = False - opts["always_verify_signature"] = False - client = salt.channel.client.AsyncReqChannel.factory(opts, io_loop=io_loop) + minion_opts["verify_master_pubkey_sign"] = False + minion_opts["always_verify_signature"] = False + client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=io_loop) signin_payload = client.auth.minion_sign_in_payload() pload = client._package_load(signin_payload) try: diff --git a/tests/pytests/unit/test_crypt.py b/tests/pytests/unit/test_crypt.py index b2bc2a85382..03920de909b 100644 --- a/tests/pytests/unit/test_crypt.py +++ b/tests/pytests/unit/test_crypt.py @@ -187,3 +187,45 @@ def test_dropfile_contents(tmp_path, master_opts): salt.crypt.dropfile(str(tmp_path), master_opts["user"], master_id=master_opts["id"]) with salt.utils.files.fopen(str(tmp_path / ".dfn"), "r") as fp: assert master_opts["id"] == fp.read() + + +def test_master_keys_without_cluster_id(tmp_path, master_opts): + master_opts["pki_dir"] = str(tmp_path) + assert master_opts["cluster_id"] is None + assert master_opts["cluster_pki_dir"] is None + mkeys = salt.crypt.MasterKeys(master_opts) + expected_master_pub = str(tmp_path / "master.pub") + expected_master_rsa = str(tmp_path / "master.pem") + assert expected_master_pub == mkeys.master_pub_path + assert expected_master_rsa == mkeys.master_rsa_path + assert mkeys.cluster_pub_path is None + assert mkeys.cluster_rsa_path is None + assert mkeys.pub_path == expected_master_pub + assert mkeys.rsa_path == expected_master_rsa + assert mkeys.key == mkeys.master_key + + +def test_master_keys_with_cluster_id(tmp_path, master_opts): + master_pki_path = tmp_path / "master_pki" + cluster_pki_path = tmp_path / "cluster_pki" + # The paths need to exist + master_pki_path.mkdir() + cluster_pki_path.mkdir() + + master_opts["pki_dir"] = str(master_pki_path) + master_opts["cluster_id"] = "cluster1" + master_opts["cluster_pki_dir"] = str(cluster_pki_path) + + mkeys = salt.crypt.MasterKeys(master_opts) + + expected_master_pub = str(master_pki_path / "master.pub") + expected_master_rsa = str(master_pki_path / "master.pem") + expected_cluster_pub = str(cluster_pki_path / "cluster.pub") + expected_cluster_rsa = str(cluster_pki_path / "cluster.pem") + assert expected_master_pub == mkeys.master_pub_path + assert expected_master_rsa == mkeys.master_rsa_path + assert expected_cluster_pub == mkeys.cluster_pub_path + assert expected_cluster_rsa == mkeys.cluster_rsa_path + assert mkeys.pub_path == expected_cluster_pub + assert mkeys.rsa_path == expected_cluster_rsa + assert mkeys.key == mkeys.cluster_key