Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/cli/connect_helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 == "" {
Expand Down
55 changes: 55 additions & 0 deletions pkg/cli/sudo.go
Original file line number Diff line number Diff line change
@@ -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)) {
Comment thread
saiyam1814 marked this conversation as resolved.
return
}

_ = os.Chown(filePath, uid, gid)
_ = os.Chown(filepath.Dir(filePath), uid, gid)
Comment on lines +53 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve symlinks before chowning kubeconfig paths

This helper validates filePath with a string prefix check, then calls os.Chown on both the file and its parent. Because os.Chown follows symlinks, a path like ~/.kube -> /etc passes the home-directory guard but causes the ownership change to apply to the symlink targets outside the user home (including the parent directory target). Under sudo, this can unexpectedly re-own system paths and break the host. Resolve and validate real paths (or reject symlink components) before performing ownership changes.

Useful? React with 👍 / 👎.

}
Loading