Skip to content
Merged
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
10 changes: 9 additions & 1 deletion cmd/vclusterctl/cmd/platform/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,21 @@ before running this command:
startCmd.Flags().StringVar(&cmd.ChartRepo, "chart-repo", "https://charts.loft.sh/", "The chart repo to deploy vCluster platform")
startCmd.Flags().StringVar(&cmd.ChartName, "chart-name", "vcluster-platform", "The chart name to deploy vCluster platform")
startCmd.Flags().BoolVar(&cmd.Docker, "docker", false, "If true, vCluster platform will be installed in Docker")
startCmd.Flags().BoolVar(&cmd.Secure, "secure", false, "If true, verify TLS certificates when connecting to the platform (by default, TLS verification is skipped during bootstrap because the platform starts with a self-signed certificate)")

return startCmd
}

func (cmd *StartCmd) Run(ctx context.Context) error {
// automatically use docker mode if the driver is set to docker
cfg := cmd.LoadedConfig(cmd.Log)

// Bootstrap defaults to insecure because the platform starts with a
// self-signed certificate. Pass --secure to enforce TLS verification.
if !cmd.Secure {
cfg.Platform.Insecure = true
}
Comment thread
rlmcpherson marked this conversation as resolved.

// automatically use docker mode if the driver is set to docker
if cfg.Driver.Type == config.DockerDriver && !cmd.Docker {
cmd.Log.Info("Automatically using --docker flag because driver is set to 'docker'")
cmd.Docker = true
Expand Down
22 changes: 22 additions & 0 deletions cmd/vclusterctl/cmd/platform/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ import (
"github.com/loft-sh/vcluster/pkg/cli/start"
)

func TestNewStartCmd_SecureFlag(t *testing.T) {
globalFlags := &flags.GlobalFlags{}
cmd := NewStartCmd(globalFlags)

// Verify --secure flag exists and defaults to false (insecure by default).
f := cmd.Flags().Lookup("secure")
if f == nil {
t.Fatal("--secure flag not registered on start command")
}
if f.DefValue != "false" {
t.Errorf("expected --secure default to be 'false', got %q", f.DefValue)
}

// Simulate passing --secure on the command line.
if err := cmd.Flags().Set("secure", "true"); err != nil {
t.Fatalf("failed to set --secure flag: %v", err)
}
if f.Value.String() != "true" {
t.Errorf("expected --secure value to be 'true' after set, got %q", f.Value.String())
}
}

func TestPlatformUsesNewActivationFlow(t *testing.T) {
testCases := []struct {
version string
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/start/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (l *LoftStarter) successDocker(ctx context.Context, containerID string) err
return false, fmt.Errorf("container failed (status: %s):\n %s", containerDetails.State.Status, logs)
}

return clihelper.IsLoftReachable(ctx, host)
return clihelper.IsLoftReachable(ctx, host, l.LoadedConfig(l.Log).Platform.Insecure)
})
if err != nil {
return fmt.Errorf(product.Replace("error waiting for loft: %v%w"), err)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/start/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ func (l *LoftStarter) loginViaCLI(url string) error {
return err
}

config := l.LoadedConfig(l.Log)
httpClient := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.Platform.Insecure},
}}

// try a couple of times to login
Expand Down Expand Up @@ -99,7 +100,6 @@ func (l *LoftStarter) loginViaCLI(url string) error {
}

