From bd8d47226d0b3c6ddd04cf7e86d1f9f80dd250ec Mon Sep 17 00:00:00 2001 From: Alejandro Pedraza Date: Thu, 31 Oct 2019 11:51:25 -0500 Subject: [PATCH] DNS rebinding protection for the dashboard (#3644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * DNS rebinding protection for the dashboard Fixes #3083 and replacement for #3629 This adds a new parameter to the `linkerd-web` container `enforcedHost` that establishes the regexp that the Host header must enforce, otherwise it returns an error. This parameter will be hard-coded for now, in `linkerd-web`'s deployment yaml. Note this also protects the dashboard because that's proxied from `linkerd-web`. Also note this means the usage of `linkerd dashboard --address` will require the user to change that parameter in the deployment yaml (or have Kustomize do it). How to test: - Run `linkerd dashboard` - Go to http://rebind.it:8080/manager.html and change the target port to 50750 - Click on “Start Attack” and wait for a minute. - The response from the dashboard will be returned, showing an 'Invalid Host header' message returned by the dashboard. If the attack would have succeeded then the dashboard's html would be shown instead. Signed-off-by: Alejandro Pedraza --- charts/linkerd2/templates/web.yaml | 2 ++ cli/cmd/testdata/install_control-plane.golden | 1 + cli/cmd/testdata/install_default.golden | 1 + cli/cmd/testdata/install_ha_output.golden | 1 + .../install_ha_with_overrides_output.golden | 1 + cli/cmd/testdata/install_helm_output.golden | 1 + cli/cmd/testdata/install_helm_output_ha.golden | 1 + .../testdata/install_no_init_container.golden | 1 + cli/cmd/testdata/install_output.golden | 1 + cli/cmd/testdata/upgrade_default.golden | 1 + cli/cmd/testdata/upgrade_external_issuer.golden | 1 + cli/cmd/testdata/upgrade_ha.golden | 1 + web/main.go | 10 +++++++++- web/srv/server.go | 16 ++++++++++++++++ 14 files changed, 38 insertions(+), 1 deletion(-) diff --git a/charts/linkerd2/templates/web.yaml b/charts/linkerd2/templates/web.yaml index 4798a19ce..3ecf31f57 100644 --- a/charts/linkerd2/templates/web.yaml +++ b/charts/linkerd2/templates/web.yaml @@ -63,6 +63,8 @@ spec: - -grafana-addr=linkerd-grafana.{{.Namespace}}.svc.{{.ClusterDomain}}:3000 - -controller-namespace={{.Namespace}} - -log-level={{.ControllerLogLevel}} + {{- $host := replace "." "\\." (printf "linkerd-web.%s.svc.%s" .Namespace .ClusterDomain) }} + - -enforced-host=^(localhost|127\.0\.0\.1|{{ $host }}|\[::1\])(:\d+)?$ {{- include "partials.linkerd.trace" . | nindent 8 -}} image: {{.WebImage}}:{{default .LinkerdVersion .ControllerImageVersion}} imagePullPolicy: {{.ImagePullPolicy}} diff --git a/cli/cmd/testdata/install_control-plane.golden b/cli/cmd/testdata/install_control-plane.golden index 473401da7..c7e3b4d93 100644 --- a/cli/cmd/testdata/install_control-plane.golden +++ b/cli/cmd/testdata/install_control-plane.golden @@ -825,6 +825,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:install-control-plane-version imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden index 2fee0e646..4cea4b855 100644 --- a/cli/cmd/testdata/install_default.golden +++ b/cli/cmd/testdata/install_default.golden @@ -1583,6 +1583,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:install-control-plane-version imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden index 1f273c7a0..966d69fb6 100644 --- a/cli/cmd/testdata/install_ha_output.golden +++ b/cli/cmd/testdata/install_ha_output.golden @@ -1696,6 +1696,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:install-control-plane-version imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden index b7eba99a9..dcf7e4449 100644 --- a/cli/cmd/testdata/install_ha_with_overrides_output.golden +++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden @@ -1696,6 +1696,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:install-control-plane-version imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden index c5af06b13..d0c59b1e5 100644 --- a/cli/cmd/testdata/install_helm_output.golden +++ b/cli/cmd/testdata/install_helm_output.golden @@ -1647,6 +1647,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:linkerd-version imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden index 56b6c7fdf..6393c0bf5 100644 --- a/cli/cmd/testdata/install_helm_output_ha.golden +++ b/cli/cmd/testdata/install_helm_output_ha.golden @@ -1760,6 +1760,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:linkerd-version imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden index acd292a8e..56187559e 100644 --- a/cli/cmd/testdata/install_no_init_container.golden +++ b/cli/cmd/testdata/install_no_init_container.golden @@ -1481,6 +1481,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:install-control-plane-version imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden index 68d95465c..430df7986 100644 --- a/cli/cmd/testdata/install_output.golden +++ b/cli/cmd/testdata/install_output.golden @@ -1580,6 +1580,7 @@ spec: - -grafana-addr=linkerd-grafana.Namespace.svc.cluster.local:3000 - -controller-namespace=Namespace - -log-level=ControllerLogLevel + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.Namespace\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: WebImage:ControllerImageVersion imagePullPolicy: ImagePullPolicy livenessProbe: diff --git a/cli/cmd/testdata/upgrade_default.golden b/cli/cmd/testdata/upgrade_default.golden index a9e7d329c..70d86df4a 100644 --- a/cli/cmd/testdata/upgrade_default.golden +++ b/cli/cmd/testdata/upgrade_default.golden @@ -1586,6 +1586,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:UPGRADE-CONTROL-PLANE-VERSION imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/upgrade_external_issuer.golden b/cli/cmd/testdata/upgrade_external_issuer.golden index 3f403e653..73d5f2242 100644 --- a/cli/cmd/testdata/upgrade_external_issuer.golden +++ b/cli/cmd/testdata/upgrade_external_issuer.golden @@ -1572,6 +1572,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:UPGRADE-CONTROL-PLANE-VERSION imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/upgrade_ha.golden b/cli/cmd/testdata/upgrade_ha.golden index 3d1ac8324..44b2fdd6b 100644 --- a/cli/cmd/testdata/upgrade_ha.golden +++ b/cli/cmd/testdata/upgrade_ha.golden @@ -1699,6 +1699,7 @@ spec: - -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000 - -controller-namespace=linkerd - -log-level=info + - -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.linkerd\.svc\.cluster\.local|\[::1\])(:\d+)?$ image: gcr.io/linkerd-io/web:UPGRADE-CONTROL-PLANE-VERSION imagePullPolicy: IfNotPresent livenessProbe: diff --git a/web/main.go b/web/main.go index 599f8bd0d..63573faf9 100644 --- a/web/main.go +++ b/web/main.go @@ -6,6 +6,7 @@ import ( "net" "os" "os/signal" + "regexp" "syscall" "time" @@ -31,6 +32,7 @@ func main() { staticDir := cmd.String("static-dir", "app/dist", "directory to search for static files") reload := cmd.Bool("reload", true, "reloading set to true or false") controllerNamespace := cmd.String("controller-namespace", "linkerd", "namespace in which Linkerd is installed") + enforcedHost := cmd.String("enforced-host", "", "regexp describing the allowed values for the Host header; protects from DNS-rebinding attacks") kubeConfigPath := cmd.String("kubeconfig", "", "path to kube config") traceCollector := flags.AddTraceFlags(cmd) @@ -73,7 +75,13 @@ func main() { } } - server := srv.NewServer(*addr, *grafanaAddr, *templateDir, *staticDir, uuid, *controllerNamespace, clusterDomain, *reload, client, k8sAPI) + reHost, err := regexp.Compile(*enforcedHost) + if err != nil { + log.Fatalf("invalid --enforced-host parameter: %s", err) + } + + server := srv.NewServer(*addr, *grafanaAddr, *templateDir, *staticDir, uuid, + *controllerNamespace, clusterDomain, *reload, reHost, client, k8sAPI) go func() { log.Infof("starting HTTP server on %+v", *addr) diff --git a/web/srv/server.go b/web/srv/server.go index 57196d85a..56c26f424 100644 --- a/web/srv/server.go +++ b/web/srv/server.go @@ -1,10 +1,13 @@ package srv import ( + "fmt" + "html" "html/template" "net/http" "path" "path/filepath" + "regexp" "time" "github.com/julienschmidt/httprouter" @@ -27,6 +30,7 @@ type ( reload bool templates map[string]*template.Template router *httprouter.Router + reHost *regexp.Regexp } templatePayload struct { @@ -44,6 +48,16 @@ type ( // this is called by the HTTP server to actually respond to a request func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if !s.reHost.MatchString(req.Host) { + error := fmt.Sprintf(`It appears that you are trying to reach this service with a host of '%s'. +This does not match /%s/ and has been denied for security reasons. +Please see https://linkerd.io/dns-rebinding for an explanation of what is happening and how to fix it.`, + html.EscapeString(req.Host), + html.EscapeString(s.reHost.String())) + + http.Error(w, error, http.StatusBadRequest) + return + } w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "SAMEORIGIN") w.Header().Set("X-XSS-Protection", "1; mode=block") @@ -62,12 +76,14 @@ func NewServer( controllerNamespace string, clusterDomain string, reload bool, + reHost *regexp.Regexp, apiClient public.APIClient, k8sAPI *k8s.KubernetesAPI, ) *http.Server { server := &Server{ templateDir: templateDir, reload: reload, + reHost: reHost, } server.router = &httprouter.Router{