diff --git a/pkg/cli/connect_helm.go b/pkg/cli/connect_helm.go index 3ca4e2d459..a273163302 100644 --- a/pkg/cli/connect_helm.go +++ b/pkg/cli/connect_helm.go @@ -193,6 +193,7 @@ func writeKubeConfig(kubeConfig *clientcmdapi.Config, vClusterName string, optio if err != nil { return err } + fixFileOwnershipUnderSudo(clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename()) log.Donef("Switched active kube context to %s", options.KubeConfigContextName) if !options.BackgroundProxy && portForwarding { @@ -227,6 +228,7 @@ func writeKubeConfig(kubeConfig *clientcmdapi.Config, vClusterName string, optio if err != nil { return fmt.Errorf("write kube config: %w", err) } + fixFileOwnershipUnderSudo(options.KubeConfig) log.Donef("Virtual cluster kube config written to: %s", options.KubeConfig) if options.Server == "" { diff --git a/pkg/cli/sudo.go b/pkg/cli/sudo.go new file mode 100644 index 0000000000..c868028b26 --- /dev/null +++ b/pkg/cli/sudo.go @@ -0,0 +1,55 @@ +package cli + +import ( + "os" + "os/user" + "path/filepath" + "strconv" + "strings" +) + +// fixFileOwnershipUnderSudo corrects ownership of a file and its parent directory +// when running under sudo. This handles the corner case where "sudo vcluster create" +// is run on a machine without an existing ~/.kube/config — the newly created file +// and directory are root-owned, making them unusable by the actual user. When the +// file already exists, overwriting preserves the original ownership (POSIX behavior), +// so this is a no-op in the common case. +// +// Only paths under the invoking user's home directory (resolved via os/user from +// SUDO_USER) are modified. System paths like /etc or /tmp are never touched. +func fixFileOwnershipUnderSudo(filePath string) { + filePath, _ = filepath.Abs(filePath) + + sudoUID := os.Getenv("SUDO_UID") + sudoGID := os.Getenv("SUDO_GID") + sudoUser := os.Getenv("SUDO_USER") + if sudoUID == "" || sudoGID == "" || sudoUser == "" { + return + } + + uid, err := strconv.Atoi(sudoUID) + if err != nil { + return + } + gid, err := strconv.Atoi(sudoGID) + if err != nil { + return + } + + // Resolve the real user's home from the system user database (passwd/LDAP/ + // directory services). This avoids hardcoding /home or /Users and handles + // non-standard home layouts. + u, err := user.Lookup(sudoUser) + if err != nil || u.HomeDir == "" { + return + } + + // Only fix ownership for paths under the user's home directory. + // Anything outside (e.g. /etc/kubernetes/admin.conf, /tmp) is left untouched. + if !strings.HasPrefix(filePath, u.HomeDir+string(os.PathSeparator)) { + return + } + + _ = os.Chown(filePath, uid, gid) + _ = os.Chown(filepath.Dir(filePath), uid, gid) +}