Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't follow non-local redirects from HTTP probes #75416

Merged
merged 2 commits into from Mar 16, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion pkg/kubelet/events/event.go
Expand Up @@ -82,7 +82,8 @@ const (
FreeDiskSpaceFailed = "FreeDiskSpaceFailed"

// Probe event reason list
ContainerUnhealthy = "Unhealthy"
ContainerUnhealthy = "Unhealthy"
ContainerProbeWarning = "ProbeWarning"

// Pod worker event reason list
FailedSync = "FailedSync"
Expand Down
16 changes: 12 additions & 4 deletions pkg/kubelet/prober/prober.go
Expand Up @@ -66,10 +66,11 @@ func newProber(
refManager *kubecontainer.RefManager,
recorder record.EventRecorder) *prober {

const followNonLocalRedirects = false
return &prober{
exec: execprobe.New(),
readinessHttp: httprobe.New(),
livenessHttp: httprobe.New(),
readinessHttp: httprobe.New(followNonLocalRedirects),
livenessHttp: httprobe.New(followNonLocalRedirects),
tcp: tcprobe.New(),
runner: runner,
refManager: refManager,
Expand All @@ -96,7 +97,7 @@ func (pb *prober) probe(probeType probeType, pod *v1.Pod, status v1.PodStatus, c
}

result, output, err := pb.runProbeWithRetries(probeType, probeSpec, pod, status, container, containerID, maxProbeRetries)
if err != nil || result != probe.Success {
if err != nil || (result != probe.Success && result != probe.Warning) {
// Probe failed in one way or another.
ref, hasRef := pb.refManager.GetRef(containerID)
if !hasRef {
Expand All @@ -115,7 +116,14 @@ func (pb *prober) probe(probeType probeType, pod *v1.Pod, status v1.PodStatus, c
}
return results.Failure, err
}
klog.V(3).Infof("%s probe for %q succeeded", probeType, ctrName)
if result == probe.Warning {
if ref, hasRef := pb.refManager.GetRef(containerID); hasRef {
pb.recorder.Eventf(ref, v1.EventTypeWarning, events.ContainerProbeWarning, "%s probe warning: %s", probeType, output)
}
klog.V(3).Infof("%s probe for %q succeeded with a warning: %s", probeType, ctrName, output)
} else {
klog.V(3).Infof("%s probe for %q succeeded", probeType, ctrName)
}
return results.Success, nil
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/kubelet/prober/prober_test.go
Expand Up @@ -233,6 +233,11 @@ func TestProbe(t *testing.T) {
execResult: probe.Success,
expectedResult: results.Success,
},
{ // Probe result is warning
probe: execProbe,
execResult: probe.Warning,
expectedResult: results.Success,
},
{ // Probe result is unknown
probe: execProbe,
execResult: probe.Unknown,
Expand Down
7 changes: 6 additions & 1 deletion pkg/probe/http/BUILD
Expand Up @@ -22,7 +22,12 @@ go_test(
name = "go_default_test",
srcs = ["http_test.go"],
embed = [":go_default_library"],
deps = ["//pkg/probe:go_default_library"],
deps = [
"//pkg/probe:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
],
)

filegroup(
Expand Down
44 changes: 38 additions & 6 deletions pkg/probe/http/http.go
Expand Up @@ -18,6 +18,7 @@ package http

import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"net/http"
Expand All @@ -32,21 +33,25 @@ import (
)

// New creates Prober that will skip TLS verification while probing.
func New() Prober {
// followNonLocalRedirects configures whether the prober should follow redirects to a different hostname.
// If disabled, redirects to other hosts will trigger a warning result.
func New(followNonLocalRedirects bool) Prober {
tlsConfig := &tls.Config{InsecureSkipVerify: true}
return NewWithTLSConfig(tlsConfig)
return NewWithTLSConfig(tlsConfig, followNonLocalRedirects)
}

// NewWithTLSConfig takes tls config as parameter.
func NewWithTLSConfig(config *tls.Config) Prober {
// followNonLocalRedirects configures whether the prober should follow redirects to a different hostname.
// If disabled, redirects to other hosts will trigger a warning result.
func NewWithTLSConfig(config *tls.Config, followNonLocalRedirects bool) Prober {
// We do not want the probe use node's local proxy set.
transport := utilnet.SetTransportDefaults(
&http.Transport{
TLSClientConfig: config,
DisableKeepAlives: true,
Proxy: http.ProxyURL(nil),
})
return httpProber{transport}
return httpProber{transport, followNonLocalRedirects}
}

// Prober is an interface that defines the Probe function for doing HTTP readiness/liveness checks.
Expand All @@ -55,12 +60,18 @@ type Prober interface {
}

type httpProber struct {
transport *http.Transport
transport *http.Transport
followNonLocalRedirects bool
}

// Probe returns a ProbeRunner capable of running an HTTP check.
func (pr httpProber) Probe(url *url.URL, headers http.Header, timeout time.Duration) (probe.Result, string, error) {
return DoHTTPProbe(url, headers, &http.Client{Timeout: timeout, Transport: pr.transport})
client := &http.Client{
Timeout: timeout,
Transport: pr.transport,
CheckRedirect: redirectChecker(pr.followNonLocalRedirects),
}
return DoHTTPProbe(url, headers, client)
}

// GetHTTPInterface is an interface for making HTTP requests, that returns a response and error.
Expand Down Expand Up @@ -102,9 +113,30 @@ func DoHTTPProbe(url *url.URL, headers http.Header, client GetHTTPInterface) (pr
}
body := string(b)
if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusBadRequest {
if res.StatusCode >= http.StatusMultipleChoices { // Redirect
klog.V(4).Infof("Probe terminated redirects for %s, Response: %v", url.String(), *res)
return probe.Warning, body, nil
}
klog.V(4).Infof("Probe succeeded for %s, Response: %v", url.String(), *res)
return probe.Success, body, nil
}
klog.V(4).Infof("Probe failed for %s with request headers %v, response body: %v", url.String(), headers, body)
return probe.Failure, fmt.Sprintf("HTTP probe failed with statuscode: %d", res.StatusCode), nil
}

func redirectChecker(followNonLocalRedirects bool) func(*http.Request, []*http.Request) error {
if followNonLocalRedirects {
return nil // Use the default http client checker.
}

return func(req *http.Request, via []*http.Request) error {
if req.URL.Hostname() != via[0].URL.Hostname() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK relative redirects can have no hostname, just "Location: /another/endpoint" , does go http client normalize them to have same hostname?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the URL in the request is parsed relative to the previous request: https://golang.org/src/net/http/client.go?s=18538:18870#L563

This case is also verified by the unit test.

return http.ErrUseLastResponse
}
// Default behavior: stop after 10 redirects.
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
}
61 changes: 57 additions & 4 deletions pkg/probe/http/http_test.go
Expand Up @@ -28,6 +28,9 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/kubernetes/pkg/probe"
)

Expand Down Expand Up @@ -64,7 +67,7 @@ func TestHTTPProbeProxy(t *testing.T) {
defer unsetEnv("no_proxy")()
defer unsetEnv("NO_PROXY")()

prober := New()
prober := New(true)

go func() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -119,7 +122,7 @@ func TestHTTPProbeChecker(t *testing.T) {
}
}

prober := New()
prober := New(true)
testCases := []struct {
handler func(w http.ResponseWriter, r *http.Request)
reqHeaders http.Header
Expand Down Expand Up @@ -223,7 +226,7 @@ func TestHTTPProbeChecker(t *testing.T) {
},
}
for i, test := range testCases {
func() {
t.Run(fmt.Sprintf("case-%2d", i), func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
test.handler(w, r)
}))
Expand Down Expand Up @@ -258,6 +261,56 @@ func TestHTTPProbeChecker(t *testing.T) {
t.Errorf("Expected response not to contain %v, got %v", test.notBody, output)
}
}
}()
})
}
}

func TestHTTPProbeChecker_NonLocalRedirects(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/redirect":
loc, _ := url.QueryUnescape(r.URL.Query().Get("loc"))
http.Redirect(w, r, loc, http.StatusFound)
case "/loop":
http.Redirect(w, r, "/loop", http.StatusFound)
case "/success":
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "", http.StatusInternalServerError)
}
})
server := httptest.NewServer(handler)
defer server.Close()

