DNS rebinding protection for the dashboard (#3644)

* 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 <alejandro@buoyant.io>
This commit is contained in:
Alejandro Pedraza 2019-10-31 11:51:25 -05:00 committed by GitHub
parent 8cf4494e78
commit bd8d47226d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 38 additions and 1 deletions

View File

@ -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}}

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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{