Skip to content
Closed
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
69 changes: 59 additions & 10 deletions pkg/cli/standalone/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"text/template"

"github.com/loft-sh/log"
"github.com/loft-sh/vcluster/pkg/constants"
)

// AddToPlatformOptions holds the configuration for connecting a standalone vcluster to the vCluster Platform.
Expand All @@ -30,7 +32,7 @@ func AddToPlatform(ctx context.Context, log log.Logger, options *AddToPlatformOp
}

log.Info("Creating systemd vcluster service platform conf drop-in file")
if err := createPlatformConf(options); err != nil {
if err := createPlatformConf(log, options); err != nil {
return err
}

Expand Down Expand Up @@ -67,35 +69,82 @@ func preflightChecks() error {
return nil
}

// createPlatformConf writes the vCluster Platform configuration to a systemd drop-in file.
func createPlatformConf(options *AddToPlatformOptions) error {
// createPlatformConf writes the access key to a root-only env file and the remaining
// vCluster Platform configuration to a systemd drop-in file.
func createPlatformConf(log log.Logger, options *AddToPlatformOptions) error {
// check if vcluster service exists
if _, err := os.Stat("/etc/systemd/system/vcluster.service"); err != nil {
if _, err := os.Stat(constants.VClusterStandaloneSystemdUnitFile); err != nil {
return fmt.Errorf("vcluster service not found: %w", err)
}

warnOnAccessKeyInUnit(log)

if err := writePlatformEnvFile(constants.VClusterStandalonePlatformEnvFile, options); err != nil {
return err
}

// create systemd platform conf file
platformConfFileBytes, err := renderSystemdPlatformConfFile(options)
platformConfFileBytes, err := renderSystemdPlatformConfFile(constants.VClusterStandalonePlatformEnvFile, options)
if err != nil {
return fmt.Errorf("failed to render systemd vcluster platform conf file: %w", err)
}

if err := os.MkdirAll("/etc/systemd/system/vcluster.service.d", 0700); err != nil {
if err := os.MkdirAll(constants.VClusterStandaloneSystemdDropInDir, 0700); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}

if err := os.WriteFile("/etc/systemd/system/vcluster.service.d/platform.conf", platformConfFileBytes, 0600); err != nil {
if err := os.WriteFile(constants.VClusterStandalonePlatformDropInFile, platformConfFileBytes, 0600); err != nil {
return fmt.Errorf("failed to write systemd service file: %w", err)
}

return nil
}

// writePlatformEnvFile writes the access key to a root-only file. It must not be a
// systemd Environment= directive: systemd serves those to any local user over D-Bus.
func writePlatformEnvFile(envPath string, options *AddToPlatformOptions) error {
// only root reads this directory; Chmod because MkdirAll leaves an existing one alone
secretsDir := filepath.Dir(envPath)
if err := os.MkdirAll(secretsDir, 0700); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
if err := os.Chmod(secretsDir, 0700); err != nil {
return fmt.Errorf("failed to chmod %s: %w", secretsDir, err)
}

// remove first so the mode below applies even if the file already exists
if err := os.Remove(envPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove %s: %w", envPath, err)
}

if err := os.WriteFile(envPath, renderPlatformEnvFile(options), 0600); err != nil {
return fmt.Errorf("failed to write %s: %w", envPath, err)
}

return nil
}

// renderPlatformEnvFile renders the systemd environment file holding the access key.
func renderPlatformEnvFile(options *AddToPlatformOptions) []byte {
return fmt.Appendf(nil, "%s=%s\n", constants.PlatformAccessKeyEnv, options.AccessKey)
}

// warnOnAccessKeyInUnit reports a key left inline by an installer predating the env file.
// The drop-in overrides it, but it stays readable until the unit is rewritten.
func warnOnAccessKeyInUnit(log log.Logger) {
unit, err := os.ReadFile(constants.VClusterStandaloneSystemdUnitFile)
if err != nil || !bytes.Contains(unit, []byte(constants.PlatformAccessKeyEnv)) {
return
}

log.Warnf("%s is still set inline in %s, where any local user can read it. Re-run install-standalone.sh, or remove that line and run 'systemctl daemon-reload'. Rotate the access key that was exposed.", constants.PlatformAccessKeyEnv, constants.VClusterStandaloneSystemdUnitFile)
}

// renderSystemdPlatformConfFile renders the systemd environment variables for the vCluster Platform connection.
func renderSystemdPlatformConfFile(options *AddToPlatformOptions) ([]byte, error) {
func renderSystemdPlatformConfFile(envPath string, options *AddToPlatformOptions) ([]byte, error) {
const platformConfTemplateText = `
[Service]
Environment=LOFT_PLATFORM_ACCESS_KEY="{{.options.AccessKey}}"
EnvironmentFile=-{{.envPath}}
Environment=LOFT_PLATFORM_HOST="{{.options.Host}}"
Environment=LOFT_PLATFORM_INSECURE="{{.options.Insecure}}"
Environment=LOFT_PLATFORM_INSTANCE_NAME="{{.options.InstanceName}}"
Expand All @@ -109,7 +158,7 @@ Environment=LOFT_PLATFORM_SKIP_CONFIG_SYNC="{{.options.SkipConfigSync}}"
}

buf := new(bytes.Buffer)
if err := serviceTemplate.Execute(buf, map[string]any{"options": options}); err != nil {
if err := serviceTemplate.Execute(buf, map[string]any{"options": options, "envPath": envPath}); err != nil {
return nil, fmt.Errorf("failed to render systemd service file: %w", err)
}

Expand Down
88 changes: 86 additions & 2 deletions pkg/cli/standalone/platform_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package standalone

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/loft-sh/vcluster/pkg/constants"
)

func TestRenderSystemdPlatformConfFile(t *testing.T) {
Expand All @@ -16,15 +20,15 @@ func TestRenderSystemdPlatformConfFile(t *testing.T) {

want := `
[Service]
Environment=LOFT_PLATFORM_ACCESS_KEY="abcd"
EnvironmentFile=-/etc/vcluster/secrets/platform.env
Environment=LOFT_PLATFORM_HOST="test.vcluster.platform"
Environment=LOFT_PLATFORM_INSECURE="false"
Environment=LOFT_PLATFORM_INSTANCE_NAME="test-instance"
Environment=LOFT_PLATFORM_PROJECT_NAME="test-project"
Environment=LOFT_PLATFORM_SKIP_CONFIG_SYNC="false"
`

got, err := renderSystemdPlatformConfFile(options)
got, err := renderSystemdPlatformConfFile(constants.VClusterStandalonePlatformEnvFile, options)
if err != nil {
t.Errorf("renderSystemdServiceFile() error = %v", err)
return
Expand All @@ -34,4 +38,84 @@ Environment=LOFT_PLATFORM_SKIP_CONFIG_SYNC="false"
if gotString != want {
t.Errorf("renderSystemdServiceFile() diff(want, got) = %s", cmp.Diff(want, gotString))
}

// the drop-in is world-readable in effect: systemd exposes Environment= values
// over D-Bus to any local user, so the access key must not appear in it
if strings.Contains(gotString, options.AccessKey) {
t.Errorf("renderSystemdServiceFile() leaked the access key into the drop-in: %s", gotString)
}

// the template spells the variable names out, so a renamed constant would silently
// stop matching what the drop-in actually sets
for _, name := range []string{
constants.PlatformHostEnv,
constants.PlatformInsecureEnv,
constants.PlatformInstanceNameEnv,
constants.PlatformProjectNameEnv,
constants.PlatformSkipConfigSyncEnv,
} {
if !strings.Contains(gotString, "Environment="+name+"=") {
t.Errorf("renderSystemdServiceFile() does not set %s: %s", name, gotString)
}
}
}

func TestWritePlatformEnvFile(t *testing.T) {
t.Parallel()

dir := filepath.Join(t.TempDir(), "secrets")
envPath := filepath.Join(dir, "platform.env")

// pre-create the file world-readable: an install from a release that predates
// the env file may have left one behind, and writing over it must still end up
// root-only
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
if err := os.WriteFile(envPath, []byte("LOFT_PLATFORM_ACCESS_KEY=stale\n"), 0644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}

if err := writePlatformEnvFile(envPath, &AddToPlatformOptions{AccessKey: "abcd"}); err != nil {
t.Fatalf("writePlatformEnvFile() error = %v", err)
}

fileInfo, err := os.Stat(envPath)
if err != nil {
t.Fatalf("Stat(%s) error = %v", envPath, err)
}
if got := fileInfo.Mode().Perm(); got != 0600 {
t.Errorf("%s mode = %#o, want 0600", envPath, got)
}

// the secrets directory ends up root-only even when it was found world-readable
dirInfo, err := os.Stat(dir)
if err != nil {
t.Fatalf("Stat(%s) error = %v", dir, err)
}
if got := dirInfo.Mode().Perm(); got != 0700 {
t.Errorf("%s mode = %#o, want 0700", dir, got)
}

content, err := os.ReadFile(envPath)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", envPath, err)
}
if want := "LOFT_PLATFORM_ACCESS_KEY=abcd\n"; string(content) != want {
t.Errorf("%s content diff(want, got) = %s", envPath, cmp.Diff(want, string(content)))
}
}

func TestRenderPlatformEnvFile(t *testing.T) {
options := &AddToPlatformOptions{
AccessKey: "abcd",
Host: "test.vcluster.platform",
}

want := "LOFT_PLATFORM_ACCESS_KEY=abcd\n"

got := string(renderPlatformEnvFile(options))
if got != want {
t.Errorf("renderPlatformEnvFile() diff(want, got) = %s", cmp.Diff(want, got))
}
}
11 changes: 11 additions & 0 deletions pkg/constants/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ package constants

const LoftChartRepo = "https://charts.loft.sh"

// Platform connection variables. Here rather than in vcluster-pro because the OSS CLI
// writes them too, from `vcluster platform add standalone`.
const (
PlatformHostEnv = "LOFT_PLATFORM_HOST"
PlatformInstanceNameEnv = "LOFT_PLATFORM_INSTANCE_NAME"
PlatformProjectNameEnv = "LOFT_PLATFORM_PROJECT_NAME"
PlatformInsecureEnv = "LOFT_PLATFORM_INSECURE"
PlatformAccessKeyEnv = "LOFT_PLATFORM_ACCESS_KEY"
PlatformSkipConfigSyncEnv = "LOFT_PLATFORM_SKIP_CONFIG_SYNC"
)

const (
VClusterFolder = ".vcluster"
ConfigFileName = "config.json"
Expand Down
45 changes: 45 additions & 0 deletions pkg/constants/standalone.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,49 @@
const (
VClusterStandaloneEndpointsAnnotation = "vcluster.loft.sh/standalone-endpoints"
VClusterStandaloneIPAddressEnvVar = "VCLUSTER_STANDALONE_IP_ADDRESS"
<<<<<<< ours

Check failure on line 6 in pkg/constants/standalone.go

View workflow job for this annotation

GitHub Actions / Execute all go tests

syntax error: unexpected <<, expected name

Check failure on line 6 in pkg/constants/standalone.go

View workflow job for this annotation

GitHub Actions / lint

expected 'IDENT', found '<<' (typecheck)

Check failure on line 6 in pkg/constants/standalone.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: unexpected <<, expected name (typecheck)
=======
VClusterStandaloneDefaultName = "standalone"

// Standalone has no host-cluster namespace, so snapshot/restore request
// ConfigMaps and Secrets live in the virtual cluster's own kube-system.
VClusterStandaloneSnapshotNamespace = "kube-system"

// VClusterStandaloneSystemdServiceName is the name of the systemd service name.
VClusterStandaloneSystemdServiceName = "vcluster"

// VClusterStandaloneSystemdUnitFile is the systemd unit file created by the standalone installer.
// Its presence on disk should indicate we are running on a standalone vCluster host.
VClusterStandaloneSystemdUnitFile = "/etc/systemd/system/" + VClusterStandaloneSystemdServiceName + ".service"

// VClusterStandaloneSystemdDropInDir holds unit overrides. The installer drops it on
// --reset-only, the CLI writes the drop-in below into it.
VClusterStandaloneSystemdDropInDir = VClusterStandaloneSystemdUnitFile + ".d"

// VClusterStandalonePlatformDropInFile is written by `vcluster platform add standalone`.
VClusterStandalonePlatformDropInFile = VClusterStandaloneSystemdDropInDir + "/platform.conf"

// VClusterStandaloneDefaultDataDir is the default standalone data directory used by
// binary installations on the host.
VClusterStandaloneDefaultDataDir = "/var/lib/vcluster"

// VClusterStandaloneConfigDir holds the host's configuration.
VClusterStandaloneConfigDir = "/etc/vcluster"

// VClusterStandaloneDefaultConfigPath is the config location for a standalone binary installation.
// Kept outside the data directory so it survives a data wipe or re-install.
VClusterStandaloneDefaultConfigPath = VClusterStandaloneConfigDir + "/vcluster.yaml"

// VClusterStandaloneSecretsDir is root-only, which is what lets the files below be
// named after the scope they cover rather than the one value they hold today.
VClusterStandaloneSecretsDir = VClusterStandaloneConfigDir + "/secrets"

// Secrets the unit loads with EnvironmentFile= rather than Environment=, which systemd
// serves to any local user over D-Bus.
VClusterStandalonePlatformEnvFile = VClusterStandaloneSecretsDir + "/platform.env"
VClusterStandaloneJoinEnvFile = VClusterStandaloneSecretsDir + "/join.env"

// StandaloneRuntimeMetadataFileName stores persisted standalone runtime metadata in the data directory.
StandaloneRuntimeMetadataFileName = "standalone-runtime-metadata"
>>>>>>> theirs
)

Check failure on line 51 in pkg/constants/standalone.go

View workflow job for this annotation

GitHub Actions / lint

expected ')', found 'EOF' (typecheck)
Loading