Skip to content

Commit 0cec313

Browse files
committed
new feature: refactor server-side PKI
the end goal of this work is to support a non file/disk based interface for pub key management. Using the existing salt.cache interface allows us to leverage existing implementations of other storage backends (ie mysql, postgres, redis, etc). Of note, this will allow a common shared view of pub keys for multi-master setups. To avoid a break-the-world scenario in this change, a fully backward compatible salt.cache.localfs_key_backcompat is provided as the new default that emulates the disk operations salt.key was doing before. open discussion items: - as is i've left master side pub/priv key as is to live in etc/salt/pki; it COULD be moved to the same interface if thats desired
1 parent 3da1f73 commit 0cec313

File tree

15 files changed

+989
-422
lines changed

15 files changed

+989
-422
lines changed

changelog/67799.added.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
refactored server-side PKI to support cache interface

doc/ref/cache/all/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ For understanding and usage of the cache modules see the :ref:`cache` topic.
1515
consul
1616
etcd_cache
1717
localfs
18+
localfs_key_backcompat
1819
mysql_cache
1920
redis_cache
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
salt.cache.localfs_key_backcompat
2+
=================================
3+
4+
.. automodule:: salt.cache.localfs_key_backcompat
5+
:members:

salt/cache/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ def __init__(self, opts, cachedir=None, **kwargs):
6262
self.cachedir = opts.get("cachedir", salt.syspaths.CACHE_DIR)
6363
else:
6464
self.cachedir = cachedir
65-
self.driver = opts.get("cache", salt.config.DEFAULT_MASTER_OPTS["cache"])
65+
self.driver = kwargs.get(
66+
"driver", opts.get("cache", salt.config.DEFAULT_MASTER_OPTS["cache"])
67+
)
6668
self._modules = None
6769
self._kwargs = kwargs
6870
self._kwargs["cachedir"] = self.cachedir

salt/cache/localfs_key_backcompat.py