newportServer := httptest.NewServer(handler)
defer newportServer.Close()

testCases := map[string]struct {
redirect string
expectLocalResult probe.Result
expectNonLocalResult probe.Result
}{
"local success": {"/success", probe.Success, probe.Success},
"local fail": {"/fail", probe.Failure, probe.Failure},
"newport success": {newportServer.URL + "/success", probe.Success, probe.Success},
"newport fail": {newportServer.URL + "/fail", probe.Failure, probe.Failure},
"bogus nonlocal": {"http://0.0.0.0/fail", probe.Warning, probe.Failure},
"redirect loop": {"/loop", probe.Failure, probe.Failure},
}
for desc, test := range testCases {
t.Run(desc+"-local", func(t *testing.T) {
prober := New(false)
target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect))
require.NoError(t, err)
result, _, _ := prober.Probe(target, nil, wait.ForeverTestTimeout)
assert.Equal(t, test.expectLocalResult, result)
})
t.Run(desc+"-nonlocal", func(t *testing.T) {
prober := New(true)
target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect))
require.NoError(t, err)
result, _, _ := prober.Probe(target, nil, wait.ForeverTestTimeout)
assert.Equal(t, test.expectNonLocalResult, result)
})
}
}
2 changes: 2 additions & 0 deletions pkg/probe/probe.go
Expand Up @@ -22,6 +22,8 @@ type Result string
const (
// Success Result
Success Result = "success"
// Warning Result. Logically success, but with additional debugging information attached.
Warning Result = "warning"
// Failure Result
Failure Result = "failure"
// Unknown Result
Expand Down
3 changes: 2 additions & 1 deletion pkg/registry/core/componentstatus/validator.go
Expand Up @@ -68,7 +68,8 @@ func (server *Server) DoServerCheck() (probe.Result, string, error) {
if server.Prober != nil {
return
}
server.Prober = httpprober.NewWithTLSConfig(server.TLSConfig)
const followNonLocalRedirects = true
server.Prober = httpprober.NewWithTLSConfig(server.TLSConfig, followNonLocalRedirects)
})

