salt icon indicating copy to clipboard operation
salt copied to clipboard

[BUG/FEATURE REQUEST/REGRESSION] Symlinks to minion keys no longer work

Open dehnert opened this issue 5 months ago • 3 comments

Description Up until salt 3007.4 (#68033), /etc/salt/pki/master/ could contain symlinks, allowing some of the master's keys to be stored outside the main config hierarchy. It would be helpful to allow this again.

I take advantage of this to store my accepted minion keys in git, which makes it easier to stand up new Salt masters. However, I don't see much of a need to store the unaccepted, denied, etc. keys in git, since those are more ephemeral -- using symlinks lets me have some of the key types in git and others not.

I suspect that 3007.4 stopped following symlinks to prevent symlink traversal attacks, but in this case, I don't think there's a risk of that, and it could be reenabled: salt isn't accessing attacker-controlled locations. Instead, /etc/salt/pki/master/ (the pki_dir) is only written by the sysadmin (who can presumably be trusted not to create bad symlinks) and Salt (which presumably isn't creating symlinks) -- there should be no way for a problematic symlink to be created, so I think the check can be safely loosened.

Setup (Please provide relevant configs and/or SLS files (be sure to remove sensitive info. There is no general set-up of Salt.)

Please be as specific as possible and give set-up details.

I think the relevant setup here is just:

# ll  /etc/salt/pki/master/minions
lrwxrwxrwx 1 salt salt 42 Jun  8  2024 /etc/salt/pki/master/minions -> ../../../../srv/salt/pki/minions.accepted/
  • [ ] on-prem machine
  • [x] VM (Virtualbox, KVM, etc. please specify) - KVM
  • [ ] VM running on a cloud service, please be explicit and add details
  • [ ] container (Kubernetes, Docker, containerd, etc. please specify)
  • [ ] or a combination, please be explicit
  • [ ] jails if it is FreeBSD
  • [ ] classic packaging
  • [x] onedir packaging
  • [ ] used bootstrap to install

Steps to Reproduce the behavior (Include debug logs if possible and relevant)

Include a symlink from under /etc/salt/pki/master/ to elsewhere (e.g. /srv/salt).

The master logs [WARNING ] Invalid minion id: wieliczka. The minion logs [ERROR ] Error while bringing up minion for multi-master. Is master at salt.dehnerts.com responding? The error message was Exception getting pillar. and TypeError: string indices must be integers (which suggests that the error reporting from the master to the minion in this case is not especially clean).

Expected behavior

Minions are recognized as usual.

Versions Report

salt --versions-report (Provided by running salt --versions-report. Please also mention any differences in master/minion versions.)
Salt Version:
          Salt: 3007.6
 
Python Version:
        Python: 3.10.17 (main, Jul  2 2025, 22:34:57) [GCC 11.2.0]
 
Dependency Versions:
          cffi: 1.16.0
      cherrypy: unknown
  cryptography: 42.0.5
      dateutil: 2.8.2
     docker-py: Not Installed
         gitdb: Not Installed
     gitpython: Not Installed
        Jinja2: 3.1.6
       libgit2: Not Installed
  looseversion: 1.3.0
      M2Crypto: Not Installed
          Mako: Not Installed
       msgpack: 1.0.7
  msgpack-pure: Not Installed
  mysql-python: Not Installed
     packaging: 24.0
     pycparser: 2.21
      pycrypto: Not Installed
  pycryptodome: 3.19.1
        pygit2: Not Installed
  python-gnupg: 0.5.2
        PyYAML: 6.0.1
         PyZMQ: 25.1.2
        relenv: 0.19.4
         smmap: Not Installed
       timelib: 0.3.0
       Tornado: 6.4.2
           ZMQ: 4.3.4
 
Salt Package Information:
  Package Type: onedir
 
System Versions:
          dist: ubuntu 24.04.2 noble
        locale: utf-8
       machine: x86_64
       release: 6.8.0-62-generic
        system: Linux
       version: Ubuntu 24.04.2 noble

Additional context

As far as I can tell, the issue is that in a couple places, clean_join is called with realpath left as its default True, when it could (I think) safely be set to False. subdir needs to be True (to avoid path traversal), but since the pki_dir isn't attacker-controlled I don't think realpath needs to be True.

On my machine, the following seems to fix the issue:

# diff -ur -x*.pyc /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6{,.patched}
diff -ur '-x*.pyc' /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6/channel/server.py /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6.patched/channel/server.py
--- /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6/channel/server.py      2025-07-11 22:29:43.776466555 +0000
+++ /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6.patched/channel/server.py      2025-07-11 22:47:07.486998588 +0000
@@ -395,7 +395,7 @@
             else:
                 pki_dir = self.opts.get("pki_dir", "")
             try:
-                pub_path = salt.utils.verify.clean_join(pki_dir, "minions", id_)
+                pub_path = salt.utils.verify.clean_join(pki_dir, "minions", id_, realpath=False)
             except salt.exceptions.SaltValidationError:
                 log.warning("Invalid minion id: %s", id_)
                 return False
diff -ur '-x*.pyc' /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6/master.py /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6.patched/master.py
--- /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6/master.py      2025-07-11 22:29:43.932469649 +0000
+++ /opt/saltstack/salt/lib/python3.10/site-packages/salt.3007.6.patched/master.py      2025-07-11 22:47:07.443997745 +0000
@@ -1444,7 +1444,7 @@
         """
         if not salt.utils.verify.valid_id(self.opts, id_):
             return False
-        pub_path = salt.utils.verify.clean_join(self.pki_dir, "minions", id_)
+        pub_path = salt.utils.verify.clean_join(self.pki_dir, "minions", id_, realpath=False)
         try:
             pub = salt.crypt.PublicKey(pub_path)
         except OSError:
@@ -1827,7 +1827,7 @@
             log.trace("Verifying signed event publish from minion")
             sig = load.pop("sig")
             this_minion_pubkey = salt.utils.verify.clean_join(
-                self.pki_dir, "minions", load["id"]
+                self.pki_dir, "minions", load["id"], realpath=False,
             )
             serialized_load = salt.serializers.msgpack.serialize(load)
             if not salt.crypt.verify_signature(

If loosening this check up again is acceptable, I can probably submit a PR.

dehnert avatar Jul 11 '25 22:07 dehnert

@dehnert Are you willing to open a PR for this against 3006.x?

dwoz avatar Jul 30 '25 22:07 dwoz

Probably. I expect it'll take me at least a couple days to find the time. (Sorry, I saw you assigned this to yourself so I didn't bother trying to put together a PR.)

dehnert avatar Jul 31 '25 05:07 dehnert

Probably. I expect it'll take me at least a couple days to find the time. (Sorry, I saw you assigned this to yourself so I didn't bother trying to put together a PR.)

No worries, thank you.

dwoz avatar Aug 01 '25 10:08 dwoz