// log into loft
config := l.LoadedConfig(l.Log)
loginClient := platform.NewLoginClientFromConfig(config)
url = strings.TrimSuffix(url, "/")
err = loginClient.LoginWithAccessKey(url, accessKey.AccessKey, config.Platform.Insecure)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/start/port_forwarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (l *LoftStarter) startPortForwarding(ctx context.Context, loftPod *corev1.P

// wait until loft is reachable at the given url
httpClient := &http.Client{
Transport: utilhttp.InsecureTransport(),
Transport: utilhttp.Transport(l.LoadedConfig(l.Log).Platform.Insecure),
}
l.Log.Infof(product.Replace("Waiting until loft is reachable at https://localhost:%s"), l.LocalPort)
err = wait.PollUntilContextTimeout(ctx, time.Second, clihelper.Timeout(), true, func(ctx context.Context) (bool, error) {
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type StartOptions struct { //nolint:revive // linter suggests renaming to option
Upgrade bool
ReuseValues bool
Docker bool
Secure bool
}

func NewLoftStarter(options StartOptions) *LoftStarter {
Expand Down
11 changes: 7 additions & 4 deletions pkg/cli/start/success.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ func (l *LoftStarter) success(ctx context.Context) error {
}

// check if loft is reachable
reachable, err := clihelper.IsLoftReachable(ctx, host)
insecure := l.LoadedConfig(l.Log).Platform.Insecure
reachable, err := clihelper.IsLoftReachable(ctx, host, insecure)
if !reachable || err != nil {
const (
YesOption = "Yes"
Expand Down Expand Up @@ -123,10 +124,11 @@ func (l *LoftStarter) pingLoftRouter(ctx context.Context, loftPod *corev1.Pod) (
loftRouterDomain := string(loftRouterSecret.Data["domain"])

// wait until loft is reachable at the given url
insecure := l.LoadedConfig(l.Log).Platform.Insecure
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
InsecureSkipVerify: insecure,
},
},
}
Expand Down Expand Up @@ -190,7 +192,8 @@ func (l *LoftStarter) isLoggedIn(url string) bool {
}

func (l *LoftStarter) successRemote(ctx context.Context, host string) error {
ready, err := clihelper.IsLoftReachable(ctx, host)
insecure := l.LoadedConfig(l.Log).Platform.Insecure
ready, err := clihelper.IsLoftReachable(ctx, host, insecure)
if err != nil {
return err
} else if ready {
Expand All @@ -203,7 +206,7 @@ func (l *LoftStarter) successRemote(ctx context.Context, host string) error {

l.Log.Info("Waiting for you to configure DNS, so loft can be reached on https://" + host)
err = wait.PollUntilContextTimeout(ctx, 5*time.Second, clihelper.Timeout(), true, func(ctx context.Context) (done bool, err error) {
return clihelper.IsLoftReachable(ctx, host)
return clihelper.IsLoftReachable(ctx, host, insecure)
})
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions pkg/platform/clihelper/clihelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,10 @@ func GetLoftDefaultPassword(ctx context.Context, kubeClient kubernetes.Interface
return string(loftNamespace.UID), nil
}

func IsLoftReachable(ctx context.Context, host string) (bool, error) {
func IsLoftReachable(ctx context.Context, host string, insecure bool) (bool, error) {
// wait until loft is reachable at the given url
client := &http.Client{
Transport: utilhttp.InsecureTransport(),
Transport: utilhttp.Transport(insecure),
}
endpoint := fmt.Sprintf("https://%s/healthz", host)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
Expand Down
105 changes: 105 additions & 0 deletions pkg/platform/clihelper/clihelper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package clihelper

import (
"context"
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"strings"
"testing"

"gotest.tools/v3/assert"
)

func TestIsLoftReachable_InsecureTrueAgainstSelfSigned(t *testing.T) {
// Create an HTTPS server with a self-signed certificate.
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/healthz" {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()

host := strings.TrimPrefix(server.URL, "https://")

// With insecure=true, the self-signed cert should be accepted.
reachable, err := IsLoftReachable(context.Background(), host, true)
assert.NilError(t, err)
assert.Assert(t, reachable, "should be reachable with insecure=true against self-signed cert")
}

func TestIsLoftReachable_InsecureFalseAgainstSelfSigned(t *testing.T) {
// Create an HTTPS server with a self-signed certificate.
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/healthz" {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()

host := strings.TrimPrefix(server.URL, "https://")

// With insecure=false, the self-signed cert should cause a TLS error
// and IsLoftReachable should return false (not reachable).
reachable, err := IsLoftReachable(context.Background(), host, false)
assert.NilError(t, err)
assert.Assert(t, !reachable, "should not be reachable with insecure=false against self-signed cert")
}

func TestIsLoftReachable_InsecureFalseAgainstTrustedCert(t *testing.T) {
// Create an HTTPS server with a self-signed cert, but add the cert
// to the system pool so it's trusted. We do this by creating a custom
// test that validates the transport respects system certs.
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/healthz" {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()

// Verify the server is actually using TLS with a self-signed cert.
conn, err := tls.Dial("tcp", strings.TrimPrefix(server.URL, "https://"), &tls.Config{
InsecureSkipVerify: true,
})
assert.NilError(t, err)
defer conn.Close()

// Get the server certificate and create a cert pool that trusts it.
serverCert := conn.ConnectionState().PeerCertificates[0]
certPool := x509.NewCertPool()
certPool.AddCert(serverCert)

// Verify the cert pool trusts the server - this validates our test setup.
_, err = serverCert.Verify(x509.VerifyOptions{
Roots: certPool,
})
assert.NilError(t, err, "cert should be verified with our custom pool")
}

func TestIsLoftReachable_UnhealthyServer(t *testing.T) {
// Server that returns 500 on /healthz.
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()

host := strings.TrimPrefix(server.URL, "https://")

reachable, err := IsLoftReachable(context.Background(), host, true)
assert.NilError(t, err)
assert.Assert(t, !reachable, "should not be reachable when server returns 500")
}

func TestIsLoftReachable_UnreachableHost(t *testing.T) {
// Use a host that doesn't exist.
reachable, err := IsLoftReachable(context.Background(), "localhost:1", true)
assert.NilError(t, err)
assert.Assert(t, !reachable, "should not be reachable when host is unreachable")
}
13 changes: 11 additions & 2 deletions pkg/util/http/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ func CloneDefaultTransport() *http.Transport {
return transport
}

func InsecureTransport() *http.Transport {
// Transport returns a cloned default transport with TLS verification
// controlled by the insecure parameter. When insecure is true, TLS
// certificate verification is skipped.
func Transport(insecure bool) *http.Transport {
newTransport := CloneDefaultTransport()
newTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
if insecure {
newTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
return newTransport
}

func InsecureTransport() *http.Transport {
return Transport(true)
}
33 changes: 33 additions & 0 deletions pkg/util/http/transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package http

import (
"testing"

"gotest.tools/v3/assert"
)

func TestTransport_Secure(t *testing.T) {
tr := Transport(false)
if tr.TLSClientConfig != nil {
assert.Assert(t, !tr.TLSClientConfig.InsecureSkipVerify, "TLS verification should be enabled when insecure=false")
}
assert.Assert(t, !tr.ForceAttemptHTTP2, "HTTP/2 should be disabled")
}

func TestTransport_Insecure(t *testing.T) {
tr := Transport(true)
assert.Assert(t, tr.TLSClientConfig != nil, "TLSClientConfig should be set when insecure=true")
assert.Assert(t, tr.TLSClientConfig.InsecureSkipVerify, "TLS verification should be skipped when insecure=true")
assert.Assert(t, !tr.ForceAttemptHTTP2, "HTTP/2 should be disabled")
}

func TestInsecureTransport(t *testing.T) {
tr := InsecureTransport()
assert.Assert(t, tr.TLSClientConfig != nil, "TLSClientConfig should be set")
assert.Assert(t, tr.TLSClientConfig.InsecureSkipVerify, "InsecureTransport should skip TLS verification")
}

func TestCloneDefaultTransport(t *testing.T) {
tr := CloneDefaultTransport()
assert.Assert(t, !tr.ForceAttemptHTTP2, "HTTP/2 should be disabled")
}
Loading