+287
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
"""
2+
Backward compatible shim layer for pki interaction
3+
4+
.. versionadded:: 3008.0
5+
6+
The ``localfs_keys_backcompat`` is a shim driver meant to allow the salt.cache
7+
subsystem to interact with the existing master pki folder/file structure
8+
without any migration from previous versions of salt. It is not meant for
9+
general purpose use and should not be used outside of the master auth system.
10+
11+
The main difference from before is the 'state' of the key, ie accepted/rejected
12+
is now stored in the data itself, as opposed to the cache equivalent of a bank
13+
previously.
14+
15+
store and fetch handle ETL from new style, where data itself contains key
16+
state, to old style, where folder and/or bank contain state.
17+
flush/list/contains/updated are left as nearly equivalent to localfs, without
18+
the .p file extension to work with legacy keys via banks.
19+
"""
20+
21+
import errno
22+
import logging
23+
import os
24+
import os.path
25+
import shutil
26+
import tempfile
27+
28+
import salt.utils.atomicfile
29+
import salt.utils.files
30+
from salt.exceptions import SaltCacheError
31+
from salt.utils.verify import valid_id
32+
33+
log = logging.getLogger(__name__)
34+
35+
__func_alias__ = {"list_": "list"}
36+
37+
BASE_MAPPING = {
38+
"minions_pre": "pending",
39+
"minions_rejected": "rejected",
40+
"minions": "accepted",
41+
"minions_denied": "denied",
42+
}
43+
44+
45+
# we explicitly override cache dir to point to pki here
46+
def init_kwargs(kwargs):
47+
"""
48+
setup kwargs for cache functions
49+
"""
50+
if __opts__.get("cluster_id"):
51+
pki_dir = __opts__["cluster_pki_dir"]
52+
else:
53+
pki_dir = __opts__["pki_dir"]
54+
55+
return {"cachedir": pki_dir, "user": kwargs.get("user")}
56+
57+
58+
def store(bank, key, data, cachedir, user, **kwargs):
59+
"""
60+
Store key state information. storing a accepted/pending/rejected state
61+
means clearing it from the other 2. denied is handled separately
62+
"""
63+
if not valid_id(__opts__, key):
64+
raise SaltCacheError(f"key {key} is not a valid minion_id")
65+
66+
if bank not in ["keys", "denied_keys"]:
67+
raise SaltCacheError(f"Unrecognized bank: {bank}")
68+
69+
if bank == "keys":
70+
if data["state"] == "rejected":
71+
base = "minions_rejected"
72+
elif data["state"] == "pending":
73+
base = "minions_pre"
74+
elif data["state"] == "accepted":
75+
base = "minions"
76+
else:
77+
raise SaltCacheError("Unrecognized data/bank: {}".format(data["state"]))
78+
data = data["pub"]
79+
elif bank == "denied_keys":
80+
# denied keys is a list post migration, but is a single key in legacy
81+
data = data[0]
82+
base = "minions_denied"
83+
84+
base = os.path.join(cachedir, base)
85+
savefn = os.path.join(base, key)
86+
87+
try:
88+
os.makedirs(base)
89+
except OSError as exc:
90+
if exc.errno != errno.EEXIST:
91+
raise SaltCacheError(
92+
f"The cache directory, {base}, could not be created: {exc}"
93+
)
94+
95+
# delete current state before re-serializing new state
96+
flush(bank, key, cachedir, **kwargs)
97+
98+
if __opts__["permissive_pki_access"]:
99+
umask = 0o0700
100+
else:
101+
umask = 0o0750
102+
103+
tmpfh, tmpfname = tempfile.mkstemp(dir=base)
104+
os.close(tmpfh)
105+
106+
if user:
107+
try:
108+
import pwd
109+
110+
uid = pwd.getpwnam(user).pw_uid
111+
os.chown(tmpfname, uid, -1)
112+
except (KeyError, ImportError, OSError):
113+
# The specified user was not found, allow the backup systems to
114+
# report the error
115+
pass
116+
117+
try:
118+
with salt.utils.files.set_umask(umask):
119+
with salt.utils.files.fopen(tmpfname, "w+b") as fh_:
120+
fh_.write(data.encode("utf-8"))
121+
# On Windows, os.rename will fail if the destination file exists.
122+
salt.utils.atomicfile.atomic_rename(tmpfname, savefn)
123+
except OSError as exc:
124+
raise SaltCacheError(
125+
f"There was an error writing the cache file, {base}: {exc}"
126+
)
127+
128+
129+
def fetch(bank, key, cachedir, **kwargs):
130+
"""
131+
Fetch and construct state data for a given minion based on the bank and id
132+
"""
133+
if not valid_id(__opts__, key):
134+
raise SaltCacheError(f"key {key} is not a valid minion_id")
135+
136+
if bank not in ["keys", "denied_keys"]:
137+
raise SaltCacheError(f"Unrecognized bank: {bank}")
138+
139+
if key == ".key_cache":
140+
raise SaltCacheError("trying to read key_cache, there is a bug at call-site")
141+
try:
142+
if bank == "keys":
143+
for state, bank in [
144+
("rejected", "minions_rejected"),
145+
("pending", "minions_pre"),
146+
("accepted", "minions"),
147+
]:
148+
pubfn = os.path.join(cachedir, bank, key)
149+
if os.path.isfile(pubfn):
150+
with salt.utils.files.fopen(pubfn, "r") as fh_:
151+
return {"state": state, "pub": fh_.read()}
152+
return None
153+
elif bank == "denied_keys":
154+
# there can be many denied keys per minion post refactor, but only 1
155+
# with the filesystem, so return a list of 1
156+
pubfn_denied = os.path.join(cachedir, "minions_denied", key)
157+
158+
if os.path.isfile(pubfn_denied):
159+
with salt.utils.files.fopen(pubfn_denied, "r") as fh_:
160+
return [fh_.read()]
161+
else:
162+
raise SaltCacheError(f'unrecognized bank "{bank}"')
163+
except OSError as exc:
164+
raise SaltCacheError(
165+
'There was an error reading the cache bank "{}", key "{}": {}'.format(
166+
bank, key, exc
167+
)
168+
)
169+
170+
171+
def updated(bank, key, cachedir, **kwargs):
172+
"""
173+
Return the epoch of the mtime for this cache file
174+
"""
175+
if not valid_id(__opts__, key):
176+
raise SaltCacheError(f"key {key} is not a valid minion_id")
177+
178+
if bank == "keys":
179+
bases = [base for base in BASE_MAPPING if base != "minions_denied"]
180+
elif bank == "denied_keys":
181+
bases = ["minions_denied"]
182+
else:
183+
raise SaltCacheError(f"Unrecognized bank: {bank}")
184+
185+
for dir in bases:
186+
key_file = os.path.join(cachedir, dir, key)
187+
if os.path.isfile(key_file):
188+
try:
189+
return int(os.path.getmtime(key_file))
190+
except OSError as exc:
191+
raise SaltCacheError(
192+
'There was an error reading the mtime for "{}": {}'.format(
193+
key_file, exc
194+
)
195+
)
196+
log.debug('pki file "%s" does not exist in accepted/rejected/pending', key)
197+
return
198+
199+
200+
def flush(bank, key=None, cachedir=None, **kwargs):
201+
"""
202+
Remove the key from the cache bank with all the key content.
203+
flush can take a legacy bank or a keys/denied_keys modern bank
204+
"""
205+
if not valid_id(__opts__, key):
206+
raise SaltCacheError(f"key {key} is not a valid minion_id")
207+
208+
if cachedir is None:
209+
raise SaltCacheError("cachedir missing")
210+
211+
if bank == "keys":
212+
bases = [base for base in BASE_MAPPING if base != "minions_denied"]
213+
elif bank == "denied_keys":
214+
bases = ["minions_denied"]
215+
else:
216+
raise SaltCacheError(f"Unrecognized bank: {bank}")
217+
218+
flushed = False
219+
220+
for base in bases:
221+
try:
222+
if key is None:
223+
target = os.path.join(cachedir, base)
224+
if not os.path.isdir(target):
225+
return False
226+
shutil.rmtree(target)
227+
else:
228+
target = os.path.join(cachedir, base, key)
229+
if not os.path.isfile(target):
230+
continue
231+
os.remove(target)
232+
flushed = True
233+
except OSError as exc:
234+
raise SaltCacheError(f'There was an error removing "{target}": {exc}')
235+
return flushed
236+
237+
238+
def list_(bank, cachedir, **kwargs):
239+
"""
240+
Return an iterable object containing all entries stored in the specified bank.
241+
"""
242+
if bank == "keys":
243+
bases = [base for base in BASE_MAPPING if base != "minions_denied"]
244+
elif bank == "denied_keys":
245+
bases = ["minions_denied"]
246+
else:
247+
raise SaltCacheError(f"Unrecognized bank: {bank}")
248+
249+
ret = []
250+
for base in bases:
251+
base = os.path.join(cachedir, os.path.normpath(base))
252+
if not os.path.isdir(base):
253+
continue
254+
try:
255+
items = os.listdir(base)
256+
except OSError as exc:
257+
raise SaltCacheError(
258+
f'There was an error accessing directory "{base}": {exc}'
259+
)
260+
for item in items:
261+
# salt foolishly dumps a file here for key cache, ignore it
262+
if valid_id(__opts__, item):
263+
ret.append(item)
264+
else:
265+
log.error("saw invalid id %s, discarding", item)
266+
return ret
267+
268+
269+
def contains(bank, key, cachedir, **kwargs):
270+
"""
271+
Checks if the specified bank contains the specified key.
272+
"""
273+
if not valid_id(__opts__, key):
274+
raise SaltCacheError(f"key {key} is not a valid minion_id")
275+
276+
if bank == "keys":
277+
bases = [base for base in BASE_MAPPING if base != "minions_denied"]
278+
elif bank == "denied_keys":
279+
bases = ["minions_denied"]
280+
else:
281+
raise SaltCacheError(f"Unrecognized bank: {bank}")
282+
283+
for base in bases:
284+
keyfile = os.path.join(cachedir, base, key)
285+
if os.path.isfile(keyfile):
286+
return True
287+
return False

salt/channel/client.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import salt.transport.frame
1919
import salt.utils.event
2020
import salt.utils.files
21-
import salt.utils.minions
2221
import salt.utils.stringutils
2322
import salt.utils.verify
2423
import salt.utils.versions
@@ -204,7 +203,7 @@ def crypted_transfer_decode_dictentry(
204203
timeout,
205204
)
206205
key = self.auth.get_keys()
207-
if "key" not in ret:
206+
if not isinstance(ret, dict) or "key" not in ret:
208207
# Reauth in the case our key is deleted on the master side.
209208
yield self.auth.authenticate()
210209
ret = yield self._send_with_retry(
@@ -235,7 +234,7 @@ def crypted_transfer_decode_dictentry(
235234
raise tornado.gen.Return(data["pillar"])
236235

237236
def verify_signature(self, data, sig):
238-
return salt.crypt.PublicKey(self.master_pubkey_path).verify(
237+
return salt.crypt.PublicKey.from_file(self.master_pubkey_path).verify(
239238
data, sig, self.opts["signing_algorithm"]
240239
)
241240

0 commit comments

Comments
 (0)