Skip to content

Commit

Permalink
Use "kubectl proxy" instead of a NodePort to expose the dashboard.
Browse files Browse the repository at this point in the history
This provides an additional level of security, by enforcing host checking, applying port randomization, and requiring explicit user intent to expose the service to the host.
  • Loading branch information
tstromberg committed Oct 3, 2018
1 parent 8e99e28 commit df54c6a
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 24 deletions.
67 changes: 53 additions & 14 deletions cmd/minikube/cmd/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ limitations under the License.
package cmd

import (
"bufio"
"fmt"
"os"
"text/template"
"os/exec"
"regexp"
"time"

"github.com/golang/glog"
"github.com/pkg/browser"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/minikube/pkg/minikube/cluster"
"k8s.io/minikube/pkg/minikube/machine"
Expand All @@ -34,6 +37,8 @@ import (

var (
dashboardURLMode bool
// Matches: 127.0.0.1:8001
hostPortRe = regexp.MustCompile(`127.0.0.1:\d{4,}`)
)

// dashboardCmd represents the dashboard command
Expand All @@ -42,6 +47,7 @@ var dashboardCmd = &cobra.Command{
Short: "Opens/displays the kubernetes dashboard URL for your local cluster",
Long: `Opens/displays the kubernetes dashboard URL for your local cluster`,
Run: func(cmd *cobra.Command, args []string) {
glog.Infof("Setting up dashboard ...")
api, err := machine.NewAPIClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting client: %s\n", err)
Expand All @@ -58,26 +64,59 @@ var dashboardCmd = &cobra.Command{
os.Exit(1)
}

urls, err := service.GetServiceURLsForService(api, namespace, svc, template.Must(template.New("dashboardServiceFormat").Parse(defaultServiceFormatTemplate)))
p, hostPort, err := kubectlProxy()
if err != nil {
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, "Check that minikube is running.")
os.Exit(1)
}
if len(urls) == 0 {
errMsg := "There appears to be no url associated with dashboard, this is not expected, exiting"
glog.Infoln(errMsg)
os.Exit(1)
glog.Fatalf("kubectl proxy: %v", err)
}
url := dashboardURL(hostPort, namespace, svc)
if dashboardURLMode {
fmt.Fprintln(os.Stdout, urls[0])
} else {
fmt.Fprintln(os.Stdout, "Opening kubernetes dashboard in default browser...")
browser.OpenURL(urls[0])
fmt.Fprintln(os.Stdout, url)
return
}
fmt.Fprintln(os.Stdout, fmt.Sprintf("Opening %s in your default browser...", url))
browser.OpenURL(url)
p.Wait()
},
}

// kubectlProxy runs "kubectl proxy", returning host:port
func kubectlProxy() (*exec.Cmd, string, error) {
glog.Infof("Searching for kubectl ...")
path, err := exec.LookPath("kubectl")
if err != nil {
return nil, "", errors.Wrap(err, "Unable to find kubectl in PATH")
}
cmd := exec.Command(path, "proxy", "--port=0")
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return nil, "", errors.Wrap(err, "stdout")
}

glog.Infof("Executing: %s %s", cmd.Path, cmd.Args)
if err := cmd.Start(); err != nil {
return nil, "", errors.Wrap(err, "start")
}
glog.Infof("proxy should be running ...")
reader := bufio.NewReader(stdoutPipe)
glog.Infof("Reading stdout pipe ...")
out, err := reader.ReadString('\n')
if err != nil {
return nil, "", errors.Wrap(err, "read")
}
return cmd, parseHostPort(out), nil
}

func dashboardURL(proxy string, ns string, svc string) string {
// Reference: https://github.com/kubernetes/dashboard/wiki/Accessing-Dashboard---1.7.X-and-above
return fmt.Sprintf("http://%s/api/v1/namespaces/%s/services/http:%s:/proxy/", proxy, ns, svc)
}

func parseHostPort(out string) string {
// Starting to serve on 127.0.0.1:8001
glog.Infof("Parsing: %s ...", out)
return hostPortRe.FindString(out)
}

func init() {
dashboardCmd.Flags().BoolVar(&dashboardURLMode, "url", false, "Display the kubernetes dashboard in the CLI instead of opening it in the default browser")
RootCmd.AddCommand(dashboardCmd)
Expand Down
8 changes: 3 additions & 5 deletions deploy/addons/dashboard/dashboard-svc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ metadata:
kubernetes.io/minikube-addons: dashboard
kubernetes.io/minikube-addons-endpoint: dashboard
spec:
type: NodePort
ports:
- port: 80
targetPort: 9090
nodePort: 30000
- port: 80
targetPort: 9090
selector:
app: kubernetes-dashboard
k8s-app: kubernetes-dashboard
15 changes: 12 additions & 3 deletions pkg/minikube/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/docker/machine/libmachine"

"github.com/pkg/browser"
"github.com/pkg/errors"
"k8s.io/api/core/v1"
Expand All @@ -35,6 +36,7 @@ import (

"text/template"

"github.com/golang/glog"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/labels"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
Expand Down Expand Up @@ -197,27 +199,34 @@ func CheckService(namespace string, service string) error {
return errors.Wrap(err, "Error getting kubernetes client")
}
services := client.Services(namespace)
glog.Infof("services: %+v", services)
err = validateService(services, service)
if err != nil {
return errors.Wrap(err, "Error validating service")
}
// Add logic here to switch between needing external endpoints or not.
endpoints := client.Endpoints(namespace)
return checkEndpointReady(endpoints, service)
glog.Infof("%s:%s endpoints: %+v", namespace, service, endpoints)
return nil
// return checkEndpointReady(endpoints, service)
}

func validateService(s corev1.ServiceInterface, service string) error {
if _, err := s.Get(service, metav1.GetOptions{}); err != nil {
svc, err := s.Get(service, metav1.GetOptions{})
if err != nil {
return errors.Wrapf(err, "Error getting service %s", service)
}
glog.Infof("%s: %+v", service, svc)
return nil
}

func checkEndpointReady(endpoints corev1.EndpointsInterface, service string) error {
endpoint, err := endpoints.Get(service, metav1.GetOptions{})
glog.Infof("%s endpoint: %+v", service, endpoint)
if err != nil {
return &util.RetriableError{Err: errors.Errorf("Error getting endpoints for service %s", service)}
}
const notReadyMsg = "Waiting, endpoint for service is not ready yet...\n"
notReadyMsg := fmt.Sprintf("Waiting, endpoint for %s is not ready yet...\n", service)
if len(endpoint.Subsets) == 0 {
fmt.Fprintf(os.Stderr, notReadyMsg)
return &util.RetriableError{Err: errors.New("Endpoint for service is not ready yet")}
Expand Down
7 changes: 5 additions & 2 deletions test/integration/addons_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,15 @@ func testDashboard(t *testing.T) {
if u.Scheme != "http" {
t.Fatalf("wrong scheme in dashboard URL, expected http, actual %s", u.Scheme)
}
_, port, err := net.SplitHostPort(u.Host)
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
t.Fatalf("failed to split dashboard host %s: %v", u.Host, err)
}
if port != "30000" {
t.Fatalf("Dashboard is exposed on wrong port, expected 30000, actual %s", port)
t.Errorf("Dashboard is exposed on wrong port, expected 30000, actual %s", port)
}
if host != "127.0.0.1" {
t.Errorf("host is %s, expected 127.0.0.1", host)
}
}

Expand Down

0 comments on commit df54c6a

Please sign in to comment.