[BUG/FEATURE REQUEST/REGRESSION] Symlinks to minion keys no longer work
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 Are you willing to open a PR for this against 3006.x?
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.)
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.