Skip to content

Commit afdb17b

Browse files
authored
Merge pull request #65228 from dwoz/issue/master/65226
[master] Fix cluster key rotation
2 parents aa20365 + f582cb2 commit afdb17b

17 files changed

Lines changed: 333 additions & 188 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ on:
1616

1717
env:
1818
COLUMNS: 190
19-
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
19+
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
2020
RELENV_DATA: "${{ github.workspace }}/.relenv"
2121

2222
permissions:

.github/workflows/nightly.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ on:
2222

2323
env:
2424
COLUMNS: 190
25-
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
25+
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
2626
RELENV_DATA: "${{ github.workspace }}/.relenv"
2727

2828
permissions:

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ on:
2121

2222
env:
2323
COLUMNS: 190
24-
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
24+
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
2525
RELENV_DATA: "${{ github.workspace }}/.relenv"
2626

2727
permissions:

.github/workflows/scheduled.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212

1313
env:
1414
COLUMNS: 190
15-
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
15+
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
1616
RELENV_DATA: "${{ github.workspace }}/.relenv"
1717

1818
permissions:

.github/workflows/staging.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ on:
3737

3838
env:
3939
COLUMNS: 190
40-
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
40+
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
4141
RELENV_DATA: "${{ github.workspace }}/.relenv"
4242

4343
permissions:

.github/workflows/templates/layout.yml.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ on:
3434

3535
env:
3636
COLUMNS: 190
37-
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
37+
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
3838
RELENV_DATA: "${{ github.workspace }}/.relenv"
3939

4040
<%- endblock env %>

salt/channel/server.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,9 +1045,7 @@ async def handle_pool_publish(self, payload, _):
10451045
"""
10461046
try:
10471047
tag, data = salt.utils.event.SaltEvent.unpack(payload)
1048-
log.error("recieved event from peer %s %r", tag, data)
10491048
if tag.startswith("cluster/peer"):
1050-
log.error("Got peer join %r", data)
10511049
peer = data["peer_id"]
10521050
aes = data["peers"][self.opts["id"]]["aes"]
10531051
sig = data["peers"][self.opts["id"]]["sig"]

salt/crypt.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import random
1717
import stat
1818
import sys
19+
import tempfile
1920
import time
2021
import traceback
2122
import uuid
@@ -1620,19 +1621,22 @@ def generate_key_string(cls, key_size=192, **kwargs):
16201621
return b64key.replace("\n", "")
16211622

16221623
@classmethod
1623-
def read_or_generate_key(cls, path, key_size=192, remove=False):
1624-
if remove:
1625-
os.remove(path)
1624+
def write_key(cls, path, key_size=192):
1625+
directory = pathlib.Path(path).parent
16261626
with salt.utils.files.set_umask(0o177):
1627-
try:
1628-
with salt.utils.files.fopen(path, "r") as fp:
1629-
return fp.read()
1630-
except FileNotFoundError:
1631-
pass
1632-
key = cls.generate_key_string(key_size)
1633-
with salt.utils.files.fopen(path, "w") as fp:
1634-
fp.write(key)
1635-
return key
1627+
fd, tmp = tempfile.mkstemp(dir=directory, prefix="aes")
1628+
os.close(fd)
1629+
with salt.utils.files.fopen(tmp, "w") as fp:
1630+
fp.write(cls.generate_key_string(key_size))
1631+
os.rename(tmp, path)
1632+
1633+
@classmethod
1634+
def read_key(cls, path):
1635+
try:
1636+
with salt.utils.files.fopen(path, "r") as fp:
1637+
return fp.read()
1638+
except FileNotFoundError:
1639+
pass
16361640

16371641
@classmethod
16381642
def extract_keys(cls, key_string, key_size):

salt/master.py

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import collections
77
import copy
88
import ctypes
9-
import functools
109
import logging
1110
import multiprocessing
1211
import os
@@ -142,7 +141,6 @@ def get_serial(cls, opts=None, event=None):
142141
def rotate_secrets(
143142
cls, opts=None, event=None, use_lock=True, owner=False, publisher=None
144143
):
145-
log.info("Rotating master AES key")
146144
if opts is None:
147145
opts = {}
148146

@@ -173,6 +171,41 @@ def rotate_secrets(
173171
log.debug("Pinging all connected minions due to key rotation")
174172
salt.utils.master.ping_all_connected_minions(opts)
175173

174+
@classmethod
175+
def rotate_cluster_secret(
176+
cls, opts=None, event=None, use_lock=True, owner=False, publisher=None
177+
):
178+
log.debug("Rotating cluster AES key")
179+
if opts is None:
180+
opts = {}
181+
182+
if use_lock:
183+
with cls.secrets["cluster_aes"]["secret"].get_lock():
184+
cls.secrets["cluster_aes"][
185+
"secret"
186+
].value = salt.utils.stringutils.to_bytes(
187+
cls.secrets["cluster_aes"]["reload"](remove=owner)
188+
)
189+
else:
190+
cls.secrets["cluster_aes"][
191+
"secret"
192+
].value = salt.utils.stringutils.to_bytes(
193+
cls.secrets["cluster_aes"]["reload"](remove=owner)
194+
)
195+
196+
if event:
197+
event.fire_event(
198+
{f"rotate_cluster_aes_key": True}, tag="rotate_cluster_aes_key"
199+
)
200+
201+
if publisher:
202+
publisher.send_aes_key_event()
203+
204+
if opts.get("ping_on_rotate"):
205+
# Ping all minions to get them to pick up the new key
206+
log.debug("Pinging all connected minions due to key rotation")
207+
salt.utils.master.ping_all_connected_minions(opts)
208+
176209

177210
class Maintenance(salt.utils.process.SignalHandlingProcess):
178211
"""
@@ -358,7 +391,7 @@ def handle_key_rotate(self, now, drop_file_wait=5):
358391

