mirror of https://github.com/linkerd/linkerd2.git
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:
parent
8cf4494e78
commit
bd8d47226d
|
@ -63,6 +63,8 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.{{.Namespace}}.svc.{{.ClusterDomain}}:3000
|
- -grafana-addr=linkerd-grafana.{{.Namespace}}.svc.{{.ClusterDomain}}:3000
|
||||||
- -controller-namespace={{.Namespace}}
|
- -controller-namespace={{.Namespace}}
|
||||||
- -log-level={{.ControllerLogLevel}}
|
- -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 -}}
|
{{- include "partials.linkerd.trace" . | nindent 8 -}}
|
||||||
image: {{.WebImage}}:{{default .LinkerdVersion .ControllerImageVersion}}
|
image: {{.WebImage}}:{{default .LinkerdVersion .ControllerImageVersion}}
|
||||||
imagePullPolicy: {{.ImagePullPolicy}}
|
imagePullPolicy: {{.ImagePullPolicy}}
|
||||||
|
|
|
@ -825,6 +825,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:install-control-plane-version
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1583,6 +1583,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:install-control-plane-version
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1696,6 +1696,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:install-control-plane-version
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1696,6 +1696,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:install-control-plane-version
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1647,6 +1647,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:linkerd-version
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1760,6 +1760,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:linkerd-version
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1481,6 +1481,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:install-control-plane-version
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1580,6 +1580,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.Namespace.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.Namespace.svc.cluster.local:3000
|
||||||
- -controller-namespace=Namespace
|
- -controller-namespace=Namespace
|
||||||
- -log-level=ControllerLogLevel
|
- -log-level=ControllerLogLevel
|
||||||
|
- -enforced-host=^(localhost|127\.0\.0\.1|linkerd-web\.Namespace\.svc\.cluster\.local|\[::1\])(:\d+)?$
|
||||||
image: WebImage:ControllerImageVersion
|
image: WebImage:ControllerImageVersion
|
||||||
imagePullPolicy: ImagePullPolicy
|
imagePullPolicy: ImagePullPolicy
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1586,6 +1586,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:UPGRADE-CONTROL-PLANE-VERSION
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1572,6 +1572,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:UPGRADE-CONTROL-PLANE-VERSION
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
|
@ -1699,6 +1699,7 @@ spec:
|
||||||
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
- -grafana-addr=linkerd-grafana.linkerd.svc.cluster.local:3000
|
||||||
- -controller-namespace=linkerd
|
- -controller-namespace=linkerd
|
||||||
- -log-level=info
|
- -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
|
image: gcr.io/linkerd-io/web:UPGRADE-CONTROL-PLANE-VERSION
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|
10
web/main.go
10
web/main.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"regexp"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ func main() {
|
||||||
staticDir := cmd.String("static-dir", "app/dist", "directory to search for static files")
|
staticDir := cmd.String("static-dir", "app/dist", "directory to search for static files")
|
||||||
reload := cmd.Bool("reload", true, "reloading set to true or false")
|
reload := cmd.Bool("reload", true, "reloading set to true or false")
|
||||||
controllerNamespace := cmd.String("controller-namespace", "linkerd", "namespace in which Linkerd is installed")
|
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")
|
kubeConfigPath := cmd.String("kubeconfig", "", "path to kube config")
|
||||||
|
|
||||||
traceCollector := flags.AddTraceFlags(cmd)
|
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() {
|
go func() {
|
||||||
log.Infof("starting HTTP server on %+v", *addr)
|
log.Infof("starting HTTP server on %+v", *addr)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package srv
|
package srv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
@ -27,6 +30,7 @@ type (
|
||||||
reload bool
|
reload bool
|
||||||
templates map[string]*template.Template
|
templates map[string]*template.Template
|
||||||
router *httprouter.Router
|
router *httprouter.Router
|
||||||
|
reHost *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
templatePayload struct {
|
templatePayload struct {
|
||||||
|
@ -44,6 +48,16 @@ type (
|
||||||
|
|
||||||
// this is called by the HTTP server to actually respond to a request
|
// this is called by the HTTP server to actually respond to a request
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.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-Content-Type-Options", "nosniff")
|
||||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||||
|
@ -62,12 +76,14 @@ func NewServer(
|
||||||
controllerNamespace string,
|
controllerNamespace string,
|
||||||
clusterDomain string,
|
clusterDomain string,
|
||||||
reload bool,
|
reload bool,
|
||||||
|
reHost *regexp.Regexp,
|
||||||
apiClient public.APIClient,
|
apiClient public.APIClient,
|
||||||
k8sAPI *k8s.KubernetesAPI,
|
k8sAPI *k8s.KubernetesAPI,
|
||||||
) *http.Server {
|
) *http.Server {
|
||||||
server := &Server{
|
server := &Server{
|
||||||
templateDir: templateDir,
|
templateDir: templateDir,
|
||||||
reload: reload,
|
reload: reload,
|
||||||
|
reHost: reHost,
|
||||||
}
|
}
|
||||||
|
|
||||||
server.router = &httprouter.Router{
|
server.router = &httprouter.Router{
|
||||||
|
|
Loading…
Reference in New Issue