Skip to content

Commit 73b0823

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 73b0823

File tree

9 files changed

+705
-402
lines changed

9 files changed

+705
-402
lines changed

salt/cache/localfs_key_backcompat.py

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