359392
if to_rotate:
360393
if self.opts.get("cluster_id", None):
361-
SMaster.rotate_secrets(
394+
SMaster.rotate_cluster_secret(
362395
self.opts, self.event, owner=True, publisher=self.ipc_publisher
363396
)
364397
else:
@@ -714,6 +747,20 @@ def _pre_flight(self):
714747
log.critical("Master failed pre flight checks, exiting\n")
715748
sys.exit(salt.defaults.exitcodes.EX_GENERIC)
716749

750+
def read_or_generate_key(self, remove=False, fs_wait=0.1):
751+
"""
752+
Used to manage a cluster aes session key file.
753+
"""
754+
path = os.path.join(self.opts["cluster_pki_dir"], ".aes")
755+
if remove:
756+
os.remove(path)
757+
key = salt.crypt.Crypticle.read_key(path)
758+
if key:
759+
return key
760+
salt.crypt.Crypticle.write_key(path)
761+
time.sleep(fs_wait)
762+
return salt.crypt.Crypticle.read_key(path)
763+
717764
def start(self):
718765
"""
719766
Turn on the master server components
@@ -731,22 +778,18 @@ def start(self):
731778
# signal handlers
732779
with salt.utils.process.default_signals(signal.SIGINT, signal.SIGTERM):
733780
if self.opts["cluster_id"]:
734-
keypath = os.path.join(self.opts["cluster_pki_dir"], ".aes")
735-
cluster_keygen = functools.partial(
736-
salt.crypt.Crypticle.read_or_generate_key,
737-
keypath,
738-
)
739781
# Setup the secrets here because the PubServerChannel may need
740782
# them as well.
741783
SMaster.secrets["cluster_aes"] = {
742784
"secret": multiprocessing.Array(
743-
ctypes.c_char, salt.utils.stringutils.to_bytes(cluster_keygen())
785+
ctypes.c_char,
786+
salt.utils.stringutils.to_bytes(self.read_or_generate_key()),
744787
),
745788
"serial": multiprocessing.Value(
746789
ctypes.c_longlong,
747790
lock=False, # We'll use the lock from 'secret'
748791
),
749-
"reload": cluster_keygen,
792+
"reload": self.read_or_generate_key,
750793
}
751794

752795
SMaster.secrets["aes"] = {
@@ -779,7 +822,7 @@ def start(self):
779822
ipc_publisher.pre_fork(self.process_manager)
780823
self.process_manager.add_process(
781824
EventMonitor,
782-
args=[self.opts],
825+
args=[self.opts, ipc_publisher],
783826
name="EventMonitor",
784827
)
785828

@@ -908,19 +951,19 @@ class EventMonitor(salt.utils.process.SignalHandlingProcess):
908951
- Handle key rotate events.
909952
"""
910953

911-
def __init__(self, opts, channels=None, name="EventMonitor"):
954+
def __init__(self, opts, ipc_publisher, channels=None, name="EventMonitor"):
912955
super().__init__(name=name)
913956
self.opts = opts
914957
if channels is None:
915958
channels = []
916959
self.channels = channels
960+
self.ipc_publisher = ipc_publisher
917961

918962
async def handle_event(self, package):
919963
"""
920964
Event handler for publish forwarder
921965
"""
922966
tag, data = salt.utils.event.SaltEvent.unpack(package)
923-
log.debug("Event monitor got event %s %r", tag, data)
924967
if tag.startswith("salt/job") and tag.endswith("/publish"):
925968
peer_id = data.pop("__peer_id", None)
926969
if peer_id:
@@ -937,9 +980,15 @@ async def handle_event(self, package):
937980
for chan in self.channels:
938981
tasks.append(asyncio.create_task(chan.publish(data)))
939982
await asyncio.gather(*tasks)
940-
elif tag == "rotate_aes_key":
941-
log.debug("Event monitor recieved rotate aes key event, rotating key.")
942-
SMaster.rotate_secrets(self.opts, owner=False)
983+
elif tag == "rotate_cluster_aes_key":
984+
peer_id = data.pop("__peer_id", None)
985+
if peer_id:
986+
log.debug("Rotating AES session key")
987+
SMaster.rotate_cluster_secret(
988+
self.opts, owner=False, publisher=self.ipc_publisher
989+
)
990+
else:
991+
log.trace("Ignore tag %s", tag)
943992

944993
def run(self):
945994
io_loop = tornado.ioloop.IOLoop()

tests/pytests/integration/cluster/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)