File several-fixes-for-security-issues.patch of Package venv-salt-minion

From 398b545e53766aeb121c3680401baf2455961f7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <psuarezhernandez@suse.com>
Date: Thu, 26 Jun 2025 10:15:12 +0100
Subject: [PATCH] Several fixes for security issues
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Several fixes for security issues

(bsc#1244561, CVE-2024-38822)
(bsc#1244564, CVE-2024-38823)
(bsc#1244565, CVE-2024-38824)
(bsc#1244566, CVE-2024-38825)
(bsc#1244567, CVE-2025-22240)
(bsc#1244568, CVE-2025-22236)
(bsc#1244570, CVE-2025-22241)
(bsc#1244571, CVE-2025-22237)
(bsc#1244572, CVE-2025-22238)
(bsc#1244574, CVE-2025-22239)
(bsc#1244575, CVE-2025-22242)

Request server hardening
- Each minion get's it's own aes session for request server
  communication.
- Request client always includes id and token, these are always
  validated server side.
- Add timestamp and enforce configurable ttl for request server
  messages.

Other relevant commit messages:

- Add deprecation message to salt.auth.pki
- Add test and fix for file_recv cve
- Prevent traversal in local_cache::save_minions
- Fix traversal in gitfs find_file
- Fix traversals in salt.utils.virt
- Fix traversal in pub_ret
- On-demand pillar fix
- Include url validation tests
- Minion event filtering
- Reasonable failures when pillars timeout
- Adjust and fix code and tests after backporting
  to openSUSE/release/3006.0

Co-authored-by: Pablo Suárez Hernández <psuarezhernandez@suse.com>

* Fix test_pillar_timeout unit test in Salt Shaker

* Fix tests failures on functional/channel/test_req_channel.py

* Fix gitfs test failures due uncomplete cleanup

* Fix cp.push module function and its integration test (#68053)

fix file_recv path verification for subdirs

Adapt backport to fit openSUSE/release/3006.0

* Make send_req_async wait longer (#68085)

Allow send_req_async to wait longer when sending a return back to the
master. The minion should wait at least as long as the max possible
return timeout.

Update creds when session key changes

Add unit test to validate session key rotation

Add changelog for #68079

* Remove token to prevent decoding errors (#68084)

Clean up verify_load calls in master request server

Remove tok in salt.channel.ReqServer.validate_token so it is not passed
to the request handlers.

Add tests around payload token removal

Add changelog for #68076

* Fix checking of non-url style git remotes (#68089)

Handle git@github.com/.. style remotes

Fix checking of non-url style git remotes

Fixes handling of git@hostname:/path/repo style remotes. Takes initial
version from #68082 and fixes it. Still uses the shortcut of converting
the remote to ssh:// URL style.

Split out converting remote to URL

Splits out converting remotes to URL form to allow testing of that
conversion - without doing that risk issues with the regex

Add additional test cases

Add additional test cases based on gitfs docs and what they say should
be valid

Make utility functions classmethods

Fix key vs remote wart

Allow subdirs in GitFS find_file check (#68083)

Add test for find_file in sub directories

Add changelog for #68072

---------

Co-authored-by: Daniel A. Wozniak <daniel.wozniak@broadcom.com>
Co-authored-by: hurzhurz <hurz@gmx.org>
---
 changelog/67941.fixed.md                      |   1 +
 changelog/68033.fixed.md                      |  56 ++
 changelog/68072.fixed.md                      |   1 +
 changelog/68076.fixed.md                      |   1 +
 changelog/68079.fixed.md                      |   1 +
 changelog/68087.fixed.md                      |   1 +
 salt/auth/pki.py                              |   7 +-
 salt/channel/client.py                        | 120 ++-
 salt/channel/server.py                        | 190 ++++-
 salt/config/__init__.py                       |   8 +
 salt/crypt.py                                 |  52 +-
 salt/daemons/masterapi.py                     |  24 +
 salt/exceptions.py                            |   5 +
 salt/fileclient.py                            |   2 -
 salt/fileserver/__init__.py                   |   2 +-
 salt/master.py                                | 120 +--
 salt/minion.py                                |  32 +-
 salt/modules/cp.py                            |   1 -
 salt/modules/event.py                         |   1 -
 salt/modules/mine.py                          |   8 -
 salt/modules/publish.py                       |   8 -
 salt/modules/saltutil.py                      |   2 -
 salt/pillar/__init__.py                       |  42 +-
 salt/pillar/git_pillar.py                     |   2 +-
 salt/returners/local_cache.py                 |  21 +-
 salt/utils/event.py                           |   2 -
 salt/utils/gitfs.py                           |  90 ++-
 salt/utils/verify.py                          |  84 ++-
 salt/utils/virt.py                            |   7 +-
 tests/conftest.py                             |  14 +-
 tests/integration/modules/test_cp.py          |   7 +-
 tests/pytests/functional/channel/conftest.py  |  41 +
 .../functional/channel/test_req_channel.py    | 444 +++++++++++
 .../pytests/functional/channel/test_server.py |  59 +-
 .../transport/server/test_req_channel.py      |   6 +
 .../integration/master/test_minion_event.py   |  47 ++
 .../integration/master/test_recv_file.py      |  29 +
 .../integration/minion/test_return_retries.py |  72 ++
 tests/pytests/unit/channel/test_server.py     |  52 +-
 .../masterapi/test_valid_minion_tag.py        |  23 +
 .../unit/fileserver/gitfs/test_gitfs.py       |  10 +-
 tests/pytests/unit/modules/test_cp.py         |   1 -
 tests/pytests/unit/pillar/test_pillar.py      |  18 +
 tests/pytests/unit/test_crypt.py              | 149 ++++
 tests/pytests/unit/test_master.py             | 159 +++-
 tests/pytests/unit/transport/test_zeromq.py   | 706 ++++++++++--------
 tests/pytests/unit/utils/test_gitfs.py        | 146 ++++
 tests/pytests/unit/utils/test_virt.py         |  21 +
 .../unit/utils/verify/test_clean_path.py      | 117 +++
 tests/pytests/unit/utils/verify/test_url.py   |  44 ++
 tests/unit/test_master.py                     |   1 +
 tests/unit/utils/test_gitfs.py                |   1 +
 tools/pkg/build.py                            |   4 +
 53 files changed, 2504 insertions(+), 558 deletions(-)
 create mode 100644 changelog/67941.fixed.md
 create mode 100644 changelog/68033.fixed.md
 create mode 100644 changelog/68072.fixed.md
 create mode 100644 changelog/68076.fixed.md
 create mode 100644 changelog/68079.fixed.md
 create mode 100644 changelog/68087.fixed.md
 create mode 100644 tests/pytests/functional/channel/test_req_channel.py
 create mode 100644 tests/pytests/integration/master/test_minion_event.py
 create mode 100644 tests/pytests/integration/master/test_recv_file.py
 create mode 100644 tests/pytests/unit/daemons/masterapi/test_valid_minion_tag.py
 create mode 100644 tests/pytests/unit/utils/test_virt.py
 create mode 100644 tests/pytests/unit/utils/verify/test_clean_path.py
 create mode 100644 tests/pytests/unit/utils/verify/test_url.py

diff --git a/changelog/67941.fixed.md b/changelog/67941.fixed.md
new file mode 100644
index 00000000000..b9c1000d155
--- /dev/null
+++ b/changelog/67941.fixed.md
@@ -0,0 +1 @@
+Fix cp.push module function and its integration test
diff --git a/changelog/68033.fixed.md b/changelog/68033.fixed.md
new file mode 100644
index 00000000000..c45c9dd5bc5
--- /dev/null
+++ b/changelog/68033.fixed.md
@@ -0,0 +1,56 @@
+CVE-2024-38822
+Multiple methods in the salt master skip minion token validation. Therefore a misbehaving minion can impersonate another minion.
+
+CVSS 2.7 V:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N
+
+CVE-2024-38823
+Salt's request server is vulnerable to replay attacks when not using a TLS encrypted transport.
+
+CVSS Score 2.7 AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N
+
+CVE-2024-38824
+Directory traversal vulnerability in recv_file method allows arbitrary files to be written to the master cache directory.
+
+CVSS Score 9.6 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
+
+CVE-2024-38825
+The salt.auth.pki module does not properly authenticate callers. The "password" field contains a public certificate which is validated against a CA certificate by the module. This is not pki authentication, as the caller does not need access to the corresponding private key for the authentication attempt to be accepted.
+
+CVSS Score 6.4 AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
+
+CVE-2025-22236
+Minion event bus authorization bypass. An attacker with access to a minion key can craft a message which may be able to execute a job on other minions (>= 3007.0).
+
+CVSS 8.1 AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
+
+CVE-2025-22237
+An attacker with access to a minion key can exploit the 'on demand' pillar functionality with a specially crafted git url which could cause and arbitrary command to be run on the master with the same privileges as the master process.
+
+CVSS 6.7 AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
+
+CVE-2025-22238
+Directory traversal attack in minion file cache creation. The master's default cache is vulnerable to a directory traversal attack. Which could be leveraged to write or overwrite 'cache' files outside of the cache directory.
+
+CVSS 4.2 AV:L/AC:L/PR:H/UI:R/S:U/C:N/I:H/A:N
+
+CVE-2025-22239
+Arbitrary event injection on Salt Master. The master's "_minion_event" method can be used by and authorized minion to send arbitrary events onto the master's event bus.
+
+CVSS 8.1 AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
+
+CVE-2025-22240
+Arbitrary directory creation or file deletion. In the find_file method of the GitFS class, a path is created using os.path.join using unvalidated input from the “tgt_env” variable. This can be exploited by an attacker to delete any file on the Master's process has permissions to
+
+CVSS 6.3 AV:L/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:H
+
+CVE-2025-22241
+File contents overwrite the VirtKey class is called when “on-demand pillar” data is requested and uses un-validated input to create paths to the “pki directory”. The functionality is used to auto-accept Minion authentication keys based on a pre-placed “authorization file” at a specific location and is present in the default configuration.
+
+CVSS 5.6 AV:L/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:N
+
+CVE-2025-22242
+Worker process denial of service through file read operation. .A vulnerability exists in the Master's “pub_ret” method which is exposed to all minions. The un-sanitized input value “jid” is used to construct a path which is then opened for reading. An attacker could exploit this vulnerabilities by attempting to read from a filename that will not return any data, e.g. by targeting a pipe node on the proc file system.
+
+CVSS 5.6 AV:L/AC:H/PR:H/UI:R/S:U/C:H/I:N/A:H
+
+This release also includes sqlite 3.50.1 to address CVE-2025-29087
diff --git a/changelog/68072.fixed.md b/changelog/68072.fixed.md
new file mode 100644
index 00000000000..c0b54445f14
--- /dev/null
+++ b/changelog/68072.fixed.md
@@ -0,0 +1 @@
+Fix GitFS file_find for file in sub-directories
diff --git a/changelog/68076.fixed.md b/changelog/68076.fixed.md
new file mode 100644
index 00000000000..33bc7daf017
--- /dev/null
+++ b/changelog/68076.fixed.md
@@ -0,0 +1 @@
+Token validation removes token from request handler payload
diff --git a/changelog/68079.fixed.md b/changelog/68079.fixed.md
new file mode 100644
index 00000000000..85c070428e1
--- /dev/null
+++ b/changelog/68079.fixed.md
@@ -0,0 +1 @@
+Fix minion connectivity issues by ensuring auth notices refreshed session token
diff --git a/changelog/68087.fixed.md b/changelog/68087.fixed.md
new file mode 100644
index 00000000000..16667416b71
--- /dev/null
+++ b/changelog/68087.fixed.md
@@ -0,0 +1 @@
+Fix file_recv path verification to allow subdirs used by cp.push
diff --git a/salt/auth/pki.py b/salt/auth/pki.py
index f33e3ccf00c..cdc059f6a92 100644
--- a/salt/auth/pki.py
+++ b/salt/auth/pki.py
@@ -17,6 +17,7 @@ TODO: Add a 'ca_dir' option to configure a directory of CA files, a la Apache.
 import logging
 
 import salt.utils.files
+import salt.utils.versions
 
 # pylint: disable=import-error
 try:
@@ -30,7 +31,7 @@ try:
             from Cryptodome.Util import asn1
         except ImportError:
             from Crypto.Util import asn1  # nosec
-        import OpenSSL
+        import OpenSSL  # pylint: disable=W8410
     HAS_DEPS = True
 except ImportError:
     HAS_DEPS = False
@@ -71,6 +72,10 @@ def auth(username, password, **kwargs):
             your_user:
               - .*
     """
+    salt.utils.versions.warn_until(
+        "Argon",
+        "This module has been deprecated as it is known to be insecure.",
+    )
     pem = password
     cacert_file = __salt__["config.get"]("external_auth:pki:ca_file")
 
diff --git a/salt/channel/client.py b/salt/channel/client.py
index 34aafb2c9e2..25f4af7689d 100644
--- a/salt/channel/client.py
+++ b/salt/channel/client.py
@@ -40,6 +40,9 @@ except ImportError:
 
 log = logging.getLogger(__name__)
 
+REQUEST_CHANNEL_TIMEOUT = 60
+REQUEST_CHANNEL_TRIES = 3
+
 
 class ReqChannel:
     """
@@ -120,6 +123,9 @@ class AsyncReqChannel:
         if io_loop is None:
             io_loop = salt.ext.tornado.ioloop.IOLoop.current()
 
+        timeout = opts.get("request_channel_timeout", REQUEST_CHANNEL_TIMEOUT)
+        tries = opts.get("request_channel_tries", REQUEST_CHANNEL_TRIES)
+
         crypt = kwargs.get("crypt", "aes")
         if crypt != "clear":
             # we don't need to worry about auth as a kwarg, since its a singleton
@@ -128,9 +134,17 @@ class AsyncReqChannel:
             auth = None
 
         transport = salt.transport.request_client(opts, io_loop)
-        return cls(opts, transport, auth)
+        return cls(opts, transport, auth, tries=tries, timeout=timeout)
 
-    def __init__(self, opts, transport, auth, **kwargs):
+    def __init__(
+        self,
+        opts,
+        transport,
+        auth,
+        timeout=REQUEST_CHANNEL_TIMEOUT,
+        tries=REQUEST_CHANNEL_TRIES,
+        **kwargs
+    ):
         self.opts = dict(opts)
         self.transport = transport
         self.auth = auth
@@ -138,6 +152,8 @@ class AsyncReqChannel:
         if self.auth:
             self.master_pubkey_path = os.path.join(self.opts["pki_dir"], self.auth.mpub)
         self._closing = False
+        self.timeout = timeout
+        self.tries = tries
 
     @property
     def crypt(self):
@@ -149,35 +165,90 @@ class AsyncReqChannel:
     def ttype(self):
         return self.transport.ttype
 
-    def _package_load(self, load):
-        return {
+    def _package_load(self, load, nonce=None):
+        """
+        Prepare the load to be sent over the wire.
+
+        For aes channels add a nonce, timestamp and signed token to the load
+        before encrypting it using our aes session key. Then wrap the encrypted
+        load with some meta data. For 'clear' encryption, no extra feilds are
+        added to the load. The unencyrpted load is wrapped with meta data.
+        """
+        if self.crypt == "aes":
+            if nonce is None:
+                nonce = uuid.uuid4().hex
+            try:
+                load["nonce"] = nonce
+                load["ts"] = int(time.time())
+                load["tok"] = self.auth.gen_token(b"salt")
+                load["id"] = self.opts["id"]
+            except TypeError:
+                # Backwards compatability for non dict loads, let the load get
+                # sent and fail to authenticate.
+                log.warning(
+                    "Invalid load passed to request channel. Type is %s should be dict.",
+                    type(load),
+                )
+
+            load = self.auth.session_crypticle.dumps(load)
+
+        ret = {
             "enc": self.crypt,
             "load": load,
-            "version": 2,
+            "version": 3,
         }
+        if self.crypt == "aes":
+            ret["id"] = self.opts["id"]
+        return ret
+
+    @salt.ext.tornado.gen.coroutine
+    def _send_with_retry(self, load, tries, timeout):
+        _try = 1
+        while True:
+            try:
+                ret = yield self.transport.send(
+                    load,
+                    timeout=timeout,
+                )
+                break
+            except Exception as exc:  # pylint: disable=broad-except
+                log.trace("Failed to send msg %r", exc)
+                if _try >= tries:
+                    raise
+                else:
+                    _try += 1
+                    continue
+        raise salt.ext.tornado.gen.Return(ret)
 
     @salt.ext.tornado.gen.coroutine
     def crypted_transfer_decode_dictentry(
         self,
         load,
         dictkey=None,
-        timeout=60,
+        timeout=None,
+        tries=None,
     ):
-        nonce = uuid.uuid4().hex
-        load["nonce"] = nonce
+        if timeout is None:
+            timeout = self.timeout
+        if tries is None:
+            tries = self.tries
         if not self.auth.authenticated:
             yield self.auth.authenticate()
-        ret = yield self.transport.send(
-            self._package_load(self.auth.crypticle.dumps(load)),
-            timeout=timeout,
+
+        nonce = uuid.uuid4().hex
+        ret = yield self._send_with_retry(
+            self._package_load(load, nonce),
+            tries,
+            timeout,
         )
         key = self.auth.get_keys()
         if "key" not in ret:
             # Reauth in the case our key is deleted on the master side.
             yield self.auth.authenticate()
-            ret = yield self.transport.send(
-                self._package_load(self.auth.crypticle.dumps(load)),
-                timeout=timeout,
+            ret = yield self._send_with_retry(
+                self._package_load(load, nonce),
+                tries,
+                timeout,
             )
         if HAS_M2:
             aes = key.private_decrypt(ret["key"], RSA.pkcs1_oaep_padding)
@@ -209,7 +280,7 @@ class AsyncReqChannel:
         return salt.crypt.verify_signature(self.master_pubkey_path, data, sig)
 
     @salt.ext.tornado.gen.coroutine
-    def _crypted_transfer(self, load, timeout=60, raw=False):
+    def _crypted_transfer(self, load, timeout, raw=False):
         """
         Send a load across the wire, with encryption
 
@@ -222,15 +293,13 @@ class AsyncReqChannel:
         :param dict load: A load to send across the wire
         :param int timeout: The number of seconds on a response before failing
         """
-        nonce = uuid.uuid4().hex
-        if load and isinstance(load, dict):
-            load["nonce"] = nonce
 
         @salt.ext.tornado.gen.coroutine
         def _do_transfer():
             # Yield control to the caller. When send() completes, resume by populating data with the Future.result
+            nonce = uuid.uuid4().hex
             data = yield self.transport.send(
-                self._package_load(self.auth.crypticle.dumps(load)),
+                self._package_load(load, nonce),
                 timeout=timeout,
             )
             # we may not have always data
@@ -238,9 +307,10 @@ class AsyncReqChannel:
             # communication, we do not subscribe to return events, we just
             # upload the results to the master
             if data:
-                data = self.auth.crypticle.loads(data, raw, nonce=nonce)
+                data = self.auth.session_crypticle.loads(data, raw, nonce=nonce)
             if not raw or self.ttype == "tcp":  # XXX Why is this needed for tcp
                 data = salt.transport.frame.decode_embedded_strs(data)
+
             raise salt.ext.tornado.gen.Return(data)
 
         if not self.auth.authenticated:
@@ -256,7 +326,7 @@ class AsyncReqChannel:
         raise salt.ext.tornado.gen.Return(ret)
 
     @salt.ext.tornado.gen.coroutine
-    def _uncrypted_transfer(self, load, timeout=60):
+    def _uncrypted_transfer(self, load, timeout):
         """
         Send a load across the wire in cleartext
 
@@ -271,7 +341,7 @@ class AsyncReqChannel:
         raise salt.ext.tornado.gen.Return(ret)
 
     @salt.ext.tornado.gen.coroutine
-    def send(self, load, tries=3, timeout=60, raw=False):
+    def send(self, load, tries=None, timeout=None, raw=False):
         """
         Send a request, return a future which will complete when we send the message
 
@@ -279,6 +349,10 @@ class AsyncReqChannel:
         :param int tries: The number of times to make before failure
         :param int timeout: The number of seconds on a response before failing
         """
+        if timeout is None:
+            timeout = self.timeout
+        if tries is None:
+            tries = self.tries
         _try = 1
         while True:
             try:
@@ -427,7 +501,7 @@ class AsyncPubChannel:
         return {
             "enc": self.crypt,
             "load": load,
-            "version": 2,
+            "version": 3,
         }
 
     @salt.ext.tornado.gen.coroutine
diff --git a/salt/channel/server.py b/salt/channel/server.py
index 59da3a2dc26..abef8aa2f0c 100644
--- a/salt/channel/server.py
+++ b/salt/channel/server.py
@@ -8,6 +8,7 @@ import binascii
 import hashlib
 import logging
 import os
+import pathlib
 import shutil
 import time
 
@@ -57,6 +58,31 @@ class ReqServerChannel:
         self.opts = opts
         self.transport = transport
         self.event = None
+        self.master_key = None
+        (pathlib.Path(self.opts["cachedir"]) / "sessions").mkdir(exist_ok=True)
+        self.sessions = {}
+
+    def session_key(self, minion):
+        """
+        Returns a session key for the given minion id.
+        """
+        now = time.time()
+        if minion in self.sessions:
+            if now - self.sessions[minion][0] < self.opts["publish_session"]:
+                return self.sessions[minion][1]
+
+        path = pathlib.Path(self.opts["cachedir"]) / "sessions" / minion
+        try:
+            if now - path.stat().st_mtime > self.opts["publish_session"]:
+                salt.crypt.Crypticle.write_key(path)
+        except FileNotFoundError:
+            salt.crypt.Crypticle.write_key(path)
+
+        self.sessions[minion] = (
+            path.stat().st_mtime,
+            salt.crypt.Crypticle.read_key(path),
+        )
+        return self.sessions[minion][1]
 
     def pre_fork(self, process_manager):
         """
@@ -104,8 +130,16 @@ class ReqServerChannel:
 
     @salt.ext.tornado.gen.coroutine
     def handle_message(self, payload):
+        if (
+            not isinstance(payload, dict)
+            or "enc" not in payload
+            or "load" not in payload
+        ):
+            log.warn("bad load received on socket")
+            raise salt.ext.tornado.gen.Return("bad load")
+        version = payload.get("version", 0)
         try:
-            payload = self._decode_payload(payload)
+            payload = self._decode_payload(payload, version)
         except Exception as exc:  # pylint: disable=broad-except
             exc_type = type(exc).__name__
             if exc_type == "AuthenticationError":
@@ -139,10 +173,6 @@ class ReqServerChannel:
                 "bad load: id {} is not a string".format(id_)
             )
 
-        version = 0
-        if "version" in payload:
-            version = payload["version"]
-
         sign_messages = False
         if version > 1:
             sign_messages = True
@@ -151,14 +181,47 @@ class ReqServerChannel:
         # anything about our key auth
         if payload["enc"] == "clear" and payload.get("load", {}).get("cmd") == "_auth":
             start = time.time()
-            ret = self._auth(payload["load"], sign_messages)
+            ret = self._auth(payload["load"], sign_messages, version)
             if self.opts.get("master_stats", False):
                 yield self.payload_handler({"cmd": "_auth", "_start": start})
             raise salt.ext.tornado.gen.Return(ret)
 
-        nonce = None
-        if version > 1:
-            nonce = payload["load"].pop("nonce", None)
+        if payload["enc"] == "aes":
+            nonce = None
+            if version > 1:
+                nonce = payload["load"].pop("nonce", None)
+
+            # Check validity of message ttl and id's match
+            if version > 2:
+                if self.opts["request_server_ttl"] > 0:
+                    ttl = time.time() - payload["load"]["ts"]
+                    if ttl > self.opts["request_server_ttl"]:
+                        log.warning(
+                            "Received request from %s with expired ttl: %d > %d",
+                            payload["load"]["id"],
+                            ttl,
+                            self.opts["request_server_ttl"],
+                        )
+                        raise salt.ext.tornado.gen.Return("bad load")
+
+                if payload["id"] != payload["load"]["id"]:
+                    log.warning(
+                        "Request id mismatch. Found '%s' but expected '%s'",
+                        payload["load"]["id"],
+                        payload["id"],
+                    )
+                    raise salt.ext.tornado.gen.Return("bad load")
+                if not salt.utils.verify.valid_id(self.opts, payload["load"]["id"]):
+                    log.warning(
+                        "Request contains invalid minion id '%s'", payload["load"]["id"]
+                    )
+                    raise salt.ext.tornado.gen.Return("bad load")
+                if not self.validate_token(payload, required=True):
+                    raise salt.ext.tornado.gen.Return("bad load")
+            # The token won't always be present in the payload for v2 and
+            # below, but if it is we always wanto validate it.
+            elif not self.validate_token(payload, required=False):
+                raise salt.ext.tornado.gen.Return("bad load")
 
         # TODO: test
         try:
@@ -174,7 +237,14 @@ class ReqServerChannel:
         if req_fun == "send_clear":
             raise salt.ext.tornado.gen.Return(ret)
         elif req_fun == "send":
-            raise salt.ext.tornado.gen.Return(self.crypticle.dumps(ret, nonce))
+            if version > 2:
+                raise salt.ext.tornado.gen.Return(
+                    salt.crypt.Crypticle(self.opts, self.session_key(id_)).dumps(
+                        ret, nonce
+                    )
+                )
+            else:
+                raise salt.ext.tornado.gen.Return(self.crypticle.dumps(ret, nonce))
         elif req_fun == "send_private":
             raise salt.ext.tornado.gen.Return(
                 self._encrypt_private(
@@ -189,7 +259,14 @@ class ReqServerChannel:
         # always attempt to return an error to the minion
         raise salt.ext.tornado.gen.Return("Server-side exception handling payload")
 
-    def _encrypt_private(self, ret, dictkey, target, nonce=None, sign_messages=True):
+    def _encrypt_private(
+        self,
+        ret,
+        dictkey,
+        target,
+        nonce=None,
+        sign_messages=True,
+    ):
         """
         The server equivalent of ReqChannel.crypted_transfer_decode_dictentry
         """
@@ -200,7 +277,8 @@ class ReqServerChannel:
         try:
             pub = salt.crypt.get_rsa_pub_key(pubfn)
         except (ValueError, IndexError, TypeError):
-            return self.crypticle.dumps({})
+            log.error("Bad load from minion")
+            return {"error": "bad load"}
         except OSError:
             log.error("AES key not found")
             return {"error": "AES key not found"}
@@ -255,27 +333,71 @@ class ReqServerChannel:
             return True
         return False
 
-    def _decode_payload(self, payload):
-        # Sometimes msgpack deserialization of random bytes could be successful,
-        # so we need to ensure payload in good shape to process this function.
-        if (
-            not isinstance(payload, dict)
-            or "enc" not in payload
-            or "load" not in payload
-        ):
-            raise SaltDeserializationError("bad load received on socket!")
-
+    def _decode_payload(self, payload, version):
         # we need to decrypt it
         if payload["enc"] == "aes":
-            try:
-                payload["load"] = self.crypticle.loads(payload["load"])
-            except salt.crypt.AuthenticationError:
-                if not self._update_aes():
-                    raise
-                payload["load"] = self.crypticle.loads(payload["load"])
+            if version > 2:
+                if salt.utils.verify.valid_id(self.opts, payload["id"]):
+                    payload["load"] = salt.crypt.Crypticle(
+                        self.opts,
+                        self.session_key(payload["id"]),
+                    ).loads(payload["load"])
+                else:
+                    raise SaltDeserializationError("Encountered invalid id")
+            else:
+                try:
+                    payload["load"] = self.crypticle.loads(payload["load"])
+                except salt.crypt.AuthenticationError:
+                    if not self._update_aes():
+                        raise
+                    payload["load"] = self.crypticle.loads(payload["load"])
         return payload
 
-    def _auth(self, load, sign_messages=False):
+    def validate_token(self, payload, required=True):
+        """
+        Validate the token (tok) and minion id (id) in the payload. If the
+        payload and token exist they will be validated even if required is
+        False.
+
+        When required is False and either the tok or id is not found in the
+        load, this check will pass.
+
+        This method has a side effect of removing the 'tok' key from the load
+        so that it is not passed along to request handlers.
+        """
+        tok = payload["load"].pop("tok", None)
+        id_ = payload["load"].get("id", None)
+        if tok is not None and id_ is not None:
+            if "cluster_id" in self.opts and self.opts["cluster_id"]:
+                pki_dir = self.opts["cluster_pki_dir"]
+            else:
+                pki_dir = self.opts.get("pki_dir", "")
+            try:
+                pub_path = salt.utils.verify.clean_join(pki_dir, "minions", id_)
+            except salt.exceptions.SaltValidationError:
+                log.warning("Invalid minion id: %s", id_)
+                return False
+            try:
+                pub = salt.crypt.get_rsa_pub_key(pub_path)
+            except OSError:
+                log.warning(
+                    "Salt minion claiming to be %s attempted to communicate with "
+                    "master, but key could not be read and verification was denied.",
+                    id_,
+                )
+                return False
+            try:
+                if salt.crypt.public_decrypt(pub, tok) != b"salt":
+                    log.error("Minion token did not validate: %s", id_)
+                    return False
+            except ValueError as err:
+                log.error("Unable to decrypt token: %s", err)
+                return False
+        elif required:
+            return False
+        return True
+
+    def _auth(self, load, sign_messages=False, version=0):
         """
         Authenticate the client, use the sent public key to encrypt the AES key
         which was generated at start up.
@@ -667,8 +789,10 @@ class ReqServerChannel:
 
             if HAS_M2:
                 ret["aes"] = pub.public_encrypt(aes, RSA.pkcs1_oaep_padding)
+                ret["session"] = pub.public_encrypt(salt.utils.stringutils.to_bytes(self.session_key(load["id"])), RSA.pkcs1_oaep_padding)
             else:
                 ret["aes"] = cipher.encrypt(aes)
+                ret["session"] = cipher.encrypt(salt.utils.stringutils.to_bytes(self.session_key(load["id"])))
         else:
             if "token" in load:
                 try:
@@ -690,8 +814,16 @@ class ReqServerChannel:
             aes = salt.master.SMaster.secrets["aes"]["secret"].value
             if HAS_M2:
                 ret["aes"] = pub.public_encrypt(aes, RSA.pkcs1_oaep_padding)
+                ret["session"] = pub.public_encrypt(salt.utils.stringutils.to_bytes(self.session_key(load["id"])), RSA.pkcs1_oaep_padding)
             else:
                 ret["aes"] = cipher.encrypt(aes)
+                ret["session"] = cipher.encrypt(salt.utils.stringutils.to_bytes(self.session_key(load["id"])))
+
+        if version < 3:
+            log.warning(
+                "Minion using legacy request server protocol, please upgrade %s",
+                load["id"],
+            )
 
         # Be aggressive about the signature
         digest = salt.utils.stringutils.to_bytes(hashlib.sha256(aes).hexdigest())
diff --git a/salt/config/__init__.py b/salt/config/__init__.py
index d4865807e6c..4f72f19f655 100644
--- a/salt/config/__init__.py
+++ b/salt/config/__init__.py
@@ -998,6 +998,10 @@ VALID_OPTS = immutabletypes.freeze(
         # Use Adler32 hashing algorithm for server_id (default False until Sodium, "adler32" after)
         # Possible values are: False, adler32, crc32
         "server_id_use_crc": (bool, str),
+        "request_server_ttl": int,
+        "request_server_aes_session": int,
+        "request_channel_timeout": int,
+        "request_channel_tries": int,
     }
 )
 
@@ -1059,6 +1063,8 @@ DEFAULT_MINION_OPTS = immutabletypes.freeze(
         "pillar_cache": False,
         "pillar_cache_ttl": 3600,
         "pillar_cache_backend": "disk",
+        "request_channel_timeout": 30,
+        "request_channel_tries": 3,
         "gpg_cache": False,
         "gpg_cache_ttl": 86400,
         "gpg_cache_backend": "disk",
@@ -1652,6 +1658,8 @@ DEFAULT_MASTER_OPTS = immutabletypes.freeze(
         "netapi_enable_clients": [],
         "maintenance_interval": 3600,
         "fileserver_interval": 3600,
+        "request_server_aes_session": 0,
+        "request_server_ttl": 0,
     }
 )
 
diff --git a/salt/crypt.py b/salt/crypt.py
index 067c84200b9..981f633d51f 100644
--- a/salt/crypt.py
+++ b/salt/crypt.py
@@ -12,9 +12,11 @@ import hashlib
 import hmac
 import logging
 import os
+import pathlib
 import random
 import stat
 import sys
+import tempfile
 import time
 import traceback
 import uuid
@@ -187,7 +189,7 @@ def _get_key_with_evict(path, timestamp, passphrase):
     """
     log.debug("salt.crypt._get_key_with_evict: Loading private key")
     if HAS_M2:
-        key = RSA.load_key(path, lambda x: bytes(passphrase))
+        key = RSA.load_key(path, lambda x: salt.utils.stringutils.to_bytes(passphrase))
     else:
         with salt.utils.files.fopen(path) as f:
             key = RSA.importKey(f.read(), passphrase)
@@ -555,6 +557,7 @@ class AsyncAuth:
             creds = AsyncAuth.creds_map[key]
             self._creds = creds
             self._crypticle = Crypticle(self.opts, creds["aes"])
+            self._session_crypticle = Crypticle(self.opts, creds["session"])
             self._authenticate_future = salt.ext.tornado.concurrent.Future()
             self._authenticate_future.set_result(True)
         else:
@@ -573,6 +576,10 @@ class AsyncAuth:
             setattr(result, key, copy.deepcopy(self.__dict__[key], memo))
         return result
 
+    @property
+    def session_crypticle(self):
+        return self._session_crypticle
+
     @property
     def creds(self):
         return self._creds
@@ -702,11 +709,16 @@ class AsyncAuth:
                     AsyncAuth.creds_map[key] = creds
                     self._creds = creds
                     self._crypticle = Crypticle(self.opts, creds["aes"])
-                elif self._creds["aes"] != creds["aes"]:
+                    self._session_crypticle = Crypticle(self.opts, creds["session"])
+                elif (
+                    self._creds["aes"] != creds["aes"]
+                    or self._creds["session"] != creds["session"]
+                ):
                     log.debug("%s The master's aes key has changed.", self)
                     AsyncAuth.creds_map[key] = creds
                     self._creds = creds
                     self._crypticle = Crypticle(self.opts, creds["aes"])
+                    self._session_crypticle = Crypticle(self.opts, creds["session"])
 
                 self._authenticate_future.set_result(
                     True
@@ -787,7 +799,6 @@ class AsyncAuth:
         clear_signed_data = payload["load"]
         clear_signature = payload["sig"]
         payload = salt.payload.loads(clear_signed_data)
-
         if "pub_key" in payload:
             auth["aes"] = self.verify_master(
                 payload, master_pub="token" in sign_in_payload
@@ -806,6 +817,16 @@ class AsyncAuth:
                 )
                 raise SaltClientError("Invalid master key")
 
+            key = self.get_keys()
+
+            if HAS_M2:
+                auth["session"] = key.private_decrypt(
+                    payload["session"], RSA.pkcs1_oaep_padding
+                )
+            else:
+                cipher = PKCS1_OAEP.new(key)
+                auth["session"] = cipher.decrypt(payload["session"])
+
         master_pubkey_path = os.path.join(self.opts["pki_dir"], self.mpub)
         if os.path.exists(master_pubkey_path) and not verify_signature(
             master_pubkey_path, clear_signed_data, clear_signature
@@ -1360,10 +1381,15 @@ class SAuth(AsyncAuth):
                 log.error("%s Got new master aes key.", self)
                 self._creds = creds
                 self._crypticle = Crypticle(self.opts, creds["aes"])
-            elif self._creds["aes"] != creds["aes"]:
+                self._session_crypticle = Crypticle(self.opts, creds["session"])
+            elif (
+                self._creds["aes"] != creds["aes"]
+                or self._creds["session"] != creds["session"]
+            ):
                 log.error("%s The master's aes key has changed.", self)
                 self._creds = creds
                 self._crypticle = Crypticle(self.opts, creds["aes"])
+                self._session_crypticle = Crypticle(self.opts, creds["session"])
 
     def sign_in(self, timeout=60, safe=True, tries=1, channel=None):
         """
@@ -1445,6 +1471,24 @@ class Crypticle:
         # Return data must be a base64-encoded string, not a unicode type
         return b64key.replace("\n", "")
 
+    @classmethod
+    def write_key(cls, path, key_size=192):
+        directory = pathlib.Path(path).parent
+        with salt.utils.files.set_umask(0o177):
+            fd, tmp = tempfile.mkstemp(dir=directory, prefix="aes")
+            os.close(fd)
+            with salt.utils.files.fopen(tmp, "w") as fp:
+                fp.write(cls.generate_key_string(key_size))
+            os.rename(tmp, path)
+
+    @classmethod
+    def read_key(cls, path):
+        try:
+            with salt.utils.files.fopen(path, "r") as fp:
+                return fp.read()
+        except FileNotFoundError:
+            pass
+
     @classmethod
     def extract_keys(cls, key_string, key_size):
         key = salt.utils.stringutils.to_bytes(base64.b64decode(key_string))
diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py
index 54aca64a769..631361c9c97 100644
--- a/salt/daemons/masterapi.py
+++ b/salt/daemons/masterapi.py
@@ -56,6 +56,27 @@ log = logging.getLogger(__name__)
 # Things to do in lower layers:
 # only accept valid minion ids
 
+MINION_EVENT_BLACKLIST = (
+    "salt/job/*/publish",
+    "salt/job/*/new",
+    "salt/job/*/return",
+    "salt/key",
+    "salt/cloud/*",
+    "salt/run/*",
+    "salt/cluster/*",
+    "salt/wheel/*/new",
+    "salt/wheel/*/return",
+    "salt/run/*",
+    "salt/cloud/*",
+)
+
+
+def valid_minion_tag(tag, blacklist=MINION_EVENT_BLACKLIST):
+    for black in blacklist:
+        if fnmatch.fnmatch(tag, black):
+            return False
+    return True
+
 
 def init_git_pillar(opts):
     """
@@ -781,6 +802,9 @@ class RemoteFuncs:
                     event_data = event["data"]
                 else:
                     event_data = event
+                if not valid_minion_tag(event["tag"]):
+                    log.warning("Filtering blacklisted event tag %s", event["tag"])
+                    continue
                 self.event.fire_event(event_data, event["tag"])  # old dup event
                 if load.get("pretag") is not None:
                     self.event.fire_event(
diff --git a/salt/exceptions.py b/salt/exceptions.py
index e351584bc03..d5525c38aea 100644
--- a/salt/exceptions.py
+++ b/salt/exceptions.py
@@ -362,6 +362,11 @@ class AuthorizationError(SaltException):
     """
 
 
+class SaltValidationError(SaltException):
+    """
+    Thrown when a value fails validation
+    """
+
 class SaltDaemonNotRunning(SaltException):
     """
     Throw when a running master/minion/syndic is not running but is needed to
diff --git a/salt/fileclient.py b/salt/fileclient.py
index f4b8d76dbed..4ed341221cd 100644
--- a/salt/fileclient.py
+++ b/salt/fileclient.py
@@ -1439,8 +1439,6 @@ class RemoteClient(Client):
         Return the metadata derived from the master_tops system
         """
         load = {"cmd": "_master_tops", "id": self.opts["id"], "opts": self.opts}
-        if self.auth:
-            load["tok"] = self.auth.gen_token(b"salt")
         return self.channel.send(load)
 
     def __enter__(self):
diff --git a/salt/fileserver/__init__.py b/salt/fileserver/__init__.py
index 4eca98d14a4..d35c99c5ca8 100644
--- a/salt/fileserver/__init__.py
+++ b/salt/fileserver/__init__.py
@@ -527,7 +527,7 @@ class Fileserver:
         if load is None:
             load = {}
         load.pop("cmd", None)
-        return self.envs(**load)
+        return self.envs(back=load.get("back", None), sources=load.get("sources", None))
 
     def init(self, back=None):
         """
diff --git a/salt/master.py b/salt/master.py
index c0cd9a366ba..ba7c751d4b4 100644
--- a/salt/master.py
+++ b/salt/master.py
@@ -61,11 +61,7 @@ from salt.config import DEFAULT_INTERVAL
 from salt.defaults import DEFAULT_TARGET_DELIM
 from salt.transport import TRANSPORTS
 from salt.utils.channel import iter_transport_opts
-from salt.utils.debug import (
-    enable_sigusr1_handler,
-    enable_sigusr2_handler,
-    inspect_stack,
-)
+from salt.utils.debug import enable_sigusr1_handler, enable_sigusr2_handler
 from salt.utils.event import tagify
 from salt.utils.odict import OrderedDict
 from salt.utils.zeromq import ZMQ_VERSION_INFO, zmq
@@ -167,6 +163,21 @@ class SMaster:
             log.debug("Pinging all connected minions due to key rotation")
             salt.utils.master.ping_all_connected_minions(opts)
 
+    @classmethod
+    def populate_secrets(cls):
+        cls.secrets["aes"] = {
+            "secret": multiprocessing.Array(
+                ctypes.c_char,
+                salt.utils.stringutils.to_bytes(
+                    salt.crypt.Crypticle.generate_key_string()
+                ),
+            ),
+            "serial": multiprocessing.Value(
+                ctypes.c_longlong, lock=False  # We'll use the lock from 'secret'
+            ),
+            "reload": salt.crypt.Crypticle.generate_key_string,
+        }
+
 
 class Maintenance(salt.utils.process.SignalHandlingProcess):
     """
@@ -702,18 +713,7 @@ class Master(SMaster):
 
             # Setup the secrets here because the PubServerChannel may need
             # them as well.
-            SMaster.secrets["aes"] = {
-                "secret": multiprocessing.Array(
-                    ctypes.c_char,
-                    salt.utils.stringutils.to_bytes(
-                        salt.crypt.Crypticle.generate_key_string()
-                    ),
-                ),
-                "serial": multiprocessing.Value(
-                    ctypes.c_longlong, lock=False  # We'll use the lock from 'secret'
-                ),
-                "reload": salt.crypt.Crypticle.generate_key_string,
-            }
+            SMaster.populate_secrets()
 
             log.info("Creating master process manager")
             # Since there are children having their own ProcessManager we should wait for kill more time.
@@ -1199,7 +1199,6 @@ class AESFuncs(TransportMethods):
         "_file_recv",
         "_pillar",
         "_minion_event",
-        "_handle_minion_event",
         "_return",
         "_syndic_return",
         "minion_runner",
@@ -1274,7 +1273,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 = salt.utils.verify.clean_join(self.opts["pki_dir"], "minions", id_)
 
         try:
             pub = salt.crypt.get_rsa_pub_key(pub_path)
@@ -1327,24 +1326,12 @@ class AESFuncs(TransportMethods):
             return False
         if not isinstance(self.opts["peer"], dict):
             return False
-        if any(
-            key not in clear_load for key in ("fun", "arg", "tgt", "ret", "tok", "id")
-        ):
+        if any(key not in clear_load for key in ("fun", "arg", "tgt", "ret", "id")):
             return False
         # If the command will make a recursive publish don't run
         if clear_load["fun"].startswith("publish."):
             return False
         # Check the permissions for this minion
-        if not self.__verify_minion(clear_load["id"], clear_load["tok"]):
-            # The minion is not who it says it is!
-            # We don't want to listen to it!
-            log.warning(
-                "Minion id %s is not who it says it is and is attempting "
-                "to issue a peer command",
-                clear_load["id"],
-            )
-            return False
-        clear_load.pop("tok")
         perms = []
         for match in self.opts["peer"]:
             if re.match(match, clear_load["id"]):
@@ -1384,23 +1371,6 @@ class AESFuncs(TransportMethods):
         """
         if any(key not in load for key in verify_keys):
             return False
-        if "tok" not in load:
-            log.error(
-                "Received incomplete call from %s for '%s', missing '%s'",
-                load["id"],
-                inspect_stack()["co_name"],
-                "tok",
-            )
-            return False
-        if not self.__verify_minion(load["id"], load["tok"]):
-            # The minion is not who it says it is!
-            # We don't want to listen to it!
-            log.warning("Minion id %s is not who it says it is!", load["id"])
-            return False
-
-        if "tok" in load:
-            load.pop("tok")
-
         return load
 
     def _master_tops(self, load):
@@ -1411,7 +1381,7 @@ class AESFuncs(TransportMethods):
         :param dict load: A payload received from a minion
         :return: The results from an external node classifier
         """
-        load = self.__verify_load(load, ("id", "tok"))
+        load = self.__verify_load(load, ("id",))
         if load is False:
             return {}
         return self.masterapi._master_tops(load, skip_verify=True)
@@ -1463,7 +1433,7 @@ class AESFuncs(TransportMethods):
         :rtype: dict
         :return: Mine data from the specified minions
         """
-        load = self.__verify_load(load, ("id", "tgt", "fun", "tok"))
+        load = self.__verify_load(load, ("id", "tgt", "fun"))
         if load is False:
             return {}
         else:
@@ -1478,7 +1448,7 @@ class AESFuncs(TransportMethods):
         :rtype: bool
         :return: True if the data has been stored in the mine
         """
-        load = self.__verify_load(load, ("id", "data", "tok"))
+        load = self.__verify_load(load, ("id", "data"))
         if load is False:
             return {}
         return self.masterapi._mine(load, skip_verify=True)
@@ -1492,7 +1462,7 @@ class AESFuncs(TransportMethods):
         :rtype: bool
         :return: Boolean indicating whether or not the given function was deleted from the mine
         """
-        load = self.__verify_load(load, ("id", "fun", "tok"))
+        load = self.__verify_load(load, ("id", "fun"))
         if load is False:
             return {}
         else:
@@ -1504,7 +1474,7 @@ class AESFuncs(TransportMethods):
 
         :param dict load: A payload received from a minion
         """
-        load = self.__verify_load(load, ("id", "tok"))
+        load = self.__verify_load(load, ("id",))
         if load is False:
             return {}
         else:
@@ -1539,20 +1509,6 @@ class AESFuncs(TransportMethods):
                 load["path"],
             )
             return False
-        if "tok" not in load:
-            log.error(
-                "Received incomplete call from %s for '%s', missing '%s'",
-                load["id"],
-                inspect_stack()["co_name"],
-                "tok",
-            )
-            return False
-        if not self.__verify_minion(load["id"], load["tok"]):
-            # The minion is not who it says it is!
-            # We don't want to listen to it!
-            log.warning("Minion id %s is not who it says it is!", load["id"])
-            return {}
-        load.pop("tok")
 
         # Join path
         sep_path = os.sep.join(load["path"])
@@ -1567,11 +1523,15 @@ class AESFuncs(TransportMethods):
             # Can overwrite master files!!
             return False
 
-        cpath = os.path.join(
-            self.opts["cachedir"], "minions", load["id"], "files", normpath
-        )
+        rpath = os.path.join(self.opts["cachedir"], "minions", load["id"], "files")
+        cpath = os.path.join(rpath, normpath)
         # One last safety check here
-        if not os.path.normpath(cpath).startswith(self.opts["cachedir"]):
+        if not salt.utils.verify.clean_path(
+            rpath,
+            cpath,
+            subdir=True,
+            realpath=not self.opts["fileserver_followsymlinks"],
+        ):
             log.warning(
                 "Attempt to write received file outside of master cache "
                 "directory! Requested path: %s. Access denied.",
@@ -1644,7 +1604,7 @@ class AESFuncs(TransportMethods):
 
         :param dict load: The minion payload
         """
-        load = self.__verify_load(load, ("id", "tok"))
+        load = self.__verify_load(load, ("id",))
         if load is False:
             return {}
         # Route to master event bus
@@ -1700,8 +1660,8 @@ class AESFuncs(TransportMethods):
         if "sig" in load:
             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"])
+            this_minion_pubkey = salt.utils.verify.clean_join(
+                self.opts["pki_dir"], "minions", load["id"]
             )
             serialized_load = salt.serializers.msgpack.serialize(load)
             if not salt.crypt.verify_signature(
@@ -1790,7 +1750,7 @@ class AESFuncs(TransportMethods):
         :rtype: dict
         :return: The runner function data
         """
-        load = self.__verify_load(clear_load, ("fun", "arg", "id", "tok"))
+        load = self.__verify_load(clear_load, ("fun", "arg", "id"))
         if load is False:
             return {}
         else:
@@ -1806,14 +1766,14 @@ class AESFuncs(TransportMethods):
         :rtype: dict
         :return: Return data corresponding to a given JID
         """
-        load = self.__verify_load(load, ("jid", "id", "tok"))
+        load = self.__verify_load(load, ("jid", "id"))
         if load is False:
             return {}
         # Check that this minion can access this data
         auth_cache = os.path.join(self.opts["cachedir"], "publish_auth")
         if not os.path.isdir(auth_cache):
             os.makedirs(auth_cache)
-        jid_fn = os.path.join(auth_cache, str(load["jid"]))
+        jid_fn = salt.utils.verify.clean_join(auth_cache, str(load["jid"]))
         with salt.utils.files.fopen(jid_fn, "r") as fp_:
             if not load["id"] == fp_.read():
                 return {}
@@ -1902,8 +1862,7 @@ class AESFuncs(TransportMethods):
         :rtype: bool
         :return: True if key was revoked, False if not
         """
-        load = self.__verify_load(load, ("id", "tok"))
-
+        load = self.__verify_load(load, ("id",))
         if not self.opts.get("allow_minion_key_revoke", False):
             log.warning(
                 "Minion %s requested key revoke, but allow_minion_key_revoke "
@@ -1911,7 +1870,6 @@ class AESFuncs(TransportMethods):
                 load["id"],
             )
             return load
-
         if load is False:
             return load
         else:
diff --git a/salt/minion.py b/salt/minion.py
index 834f0848c6a..d9201e20109 100644
--- a/salt/minion.py
+++ b/salt/minion.py
@@ -873,13 +873,15 @@ class MinionBase:
                                 self.opts["master"] = proto_data["master"]
                                 return
 
-    def _return_retry_timer(self):
+    def _return_retry_timer(self, max=False):
         """
         Based on the minion configuration, either return a randomized timer or
         just return the value of the return_retry_timer.
         """
         msg = "Minion return retry timer set to %s seconds"
         if self.opts.get("return_retry_timer_max"):
+            if max:
+                return self.opts["return_retry_timer_max"]
             try:
                 random_retry = random.randint(
                     self.opts["return_retry_timer"], self.opts["return_retry_timer_max"]
@@ -1974,7 +1976,20 @@ class Minion(MinionBase):
                 ret["return"] = "ERROR executing '{}': {}".format(function_name, exc)
                 ret["out"] = "nested"
                 ret["retcode"] = salt.defaults.exitcodes.EX_GENERIC
+            except SaltClientError as exc:
+                log.error(
+                    "Problem executing '%s': %s",
+                    function_name,
+                    exc,
+                )
+                ret["return"] = "ERROR executing '{}': {}".format(function_name, exc)
+                ret["out"] = "nested"
+                ret["retcode"] = salt.defaults.exitcodes.EX_GENERIC
             except TypeError as exc:
+                # XXX: This can ba extreemly missleading when something outside of a
+                # execution module call raises a TypeError. Make this it's own
+                # type of exception when we start validating state and
+                # execution argument module inputs.
                 msg = "Passed invalid arguments to {}: {}\n{}".format(
                     function_name,
                     exc,
@@ -2026,7 +2041,11 @@ class Minion(MinionBase):
             else:
                 log.warning("The metadata parameter must be a dictionary. Ignoring.")
         if minion_instance.connected:
-            minion_instance._return_pub(ret)
+            minion_instance._return_pub(
+                ret,
+                timeout=minion_instance.opts["return_retry_tries"]
+                * minion_instance._return_retry_timer(max=True),
+            )
 
         # Add default returners from minion config
         # Should have been coverted to comma-delimited string already
@@ -2675,6 +2694,15 @@ class Minion(MinionBase):
                 force_refresh=data.get("force_refresh", False),
                 notify=data.get("notify", False),
             )
+        elif tag.startswith("__master_req_channel_payload"):
+            try:
+                yield _minion.req_channel.send(
+                    data,
+                    timeout=_minion._return_retry_timer(),
+                    tries=_minion.opts["return_retry_tries"],
+                )
+            except salt.exceptions.SaltReqTimeoutError:
+                log.error("Timeout encountered while sending %r request", data)
         elif tag.startswith("pillar_refresh"):
             yield _minion.pillar_refresh(
                 force_refresh=data.get("force_refresh", False),
diff --git a/salt/modules/cp.py b/salt/modules/cp.py
index e9ac77434e4..7f0c46bde5a 100644
--- a/salt/modules/cp.py
+++ b/salt/modules/cp.py
@@ -964,7 +964,6 @@ def push(path, keep_symlinks=False, upload_path=None, remove_source=False):
         "id": __opts__["id"],
         "path": load_path_list,
         "size": os.path.getsize(path),
-        "tok": auth.gen_token(b"salt"),
     }
 
     with salt.channel.client.ReqChannel.factory(__opts__) as channel:
diff --git a/salt/modules/event.py b/salt/modules/event.py
index bf6d4bde0d7..3fd3bcf8898 100644
--- a/salt/modules/event.py
+++ b/salt/modules/event.py
@@ -64,7 +64,6 @@ def fire_master(data, tag, preload=None):
             "id": __opts__["id"],
             "tag": tag,
             "data": data,
-            "tok": auth.gen_token(b"salt"),
             "cmd": "_minion_event",
         }
 
diff --git a/salt/modules/mine.py b/salt/modules/mine.py
index f8d55464019..3c2073a3cb0 100644
--- a/salt/modules/mine.py
+++ b/salt/modules/mine.py
@@ -67,14 +67,6 @@ def _mine_send(load, opts):
 
 
 def _mine_get(load, opts):
-    if opts.get("transport", "") in salt.transport.TRANSPORTS:
-        try:
-            load["tok"] = _auth().gen_token(b"salt")
-        except AttributeError:
-            log.error(
-                "Mine could not authenticate with master. Mine could not be retrieved."
-            )
-            return False
     with salt.channel.client.ReqChannel.factory(opts) as channel:
         return channel.send(load)
 
diff --git a/salt/modules/publish.py b/salt/modules/publish.py
index a82cb3ac989..5a7db345911 100644
--- a/salt/modules/publish.py
+++ b/salt/modules/publish.py
@@ -122,8 +122,6 @@ def _publish(
         master_uri = __opts__["master_uri"]
 
     log.info("Publishing '%s' to %s", fun, master_uri)
-    auth = salt.crypt.SAuth(__opts__)
-    tok = auth.gen_token(b"salt")
     load = {
         "cmd": "minion_pub",
         "fun": fun,
@@ -131,7 +129,6 @@ def _publish(
         "tgt": tgt,
         "tgt_type": tgt_type,
         "ret": returner,
-        "tok": tok,
         "tmo": timeout,
         "form": form,
         "id": __opts__["id"],
@@ -157,7 +154,6 @@ def _publish(
                 load = {
                     "cmd": "pub_ret",
                     "id": __opts__["id"],
-                    "tok": tok,
                     "jid": peer_data["jid"],
                 }
                 ret = channel.send(load)
@@ -187,7 +183,6 @@ def _publish(
             load = {
                 "cmd": "pub_ret",
                 "id": __opts__["id"],
-                "tok": tok,
                 "jid": peer_data["jid"],
             }
             ret = channel.send(load)
@@ -334,13 +329,10 @@ def runner(fun, arg=None, timeout=5):
     if "master_uri" not in __opts__:
         return "No access to master. If using salt-call with --local, please remove."
     log.info("Publishing runner '%s' to %s", fun, __opts__["master_uri"])
-    auth = salt.crypt.SAuth(__opts__)
-    tok = auth.gen_token(b"salt")
     load = {
         "cmd": "minion_runner",
         "fun": fun,
         "arg": arg,
-        "tok": tok,
         "tmo": timeout,
         "id": __opts__["id"],
         "no_parse": __opts__.get("no_parse", []),
diff --git a/salt/modules/saltutil.py b/salt/modules/saltutil.py
index 320b9c34fa3..857b227f9ac 100644
--- a/salt/modules/saltutil.py
+++ b/salt/modules/saltutil.py
@@ -1544,11 +1544,9 @@ def revoke_auth(preserve_minion_cache=False):
         with salt.channel.client.ReqChannel.factory(
             __opts__, master_uri=master
         ) as channel:
-            tok = channel.auth.gen_token(b"salt")
             load = {
                 "cmd": "revoke_auth",
                 "id": __opts__["id"],
-                "tok": tok,
                 "preserve_minion_cache": preserve_minion_cache,
             }
             try:
diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py
index 26312b3bd53..8f6bd90d0a4 100644
--- a/salt/pillar/__init__.py
+++ b/salt/pillar/__init__.py
@@ -8,6 +8,7 @@ import fnmatch
 import logging
 import os
 import sys
+import time
 import traceback
 
 import salt.channel.client
@@ -262,6 +263,7 @@ class AsyncRemotePillar(RemotePillarMixin):
         if self.ext:
             load["ext"] = self.ext
         try:
+            start = time.monotonic()
             ret_pillar = yield self.channel.crypted_transfer_decode_dictentry(
                 load,
                 dictkey="pillar",
@@ -269,6 +271,10 @@ class AsyncRemotePillar(RemotePillarMixin):
         except salt.crypt.AuthenticationError as exc:
             log.error(exc.message)
             raise SaltClientError("Exception getting pillar.")
+        except salt.exceptions.SaltReqTimeoutError:
+            raise SaltClientError(
+                f"Pillar timed out after {int(time.monotonic() - start)} seconds"
+            )
         except Exception:  # pylint: disable=broad-except
             log.exception("Exception getting pillar:")
             raise SaltClientError("Exception getting pillar.")
@@ -355,10 +361,23 @@ class RemotePillar(RemotePillarMixin):
         }
         if self.ext:
             load["ext"] = self.ext
-        ret_pillar = self.channel.crypted_transfer_decode_dictentry(
-            load,
-            dictkey="pillar",
-        )
+
+        try:
+            start = time.monotonic()
+            ret_pillar = self.channel.crypted_transfer_decode_dictentry(
+                load,
+                dictkey="pillar",
+            )
+        except salt.crypt.AuthenticationError as exc:
+            log.error(exc.message)
+            raise SaltClientError("Exception getting pillar.")
+        except salt.exceptions.SaltReqTimeoutError:
+            raise SaltClientError(
+                f"Pillar timed out after {int(time.monotonic() - start)} seconds"
+            )
+        except Exception:  # pylint: disable=broad-except
+            log.exception("Exception getting pillar:")
+            raise SaltClientError("Exception getting pillar.")
 
         if not isinstance(ret_pillar, dict):
             log.error(
@@ -594,10 +613,15 @@ class Pillar:
 
     def __valid_on_demand_ext_pillar(self, opts):
         """
-        Check to see if the on demand external pillar is allowed
+        Check to see if the on demand external pillar is allowed.
+
+        If this check fails self.ext is set to None, this is important to
+        prevent an on-demand pillare from being rendered when it should not be
+        allowed.
         """
         if not isinstance(self.ext, dict):
             log.error("On-demand pillar %s is not formatted as a dictionary", self.ext)
+            self.ext = None
             return False
 
         on_demand = opts.get("on_demand_ext_pillar", [])
@@ -609,6 +633,7 @@ class Pillar:
                 "The 'on_demand_ext_pillar' configuration option is "
                 "malformed, it should be a list of ext_pillar module names"
             )
+            self.ext = None
             return False
 
         if invalid_on_demand:
@@ -620,6 +645,7 @@ class Pillar:
                 ", ".join(sorted(invalid_on_demand)),
                 ", ".join(on_demand),
             )
+            self.ext = None
             return False
         return True
 
@@ -935,7 +961,7 @@ class Pillar:
                 saltenv,
                 sls,
                 _pillar_rend=True,
-                **defaults
+                **defaults,
             )
         except Exception as exc:  # pylint: disable=broad-except
             msg = "Rendering SLS '{}' failed, render error:\n{}".format(sls, exc)
@@ -1114,7 +1140,7 @@ class Pillar:
                     self.minion_id,
                     pillar,
                     extra_minion_data=self.extra_minion_data,
-                    **val
+                    **val,
                 )
             else:
                 ext = self.ext_pillars[key](self.minion_id, pillar, **val)
@@ -1124,7 +1150,7 @@ class Pillar:
                     self.minion_id,
                     pillar,
                     *val,
-                    extra_minion_data=self.extra_minion_data
+                    extra_minion_data=self.extra_minion_data,
                 )
             else:
                 ext = self.ext_pillars[key](self.minion_id, pillar, *val)
diff --git a/salt/pillar/git_pillar.py b/salt/pillar/git_pillar.py
index e8ece28a819..6256e6040ee 100644
--- a/salt/pillar/git_pillar.py
+++ b/salt/pillar/git_pillar.py
@@ -437,7 +437,7 @@ def ext_pillar(minion_id, pillar, *repos):  # pylint: disable=unused-argument
     opts["__git_pillar"] = True
     git_pillar = salt.utils.gitfs.GitPillar(
         opts,
-        repos,
+        list(repos),
         per_remote_overrides=PER_REMOTE_OVERRIDES,
         per_remote_only=PER_REMOTE_ONLY,
         global_only=GLOBAL_ONLY,
diff --git a/salt/returners/local_cache.py b/salt/returners/local_cache.py
index 1530d94ddfc..c0ea8e8ee21 100644
--- a/salt/returners/local_cache.py
+++ b/salt/returners/local_cache.py
@@ -8,6 +8,7 @@ import errno
 import glob
 import logging
 import os
+import pathlib
 import shutil
 import time
 
@@ -231,6 +232,8 @@ def save_minions(jid, minions, syndic_id=None):
     """
     Save/update the serialized list of minions for a given job
     """
+    import salt.utils.verify
+
     # Ensure we have a list for Python 3 compatibility
     minions = list(minions)
 
@@ -254,10 +257,20 @@ def save_minions(jid, minions, syndic_id=None):
         else:
             raise
 
-    if syndic_id is not None:
-        minions_path = os.path.join(jid_dir, SYNDIC_MINIONS_P.format(syndic_id))
-    else:
-        minions_path = os.path.join(jid_dir, MINIONS_P)
+    try:
+        if syndic_id is not None:
+            name = SYNDIC_MINIONS_P.format(syndic_id)
+        else:
+            name = MINIONS_P
+        minions_path = salt.utils.verify.clean_join(jid_dir, name)
+        target_name = pathlib.Path(minions_path).resolve().name
+        if name != target_name:
+            raise salt.exceptions.SaltValidationError(
+                f"Filenames do not match: {name} != {target_name}"
+            )
+    except salt.exceptions.SaltValidationError as exc:
+        log.error("Error %s", exc)
+        return
 
     try:
         if not os.path.exists(jid_dir):
diff --git a/salt/utils/event.py b/salt/utils/event.py
index 36b530d1af4..c973926db93 100644
--- a/salt/utils/event.py
+++ b/salt/utils/event.py
@@ -1494,11 +1494,9 @@ class StateFire:
 
         load.update(
             {
-                "id": self.opts["id"],
                 "tag": tag,
                 "data": data,
                 "cmd": "_minion_event",
-                "tok": self.auth.gen_token(b"salt"),
             }
         )
 
diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py
index 6f691f3869a..2a8ecf1d0cb 100644
--- a/salt/utils/gitfs.py
+++ b/salt/utils/gitfs.py
@@ -14,6 +14,7 @@ import io
 import logging
 import multiprocessing
 import os
+import re
 import shlex
 import shutil
 import stat
@@ -203,7 +204,7 @@ def enforce_types(key, val):
     else:
         try:
             return expected(val)
-        except Exception as exc:  # pylint: disable=broad-except
+        except Exception:  # pylint: disable=broad-except
             log.error(
                 "Failed to enforce type for key=%s with val=%s, falling back "
                 "to a string",
@@ -2435,6 +2436,56 @@ class GitBase:
                 global_only,
             )
 
+    @classmethod
+    def split_name(cls, remote):
+        """
+        Given a string determine if it is a url or a name and url combination.
+
+        Examples:
+
+           None, "https://github.com/saltstack/salt.git" == split_name("https://github.com/saltstack/salt.git")
+
+           "__env__", "https://github.com/saltstack/salt.git" == split_name("__env__ https://github.com/saltstack/salt.git")
+        """
+        parts = remote.split(" ", 1)
+        if len(parts) == 1:
+            return None, remote
+        maybename, maybeurl = parts
+        if not salt.utils.verify.url(maybename):
+            return maybename, maybeurl
+        return None, remote
+
+    @classmethod
+    def remote_to_url(cls, remote):
+        """
+        Convert a remote to a URL
+
+        Remotes should be in url format with the exception of some ssh remotes
+        which can be in a `git@...` format. This method handles the special ssh
+        remote case by converting to `ssh://` style URLs.
+        """
+        pattern = r"^([^@:/]+)@([^:]+):(.+)$"
+        match = re.match(pattern, remote)
+        if match:
+            user, host, path = match.groups()
+            if not path.startswith("/"):
+                path = f"/{path}"
+            url = f"ssh://{user}@{host}{path}"
+            return url
+        return remote
+
+    @classmethod
+    def validate_remote(cls, remote):
+        """
+        Validate a remote repository config.
+
+        """
+        _, remote = cls.split_name(remote)
+        url = cls.remote_to_url(remote)
+        if salt.utils.verify.url(url):
+            return True
+        return False
+
     def init_remotes(
         self,
         remotes,
@@ -2487,7 +2538,25 @@ class GitBase:
             per_remote_defaults[param] = enforce_types(key, self.opts[key])
 
         self.remotes = []
-        for remote in remotes:
+        # In case a tuple is passed.
+        remotes = list(remotes)
+        for remote in list(remotes):
+
+            if isinstance(remote, dict):
+                for key in list(remote):
+                    if not self.validate_remote(key):
+                        log.warning("Found bad url data %r", key)
+                        remote.pop(key)
+                        continue
+                # None of the remotes were valid
+                if not remote:
+                    remotes.remove(remote)
+            else:
+                if not self.validate_remote(remote):
+                    log.warning("Found bad url data %r", remote)
+                    remotes.remove(remote)
+                    continue
+
             repo_obj = self.git_providers[self.provider](
                 self.opts,
                 remote,
@@ -3081,14 +3150,19 @@ class GitFS(GitBase):
         if os.path.isabs(path):
             return fnd
 
-        dest = salt.utils.path.join(self.cache_root, "refs", tgt_env, path)
-        hashes_glob = salt.utils.path.join(
-            self.hash_cachedir, tgt_env, "{}.hash.*".format(path)
+        # dest = salt.utils.path.join(self.cache_root, "refs", tgt_env, path)
+        dest = salt.utils.verify.clean_join(
+            self.cache_root, "refs", tgt_env, path, subdir=True
+        )
+        hashes_glob = salt.utils.verify.clean_join(
+            self.hash_cachedir, tgt_env, f"{path}.hash.*", subdir=True
+        )
+        blobshadest = salt.utils.verify.clean_join(
+            self.hash_cachedir, tgt_env, f"{path}.hash.blob_sha1", subdir=True
         )
-        blobshadest = salt.utils.path.join(
-            self.hash_cachedir, tgt_env, "{}.hash.blob_sha1".format(path)
+        lk_fn = salt.utils.verify.clean_join(
+            self.hash_cachedir, tgt_env, f"{path}.lk", subdir=True
         )
-        lk_fn = salt.utils.path.join(self.hash_cachedir, tgt_env, "{}.lk".format(path))
         destdir = os.path.dirname(dest)
         hashdir = os.path.dirname(blobshadest)
         if not os.path.isdir(destdir):
diff --git a/salt/utils/verify.py b/salt/utils/verify.py
index 879128f2312..ed43e205a89 100644
--- a/salt/utils/verify.py
+++ b/salt/utils/verify.py
@@ -10,6 +10,7 @@ import re
 import socket
 import stat
 import sys
+import urllib.parse
 
 import salt.defaults.exitcodes
 import salt.utils.files
@@ -17,7 +18,12 @@ import salt.utils.path
 import salt.utils.platform
 import salt.utils.user
 from salt._logging import LOG_LEVELS
-from salt.exceptions import CommandExecutionError, SaltClientError, SaltSystemExit
+from salt.exceptions import (
+    CommandExecutionError,
+    SaltClientError,
+    SaltSystemExit,
+    SaltValidationError,
+)
 
 # Original Author: Jeff Schroeder <jeffschroeder@computer.org>
 
@@ -510,28 +516,44 @@ def _realpath(path):
     return os.path.realpath(path)
 
 
-def clean_path(root, path, subdir=False):
+def clean_path(root, path, subdir=False, realpath=True):
     """
     Accepts the root the path needs to be under and verifies that the path is
     under said root. Pass in subdir=True if the path can result in a
     subdirectory of the root instead of having to reside directly in the root
     """
-    real_root = _realpath(root)
-    if not os.path.isabs(real_root):
-        return ""
+    if not os.path.isabs(root):
+        root = os.path.join(os.getcwd(), root)
+    normroot = os.path.normpath(root)
     if not os.path.isabs(path):
-        path = os.path.join(root, path)
-    path = os.path.normpath(path)
-    real_path = _realpath(path)
+        path = os.path.join(normroot, path)
+    normpath = os.path.normpath(path)
+    if realpath:
+        normroot = _realpath(normroot)
+        normpath = _realpath(normpath)
     if subdir:
-        if real_path.startswith(real_root):
-            return real_path
+        if os.path.commonpath([normpath, normroot]) == normroot:
+            return normpath
     else:
-        if os.path.dirname(real_path) == os.path.normpath(real_root):
-            return real_path
+        if os.path.dirname(normpath) == normroot:
+            return normpath
     return ""
 
 
+def clean_join(root, *paths, subdir=False, realpath=True):
+    """
+    Performa a join and then check the result against the clean_path method. If
+    clean_path fails a SaltValidationError is raised.
+    """
+    parent = root
+    for path in paths:
+        child = os.path.join(parent, path)
+        if not clean_path(parent, child, subdir, realpath):
+            raise SaltValidationError(f"Invalid path: {path!r}")
+        parent = child
+    return child
+
+
 def valid_id(opts, id_):
     """
     Returns if the passed id is valid
@@ -745,3 +767,41 @@ def win_verify_env(path, dirs, permissive=False, pki_dir="", skip_extra=False):
     if skip_extra is False:
         # Run the extra verification checks
         zmq_version()
+
+
+SCHEMES = (
+    "http",
+    "https",
+    "ssh",
+    "ftp",
+    "sftp",
+    "file",
+)
+
+
+class URLValidator:
+
+    PCHAR = r"^([a-z0-9\-._~!$&'();=:@,]|%\d\d)+$"
+    ALL_VALID = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;="
+
+    @classmethod
+    def pchar_matcher(cls):
+        return re.compile(cls.PCHAR, re.IGNORECASE)
+
+    def __init__(self, schemes=SCHEMES):
+        self.schemes = schemes
+
+    def __call__(self, data):
+        if any([x not in self.ALL_VALID for x in data]):
+            return False
+        parsed = urllib.parse.urlparse(data)
+        if parsed.scheme not in self.schemes:
+            return False
+        matcher = self.pchar_matcher()
+        for part in parsed.path.split("/"):
+            if part and not matcher.match(part):
+                return False
+        return True
+
+
+url = URLValidator()
diff --git a/salt/utils/virt.py b/salt/utils/virt.py
index fcd3d4fd4ea..77dd1692fdc 100644
--- a/salt/utils/virt.py
+++ b/salt/utils/virt.py
@@ -11,6 +11,7 @@ import urllib
 import urllib.parse
 
 import salt.utils.files
+import salt.utils.verify
 
 # pylint: disable=E0611
 
@@ -64,10 +65,10 @@ class VirtKey:
         self.opts = opts
         self.hyper = hyper
         self.id = id_
-        path = os.path.join(self.opts["pki_dir"], "virtkeys", hyper)
+        path = salt.utils.verify.clean_join(self.opts["pki_dir"], "virtkeys", hyper)
         if not os.path.isdir(path):
             os.makedirs(path)
-        self.path = os.path.join(path, id_)
+        self.path = salt.utils.verify.clean_join(path, id_)
 
     def accept(self, pub):
         """
@@ -99,7 +100,7 @@ class VirtKey:
             )
             return False
 
-        pubfn = os.path.join(self.opts["pki_dir"], "minions", self.id)
+        pubfn = salt.utils.verify.clean_join(self.opts["pki_dir"], "minions", self.id)
         with salt.utils.files.fopen(pubfn, "w+") as fp_:
             fp_.write(pub)
         self.void()
diff --git a/tests/conftest.py b/tests/conftest.py
index ad57b4adef4..436d5a8a126 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1048,7 +1048,9 @@ def salt_syndic_master_factory(
     config_defaults["syndic_master"] = "localhost"
     config_defaults["transport"] = request.config.getoption("--transport")
 
-    config_overrides = {"log_level_logfile": "quiet"}
+    config_overrides = {
+        "log_level_logfile": "info",
+    }
     ext_pillar = []
     if salt.utils.platform.is_windows():
         ext_pillar.append(
@@ -1124,7 +1126,7 @@ def salt_syndic_factory(salt_factories, salt_syndic_master_factory):
         opts["aliases.file"] = os.path.join(RUNTIME_VARS.TMP, "aliases")
         opts["transport"] = salt_syndic_master_factory.config["transport"]
         config_defaults["syndic"] = opts
-    config_overrides = {"log_level_logfile": "quiet"}
+    config_overrides = {"log_level_logfile": "info"}
     factory = salt_syndic_master_factory.salt_syndic_daemon(
         "syndic",
         defaults=config_defaults,
@@ -1161,7 +1163,9 @@ def salt_master_factory(
     config_defaults["syndic_master"] = "localhost"
     config_defaults["transport"] = salt_syndic_master_factory.config["transport"]
 
-    config_overrides = {"log_level_logfile": "quiet"}
+    config_overrides = {
+        "log_level_logfile": "info",
+    }
     ext_pillar = []
     if salt.utils.platform.is_windows():
         ext_pillar.append(
@@ -1266,7 +1270,7 @@ def salt_minion_factory(salt_master_factory):
     config_defaults["transport"] = salt_master_factory.config["transport"]
 
     config_overrides = {
-        "log_level_logfile": "quiet",
+        "log_level_logfile": "info",
         "file_roots": salt_master_factory.config["file_roots"].copy(),
         "pillar_roots": salt_master_factory.config["pillar_roots"].copy(),
     }
@@ -1297,7 +1301,7 @@ def salt_sub_minion_factory(salt_master_factory):
     config_defaults["transport"] = salt_master_factory.config["transport"]
 
     config_overrides = {
-        "log_level_logfile": "quiet",
+        "log_level_logfile": "info",
         "file_roots": salt_master_factory.config["file_roots"].copy(),
         "pillar_roots": salt_master_factory.config["pillar_roots"].copy(),
     }
diff --git a/tests/integration/modules/test_cp.py b/tests/integration/modules/test_cp.py
index d417f90ddc1..c31ff99981c 100644
--- a/tests/integration/modules/test_cp.py
+++ b/tests/integration/modules/test_cp.py
@@ -608,14 +608,11 @@ class CPModuleTest(ModuleCase):
         try:
             self.run_function("cp.push", [log_to_xfer])
             tgt_cache_file = os.path.join(
-                RUNTIME_VARS.TMP,
-                "master-minion-root",
-                "cache",
+                RUNTIME_VARS.RUNTIME_CONFIGS["master"]["cachedir"],
                 "minions",
                 "minion",
                 "files",
-                RUNTIME_VARS.TMP,
-                log_to_xfer,
+                os.path.splitdrive(os.path.normpath(log_to_xfer.lstrip(os.sep)))[1],
             )
             self.assertTrue(
                 os.path.isfile(tgt_cache_file), "File was not cached on the master"
diff --git a/tests/pytests/functional/channel/conftest.py b/tests/pytests/functional/channel/conftest.py
index 62e53c27fb4..387e3bcf4e5 100644
--- a/tests/pytests/functional/channel/conftest.py
+++ b/tests/pytests/functional/channel/conftest.py
@@ -2,6 +2,7 @@ import ctypes
 import multiprocessing
 
 import pytest
+from saltfactories.utils import random_string
 
 import salt.crypt
 import salt.master
@@ -24,3 +25,43 @@ def _prepare_aes():
     finally:
         if old_aes:
             salt.master.SMaster.secrets["aes"] = old_aes
+
+
+def transport_ids(value):
+    return f"Transport({value})"
+
+
+@pytest.fixture(params=("zeromq", "tcp"), ids=transport_ids)
+def transport(request):
+    return request.param
+
+
+@pytest.fixture
+def salt_master(salt_factories, transport):
+    config_defaults = {
+        "transport": transport,
+        "auto_accept": True,
+        "sign_pub_messages": False,
+    }
+    factory = salt_factories.salt_master_daemon(
+        random_string(f"server-{transport}-master-"),
+        defaults=config_defaults,
+    )
+    return factory
+
+
+@pytest.fixture
+def salt_minion(salt_master, transport):
+    config_defaults = {
+        "transport": transport,
+        "master_ip": "127.0.0.1",
+        "master_port": salt_master.config["ret_port"],
+        "auth_timeout": 5,
+        "auth_tries": 1,
+        "master_uri": f"tcp://127.0.0.1:{salt_master.config['ret_port']}",
+    }
+    factory = salt_master.salt_minion_daemon(
+        random_string("server-{transport}-minion-"),
+        defaults=config_defaults,
+    )
+    return factory
diff --git a/tests/pytests/functional/channel/test_req_channel.py b/tests/pytests/functional/channel/test_req_channel.py
new file mode 100644
index 00000000000..4f5f52d66f4
--- /dev/null
+++ b/tests/pytests/functional/channel/test_req_channel.py
@@ -0,0 +1,444 @@
+import ctypes
+import logging
+import multiprocessing
+import pathlib
+import shutil
+import time
+
+import pytest
+from pytestshellutils.utils.processes import terminate_process
+
+import salt.channel.client
+import salt.channel.server
+import salt.config
+import salt.crypt
+import salt.exceptions
+import salt.ext.tornado.gen
+import salt.master
+import salt.utils.platform
+import salt.utils.process
+import salt.utils.stringutils
+
+log = logging.getLogger(__name__)
+
+
+pytestmark = [
+    pytest.mark.skip_on_spawning_platform(
+        reason="These tests are currently broken on spawning platforms. Need to be rewritten.",
+    ),
+    pytest.mark.slow_test,
+    pytest.mark.skipif(
+        "grains['osfinger'] == 'Rocky Linux-8' and grains['osarch'] == 'aarch64'",
+        reason="Temporarily skip on Rocky Linux 8 Arm64",
+    ),
+]
+
+
+class ReqServerChannelProcess(salt.utils.process.SignalHandlingProcess):
+
+    def __init__(self, config, req_channel_crypt):
+        super().__init__()
+        self._closing = False
+        self.config = config
+        self.req_channel_crypt = req_channel_crypt
+        self.process_manager = salt.utils.process.ProcessManager(
+            name="ReqServer-ProcessManager"
+        )
+        self.req_server_channel = salt.channel.server.ReqServerChannel.factory(
+            self.config
+        )
+        self.req_server_channel.pre_fork(self.process_manager)
+        self.io_loop = None
+        self.running = multiprocessing.Event()
+
+    def run(self):
+        salt.master.SMaster.secrets["aes"] = {
+            "secret": multiprocessing.Array(
+                ctypes.c_char,
+                salt.utils.stringutils.to_bytes(
+                    salt.crypt.Crypticle.generate_key_string()
+                ),
+            ),
+            "serial": multiprocessing.Value(
+                ctypes.c_longlong, lock=False  # We'll use the lock from 'secret'
+            ),
+        }
+
+        self.io_loop = salt.ext.tornado.ioloop.IOLoop()
+        self.io_loop.make_current()
+        self.req_server_channel.post_fork(self._handle_payload, io_loop=self.io_loop)
+        self.io_loop.add_callback(self.running.set)
+        try:
+            self.io_loop.start()
+        except KeyboardInterrupt:
+            pass
+
+    def _handle_signals(self, signum, sigframe):
+        self.close()
+        super()._handle_signals(signum, sigframe)
+
+    def __enter__(self):
+        self.start()
+        self.running.wait()
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+        self.terminate()
+
+    def close(self):
+        if self._closing:
+            return
+        self._closing = True
+        if self.req_server_channel is not None:
+            self.req_server_channel.close()
+            self.req_server_channel = None
+        if self.process_manager is not None:
+            self.process_manager.terminate()
+            # Really terminate any process still left behind
+            for pid in self.process_manager._process_map:
+                terminate_process(pid=pid, kill_children=True, slow_stop=False)
+            self.process_manager = None
+
+    @salt.ext.tornado.gen.coroutine
+    def _handle_payload(self, payload):
+        if self.req_channel_crypt == "clear":
+            raise salt.ext.tornado.gen.Return((payload, {"fun": "send_clear"}))
+        raise salt.ext.tornado.gen.Return((payload, {"fun": "send"}))
+
+
+@pytest.fixture
+def req_server_channel(salt_master, req_channel_crypt):
+    req_server_channel_process = ReqServerChannelProcess(
+        salt_master.config.copy(), req_channel_crypt
+    )
+    try:
+        with req_server_channel_process:
+            yield
+    finally:
+        terminate_process(
+            pid=req_server_channel_process.pid, kill_children=True, slow_stop=False
+        )
+
+
+@pytest.fixture
+def req_server_opts(tmp_path):
+    sock_dir = tmp_path / "sock"
+    pki_dir = tmp_path / "pki"
+    cache_dir = tmp_path / "cache"
+    sock_dir.mkdir()
+    pki_dir.mkdir()
+    cache_dir.mkdir()
+    yield {
+        "sock_dir": sock_dir,
+        "pki_dir": pki_dir,
+        "cachedir": cache_dir,
+        "key_pass": "meh",
+        "keysize": 2048,
+        "cluster_id": None,
+        "master_sign_pubkey": False,
+        "pub_server_niceness": None,
+        "con_cache": False,
+        "zmq_monitor": False,
+        "request_server_ttl": 60,
+        "publish_session": 600,
+    }
+
+
+@pytest.fixture
+def req_server(req_server_opts):
+    server = salt.channel.server.ReqServerChannel.factory(req_server_opts)
+    try:
+        yield server
+    finally:
+        server.close()
+
+
+@pytest.fixture
+def minion1_id():
+    yield "minion1"
+
+
+@pytest.fixture
+def minion1_key(minion1_id, tmp_path, req_server_opts):
+    minionpki = tmp_path / minion1_id
+    minionpki.mkdir()
+    key1 = pathlib.Path(salt.crypt.gen_keys(minionpki, minion1_id, 2048))
+
+    pki = pathlib.Path(req_server_opts["pki_dir"])
+    (pki / "minions").mkdir(exist_ok=True)
+    shutil.copy2(key1.with_suffix(".pub"), pki / "minions" / minion1_id)
+    yield salt.crypt.get_rsa_key(key1, None)
+
+
+@pytest.fixture
+def minion2_id():
+    yield "minion2"
+
+
+@pytest.fixture
+def minion2_key(minion2_id, tmp_path, req_server_opts):
+    minionpki = tmp_path / minion2_id
+    minionpki.mkdir()
+    key2 = pathlib.Path(salt.crypt.gen_keys(minionpki, minion2_id, 2048))
+
+    pki = pathlib.Path(req_server_opts["pki_dir"])
+    (pki / "minions").mkdir(exist_ok=True)
+    shutil.copy2(key2.with_suffix(".pub"), pki / "minions" / minion2_id)
+    yield salt.crypt.get_rsa_key(key2, None)
+
+
+def req_channel_crypt_ids(value):
+    return f"ReqChannel(crypt='{value}')"
+
+
+@pytest.fixture(params=["clear", "aes"], ids=req_channel_crypt_ids)
+def req_channel_crypt(request):
+    return request.param
+
+
+@pytest.fixture
+def push_channel(req_server_channel, salt_minion, req_channel_crypt):
+    with salt.channel.client.ReqChannel.factory(
+        salt_minion.config, crypt=req_channel_crypt
+    ) as _req_channel:
+        try:
+            yield _req_channel
+        finally:
+            # Force termination of singleton
+            _req_channel.obj._refcount = 0
+
+
+def test_basic(push_channel):
+    """
+    Test a variety of messages, make sure we get the expected responses
+    """
+    if push_channel.crypt == "aes":
+        pytest.skip(reason="test not valid for encrypted channel")
+    msgs = [
+        {"foo": "bar"},
+        {"bar": "baz"},
+        {"baz": "qux", "list": [1, 2, 3]},
+    ]
+
+    for msg in msgs:
+        ret = push_channel.send(dict(msg), timeout=5, tries=1)
+        assert ret["load"] == msg
+
+
+def test_normalization(push_channel):
+    """
+    Since we use msgpack, we need to test that list types are converted to lists
+    """
+    if push_channel.crypt == "aes":
+        pytest.skip(reason="test not valid for encrypted channel")
+    types = {
+        "list": list,
+    }
+    msgs = [
+        {"list": tuple([1, 2, 3])},
+    ]
+    for msg in msgs:
+        ret = push_channel.send(msg, timeout=5, tries=1)
+        for key, value in ret["load"].items():
+            assert types[key] == type(value)
+
+
+def test_badload(push_channel, req_channel_crypt):
+    """
+    Test a variety of bad requests, make sure that we get some sort of error
+    """
+    if push_channel.crypt == "aes":
+        pytest.skip(reason="test not valid for encrypted channel")
+    msgs = ["", [], tuple()]
+    if req_channel_crypt == "clear":
+        for msg in msgs:
+            ret = push_channel.send(msg, timeout=5, tries=1)
+            assert ret == "payload and load must be a dict"
+    else:
+        for msg in msgs:
+            with pytest.raises(salt.exceptions.AuthenticationError):
+                push_channel.send(msg, timeout=5, tries=1)
+
+
+async def test_req_channel_ttl_v2(req_server, io_loop):
+    req_server.opts["request_server_ttl"] = 60
+
+    async def handler(payload):
+        return payload, {"fun": "send"}
+
+    req_server.post_fork(handler, io_loop)
+    payload = {
+        "enc": "aes",
+        "version": 2,
+        "load": req_server.crypticle.dumps(
+            {
+                "ts": int(time.time() - 61),
+            }
+        ),
+    }
+    ret = await req_server.handle_message(payload)
+    ret = req_server.crypticle.loads(ret)
+    assert ret == payload
+
+
+async def test_req_channel_ttl_valid(req_server, io_loop, minion1_id, minion1_key):
+    req_server.opts["request_server_ttl"] = 60
+    req_server.opts["publish_session"] = 600
+    tok = salt.crypt.private_encrypt(minion1_key, b"salt")
+
+    async def handler(payload):
+        return payload, {"fun": "send"}
+
+    req_server.post_fork(handler, io_loop)
+    key = req_server.session_key(minion1_id)
+
+    crypticle = salt.crypt.Crypticle(req_server.opts, key)
+    payload = {
+        "enc": "aes",
+        "id": minion1_id,
+        "version": 3,
+        "load": crypticle.dumps(
+            {
+                "ts": int(time.time()),
+                "id": minion1_id,
+                "tok": tok,
+            }
+        ),
+    }
+    ret = await req_server.handle_message(payload)
+    ret = crypticle.loads(ret)
+    assert ret == payload
+
+
+async def test_req_channel_ttl_expired(
+    req_server, io_loop, caplog, minion1_id, minion1_key
+):
+    req_server.opts["request_server_ttl"] = 60
+    req_server.opts["publish_session"] = 600
+    tok = salt.crypt.private_encrypt(minion1_key, b"salt")
+
+    async def handler(payload):
+        return payload, {"fun": "send"}
+
+    req_server.post_fork(handler, io_loop)
+    key = req_server.session_key(minion1_id)
+    crypticle = salt.crypt.Crypticle(req_server.opts, key)
+    payload = {
+        "enc": "aes",
+        "id": minion1_id,
+        "version": 3,
+        "load": crypticle.dumps(
+            {
+                "ts": int(time.time() - 61),
+                "id": minion1_id,
+                "tok": tok,
+            }
+        ),
+    }
+    with caplog.at_level(logging.WARNING):
+        ret = await req_server.handle_message(payload)
+        assert f"Received request from {minion1_id} with expired ttl" in caplog.text
+        assert ret == "bad load"
+
+
+async def test_req_channel_id_invalid_chars(
+    req_server, minion1_id, minion1_key, io_loop, caplog
+):
+    req_server.opts["request_server_ttl"] = 60
+    req_server.opts["publish_session"] = 600
+    tok = salt.crypt.private_encrypt(minion1_key, b"salt")
+
+    async def handler(payload):
+        return payload, {"fun": "send"}
+
+    req_server.post_fork(handler, io_loop)
+    key = req_server.session_key(minion1_id)
+    crypticle = salt.crypt.Crypticle(req_server.opts, key)
+    payload = {
+        "enc": "aes",
+        "id": f"{minion1_id}\0",
+        "version": 3,
+        "load": crypticle.dumps(
+            {
+                "ts": int(time.time()),
+                "id": f"{minion1_id}\0",
+                "tok": tok,
+            }
+        ),
+    }
+    with caplog.at_level(logging.WARNING):
+        ret = await req_server.handle_message(payload)
+        assert (
+            "Bad load from minion: SaltDeserializationError: Encountered invalid id"
+            in caplog.text
+        )
+        assert ret == "bad load"
+
+
+async def test_req_channel_id_mismatch(
+    req_server, io_loop, caplog, minion1_id, minion1_key
+):
+
+    id2 = "minion2"
+
+    async def handler(payload):
+        return payload, {"fun": "send"}
+
+    req_server.post_fork(handler, io_loop)
+    key = req_server.session_key(minion1_id)
+    crypticle = salt.crypt.Crypticle(req_server.opts, key)
+    payload = {
+        "enc": "aes",
+        "id": minion1_id,
+        "version": 3,
+        "load": crypticle.dumps(
+            {
+                "ts": int(time.time()),
+                "id": id2,
+            }
+        ),
+    }
+    with caplog.at_level(logging.WARNING):
+        ret = await req_server.handle_message(payload)
+        assert (
+            f"Request id mismatch. Found '{id2}' but expected '{minion1_id}'"
+            in caplog.text
+        )
+        assert ret == "bad load"
+
+
+async def test_req_channel_v2_invalid_token(
+    req_server,
+    io_loop,
+    caplog,
+    tmp_path,
+    minion1_id,
+    minion1_key,
+    minion2_key,
+    minion2_id,
+):
+
+    tok2 = salt.crypt.private_encrypt(minion2_key, b"salt")
+
+    async def handler(payload):
+        return payload, {"fun": "send"}
+
+    req_server.post_fork(handler, io_loop)
+
+    # Minion 1 is trying to impersonate minion2's token.
+    payload = {
+        "enc": "aes",
+        "version": 2,
+        "load": req_server.crypticle.dumps(
+            {
+                "ts": int(time.time()),
+                "id": minion1_id,
+                "tok": tok2,
+            }
+        ),
+    }
+    with caplog.at_level(logging.WARNING):
+        ret = await req_server.handle_message(payload)
+        assert "Minion token did not validate:" in caplog.text
+        assert ret == "bad load"
diff --git a/tests/pytests/functional/channel/test_server.py b/tests/pytests/functional/channel/test_server.py
index bdf96679b78..30bf2d35cba 100644
--- a/tests/pytests/functional/channel/test_server.py
+++ b/tests/pytests/functional/channel/test_server.py
@@ -9,7 +9,6 @@ import time
 from pathlib import Path
 
 import pytest
-from pytestshellutils.utils import ports
 from saltfactories.utils import random_string
 
 import salt.channel.client
@@ -60,44 +59,38 @@ def transport(request):
 
 
 @pytest.fixture
-def master_config(root_dir, transport):
-    master_conf = salt.config.master_config("")
-    master_conf["transport"] = transport
-    master_conf["id"] = "master"
-    master_conf["root_dir"] = str(root_dir)
-    master_conf["sock_dir"] = str(root_dir)
-    master_conf["interface"] = "127.0.0.1"
-    master_conf["publish_port"] = ports.get_unused_localhost_port()
-    master_conf["ret_port"] = ports.get_unused_localhost_port()
-    master_conf["pki_dir"] = str(root_dir / "pki")
-    os.makedirs(master_conf["pki_dir"])
-    salt.crypt.gen_keys(master_conf["pki_dir"], "master", 4096)
-    minions_keys = os.path.join(master_conf["pki_dir"], "minions")
-    os.makedirs(minions_keys)
-    yield master_conf
+def master_config(master_opts, transport):
+    master_opts.update(
+        transport=transport,
+        id="master",
+        interface="127.0.0.1",
+    )
+    salt.crypt.gen_keys(master_opts["pki_dir"], "master", 4096)
+    yield master_opts
 
 
 @pytest.fixture
-def minion_config(master_config, channel_minion_id):
-    minion_conf = salt.config.minion_config(
-        "", minion_id=channel_minion_id, cache_minion_id=False
+def minion_config(minion_opts, master_config, channel_minion_id):
+    minion_opts.update(
+        transport=master_config["transport"],
+        root_dir=master_config["root_dir"],
+        id=channel_minion_id,
+        cachedir=master_config["cachedir"],
+        sock_dir=master_config["sock_dir"],
+        ret_port=master_config["ret_port"],
+        interface="127.0.0.1",
+        pki_dir=os.path.join(master_config["root_dir"], "pki_minion"),
+        master_port=master_config["ret_port"],
+        master_ip="127.0.0.1",
+        master_uri="tcp://127.0.0.1:{}".format(master_config["ret_port"]),
     )
-    minion_conf["transport"] = master_config["transport"]
-    minion_conf["root_dir"] = master_config["root_dir"]
-    minion_conf["id"] = channel_minion_id
-    minion_conf["sock_dir"] = master_config["sock_dir"]
-    minion_conf["ret_port"] = master_config["ret_port"]
-    minion_conf["interface"] = "127.0.0.1"
-    minion_conf["pki_dir"] = os.path.join(master_config["root_dir"], "pki_minion")
-    os.makedirs(minion_conf["pki_dir"])
-    minion_conf["master_port"] = master_config["ret_port"]
-    minion_conf["master_ip"] = "127.0.0.1"
-    minion_conf["master_uri"] = "tcp://127.0.0.1:{}".format(master_config["ret_port"])
-    salt.crypt.gen_keys(minion_conf["pki_dir"], "minion", 4096)
-    minion_pub = os.path.join(minion_conf["pki_dir"], "minion.pub")
+    pathlib.Path(minion_opts["pki_dir"]).mkdir(exist_ok=True)
+    pathlib.Path(master_config["pki_dir"]).mkdir(exist_ok=True)
+    salt.crypt.gen_keys(minion_opts["pki_dir"], "minion", 4096)
+    minion_pub = os.path.join(minion_opts["pki_dir"], "minion.pub")
     pub_on_master = os.path.join(master_config["pki_dir"], "minions", channel_minion_id)
     shutil.copyfile(minion_pub, pub_on_master)
-    return minion_conf
+    return minion_opts
 
 
 @pytest.fixture
diff --git a/tests/pytests/functional/transport/server/test_req_channel.py b/tests/pytests/functional/transport/server/test_req_channel.py
index 555c040c1ca..0ea71318f59 100644
--- a/tests/pytests/functional/transport/server/test_req_channel.py
+++ b/tests/pytests/functional/transport/server/test_req_channel.py
@@ -97,6 +97,12 @@ class ReqServerChannelProcess(salt.utils.process.SignalHandlingProcess):
     def _handle_payload(self, payload):
         if self.req_channel_crypt == "clear":
             raise salt.ext.tornado.gen.Return((payload, {"fun": "send_clear"}))
+        for key in (
+            "id",
+            "ts",
+            "tok",
+        ):
+            payload["load"].pop(key, None)
         raise salt.ext.tornado.gen.Return((payload, {"fun": "send"}))
 
 
diff --git a/tests/pytests/integration/master/test_minion_event.py b/tests/pytests/integration/master/test_minion_event.py
new file mode 100644
index 00000000000..d828d7ebcec
--- /dev/null
+++ b/tests/pytests/integration/master/test_minion_event.py
@@ -0,0 +1,47 @@
+import logging
+
+import salt.channel.client
+import salt.config
+import salt.crypt
+import salt.utils.args
+import salt.utils.jid
+
+log = logging.getLogger(__name__)
+
+
+def test_minoin_event_blacklist(salt_master, salt_minion, salt_cli, caplog):
+    ret = salt_cli.run("test.ping", minion_tgt=salt_minion.id)
+    assert ret.returncode == 0
+
+    opts = salt.config.minion_config(salt_minion.config_file)
+    opts["master_uri"] = "tcp://{}:{}".format(opts["master"], opts["master_port"])
+
+    jid = salt.utils.jid.gen_jid(opts)
+    auth = salt.crypt.SAuth(opts)
+    tok = auth.gen_token(b"salt")
+
+    load = {
+        "cmd": "_minion_event",
+        "tok": tok,
+        "id": opts["id"],
+        "events": [
+            {
+                "data": {
+                    "fun": "test.ping",
+                    "arg": [],
+                    "jid": jid,
+                    "ret": "",
+                    "tgt": salt_minion.id,
+                    "tgt_type": "glob",
+                    "user": "root",
+                    "__peer_id": "salt",
+                },
+                "tag": f"salt/job/{jid}/publish",
+            }
+        ],
+    }
+    with caplog.at_level(logging.WARNING):
+        with salt.channel.client.ReqChannel.factory(opts) as channel:
+            channel.send(load, tries=1, timeout=10000)
+            log.info("payload sent, jid was %s", jid)
+        assert "Filtering blacklisted" in caplog.text
diff --git a/tests/pytests/integration/master/test_recv_file.py b/tests/pytests/integration/master/test_recv_file.py
new file mode 100644
index 00000000000..71a59ea839c
--- /dev/null
+++ b/tests/pytests/integration/master/test_recv_file.py
@@ -0,0 +1,29 @@
+import getpass
+import pathlib
+
+import salt.channel.client
+
+
+def test_file_recv_path(salt_master, salt_minion):
+    config = salt_minion.config.copy()
+    config["master_uri"] = f"tcp://127.0.0.1:{salt_master.config['ret_port']}"
+    keyfile = f".{getpass.getuser()}_key"
+    data = b"asdf"
+    load_path_list = ["..", "..", "..", keyfile]
+    cachedir = salt_master.config["cachedir"]
+    assert (pathlib.Path(cachedir) / keyfile).exists()
+    assert (pathlib.Path(cachedir) / keyfile).read_bytes() != data
+    with salt.channel.client.ReqChannel.factory(config, crypt="aes") as channel:
+        load = {
+            "cmd": "_file_recv",
+            "id": salt_minion.config["id"],
+            "path": load_path_list,
+            "size": len(data),
+            "tok": channel.auth.gen_token(b"salt"),
+            "loc": 0,
+            "data": b"asdf",
+        }
+        ret = channel.send(load)
+    assert ret is False
+    assert (pathlib.Path(cachedir) / keyfile).exists()
+    assert (pathlib.Path(cachedir) / keyfile).read_bytes() != data
diff --git a/tests/pytests/integration/minion/test_return_retries.py b/tests/pytests/integration/minion/test_return_retries.py
index a7f5eaeff16..00bfb908ae7 100644
--- a/tests/pytests/integration/minion/test_return_retries.py
+++ b/tests/pytests/integration/minion/test_return_retries.py
@@ -1,8 +1,11 @@
 import time
+import sys
 
 import pytest
 from saltfactories.utils import random_string
 
+from tests.support.helpers import dedent
+
 
 @pytest.fixture(scope="function")
 def salt_minion_retry(salt_master_factory, salt_minion_id):
@@ -50,3 +53,72 @@ def test_publish_retry(salt_master, salt_minion_retry, salt_cli, salt_run_cli):
 
     assert salt_minion_retry.id in data
     assert data[salt_minion_retry.id] is True
+
+
+@pytest.mark.slow_test
+def test_pillar_timeout(salt_master_factory):
+    cmd = (
+        sys.executable
+        + ' -c "import time; time.sleep(4.8); print(\'{\\"foo\\": \\"bar\\"}\');"'
+    ).strip()
+    master_overrides = {
+        "ext_pillar": [
+            {"cmd_json": cmd},
+        ],
+        "auto_accept": True,
+        "worker_threads": 3,
+        "peer": True,
+    }
+    minion_overrides = {
+        "auth_timeout": 20,
+        "request_channel_timeout": 5,
+        "request_channel_tries": 1,
+    }
+    sls_name = "issue-50221"
+    sls_contents = dedent(
+        """
+    custom_test_state:
+      test.configurable_test_state:
+        - name: example
+        - changes: True
+        - result: True
+        - comment: "Nothing has acutally been changed"
+    """
+    )
+    master = salt_master_factory.salt_master_daemon(
+        "pillar-timeout-master",
+        overrides=master_overrides,
+    )
+    minion1 = master.salt_minion_daemon(
+        random_string("pillar-timeout-1-"),
+        overrides=minion_overrides,
+    )
+    minion2 = master.salt_minion_daemon(
+        random_string("pillar-timeout-2-"),
+        overrides=minion_overrides,
+    )
+    minion3 = master.salt_minion_daemon(
+        random_string("pillar-timeout-3-"),
+        overrides=minion_overrides,
+    )
+    minion4 = master.salt_minion_daemon(
+        random_string("pillar-timeout-4-"),
+        overrides=minion_overrides,
+    )
+    cli = master.salt_cli()
+    sls_tempfile = master.state_tree.base.temp_file(
+        "{}.sls".format(sls_name), sls_contents
+    )
+    with master.started(), minion1.started(), minion2.started(), minion3.started(), minion4.started(), sls_tempfile:
+        proc = cli.run("state.sls", sls_name, minion_tgt="*")
+        # At least one minion should have a Pillar timeout
+        print(proc)
+        assert proc.returncode == 1
+        minion_timed_out = False
+        # Find the minion that has a Pillar timeout
+        for key in proc.data:
+            if isinstance(proc.data[key], str):
+                if proc.data[key].find("Pillar timed out") != -1:
+                    minion_timed_out = True
+                    break
+        assert minion_timed_out is True
diff --git a/tests/pytests/unit/channel/test_server.py b/tests/pytests/unit/channel/test_server.py
index 3fa5d94bead..3f6262f89b1 100644
--- a/tests/pytests/unit/channel/test_server.py
+++ b/tests/pytests/unit/channel/test_server.py
@@ -7,8 +7,11 @@ import salt.ext.tornado.gen
 from tests.support.mock import MagicMock, patch
 
 
-def test__auth_cmd_stats_passing():
-    req_server_channel = server.ReqServerChannel({"master_stats": True}, None)
+def test__auth_cmd_stats_passing(master_opts):
+    master_opts.update(
+        {"master_stats": True}
+    )
+    req_server_channel = server.ReqServerChannel(master_opts, None)
 
     fake_ret = {"enc": "clear", "load": b"FAKELOAD"}
 
@@ -32,3 +35,48 @@ def test__auth_cmd_stats_passing():
         )
         assert auth_call_duration >= 0.03
         assert auth_call_duration < 0.05
+
+
+@pytest.fixture
+def root_dir(tmp_path):
+    (tmp_path / "var").mkdir()
+    (tmp_path / "var" / "cache").mkdir()
+    (tmp_path / "etc").mkdir()
+    (tmp_path / "etc" / "salt").mkdir()
+    (tmp_path / "etc" / "salt" / "pki").mkdir()
+    (tmp_path / "etc" / "salt" / "pki" / "minions").mkdir()
+    yield tmp_path
+
+
+def test_req_server_validate_token_removes_token(root_dir):
+    opts = {
+        "master_uri": "tcp://127.0.0.1:4505",
+        "cachedir": str(root_dir / "var" / "cache"),
+        "pki_dir": str(root_dir / "etc" / "salt" / "pki"),
+    }
+    reqsrv = server.ReqServerChannel.factory(opts)
+    payload = {
+        "load": {
+            "id": "minion",
+            "tok": "asdf",
+        }
+    }
+    assert reqsrv.validate_token(payload) is False
+    assert "tok" not in payload["load"]
+
+
+def test_req_server_validate_token_removes_token_id_traversal(root_dir):
+    opts = {
+        "master_uri": "tcp://127.0.0.1:4505",
+        "cachedir": str(root_dir / "var" / "cache"),
+        "pki_dir": str(root_dir / "etc" / "salt" / "pki"),
+    }
+    reqsrv = server.ReqServerChannel.factory(opts)
+    payload = {
+        "load": {
+            "id": "minion/../../foo",
+            "tok": "asdf",
+        }
+    }
+    assert reqsrv.validate_token(payload) is False
+    assert "tok" not in payload["load"]
diff --git a/tests/pytests/unit/daemons/masterapi/test_valid_minion_tag.py b/tests/pytests/unit/daemons/masterapi/test_valid_minion_tag.py
new file mode 100644
index 00000000000..3a3b18e1d90
--- /dev/null
+++ b/tests/pytests/unit/daemons/masterapi/test_valid_minion_tag.py
@@ -0,0 +1,23 @@
+import pytest
+
+import salt.daemons.masterapi
+
+
+@pytest.mark.parametrize(
+    "tag, valid",
+    [
+        ("salt/job/20160829225914848058/publish", False),
+        ("salt/key", False),
+        ("salt/cluster/fobar", False),
+        ("salt/job/20160829225914848058/return", False),
+        ("salt/job/20160829225914848058/new", False),
+        ("salt/wheel/20160829225914848058/new", False),
+        ("salt/run/20160829225914848058/new", False),
+        ("salt/run/20160829225914848058/ret", False),
+        ("salt/run/20160829225914848058/args", False),
+        ("salt/cloud/20160829225914848058/new", False),
+        ("salt/cloud/20160829225914848058/ret", False),
+    ],
+)
+def test_valid_minion_tag(tag, valid):
+    assert salt.daemons.masterapi.valid_minion_tag(tag) is valid
diff --git a/tests/pytests/unit/fileserver/gitfs/test_gitfs.py b/tests/pytests/unit/fileserver/gitfs/test_gitfs.py
index 4c7e8dd7c5c..7896fd93bb7 100644
--- a/tests/pytests/unit/fileserver/gitfs/test_gitfs.py
+++ b/tests/pytests/unit/fileserver/gitfs/test_gitfs.py
@@ -187,7 +187,7 @@ def cache_dir(tmp_path):
 def configure_loader_modules(provider, sock_dir, repo_dir, cache_dir):
     opts = {
         "sock_dir": str(sock_dir),
-        "gitfs_remotes": ["file://" + str(repo_dir)],
+        "gitfs_remotes": [f"file://{repo_dir}"],
         "cachedir": str(cache_dir),
         "gitfs_root": "",
         "fileserver_backend": ["gitfs"],
@@ -348,7 +348,7 @@ def test_ref_types_per_remote(repo_dir, unicode_dirname, tag_name):
     Test the per_remote ref_types config option, using a different
     ref_types setting than the global test.
     """
-    remotes = [{"file://" + repo_dir: [{"ref_types": ["tag"]}]}]
+    remotes = [{f"file://{repo_dir}": [{"ref_types": ["tag"]}]}]
     with patch.dict(gitfs.__opts__, {"gitfs_remotes": remotes}):
         gitfs.update()
         ret = gitfs.envs(ignore_cache=True)
@@ -435,7 +435,7 @@ def test_disable_saltenv_mapping_global_with_mapping_defined_per_remote(repo_dir
     opts = {
         "gitfs_disable_saltenv_mapping": True,
         "gitfs_remotes": [
-            {repo_dir: [{"saltenv": [{"bar": [{"ref": "somebranch"}]}]}]}
+            {f"file://{repo_dir}": [{"saltenv": [{"bar": [{"ref": "somebranch"}]}]}]}
         ],
     }
     with patch.dict(gitfs.__opts__, opts):
@@ -454,7 +454,7 @@ def test_disable_saltenv_mapping_per_remote_with_mapping_defined_globally(repo_d
     option.
     """
     opts = {
-        "gitfs_remotes": [{repo_dir: [{"disable_saltenv_mapping": True}]}],
+        "gitfs_remotes": [{f"file://{repo_dir}": [{"disable_saltenv_mapping": True}]}],
         "gitfs_saltenv": [{"hello": [{"ref": "somebranch"}]}],
     }
 
@@ -476,7 +476,7 @@ def test_disable_saltenv_mapping_per_remote_with_mapping_defined_per_remote(repo
     opts = {
         "gitfs_remotes": [
             {
-                repo_dir: [
+                f"file://{repo_dir}": [
                     {"disable_saltenv_mapping": True},
                     {"saltenv": [{"world": [{"ref": "somebranch"}]}]},
                 ]
diff --git a/tests/pytests/unit/modules/test_cp.py b/tests/pytests/unit/modules/test_cp.py
index 6caa9ef5938..150c1d24344 100644
--- a/tests/pytests/unit/modules/test_cp.py
+++ b/tests/pytests/unit/modules/test_cp.py
@@ -153,7 +153,6 @@ def test_push():
             dict(
                 loc=fh_.tell(),  # pylint: disable=resource-leakage
                 cmd="_file_recv",
-                tok="token",
                 path=["saltines", "test.file"],
                 size=10,
                 data=b"",  # data is empty here because load['data'] is overwritten
diff --git a/tests/pytests/unit/pillar/test_pillar.py b/tests/pytests/unit/pillar/test_pillar.py
index 75603aa0fe4..11eda34318b 100644
--- a/tests/pytests/unit/pillar/test_pillar.py
+++ b/tests/pytests/unit/pillar/test_pillar.py
@@ -7,6 +7,7 @@ import salt.loader
 import salt.pillar
 import salt.utils.cache
 from salt.utils.odict import OrderedDict
+from tests.support.mock import MagicMock
 
 
 @pytest.mark.parametrize(
@@ -157,3 +158,20 @@ def test_pillar_get_cache_disk(temp_salt_minion, caplog):
             in caplog.messages
         )
         assert fresh_pillar == {}
+
+
+def test_remote_pillar_timeout(temp_salt_minion, tmp_path):
+    opts = temp_salt_minion.config.copy()
+    opts["master_uri"] = "tcp://127.0.0.1:12323"
+    grains = salt.loader.grains(opts)
+    pillar = salt.pillar.RemotePillar(
+        opts,
+        grains,
+        temp_salt_minion.id,
+        "base",
+    )
+    mock = MagicMock()
+    mock.side_effect = salt.exceptions.SaltReqTimeoutError()
+    pillar.channel.crypted_transfer_decode_dictentry = mock
+    with pytest.raises(salt.exceptions.SaltClientError):
+        pillar.compile_pillar()
diff --git a/tests/pytests/unit/test_crypt.py b/tests/pytests/unit/test_crypt.py
index e3c98ab6366..0a80782b3a9 100644
--- a/tests/pytests/unit/test_crypt.py
+++ b/tests/pytests/unit/test_crypt.py
@@ -100,6 +100,16 @@ bQIDAQAB
 """
 
 
+@pytest.fixture
+def minion_root(tmp_path):
+    root = tmp_path / "root"
+    root.mkdir()
+    (root / "etc").mkdir()
+    (root / "etc" / "salt").mkdir()
+    (root / "etc" / "salt" / "pki").mkdir()
+    yield root
+
+
 def test_get_rsa_pub_key_bad_key(tmp_path):
     """
     get_rsa_pub_key raises InvalidKeyError when encoutering a bad key
@@ -170,3 +180,142 @@ def test_verify_signature_bad_sig(tmp_path):
     msg = b"foo bar"
     sig = salt.crypt.sign_message(str(tmp_path.joinpath("foo.pem")), msg)
     assert not salt.crypt.verify_signature(str(tmp_path.joinpath("bar.pub")), msg, sig)
+
+
+async def test_auth_aes_key_rotation(minion_root, io_loop):
+    pki_dir = minion_root / "etc" / "salt" / "pki"
+    opts = {
+        "id": "minion",
+        "__role": "minion",
+        "pki_dir": str(pki_dir),
+        "master_uri": "tcp://127.0.0.1:4505",
+        "keysize": 4096,
+        "acceptance_wait_time": 60,
+        "acceptance_wait_time_max": 60,
+    }
+    credskey = (
+        opts["pki_dir"],  # where the keys are stored
+        opts["id"],  # minion ID
+        opts["master_uri"],  # master ID
+    )
+    salt.crypt.gen_keys(pki_dir, "minion", opts["keysize"])
+
+    aes = salt.crypt.Crypticle.generate_key_string()
+    session = salt.crypt.Crypticle.generate_key_string()
+
+    auth = salt.crypt.AsyncAuth(opts, io_loop)
+
+    async def mock_sign_in(*args, **kwargs):
+        return mock_sign_in.response
+
+    mock_sign_in.response = {
+        "enc": "pub",
+        "aes": aes,
+        "session": session,
+    }
+    auth.sign_in = mock_sign_in
+
+    assert credskey not in auth.creds_map
+
+    await auth.authenticate()
+
+    assert credskey in auth.creds_map
+    assert auth.creds_map[credskey]["aes"] == aes
+    assert auth.creds_map[credskey]["session"] == session
+
+    aes1 = salt.crypt.Crypticle.generate_key_string()
+
+    mock_sign_in.response = {
+        "enc": "pub",
+        "aes": aes1,
+        "session": session,
+    }
+
+    await auth.authenticate()
+
+    assert credskey in auth.creds_map
+    assert auth.creds_map[credskey]["aes"] == aes1
+    assert auth.creds_map[credskey]["session"] == session
+
+    session1 = salt.crypt.Crypticle.generate_key_string()
+    mock_sign_in.response = {
+        "enc": "pub",
+        "aes": aes1,
+        "session": session1,
+    }
+
+    await auth.authenticate()
+
+    assert credskey in auth.creds_map
+    assert auth.creds_map[credskey]["aes"] == aes1
+    assert auth.creds_map[credskey]["session"] == session1
+
+
+def test_sauth_aes_key_rotation(minion_root, io_loop):
+
+    pki_dir = minion_root / "etc" / "salt" / "pki"
+    opts = {
+        "id": "minion",
+        "__role": "minion",
+        "pki_dir": str(pki_dir),
+        "master_uri": "tcp://127.0.0.1:4505",
+        "keysize": 4096,
+        "acceptance_wait_time": 60,
+        "acceptance_wait_time_max": 60,
+    }
+    credskey = (
+        opts["pki_dir"],  # where the keys are stored
+        opts["id"],  # minion ID
+        opts["master_uri"],  # master ID
+    )
+    salt.crypt.gen_keys(pki_dir, "minion", opts["keysize"])
+
+    aes = salt.crypt.Crypticle.generate_key_string()
+    session = salt.crypt.Crypticle.generate_key_string()
+
+    auth = salt.crypt.SAuth(opts, io_loop)
+
+    def mock_sign_in(*args, **kwargs):
+        return mock_sign_in.response
+
+    mock_sign_in.response = {
+        "enc": "pub",
+        "aes": aes,
+        "session": session,
+    }
+    auth.sign_in = mock_sign_in
+
+    assert auth._creds is None
+
+    auth.authenticate()
+
+    assert isinstance(auth._creds, dict)
+    assert auth._creds["aes"] == aes
+    assert auth._creds["session"] == session
+
+    aes1 = salt.crypt.Crypticle.generate_key_string()
+
+    mock_sign_in.response = {
+        "enc": "pub",
+        "aes": aes1,
+        "session": session,
+    }
+
+    auth.authenticate()
+
+    assert isinstance(auth._creds, dict)
+    assert auth._creds["aes"] == aes1
+    assert auth._creds["session"] == session
+
+    session1 = salt.crypt.Crypticle.generate_key_string()
+    mock_sign_in.response = {
+        "enc": "pub",
+        "aes": aes1,
+        "session": session1,
+    }
+
+    auth.authenticate()
+
+    assert isinstance(auth._creds, dict)
+    assert auth._creds["aes"] == aes1
+    assert auth._creds["session"] == session1
diff --git a/tests/pytests/unit/test_master.py b/tests/pytests/unit/test_master.py
index 7fccb24d73b..833f966e058 100644
--- a/tests/pytests/unit/test_master.py
+++ b/tests/pytests/unit/test_master.py
@@ -3,20 +3,26 @@ import time
 
 import pytest
 
+import salt.config
+import salt.crypt
 import salt.master
+import salt.utils.files
 import salt.utils.platform
 from tests.support.mock import MagicMock, patch
+from tests.support.runtests import RUNTIME_VARS
 
 
 @pytest.fixture
 def encrypted_requests(tmp_path):
     # To honor the comment on AESFuncs
+    (tmp_path / "pki").mkdir()
     return salt.master.AESFuncs(
         opts={
+            "pki_dir": str(tmp_path / "pki"),
             "cachedir": str(tmp_path / "cache"),
             "sock_dir": str(tmp_path / "sock_drawer"),
             "conf_file": str(tmp_path / "config.conf"),
-            "fileserver_backend": "local",
+            "fileserver_backend": ["local"],
             "master_job_cache": False,
         }
     )
@@ -307,3 +313,154 @@ def test_collect__auth_to_master_stats():
         assert mworker.stats["_auth"]["mean"] < 0.04
         handle_aes_mock.assert_not_called()
         handle_clear_mock.assert_not_called()
+
+
+def test_pub_ret_traversal(encrypted_requests, tmp_path):
+    """
+    master's  AESFuncs._syndic_return method cachdir creation is not vulnerable to a directory traversal
+    """
+    salt.crypt.gen_keys(tmp_path, "minion", 2048)
+
+    minions = pathlib.Path(encrypted_requests.opts["pki_dir"]) / "minions"
+    minions.mkdir()
+
+    with salt.utils.files.fopen(minions / "minion", "wb") as wfp:
+        with salt.utils.files.fopen(tmp_path / "minion.pub", "rb") as rfp:
+            wfp.write(rfp.read())
+
+    priv = salt.crypt.get_rsa_key(tmp_path / "minion.pem", None)
+    with pytest.raises(salt.exceptions.SaltValidationError):
+        encrypted_requests.pub_ret(
+            {
+                "tok": salt.crypt.private_encrypt(priv, b"salt"),
+                "id": "minion",
+                "jid": "asdf/../../../sdf",
+                "return": {},
+            }
+        )
+
+
+def _git_pillar_base_config(tmp_path):
+    return {
+        "__role": "master",
+        "pki_dir": str(tmp_path / "pki"),
+        "cachedir": str(tmp_path / "cache"),
+        "sock_dir": str(tmp_path / "sock_drawer"),
+        "conf_file": str(tmp_path / "config.conf"),
+        "fileserver_backend": ["local"],
+        "master_job_cache": False,
+        "file_client": "local",
+        "pillar_cache": False,
+        "state_top": "top.sls",
+        "pillar_roots": {
+            "base": [str(tmp_path / "pillar")],
+        },
+        "render_dirs": [str(pathlib.Path(RUNTIME_VARS.SALT_CODE_DIR) / "renderer")],
+        "renderer": "jinja|yaml",
+        "renderer_blacklist": [],
+        "renderer_whitelist": [],
+        "optimization_order": [0, 1, 2],
+        "on_demand_ext_pillar": [],
+        "git_pillar_user": "",
+        "git_pillar_password": "",
+        "git_pillar_pubkey": "",
+        "git_pillar_privkey": "",
+        "git_pillar_passphrase": "",
+        "git_pillar_insecure_auth": False,
+        "git_pillar_refspecs": salt.config._DFLT_REFSPECS,
+        "git_pillar_ssl_verify": True,
+        "git_pillar_branch": "master",
+        "git_pillar_base": "master",
+        "git_pillar_root": "",
+        "git_pillar_env": "",
+        "git_pillar_fallback": "",
+    }
+
+
+@pytest.fixture
+def allowed_funcs(tmp_path):
+    """
+    Configuration with git on demand pillar allowed
+    """
+    opts = _git_pillar_base_config(tmp_path)
+    opts["on_demand_ext_pillar"] = ["git"]
+    salt.crypt.gen_keys(str(tmp_path), "minion", 2048)
+    master_pki = tmp_path / "pki"
+    master_pki.mkdir()
+    accepted_pki = master_pki / "minions"
+    accepted_pki.mkdir()
+    (accepted_pki / "minion.pub").write_text((tmp_path / "minion.pub").read_text())
+
+    return salt.master.AESFuncs(opts=opts)
+
+
+def test_on_demand_allowed_command_injection(allowed_funcs, tmp_path, caplog):
+    """
+    Verify on demand pillars validate remote urls
+    """
+    pwnpath = tmp_path / "pwn"
+    assert not pwnpath.exists()
+    load = {
+        "cmd": "_pillar",
+        "saltenv": "base",
+        "pillarenv": "base",
+        "id": "carbon",
+        "grains": {},
+        "ver": 2,
+        "ext": {
+            "git": [
+                f'base ssh://fake@git/repo\n[core]\nsshCommand = touch {pwnpath}\n[remote "origin"]\n'
+            ]
+        },
+        "clean_cache": True,
+    }
+    with caplog.at_level(level="WARNING"):
+        ret = allowed_funcs._pillar(load)
+    assert not pwnpath.exists()
+    assert "Found bad url data" in caplog.text
+
+
+@pytest.fixture
+def not_allowed_funcs(tmp_path):
+    """
+    Configuration with no on demand pillars allowed
+    """
+    opts = _git_pillar_base_config(tmp_path)
+    opts["on_demand_ext_pillar"] = []
+    salt.crypt.gen_keys(str(tmp_path), "minion", 2048)
+    master_pki = tmp_path / "pki"
+    master_pki.mkdir()
+    accepted_pki = master_pki / "minions"
+    accepted_pki.mkdir()
+    (accepted_pki / "minion.pub").write_text((tmp_path / "minion.pub").read_text())
+
+    return salt.master.AESFuncs(opts=opts)
+
+
+def test_on_demand_not_allowed(not_allowed_funcs, tmp_path, caplog):
+    """
+    Verify on demand pillars do not render when not allowed
+    """
+    pwnpath = tmp_path / "pwn"
+    assert not pwnpath.exists()
+    load = {
+        "cmd": "_pillar",
+        "saltenv": "base",
+        "pillarenv": "base",
+        "id": "carbon",
+        "grains": {},
+        "ver": 2,
+        "ext": {
+            "git": [
+                f'base ssh://fake@git/repo\n[core]\nsshCommand = touch {pwnpath}\n[remote "origin"]\n'
+            ]
+        },
+        "clean_cache": True,
+    }
+    with caplog.at_level(level="WARNING"):
+        ret = not_allowed_funcs._pillar(load)
+    assert not pwnpath.exists()
+    assert (
+        "The following ext_pillar modules are not allowed for on-demand pillar data: git."
+        in caplog.text
+    )
diff --git a/tests/pytests/unit/transport/test_zeromq.py b/tests/pytests/unit/transport/test_zeromq.py
index c7cbc538641..558f7537784 100644
--- a/tests/pytests/unit/transport/test_zeromq.py
+++ b/tests/pytests/unit/transport/test_zeromq.py
@@ -586,23 +586,25 @@ def test_zeromq_async_pub_channel_filtering_decode_message(
     assert res.result()["enc"] == "aes"
 
 
-def test_req_server_chan_encrypt_v2(pki_dir):
+def test_req_server_chan_encrypt_v2(pki_dir, master_opts):
     loop = salt.ext.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"}
@@ -616,7 +618,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
@@ -630,23 +632,25 @@ def test_req_server_chan_encrypt_v2(pki_dir):
     assert data["pillar"] == pillar_data
 
 
-def test_req_server_chan_encrypt_v1(pki_dir):
+def test_req_server_chan_encrypt_v1(pki_dir, master_opts):
     loop = salt.ext.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"}
@@ -661,29 +665,31 @@ 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
 
 
-def test_req_chan_decode_data_dict_entry_v1(pki_dir):
+def test_req_chan_decode_data_dict_entry_v1(pki_dir, master_opts, minion_opts):
     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 = dict(master_opts, 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)
     dictkey = "pillar"
     target = "minion"
     pillar_data = {"pillar1": "meh"}
@@ -699,24 +705,26 @@ def test_req_chan_decode_data_dict_entry_v1(pki_dir):
     assert ret_pillar_data == pillar_data
 
 
-async def test_req_chan_decode_data_dict_entry_v2(pki_dir):
+async def test_req_chan_decode_data_dict_entry_v2(pki_dir, master_opts, minion_opts):
     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"
@@ -724,19 +732,25 @@ 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)
+    auth._session_crypticle = salt.crypt.Crypticle(
+        minion_opts, server.session_key(target)
+    )
     client.auth = MagicMock()
     client.auth.mpub = auth.mpub
     client.auth.authenticated = True
     client.auth.get_keys = auth.get_keys
+    client.auth.gen_token = auth.gen_token
     client.auth.crypticle.dumps = auth.crypticle.dumps
     client.auth.crypticle.loads = auth.crypticle.loads
+    client.auth.session_crypticle.dumps = auth.session_crypticle.dumps
+    client.auth.session_crypticle.loads = auth.session_crypticle.loads
     client.transport = MagicMock()
 
     @salt.ext.tornado.gen.coroutine
     def mocksend(msg, timeout=60, tries=3):
         client.transport.msg = msg
-        load = client.auth.crypticle.loads(msg["load"])
+        load = client.auth.session_crypticle.loads(msg["load"])
         ret = server._encrypt_private(
             pillar_data, dictkey, target, nonce=load["nonce"], sign_messages=True
         )
@@ -753,7 +767,7 @@ async def test_req_chan_decode_data_dict_entry_v2(pki_dir):
         "pillarenv": "base",
         "pillar_override": True,
         "extra_minion_data": {},
-        "ver": "2",
+        "ver": "3",
         "cmd": "_pillar",
     }
     ret = await client.crypted_transfer_decode_dictentry(
@@ -761,28 +775,30 @@ async def test_req_chan_decode_data_dict_entry_v2(pki_dir):
         dictkey="pillar",
     )
     assert "version" in client.transport.msg
-    assert client.transport.msg["version"] == 2
+    assert client.transport.msg["version"] == 3
     assert ret == {"pillar1": "meh"}
 
 
-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(pki_dir, master_opts, minion_opts):
     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"
@@ -791,7 +807,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
@@ -831,24 +847,26 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_nonce(pki_dir):
     assert "Pillar nonce verification failed." == excinfo.value.message
 
 
-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(pki_dir, master_opts, minion_opts):
     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"
@@ -857,19 +875,25 @@ 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)
+    auth._session_crypticle = salt.crypt.Crypticle(
+        minion_opts, server.session_key(target)
+    )
     client.auth = MagicMock()
     client.auth.mpub = auth.mpub
     client.auth.authenticated = True
     client.auth.get_keys = auth.get_keys
+    client.auth.gen_token = auth.gen_token
     client.auth.crypticle.dumps = auth.crypticle.dumps
     client.auth.crypticle.loads = auth.crypticle.loads
+    client.auth.session_crypticle.dumps = auth.session_crypticle.dumps
+    client.auth.session_crypticle.loads = auth.session_crypticle.loads
     client.transport = MagicMock()
 
     @salt.ext.tornado.gen.coroutine
     def mocksend(msg, timeout=60, tries=3):
         client.transport.msg = msg
-        load = client.auth.crypticle.loads(msg["load"])
+        load = client.auth.session_crypticle.loads(msg["load"])
         ret = server._encrypt_private(
             pillar_data, dictkey, target, nonce=load["nonce"], sign_messages=True
         )
@@ -901,7 +925,7 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_signature(pki_dir):
         "pillarenv": "base",
         "pillar_override": True,
         "extra_minion_data": {},
-        "ver": "2",
+        "ver": "3",
         "cmd": "_pillar",
     }
 
@@ -913,24 +937,26 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_signature(pki_dir):
     assert "Pillar payload signature failed to validate." == excinfo.value.message
 
 
-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(pki_dir, master_opts, minion_opts):
     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"
@@ -939,19 +965,25 @@ 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)
+    auth._session_crypticle = salt.crypt.Crypticle(
+        minion_opts, server.session_key(target)
+    )
     client.auth = MagicMock()
     client.auth.mpub = auth.mpub
     client.auth.authenticated = True
     client.auth.get_keys = auth.get_keys
+    client.auth.gen_token = auth.gen_token
     client.auth.crypticle.dumps = auth.crypticle.dumps
     client.auth.crypticle.loads = auth.crypticle.loads
+    client.auth.session_crypticle.dumps = auth.session_crypticle.dumps
+    client.auth.session_crypticle.loads = auth.session_crypticle.loads
     client.transport = MagicMock()
 
     @salt.ext.tornado.gen.coroutine
     def mocksend(msg, timeout=60, tries=3):
         client.transport.msg = msg
-        load = client.auth.crypticle.loads(msg["load"])
+        load = client.auth.session_crypticle.loads(msg["load"])
         ret = server._encrypt_private(
             pillar_data, dictkey, target, nonce=load["nonce"], sign_messages=True
         )
@@ -967,7 +999,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(master_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)
@@ -1002,25 +1034,27 @@ async def test_req_chan_decode_data_dict_entry_v2_bad_key(pki_dir):
     assert "Key verification failed." == excinfo.value.message
 
 
-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(pki_dir, master_opts, minion_opts):
+    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,
@@ -1028,10 +1062,13 @@ 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
+    server.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=False
+    )
     server.master_key = salt.crypt.MasterKeys(server.opts)
 
     pub = salt.crypt.get_rsa_pub_key(str(pki_dir.joinpath("minion", "minion.pub")))
@@ -1055,25 +1092,27 @@ async def test_req_serv_auth_v1(pki_dir):
     assert "load" not in ret
 
 
-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(pki_dir, master_opts, minion_opts):
+    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,
@@ -1081,10 +1120,13 @@ 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
+    server.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=False
+    )
     server.master_key = salt.crypt.MasterKeys(server.opts)
 
     pub = salt.crypt.get_rsa_pub_key(str(pki_dir.joinpath("minion", "minion.pub")))
@@ -1110,26 +1152,28 @@ async def test_req_serv_auth_v2(pki_dir):
     assert "load" in ret
 
 
-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(pki_dir, io_loop, master_opts, minion_opts):
+    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,
@@ -1137,19 +1181,26 @@ 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.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=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)
+    client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=io_loop)
+    auth_client = salt.channel.client.AsyncReqChannel.factory(
+        minion_opts, io_loop=io_loop, crypt="clear"
+    )
     signin_payload = client.auth.minion_sign_in_payload()
-    pload = client._package_load(signin_payload)
+    pload = auth_client._package_load(signin_payload)
     assert "version" in pload
-    assert pload["version"] == 2
+    assert pload["version"] == 3
 
     ret = server._auth(pload["load"], sign_messages=True)
     assert "sig" in ret
@@ -1159,26 +1210,28 @@ async def test_req_chan_auth_v2(pki_dir, io_loop):
     assert "publish_port" in ret
 
 
-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(pki_dir, io_loop, minion_opts, master_opts):
+    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,
@@ -1186,7 +1239,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
@@ -1194,22 +1247,28 @@ async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop):
     server = salt.channel.server.ReqServerChannel.factory(master_opts)
     server.auto_key = salt.daemons.masterapi.AutoKey(server.opts)
     server.cache_cli = False
+    server.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=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)
+    auth_client = salt.channel.client.AsyncReqChannel.factory(
+        minion_opts, io_loop=io_loop, crypt="clear"
+    )
     signin_payload = client.auth.minion_sign_in_payload()
-    pload = client._package_load(signin_payload)
+    pload = auth_client._package_load(signin_payload)
     assert "version" in pload
-    assert pload["version"] == 2
+    assert pload["version"] == 3
 
     server_reply = server._auth(pload["load"], sign_messages=True)
     # With version 2 we always get a clear signed response
@@ -1233,10 +1292,14 @@ async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop):
     server = salt.channel.server.ReqServerChannel.factory(master_opts)
     server.auto_key = salt.daemons.masterapi.AutoKey(server.opts)
     server.cache_cli = False
+    server.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=False
+    )
     server.master_key = salt.crypt.MasterKeys(server.opts)
 
     signin_payload = client.auth.minion_sign_in_payload()
-    pload = client._package_load(signin_payload)
+
+    pload = auth_client._package_load(signin_payload)
     server_reply = server._auth(pload["load"], sign_messages=True)
     ret = client.auth.handle_signin_response(signin_payload, server_reply)
 
@@ -1250,28 +1313,30 @@ async def test_req_chan_auth_v2_with_master_signing(pki_dir, io_loop):
     )
 
 
-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(pki_dir, io_loop, master_opts, minion_opts):
 
     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,
@@ -1279,19 +1344,26 @@ 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.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=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)
+    client = salt.channel.client.AsyncReqChannel.factory(minion_opts, io_loop=io_loop)
+    auth_client = salt.channel.client.AsyncReqChannel.factory(
+        minion_opts, io_loop=io_loop, crypt="clear"
+    )
     signin_payload = client.auth.minion_sign_in_payload()
-    pload = client._package_load(signin_payload)
+    pload = auth_client._package_load(signin_payload)
     assert "version" in pload
-    assert pload["version"] == 2
+    assert pload["version"] == 3
 
     ret = server._auth(pload["load"], sign_messages=True)
     assert "sig" in ret
@@ -1299,7 +1371,7 @@ async def test_req_chan_auth_v2_new_minion_with_master_pub(pki_dir, io_loop):
     assert ret == "retry"
 
 
-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(pki_dir, io_loop, master_opts, minion_opts):
 
     pki_dir.joinpath("master", "minions", "minion").unlink()
 
@@ -1311,25 +1383,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,
@@ -1337,19 +1411,25 @@ 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.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=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)
+    auth_client = salt.channel.client.AsyncReqChannel.factory(
+        minion_opts, io_loop=io_loop, crypt="clear"
+    )
+    pload = auth_client._package_load(signin_payload)
     assert "version" in pload
-    assert pload["version"] == 2
+    assert pload["version"] == 3
 
     ret = server._auth(pload["load"], sign_messages=True)
     assert "sig" in ret
@@ -1357,29 +1437,31 @@ async def test_req_chan_auth_v2_new_minion_with_master_pub_bad_sig(pki_dir, io_l
         ret = client.auth.handle_signin_response(signin_payload, ret)
 
 
-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(pki_dir, io_loop, minion_opts, master_opts):
 
     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,
@@ -1387,19 +1469,25 @@ 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.event = salt.utils.event.get_master_event(
+        master_opts, master_opts["sock_dir"], listen=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)
+    auth_client = salt.channel.client.AsyncReqChannel.factory(
+        minion_opts, io_loop=io_loop, crypt="clear"
+    )
     signin_payload = client.auth.minion_sign_in_payload()
-    pload = client._package_load(signin_payload)
+    pload = auth_client._package_load(signin_payload)
     assert "version" in pload
-    assert pload["version"] == 2
+    assert pload["version"] == 3
 
     ret = server._auth(pload["load"], sign_messages=True)
     assert "sig" in ret
@@ -1436,13 +1524,14 @@ async def test_req_server_garbage_request(io_loop):
     request_server.stream.send.assert_called_once_with(valid_response)
 
 
-async def test_req_chan_bad_payload_to_decode(pki_dir, io_loop):
+async def test_req_chan_bad_payload_to_decode(pki_dir, io_loop, caplog):
     opts = {
         "master_uri": "tcp://127.0.0.1:4506",
         "interface": "127.0.0.1",
         "ret_port": 4506,
         "ipv6": False,
         "sock_dir": ".",
+        "cachedir": "",
         "pki_dir": str(pki_dir.joinpath("minion")),
         "id": "minion",
         "__role": "minion",
@@ -1467,9 +1556,16 @@ async def test_req_chan_bad_payload_to_decode(pki_dir, io_loop):
     master_opts["master_sign_pubkey"] = False
     server = salt.channel.server.ReqServerChannel.factory(master_opts)
 
-    with pytest.raises(salt.exceptions.SaltDeserializationError):
-        server._decode_payload(None)
-    with pytest.raises(salt.exceptions.SaltDeserializationError):
-        server._decode_payload({})
-    with pytest.raises(salt.exceptions.SaltDeserializationError):
-        server._decode_payload(12345)
+    with caplog.at_level(logging.WARNING):
+        await server.handle_message(None)
+        assert "bad load received on socket" in caplog.text
+    caplog.clear()
+
+    with caplog.at_level(logging.WARNING):
+        await server.handle_message({})
+        assert "bad load received on socket" in caplog.text
+    caplog.clear()
+
+    with caplog.at_level(logging.WARNING):
+        await server.handle_message(12345)
+        assert "bad load received on socket" in caplog.text
diff --git a/tests/pytests/unit/utils/test_gitfs.py b/tests/pytests/unit/utils/test_gitfs.py
index 3c4a85a856a..71c6c254b52 100644
--- a/tests/pytests/unit/utils/test_gitfs.py
+++ b/tests/pytests/unit/utils/test_gitfs.py
@@ -4,6 +4,7 @@ import time
 import pytest
 
 import salt.config
+import salt.exceptions
 import salt.fileserver.gitfs
 import salt.utils.gitfs
 from salt.exceptions import FileserverConfigError
@@ -260,3 +261,148 @@ def test_checkout_pygit2_with_home_env_unset(_prepare_provider):
 )
 def test_get_cachedir_basename_pygit2(_prepare_provider):
     assert "_" == _prepare_provider.get_cache_basename()
+
+
+def test_find_file(tmp_path):
+    opts = {
+        "cachedir": f"{tmp_path / 'cache'}",
+        "gitfs_user": "",
+        "gitfs_password": "",
+        "gitfs_pubkey": "",
+        "gitfs_privkey": "",
+        "gitfs_passphrase": "",
+        "gitfs_insecure_auth": False,
+        "gitfs_refspecs": salt.config._DFLT_REFSPECS,
+        "gitfs_ssl_verify": True,
+        "gitfs_branch": "master",
+        "gitfs_base": "master",
+        "gitfs_root": "",
+        "gitfs_env": "",
+        "gitfs_fallback": "",
+    }
+    remotes = []
+
+    gitfs = salt.utils.gitfs.GitFS(opts, remotes)
+    assert gitfs.find_file("asdf") == {"path": "", "rel": ""}
+
+
+def test_find_file_bad_path(tmp_path):
+    opts = {
+        "cachedir": f"{tmp_path / 'cache'}",
+        "gitfs_user": "",
+        "gitfs_password": "",
+        "gitfs_pubkey": "",
+        "gitfs_privkey": "",
+        "gitfs_passphrase": "",
+        "gitfs_insecure_auth": False,
+        "gitfs_refspecs": salt.config._DFLT_REFSPECS,
+        "gitfs_ssl_verify": True,
+        "gitfs_branch": "master",
+        "gitfs_base": "master",
+        "gitfs_root": "",
+        "gitfs_env": "",
+        "gitfs_fallback": "",
+    }
+    remotes = []
+
+    gitfs = salt.utils.gitfs.GitFS(opts, remotes)
+    with pytest.raises(salt.exceptions.SaltValidationError):
+        gitfs.find_file("sdf/../../../asdf")
+
+
+def test_find_file_bad_env(tmp_path):
+    opts = {
+        "cachedir": f"{tmp_path / 'cache'}",
+        "gitfs_user": "",
+        "gitfs_password": "",
+        "gitfs_pubkey": "",
+        "gitfs_privkey": "",
+        "gitfs_passphrase": "",
+        "gitfs_insecure_auth": False,
+        "gitfs_refspecs": salt.config._DFLT_REFSPECS,
+        "gitfs_ssl_verify": True,
+        "gitfs_branch": "master",
+        "gitfs_base": "master",
+        "gitfs_root": "",
+        "gitfs_env": "",
+        "gitfs_fallback": "",
+    }
+    remotes = []
+
+    gitfs = salt.utils.gitfs.GitFS(opts, remotes)
+    with pytest.raises(salt.exceptions.SaltValidationError):
+        gitfs.find_file("asdf", tgt_env="asd/../../../sdf")
+
+
+@pytest.mark.parametrize(
+    "remote,valid",
+    [
+        ("git@github.com:/saltstack/salt", True),
+        ("git@github.com:saltstack/salt", True),
+        ("git@github.com/saltstack/salt", False),
+        ("ssh://git@github.com/saltstack/salt.git", True),
+        ("ssh://git@github.com:22/saltstack/salt.git", True),
+        ("https://github.com/salttack/salt.git", True),
+        ("https://github.com/\nsaltstack/salt.git", False),
+        ("https://git:mypassword@github.com/saltstack/salt.git", True),
+        ("file:///srv/git/salt.git", True),
+    ],
+)
+def test_remote_validation(remote, valid):
+    assert salt.utils.gitfs.GitFS.validate_remote(remote) is valid
+
+
+@pytest.mark.parametrize(
+    "remote,result",
+    [
+        ("git@github.com:/saltstack/salt", "ssh://git@github.com/saltstack/salt"),
+        ("git@github.com:saltstack/salt", "ssh://git@github.com/saltstack/salt"),
+        (
+            "ssh://git@github.com/saltstack/salt.git",
+            "ssh://git@github.com/saltstack/salt.git",
+        ),
+        (
+            "ssh://git@github.com:22/saltstack/salt.git",
+            "ssh://git@github.com:22/saltstack/salt.git",
+        ),
+        (
+            "https://github.com/salttack/salt.git",
+            "https://github.com/salttack/salt.git",
+        ),
+        (
+            "https://git:mypassword@github.com/saltstack/salt.git",
+            "https://git:mypassword@github.com/saltstack/salt.git",
+        ),
+        ("file:///srv/git/salt.git", "file:///srv/git/salt.git"),
+    ],
+)
+def test_remote_to_url(remote, result):
+    assert salt.utils.gitfs.GitFS.remote_to_url(remote) == result
+
+
+def test_find_file_subdir(tmp_path):
+    root = tmp_path / "root"
+    root.mkdir()
+    (root / "refs").mkdir()
+    (root / "refs" / "base").mkdir()
+    opts = {
+        "cachedir": f"{tmp_path / 'cache'}",
+        "gitfs_user": "",
+        "gitfs_password": "",
+        "gitfs_pubkey": "",
+        "gitfs_privkey": "",
+        "gitfs_passphrase": "",
+        "gitfs_insecure_auth": False,
+        "gitfs_refspecs": salt.config._DFLT_REFSPECS,
+        "gitfs_ssl_verify": True,
+        "gitfs_branch": "master",
+        "gitfs_base": "master",
+        "gitfs_root": "",
+        "gitfs_env": "",
+        "gitfs_fallback": "",
+    }
+    remotes = []
+    gitfs = salt.utils.gitfs.GitFS(opts, remotes)
+    gitfs.cache_root = str(root)
+    ret = gitfs.find_file("foo/init.sls")
+    assert ret == {"path": "", "rel": ""}
diff --git a/tests/pytests/unit/utils/test_virt.py b/tests/pytests/unit/utils/test_virt.py
new file mode 100644
index 00000000000..7130d74cdad
--- /dev/null
+++ b/tests/pytests/unit/utils/test_virt.py
@@ -0,0 +1,21 @@
+import pytest
+
+import salt.exceptions
+import salt.utils.virt
+
+
+def test_virt_key(tmp_path):
+    opts = {"pki_dir": f"{tmp_path / 'pki'}"}
+    salt.utils.virt.VirtKey("asdf", "minion", opts)
+
+
+def test_virt_key_bad_hyper(tmp_path):
+    opts = {"pki_dir": f"{tmp_path / 'pki'}"}
+    with pytest.raises(salt.exceptions.SaltValidationError):
+        salt.utils.virt.VirtKey("asdf/../../../sdf", "minion", opts)
+
+
+def test_virt_key_bad_id_(tmp_path):
+    opts = {"pki_dir": f"{tmp_path / 'pki'}"}
+    with pytest.raises(salt.exceptions.SaltValidationError):
+        salt.utils.virt.VirtKey("hyper", "minion/../../", opts)
diff --git a/tests/pytests/unit/utils/verify/test_clean_path.py b/tests/pytests/unit/utils/verify/test_clean_path.py
new file mode 100644
index 00000000000..da2c6b74edb
--- /dev/null
+++ b/tests/pytests/unit/utils/verify/test_clean_path.py
@@ -0,0 +1,117 @@
+"""
+salt.utils.clean_path works as expected
+"""
+
+import ctypes
+import os
+
+import pytest
+
+import salt.utils.verify
+from tests.support.mock import patch
+
+
+class Symlink:
+    """
+    symlink(source, link_name) Creates a symbolic link pointing to source named
+    link_name
+    """
+
+    def __init__(self):
+        self._csl = None
+
+    def __call__(self, source, link_name):
+        if self._csl is None:
+            self._csl = ctypes.windll.kernel32.CreateSymbolicLinkW
+            self._csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
+            self._csl.restype = ctypes.c_ubyte
+        flags = 0
+        if source is not None and source.is_dir():
+            flags = 1
+
+        if self._csl(str(link_name), str(source), flags) == 0:
+            raise ctypes.WinError()
+
+
+@pytest.fixture(scope="module")
+def symlink():
+    return Symlink()
+
+
+@pytest.fixture
+def setup_links(tmp_path, symlink):
+    to_path = tmp_path / "linkto"
+    from_path = tmp_path / "linkfrom"
+    if salt.utils.platform.is_windows():
+        kwargs = {}
+    else:
+        kwargs = {"target_is_directory": True}
+    if salt.utils.platform.is_windows():
+        symlink(to_path, from_path, **kwargs)
+    else:
+        from_path.symlink_to(to_path, **kwargs)
+    return to_path, from_path
+
+
+def test_clean_path_symlinked_src(setup_links):
+    to_path, from_path = setup_links
+    test_path = from_path / "test"
+    expect_path = str(to_path / "test")
+    ret = salt.utils.verify.clean_path(str(from_path), str(test_path))
+    assert ret == expect_path, f"{ret} is not {expect_path}"
+
+
+def test_clean_path_symlinked_tgt(setup_links):
+    to_path, from_path = setup_links
+    test_path = to_path / "test"
+    expect_path = str(to_path / "test")
+    ret = salt.utils.verify.clean_path(str(from_path), str(test_path))
+    assert ret == expect_path, f"{ret} is not {expect_path}"
+
+
+def test_clean_path_symlinked_src_unresolved(setup_links):
+    to_path, from_path = setup_links
+    test_path = from_path / "test"
+    expect_path = str(from_path / "test")
+    ret = salt.utils.verify.clean_path(str(from_path), str(test_path), realpath=False)
+    assert ret == expect_path, f"{ret} is not {expect_path}"
+
+
+def test_clean_path_valid(tmp_path):
+    path_a = str(tmp_path / "foo")
+    path_b = str(tmp_path / "foo" / "bar")
+    assert salt.utils.verify.clean_path(path_a, path_b) == path_b
+
+
+def test_clean_path_invalid(tmp_path):
+    path_a = str(tmp_path / "foo")
+    path_b = str(tmp_path / "baz" / "bar")
+    assert salt.utils.verify.clean_path(path_a, path_b) == ""
+
+
+def test_clean_path_relative_root(tmp_path):
+    with patch("os.getcwd", return_value=str(tmp_path)):
+        path_a = "foo"
+        path_b = str(tmp_path / "foo" / "bar")
+        assert salt.utils.verify.clean_path(path_a, path_b) == path_b
+
+
+def test_clean_traverse_in_path_a(tmp_path):
+    path_a = str(tmp_path)
+    path_b = str(tmp_path / "foo" / ".." / "bar")
+    assert salt.utils.verify.clean_path(path_a, path_b) == os.path.normpath(path_b)
+
+
+def test_clean_traverse_in_path_b(tmp_path):
+    path_a = str(tmp_path)
+    path_b = str(tmp_path / "foo.foo/../bar")
+    assert salt.utils.verify.clean_path(path_a, path_b) == os.path.normpath(path_b)
+
+
+def test_clean_traverse_in_path_c(tmp_path):
+    path_a = str(tmp_path)
+    path_b = str(tmp_path / "foo/../bar/bang")
+    assert salt.utils.verify.clean_path(path_a, path_b) == ""
+    assert salt.utils.verify.clean_path(
+        path_a, path_b, subdir=True
+    ) == os.path.normpath(path_b)
diff --git a/tests/pytests/unit/utils/verify/test_url.py b/tests/pytests/unit/utils/verify/test_url.py
new file mode 100644
index 00000000000..7f5ad67c3be
--- /dev/null
+++ b/tests/pytests/unit/utils/verify/test_url.py
@@ -0,0 +1,44 @@
+import pytest
+
+import salt.utils.verify
+
+
+@pytest.mark.parametrize(
+    "data, result",
+    [
+        ("https://saltproject.io", True),
+        (
+            "https://mail.google.com/mail/u/0/#inbox/FMfcgzQbdrMwJwbwbPfCFLjMRQvWVcJK",
+            True,
+        ),
+        ("http://parts.org/foo/bar=/bat", True),
+        ("foobar://saltproject.io", False),
+        ("http://parts.org/foo/b\nar=/bat", False),
+        (
+            'base ssh://fake@git/repo\n[core]\nsshCommand = touch /tmp/pwn\n[remote "origin"]\n',
+            False,
+        ),
+        (
+            'ssh://fake@git/repo\n[core]\nsshCommand = touch /tmp/pwn\n[remote "origin"]\n',
+            False,
+        ),
+        ("https://github.com/saltstack/salt-test-pillar-gitfs.git", True),
+    ],
+)
+def test_url_validator(data, result):
+    assert salt.utils.verify.URLValidator()(data) is result
+
+
+@pytest.mark.parametrize(
+    "data, result",
+    [
+        ("asdf", True),
+        ("asdf-", True),
+        ("0123456789abcdefghijklmnopqrstuv-._~!$&'():@,", True),
+        ("0123456789ABCDEFGHIJKLMNOPQRSTUV-._~!$&'():@,", True),
+        ("abcd\\efg", False),
+    ],
+)
+def test_pchar_validator(data, result):
+    matcher = salt.utils.verify.URLValidator.pchar_matcher()
+    assert bool(matcher.match(data)) == result
diff --git a/tests/unit/test_master.py b/tests/unit/test_master.py
index 96fe2a54595..d5eb400cf84 100644
--- a/tests/unit/test_master.py
+++ b/tests/unit/test_master.py
@@ -49,6 +49,7 @@ class TransportMethodsTest(TestCase):
             "_AESFuncs__verify_load",
             "_AESFuncs__verify_minion",
             "_AESFuncs__verify_minion_publish",
+            "_handle_minion_event",
             "__class__",
             "__delattr__",
             "__dir__",
diff --git a/tests/unit/utils/test_gitfs.py b/tests/unit/utils/test_gitfs.py
index 259ea056fcd..6e99a219ca3 100644
--- a/tests/unit/utils/test_gitfs.py
+++ b/tests/unit/utils/test_gitfs.py
@@ -95,6 +95,7 @@ class TestGitBase(TestCase, AdaptedConfigurationTestCaseMixin):
             remote.fetched = False
         del self.main_class
         self._tmp_dir.cleanup()
+        _clear_instance_map()
 
     def test_update_all(self):
         self.main_class.update()
diff --git a/tools/pkg/build.py b/tools/pkg/build.py
index b3f92ef615c..80ab2381031 100644
--- a/tools/pkg/build.py
+++ b/tools/pkg/build.py
@@ -415,6 +415,10 @@ def onedir_dependencies(
             ]
         )
 
+    # Cryptography needs openssl dir set to link to the proper openssl libs.
+    if platform == "macos":
+        env["OPENSSL_DIR"] = f"{dest}"
+
     version_info = ctx.run(
         str(python_bin),
         "-c",
-- 
2.50.0

openSUSE Build Service is sponsored by