Skip to content

Commit dc02f0c

Browse files
authored
TPT-4221: Enhance security of kubeconfig file permission (linode#919)
1 parent 41f8182 commit dc02f0c

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

linodecli/plugins/get-kubeconfig.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import argparse
1010
import base64
11+
import os
1112
import sys
1213
from pathlib import Path
1314

@@ -19,6 +20,11 @@
1920

2021
PLUGIN_BASE = "linode-cli get-kubeconfig"
2122

23+
# Kubeconfigs contain credentials, so they should only be
24+
# accessible by the user that created them.
25+
KUBECONFIG_FILE_MODE = 0o600
26+
KUBECONFIG_DIR_MODE = 0o700
27+
2228

2329
def call(args, context):
2430
"""
@@ -147,8 +153,27 @@ def _load_config(filepath):
147153

148154
# Dumps data to a yaml file
149155
def _dump_config(filepath, data):
150-
Path.mkdir(filepath.parent, exist_ok=True)
151-
with open(filepath, "w", encoding="utf-8") as file_descriptor:
156+
filepath.parent.mkdir(mode=KUBECONFIG_DIR_MODE, parents=True, exist_ok=True)
157+
158+
# Create the file with restrictive permissions rather than chmod-ing it
159+
# afterwards, so its contents are never briefly readable by other users.
160+
# NOTE: The mode is only applied when the file is created.
161+
def opener(path, flags):
162+
return os.open(path, flags, mode=KUBECONFIG_FILE_MODE)
163+
164+
with open(
165+
filepath, "w", encoding="utf-8", opener=opener
166+
) as file_descriptor:
167+
# Tighten the permissions of pre-existing files that are readable or
168+
# writable by users other than the owner.
169+
# NOTE: os.fchmod is not available on Windows, where POSIX file modes
170+
# are not meaningful anyway.
171+
if (
172+
hasattr(os, "fchmod")
173+
and os.fstat(file_descriptor.fileno()).st_mode & 0o077
174+
):
175+
os.fchmod(file_descriptor.fileno(), KUBECONFIG_FILE_MODE)
176+
152177
yaml.dump(data, file_descriptor)
153178

154179

tests/unit/test_plugin_kubeconfig.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,64 @@ def test_merge(mock_cli, fake_kubeconfig_file):
204204
assert result["dictionary"] == yaml_a["dictionary"]
205205

206206

207+
# Ensure newly created kubeconfig files are not world/group-readable
208+
@pytest.mark.skipif(
209+
os.name == "nt", reason="POSIX file modes are not supported on Windows"
210+
)
211+
def test_written_config_permissions(mock_cli):
212+
mock_cli.call_operation = mock_call_operation
213+
214+
with tempfile.TemporaryDirectory() as temp_dir:
215+
file_path = os.path.join(temp_dir, "new_dir", "nested", "config")
216+
217+
try:
218+
plugin.call(
219+
[
220+
"--label",
221+
"nonempty_data",
222+
"--kubeconfig",
223+
file_path,
224+
],
225+
PluginContext("REALTOKEN", mock_cli),
226+
)
227+
except SystemExit as err:
228+
assert err.code == 0
229+
230+
assert os.path.exists(file_path)
231+
assert os.stat(file_path).st_mode & 0o777 == 0o600
232+
assert os.stat(os.path.dirname(file_path)).st_mode & 0o777 == 0o700
233+
234+
235+
# Ensure pre-existing world-readable kubeconfig files get tightened
236+
@pytest.mark.skipif(
237+
os.name == "nt", reason="POSIX file modes are not supported on Windows"
238+
)
239+
def test_existing_config_permissions_tightened(mock_cli):
240+
mock_cli.call_operation = mock_call_operation
241+
242+
with tempfile.TemporaryDirectory() as temp_dir:
243+
file_path = os.path.join(temp_dir, "config")
244+
245+
with open(file_path, "w", encoding="utf-8") as file:
246+
file.write(TEST_YAML_CONTENT_A)
247+
os.chmod(file_path, 0o644)
248+
249+
try:
250+
plugin.call(
251+
[
252+
"--label",
253+
"nonempty_data",
254+
"--kubeconfig",
255+
file_path,
256+
],
257+
PluginContext("REALTOKEN", mock_cli),
258+
)
259+
except SystemExit as err:
260+
assert err.code == 0
261+
262+
assert os.stat(file_path).st_mode & 0o777 == 0o600
263+
264+
207265
def test_merge_to_empty_config(mock_cli, fake_kubeconfig_file_without_entries):
208266
stdout_buf = io.StringIO()
209267
mock_cli.call_operation = mock_call_operation

0 commit comments

Comments
 (0)