Skip to content

Commit a946bc4

Browse files
authored
{AKS} az aks get-credentials: Harden kubeconfig write with symlink check and atomic replace (#33156)
1 parent 5dbd918 commit a946bc4

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

src/azure-cli/azure/cli/command_modules/acs/custom.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,11 +2050,32 @@ def merge_kubernetes_configurations(existing_file, addition_file, replace, conte
20502050
existing_file_perms,
20512051
)
20522052

2053+
# Refuse to write through a symlink
2054+
if os.path.islink(existing_file):
2055+
raise CLIError(
2056+
'Kubeconfig path "{}" is a symbolic link. '
2057+
'Refusing to write to prevent symlink-following attacks.'.format(existing_file)
2058+
)
2059+
2060+
# Atomic write: write to a temp file in the same directory, then replace
2061+
parent_dir = os.path.dirname(existing_file) or '.'
2062+
tmp_fd, tmp_path = tempfile.mkstemp(dir=parent_dir)
20532063
try:
2054-
with open(existing_file, 'w+') as stream:
2064+
with os.fdopen(tmp_fd, 'w') as stream:
20552065
yaml.safe_dump(existing, stream, default_flow_style=False)
2056-
except OSError as ex:
2057-
if getattr(ex, 'errno', 0) in (errno.EACCES, errno.EPERM, errno.EROFS):
2066+
# Preserve existing file permissions if available, otherwise default to 0600
2067+
if os.path.exists(existing_file):
2068+
existing_mode = stat.S_IMODE(os.stat(existing_file).st_mode)
2069+
os.chmod(tmp_path, existing_mode)
2070+
else:
2071+
os.chmod(tmp_path, 0o600)
2072+
os.replace(tmp_path, existing_file)
2073+
except Exception as ex: # pylint: disable=broad-except
2074+
try:
2075+
os.unlink(tmp_path)
2076+
except OSError:
2077+
pass
2078+
if isinstance(ex, OSError) and getattr(ex, 'errno', 0) in (errno.EACCES, errno.EPERM, errno.EROFS):
20582079
raise FileOperationError(
20592080
'Permission denied when trying to write to {}. '
20602081
'Please ensure you have write access to this file, or specify a different file path '

src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,41 @@ def test_merge_credentials_already_present(self):
484484
self.assertEqual(merged['users'], expected_users)
485485
self.assertEqual(merged['current-context'], obj2['current-context'])
486486

487+
@unittest.skipIf(os.name == 'nt', 'Symlink test not applicable on Windows')
488+
def test_merge_credentials_rejects_symlink(self):
489+
# Create a real kubeconfig file and a symlink pointing to it
490+
target = tempfile.NamedTemporaryFile(delete=False, suffix='.kubeconfig')
491+
target.close()
492+
with open(target.name, 'w') as f:
493+
yaml.safe_dump({'clusters': [], 'contexts': [], 'users': [],
494+
'current-context': '', 'kind': 'Config'}, f)
495+
self.addCleanup(os.remove, target.name)
496+
497+
symlink_path = target.name + '.link'
498+
os.symlink(target.name, symlink_path)
499+
self.addCleanup(lambda: os.remove(symlink_path) if os.path.islink(symlink_path) else None)
500+
501+
addition = tempfile.NamedTemporaryFile(delete=False)
502+
addition.close()
503+
obj = {
504+
'clusters': [{'cluster': {'server': 'https://test'}, 'name': 'c1'}],
505+
'contexts': [{'context': {'cluster': 'c1', 'user': 'u1'}, 'name': 'ctx1'}],
506+
'users': [{'name': 'u1', 'user': {'token': 'tok'}}],
507+
'current-context': 'ctx1',
508+
}
509+
with open(addition.name, 'w') as f:
510+
yaml.safe_dump(obj, f)
511+
self.addCleanup(os.remove, addition.name)
512+
513+
# Should raise CLIError when existing_file is a symlink
514+
with self.assertRaises(CLIError):
515+
merge_kubernetes_configurations(symlink_path, addition.name, False)
516+
517+
# Verify the symlink target was not modified
518+
with open(target.name, 'r') as f:
519+
content = yaml.safe_load(f)
520+
self.assertEqual(content['clusters'], [])
521+
487522
@mock.patch('azure.cli.command_modules.acs.addonconfiguration.get_rg_location', return_value='eastus')
488523
@mock.patch('azure.cli.command_modules.acs.addonconfiguration.get_resource_groups_client', autospec=True)
489524
@mock.patch('azure.cli.command_modules.acs.addonconfiguration.get_resources_client', autospec=True)

0 commit comments

Comments
 (0)