From cb385b94c2e130d88f8d233fb71766df5854e414 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 20 Aug 2026 23:57:12 +0000 Subject: [PATCH] fix(aks): align kubeconfig file permissions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/tools/kubectl/kube_config.go | 43 ++++++++-- cli/azd/pkg/tools/kubectl/kube_config_test.go | 79 ++++++++++++++++--- 2 files changed, 107 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/tools/kubectl/kube_config.go b/cli/azd/pkg/tools/kubectl/kube_config.go index a17ae4761e4..59a03ca5d26 100644 --- a/cli/azd/pkg/tools/kubectl/kube_config.go +++ b/cli/azd/pkg/tools/kubectl/kube_config.go @@ -50,14 +50,12 @@ func (kcm *KubeConfigManager) SaveKubeConfig(ctx context.Context, configName str return "", fmt.Errorf("failed marshalling KubeConfig to yaml: %w", err) } - // Create .kube config folder if it doesn't already exist - if err := os.MkdirAll(kcm.configPath, osutil.PermissionDirectory); err != nil { + if err := ensureKubeConfigDirectory(kcm.configPath); err != nil { return "", fmt.Errorf("failed creating .kube config directory, %w", err) } outFilePath := filepath.Join(kcm.configPath, configName) - err = os.WriteFile(outFilePath, kubeConfigRaw, osutil.PermissionFile) - if err != nil { + if err := writeKubeConfig(outFilePath, kubeConfigRaw); err != nil { return "", fmt.Errorf("failed writing kube config file: %w", err) } @@ -93,9 +91,12 @@ func (kcm *KubeConfigManager) MergeConfigs(ctx context.Context, newConfigName st } kubeConfigRaw := []byte(res.Stdout) + if err := ensureKubeConfigDirectory(kcm.configPath); err != nil { + return "", fmt.Errorf("failed securing .kube config directory, %w", err) + } + outFilePath := filepath.Join(kcm.configPath, newConfigName) - err = os.WriteFile(outFilePath, kubeConfigRaw, osutil.PermissionFile) - if err != nil { + if err := writeKubeConfig(outFilePath, kubeConfigRaw); err != nil { return "", fmt.Errorf("failed writing new kube config: %w", err) } @@ -123,3 +124,33 @@ func getKubeConfigDir() (string, error) { } return filepath.Join(userHomeDir, ".kube"), nil } + +func ensureKubeConfigDirectory(path string) error { + if err := os.MkdirAll(path, osutil.PermissionDirectoryOwnerOnly); err != nil { + return err + } + + return os.Chmod(path, osutil.PermissionDirectoryOwnerOnly) +} + +func writeKubeConfig(path string, contents []byte) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, osutil.PermissionFileOwnerOnly) + if err != nil { + return err + } + + if err := file.Chmod(osutil.PermissionFileOwnerOnly); err != nil { + _ = file.Close() + return err + } + if err := file.Truncate(0); err != nil { + _ = file.Close() + return err + } + if _, err := file.Write(contents); err != nil { + _ = file.Close() + return err + } + + return file.Close() +} diff --git a/cli/azd/pkg/tools/kubectl/kube_config_test.go b/cli/azd/pkg/tools/kubectl/kube_config_test.go index 9f43e1d3018..e102419f64c 100644 --- a/cli/azd/pkg/tools/kubectl/kube_config_test.go +++ b/cli/azd/pkg/tools/kubectl/kube_config_test.go @@ -5,11 +5,14 @@ package kubectl import ( "fmt" + "os" "path/filepath" + "runtime" "strings" "testing" "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/test/mocks" "github.com/stretchr/testify/require" ) @@ -25,20 +28,12 @@ func Test_MergeKubeConfig(t *testing.T) { cli := NewCli(mockContext.CommandRunner) kubeConfigManager, err := NewKubeConfigManager(cli) require.NoError(t, err) + kubeConfigManager.configPath = filepath.Join(t.TempDir(), ".kube") config1 := createTestCluster("cluster1", "user1") config2 := createTestCluster("cluster2", "user2") config3 := createTestCluster("cluster3", "user3") - defer func() { - err := kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config1") - require.NoError(t, err) - err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config2") - require.NoError(t, err) - err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config3") - require.NoError(t, err) - }() - kubeConfigPath, err := kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config1", config1) require.NoError(t, err) require.NotEmpty(t, kubeConfigPath) @@ -58,6 +53,72 @@ func Test_MergeKubeConfig(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, kubeConfigPath) require.Contains(t, kubeConfigPath, filepath.Join(".kube", "config")) + + if runtime.GOOS != "windows" { + requirePermissions(t, kubeConfigManager.configPath, osutil.PermissionDirectoryOwnerOnly) + requirePermissions(t, filepath.Join(kubeConfigManager.configPath, "config1"), osutil.PermissionFileOwnerOnly) + requirePermissions(t, filepath.Join(kubeConfigManager.configPath, "config2"), osutil.PermissionFileOwnerOnly) + requirePermissions(t, filepath.Join(kubeConfigManager.configPath, "config3"), osutil.PermissionFileOwnerOnly) + requirePermissions(t, kubeConfigPath, osutil.PermissionFileOwnerOnly) + } + + require.NoError(t, kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config1")) + _, err = os.Stat(filepath.Join(kubeConfigManager.configPath, "config1")) + require.ErrorIs(t, err, os.ErrNotExist) +} + +func Test_KubeConfigPermissions(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows does not support Unix permission bits") + } + + mockContext := mocks.NewMockContext(t.Context()) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl config view") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "apiVersion: v1\nusers:\n- name: user\n user:\n token: secret\n", ""), nil + }) + + configPath := filepath.Join(t.TempDir(), ".kube") + require.NoError(t, os.Mkdir(configPath, osutil.PermissionDirectory)) + require.NoError(t, os.Chmod(configPath, osutil.PermissionDirectory)) + + clusterConfigPath := filepath.Join(configPath, "cluster") + require.NoError(t, os.WriteFile(clusterConfigPath, []byte("old"), osutil.PermissionFile)) + require.NoError(t, os.Chmod(clusterConfigPath, osutil.PermissionFile)) + mergedConfigPath := filepath.Join(configPath, "config") + require.NoError(t, os.WriteFile(mergedConfigPath, []byte("old"), osutil.PermissionFile)) + require.NoError(t, os.Chmod(mergedConfigPath, osutil.PermissionFile)) + + manager := &KubeConfigManager{ + cli: NewCli(mockContext.CommandRunner), + configPath: configPath, + } + + savedPath, err := manager.SaveKubeConfig( + *mockContext.Context, + "cluster", + createTestCluster("cluster", "user"), + ) + require.NoError(t, err) + require.Equal(t, clusterConfigPath, savedPath) + + require.NoError(t, os.Chmod(configPath, osutil.PermissionDirectory)) + mergedPath, err := manager.MergeConfigs(*mockContext.Context, "config", "cluster") + require.NoError(t, err) + require.Equal(t, mergedConfigPath, mergedPath) + + requirePermissions(t, configPath, osutil.PermissionDirectoryOwnerOnly) + requirePermissions(t, clusterConfigPath, osutil.PermissionFileOwnerOnly) + requirePermissions(t, mergedConfigPath, osutil.PermissionFileOwnerOnly) +} + +func requirePermissions(t *testing.T, path string, expected os.FileMode) { + t.Helper() + + info, err := os.Stat(path) + require.NoError(t, err) + require.Equal(t, expected, info.Mode().Perm()) } func createTestCluster(clusterName, username string) *KubeConfig {