scheme := "http"
Expand Down
1 change: 1 addition & 0 deletions test/e2e/common/BUILD
Expand Up @@ -48,6 +48,7 @@ go_library(
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/conditions:go_default_library",
"//pkg/kubelet:go_default_library",
"//pkg/kubelet/events:go_default_library",
"//pkg/kubelet/images:go_default_library",
"//pkg/kubelet/sysctl:go_default_library",
"//pkg/security/apparmor:go_default_library",
Expand Down
79 changes: 79 additions & 0 deletions test/e2e/common/container_probe.go
Expand Up @@ -18,13 +18,16 @@ package common

import (
"fmt"
"net/url"
"time"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/uuid"
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/kubelet/events"
"k8s.io/kubernetes/test/e2e/framework"
testutils "k8s.io/kubernetes/test/utils"

Expand Down Expand Up @@ -305,6 +308,82 @@ var _ = framework.KubeDescribe("Probing container", func() {
},
}, 1, defaultObservationTimeout)
})

/*
Release : v1.14
Testname: Pod http liveness probe, redirected to a local address
Description: A Pod is created with liveness probe on http endpoint /redirect?loc=healthz. The http handler on the /redirect will redirect to the /healthz endpoint, which will return a http error after 10 seconds since the Pod is started. This MUST result in liveness check failure. The Pod MUST now be killed and restarted incrementing restart count to 1.
*/
It("should be restarted with a local redirect http liveness probe", func() {
runLivenessTest(f, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "liveness-http-redirect",
Labels: map[string]string{"test": "liveness"},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "liveness",
Image: imageutils.GetE2EImage(imageutils.Liveness),
Command: []string{"/server"},
LivenessProbe: &v1.Probe{
Handler: v1.Handler{
HTTPGet: &v1.HTTPGetAction{
Path: "/redirect?loc=" + url.QueryEscape("/healthz"),
Port: intstr.FromInt(8080),
},
},
InitialDelaySeconds: 15,
FailureThreshold: 1,
},
},
},
},
}, 1, defaultObservationTimeout)
})

/*
Release : v1.14
Testname: Pod http liveness probe, redirected to a non-local address
Description: A Pod is created with liveness probe on http endpoint /redirect with a redirect to http://0.0.0.0/. The http handler on the /redirect should not follow the redirect, but instead treat it as a success and generate an event.
*/
It("should *not* be restarted with a non-local redirect http liveness probe", func() {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "liveness-http-redirect",
Labels: map[string]string{"test": "liveness"},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "liveness",
Image: imageutils.GetE2EImage(imageutils.Liveness),
Command: []string{"/server"},
LivenessProbe: &v1.Probe{
Handler: v1.Handler{
HTTPGet: &v1.HTTPGetAction{
Path: "/redirect?loc=" + url.QueryEscape("http://0.0.0.0/"),
Port: intstr.FromInt(8080),
},
},
InitialDelaySeconds: 15,
FailureThreshold: 1,
},
},
},
},
}
runLivenessTest(f, pod, 0, defaultObservationTimeout)
// Expect an event of type "ProbeWarning".
expectedEvent := fields.Set{
"involvedObject.kind": "Pod",
"involvedObject.name": pod.Name,
"involvedObject.namespace": f.Namespace.Name,
"reason": events.ContainerProbeWarning,
}.AsSelector().String()
framework.ExpectNoError(framework.WaitTimeoutForPodEvent(
f.ClientSet, pod.Name, f.Namespace.Name, expectedEvent, "0.0.0.0", framework.PodEventTimeout))
})
})

func getContainerStartedTime(p *v1.Pod, containerName string) (time.Time, error) {
Expand Down
2 changes: 1 addition & 1 deletion test/images/liveness/VERSION
@@ -1 +1 @@
1.0
1.1