Skip to content

Commit c97c671

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 fc36d87 commit c97c671

File tree

9 files changed

+707
-406
lines changed

9 files changed

+707
-406
lines changed

salt/cache/localfs_key_backcompat.py

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"""
2+
Backward compatible shim layer for pki interaction
3+
4+
.. versionadded:: xxx
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+
import salt.utils.verify
31+
from salt.exceptions import SaltCacheError
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+
given the kwarg inputs give the mapping for each cache func call
49+
"""
50+
if __opts__["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+
# All possible removefns, once we determine the state we will remove current state
64+
# Unless the state is denied, then we remove all of these states
65+
removefns = {"minions", "minions_pre", "minions_rejected"}
66+
67+
if bank == "keys":
68+
if data["state"] == "rejected":
69+
base = "minions_rejected"
70+
elif data["state"] == "pending":
71+
base = "minions_pre"
72+
elif data["state"] == "accepted":
73+
base = "minions"
74+
else:
75+
raise SaltCacheError("Unrecognized data/bank: {}".format(data["state"]))
76+
data = data["pub"]
77+
removefns.remove(base)
78+
elif bank == "denied_keys":
79+
# denied keys is a list post migration, but is a single key in legacy
80+
data = data[0]
81+
base = "minions_denied"
82+
removefns = {}
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+
if __opts__["permissive_pki_access"]:
96+
umask = 0o0700
97+
mod = 0o0440
98+
else:
99+
umask = 0o0750
100+
mod = 0o0400
101+
102+
tmpfh, tmpfname = tempfile.mkstemp(dir=base)
103+
os.close(tmpfh)
104+
105+
if user:
106+
try:
107+
import pwd
108+
109+
uid = pwd.getpwnam(user).pw_uid
110+
os.chown(tmpfname, uid, -1)
111+
except (KeyError, ImportError, OSError):
112+
# The specified user was not found, allow the backup systems to
113+
# report the error
114+
pass
115+
116+
try:
117+
with salt.utils.files.set_umask(umask):
118+
with salt.utils.files.fopen(tmpfname, "w+b") as fh_:
119+
fh_.write(data.encode("utf-8"))
120+
# On Windows, os.rename will fail if the destination file exists.
121+
salt.utils.atomicfile.atomic_rename(tmpfname, savefn)
122+
except OSError as exc:
123+
raise SaltCacheError(
124+
f"There was an error writing the cache file, {base}: {exc}"
125+
)
126+
127+
# legacy dirs are banks, keys are ids
128+
for bank in removefns:
129+
flush(bank, key, cachedir, **kwargs)
130+
131+
132+
def fetch(bank, key, cachedir, **kwargs):
133+
"""
134+
Fetch and construct state data for a given minion based on the bank and id
135+
"""
136+
if key == ".key_cache":
137+
raise SaltCacheError("trying to read key_cache, there is a bug at call-site")
138+
try:
139+
if bank == "keys":
140+
for state, bank in [
141+
("rejected", "minions_rejected"),
142+
("pending", "minions_pre"),
143+
("accepted", "minions"),
144+
]:
145+
pubfn = os.path.join(cachedir, bank, key)
146+
if os.path.isfile(pubfn):
147+
with salt.utils.files.fopen(pubfn, "r") as fh_:
148+
return {"state": state, "pub": fh_.read()}
149+
return None
150+
elif bank == "denied_keys":
151+
# there can be many denied keys per minion post refactor, but only 1
152+
# with the filesystem, so return a list of 1
153+
pubfn_denied = os.path.join(cachedir, "minions_denied", key)
154+
155+
if os.path.isfile(pubfn_denied):
156+
with salt.utils.files.fopen(pubfn_denied, "r") as fh_:
157+
return [fh_.read()]
158+
else:
159+
raise SaltCacheError(f'unrecognized bank "{bank}"')
160+
except OSError as exc:
161+
raise SaltCacheError(
162+
'There was an error reading the cache bank "{}", key "{}": {}'.format(
163+
bank, key, exc
164+
)
165+
)
166+
167+
168+
def updated(bank, key, cachedir, **kwargs):
169+
"""
170+
Return the epoch of the mtime for this cache file
171+
"""
172+
key_file = os.path.join(cachedir, os.path.normpath(bank), key)
173+
if not os.path.isfile(key_file):
174+
log.warning('Cache file "%s" does not exist', key_file)
175+
return None
176+
try:
177+
return int(os.path.getmtime(key_file))
178+
except OSError as exc:
179+
raise SaltCacheError(
180+
f'There was an error reading the mtime for "{key_file}": {exc}'
181+
)
182+
183+
184+
def flush(bank, key=None, cachedir=None, **kwargs):
185+
"""
186+
Remove the key from the cache bank with all the key content.
187+
flush can take a legacy bank or a keys/denied_keys modern bank
188+
"""
189+
if cachedir is None:
190+
raise SaltCacheError("cachedir missing")
191+
192+
flushed = False
193+
194+
if bank not in BASE_MAPPING:
195+
if bank == "keys":
196+
bases = [base for base in BASE_MAPPING if base != "minions_denied"]
197+
elif bank == "denied_keys":
198+
bases = ["minions_denied"]
199+
else:
200+
bases = [bank]
201+
202+
for base in bases:
203+
try:
204+
if key is None:
205+
target = os.path.join(cachedir, base)
206+
if not os.path.isdir(target):
207+
return False
208+
shutil.rmtree(target)
209+
else:
210+
target = os.path.join(cachedir, base, key)
211+
if not os.path.isfile(target):
212+
continue
213+
os.remove(target)
214+
flushed = True
215+
except OSError as exc:
216+
raise SaltCacheError(f'There was an error removing "{target}": {exc}')
217+
return flushed
218+
219+
220+
def list_(bank, cachedir, **kwargs):
221+
"""
222+
Return an iterable object containing all entries stored in the specified bank.
223+
"""
224+
225+
if bank != "keys" and bank != "denied_keys" and bank not in BASE_MAPPING:
226+
raise SaltCacheError(f'Invalid bank "{bank}"')
227+
228+
if bank not in BASE_MAPPING:
229+
if bank == "keys":
230+
bases = [base for base in BASE_MAPPING if base != "minions_denied"]
231+
elif bank == "denied_keys":
232+
bases = ["minions_denied"]
233+
else:
234+
bases = [bank]
235+
236+
ret = []
237+
for base in bases:
238+
base = os.path.join(cachedir, os.path.normpath(base))
239+
if not os.path.isdir(base):
240+
continue
241+
try:
242+
items = os.listdir(base)
243+
except OSError as exc:
244+
raise SaltCacheError(
245+
f'There was an error accessing directory "{base}": {exc}'
246+
)
247+
for item in items:
248+
# salt foolishly dumps a file here, ignore it
249+
if salt.utils.verify.valid_id(__opts__, item):
250+
ret.append(item)
251+
else:
252+
log.error("saw invalid id %s, discarding", item)
253+
return ret
254+
255+
256+
def contains(bank, key, cachedir, **kwargs):
257+
"""
258+
Checks if the specified bank contains the specified key.
259+
"""
260+
if key is None:
261+
base = os.path.join(cachedir, os.path.normpath(bank))
262+
return os.path.isdir(base)
263+
else:
264+
keyfile = os.path.join(cachedir, os.path.normpath(bank), key)
265+
return os.path.isfile(keyfile)

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(path=self.master_pubkey_path).verify(
239238
data, sig, self.opts["signing_algorithm"]
240239
)
241240

0 commit comments

Comments
 (0)