Merge pull request #9287 from brendandburns/kubectl

Add some XSRF protection to kubectl proxy.
This commit is contained in:
Abhi Shah 2015-06-12 13:51:08 -07:00
commit 86b6150fb0
6 changed files with 285 additions and 6 deletions

View File

@ -520,11 +520,16 @@ _kubectl_proxy()
flags_with_completion=() flags_with_completion=()
flags_completion=() flags_completion=()
flags+=("--accept-hosts=")
flags+=("--accept-paths=")
flags+=("--api-prefix=") flags+=("--api-prefix=")
flags+=("--disable-filter")
flags+=("--help") flags+=("--help")
flags+=("-h") flags+=("-h")
flags+=("--port=") flags+=("--port=")
two_word_flags+=("-p") two_word_flags+=("-p")
flags+=("--reject-methods=")
flags+=("--reject-paths=")
flags+=("--www=") flags+=("--www=")
two_word_flags+=("-w") two_word_flags+=("-w")
flags+=("--www-prefix=") flags+=("--www-prefix=")

View File

@ -40,9 +40,14 @@ $ kubectl proxy --api-prefix=/k8s-api
### Options ### Options
``` ```
--accept-hosts="^localhost$,^127\\.0\\.0\\.1$,^\\[::1\\]$": Regular expression for hosts that the proxy should accept.
--accept-paths="^/api/.*": Regular expression for paths that the proxy should accept.
--api-prefix="/api/": Prefix to serve the proxied API under. --api-prefix="/api/": Prefix to serve the proxied API under.
--disable-filter=false: If true, disable request filtering in the proxy. This is dangerous, and can leave you vulnerable to XSRF attacks. Use with caution.
-h, --help=false: help for proxy -h, --help=false: help for proxy
-p, --port=8001: The port on which to run the proxy. -p, --port=8001: The port on which to run the proxy.
--reject-methods="POST,PUT,PATCH": Regular expression for HTTP methods that the proxy should reject.
--reject-paths="^/api/.*/exec,^/api/.*/run": Regular expression for paths that the proxy should reject.
-w, --www="": Also serve static files from the given directory under the specified prefix. -w, --www="": Also serve static files from the given directory under the specified prefix.
-P, --www-prefix="/static/": Prefix to serve static files under, if static file directory is specified. -P, --www-prefix="/static/": Prefix to serve static files under, if static file directory is specified.
``` ```
@ -79,6 +84,6 @@ $ kubectl proxy --api-prefix=/k8s-api
### SEE ALSO ### SEE ALSO
* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager
###### Auto generated by spf13/cobra at 2015-06-05 21:08:36.513099878 +0000 UTC ###### Auto generated by spf13/cobra at 2015-06-11 03:49:29.837564354 +0000 UTC
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_proxy.md?pixel)]() [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_proxy.md?pixel)]()

View File

@ -38,10 +38,22 @@ The above lets you 'curl localhost:8001/custom/api/v1/pods'
.SH OPTIONS .SH OPTIONS
.PP
\fB\-\-accept\-hosts\fP="^localhost$,^127\\.0\\.0\\.1$,^\\[::1\\]$"
Regular expression for hosts that the proxy should accept.
.PP
\fB\-\-accept\-paths\fP="^/api/.*"
Regular expression for paths that the proxy should accept.
.PP .PP
\fB\-\-api\-prefix\fP="/api/" \fB\-\-api\-prefix\fP="/api/"
Prefix to serve the proxied API under. Prefix to serve the proxied API under.
.PP
\fB\-\-disable\-filter\fP=false
If true, disable request filtering in the proxy. This is dangerous, and can leave you vulnerable to XSRF attacks. Use with caution.
.PP .PP
\fB\-h\fP, \fB\-\-help\fP=false \fB\-h\fP, \fB\-\-help\fP=false
help for proxy help for proxy
@ -50,6 +62,14 @@ The above lets you 'curl localhost:8001/custom/api/v1/pods'
\fB\-p\fP, \fB\-\-port\fP=8001 \fB\-p\fP, \fB\-\-port\fP=8001
The port on which to run the proxy. The port on which to run the proxy.
.PP
\fB\-\-reject\-methods\fP="POST,PUT,PATCH"
Regular expression for HTTP methods that the proxy should reject.
.PP
\fB\-\-reject\-paths\fP="^/api/.\fI/exec,^/api/.\fP/run"
Regular expression for paths that the proxy should reject.
.PP .PP
\fB\-w\fP, \fB\-\-www\fP="" \fB\-w\fP, \fB\-\-www\fP=""
Also serve static files from the given directory under the specified prefix. Also serve static files from the given directory under the specified prefix.

View File

@ -65,7 +65,12 @@ The above lets you 'curl localhost:8001/custom/api/v1/pods'
cmd.Flags().StringP("www", "w", "", "Also serve static files from the given directory under the specified prefix.") cmd.Flags().StringP("www", "w", "", "Also serve static files from the given directory under the specified prefix.")
cmd.Flags().StringP("www-prefix", "P", "/static/", "Prefix to serve static files under, if static file directory is specified.") cmd.Flags().StringP("www-prefix", "P", "/static/", "Prefix to serve static files under, if static file directory is specified.")
cmd.Flags().StringP("api-prefix", "", "/api/", "Prefix to serve the proxied API under.") cmd.Flags().StringP("api-prefix", "", "/api/", "Prefix to serve the proxied API under.")
cmd.Flags().String("accept-paths", kubectl.DefaultPathAcceptRE, "Regular expression for paths that the proxy should accept.")
cmd.Flags().String("reject-paths", kubectl.DefaultPathRejectRE, "Regular expression for paths that the proxy should reject.")
cmd.Flags().String("accept-hosts", kubectl.DefaultHostAcceptRE, "Regular expression for hosts that the proxy should accept.")
cmd.Flags().String("reject-methods", kubectl.DefaultMethodRejectRE, "Regular expression for HTTP methods that the proxy should reject.")
cmd.Flags().IntP("port", "p", 8001, "The port on which to run the proxy.") cmd.Flags().IntP("port", "p", 8001, "The port on which to run the proxy.")
cmd.Flags().Bool("disable-filter", false, "If true, disable request filtering in the proxy. This is dangerous, and can leave you vulnerable to XSRF attacks. Use with caution.")
return cmd return cmd
} }
@ -87,7 +92,17 @@ func RunProxy(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command) error {
if !strings.HasSuffix(apiProxyPrefix, "/") { if !strings.HasSuffix(apiProxyPrefix, "/") {
apiProxyPrefix += "/" apiProxyPrefix += "/"
} }
server, err := kubectl.NewProxyServer(cmdutil.GetFlagString(cmd, "www"), apiProxyPrefix, staticPrefix, clientConfig) filter := &kubectl.FilterServer{
AcceptPaths: kubectl.MakeRegexpArrayOrDie(cmdutil.GetFlagString(cmd, "accept-paths")),
RejectPaths: kubectl.MakeRegexpArrayOrDie(cmdutil.GetFlagString(cmd, "reject-paths")),
AcceptHosts: kubectl.MakeRegexpArrayOrDie(cmdutil.GetFlagString(cmd, "accept-hosts")),
}
if cmdutil.GetFlagBool(cmd, "disable-filter") {
glog.Warning("Request filter disabled, your proxy is vulnerable to XSRF attacks, please be cautious")
filter = nil
}
server, err := kubectl.NewProxyServer(cmdutil.GetFlagString(cmd, "www"), apiProxyPrefix, staticPrefix, filter, clientConfig)
if err != nil { if err != nil {
return err return err
} }

View File

@ -21,11 +21,86 @@ import (
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"regexp"
"strings" "strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/golang/glog"
) )
const (
DefaultHostAcceptRE = "^localhost$,^127\\.0\\.0\\.1$,^\\[::1\\]$"
DefaultPathAcceptRE = "^/api/.*"
DefaultPathRejectRE = "^/api/.*/exec,^/api/.*/run"
DefaultMethodRejectRE = "POST,PUT,PATCH"
)
// FilterServer rejects requests which don't match one of the specified regular expressions
type FilterServer struct {
// Only paths that match this regexp will be accepted
AcceptPaths []*regexp.Regexp
// Paths that match this regexp will be rejected, even if they match the above
RejectPaths []*regexp.Regexp
// Hosts are required to match this list of regexp
AcceptHosts []*regexp.Regexp
// Methods that match this regexp are rejected
RejectMethods []*regexp.Regexp
// The delegate to call to handle accepted requests.
delegate http.Handler
}
// Splits a comma separated list of regexps into a array of Regexp objects.
func MakeRegexpArray(str string) ([]*regexp.Regexp, error) {
parts := strings.Split(str, ",")
result := make([]*regexp.Regexp, len(parts))
for ix := range parts {
re, err := regexp.Compile(parts[ix])
if err != nil {
return nil, err
}
result[ix] = re
}
return result, nil
}
func MakeRegexpArrayOrDie(str string) []*regexp.Regexp {
result, err := MakeRegexpArray(str)
if err != nil {
glog.Fatalf("Error compiling re: %v", err)
}
return result
}
func matchesRegexp(str string, regexps []*regexp.Regexp) bool {
for _, re := range regexps {
if re.MatchString(str) {
return true
}
}
return false
}
func (f *FilterServer) accept(method, path, host string) bool {
if matchesRegexp(path, f.RejectPaths) {
return false
}
if matchesRegexp(method, f.RejectMethods) {
return false
}
if matchesRegexp(path, f.AcceptPaths) && matchesRegexp(host, f.AcceptHosts) {
return true
}
return false
}
func (f *FilterServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if f.accept(req.Method, req.URL.Path, req.Host) {
f.delegate.ServeHTTP(rw, req)
}
rw.WriteHeader(http.StatusForbidden)
rw.Write([]byte("<h3>Unauthorized</h3>"))
}
// ProxyServer is a http.Handler which proxies Kubernetes APIs to remote API server. // ProxyServer is a http.Handler which proxies Kubernetes APIs to remote API server.
type ProxyServer struct { type ProxyServer struct {
mux *http.ServeMux mux *http.ServeMux
@ -34,7 +109,7 @@ type ProxyServer struct {
// NewProxyServer creates and installs a new ProxyServer. // NewProxyServer creates and installs a new ProxyServer.
// It automatically registers the created ProxyServer to http.DefaultServeMux. // It automatically registers the created ProxyServer to http.DefaultServeMux.
func NewProxyServer(filebase string, apiProxyPrefix string, staticPrefix string, cfg *client.Config) (*ProxyServer, error) { func NewProxyServer(filebase string, apiProxyPrefix string, staticPrefix string, filter *FilterServer, cfg *client.Config) (*ProxyServer, error) {
host := cfg.Host host := cfg.Host
if !strings.HasSuffix(host, "/") { if !strings.HasSuffix(host, "/") {
host = host + "/" host = host + "/"
@ -47,11 +122,19 @@ func NewProxyServer(filebase string, apiProxyPrefix string, staticPrefix string,
if proxy.Transport, err = client.TransportFor(cfg); err != nil { if proxy.Transport, err = client.TransportFor(cfg); err != nil {
return nil, err return nil, err
} }
var server http.Handler
if strings.HasPrefix(apiProxyPrefix, "/api") { if strings.HasPrefix(apiProxyPrefix, "/api") {
proxy.mux.Handle(apiProxyPrefix, proxy) server = proxy
} else { } else {
proxy.mux.Handle(apiProxyPrefix, http.StripPrefix(apiProxyPrefix, proxy)) server = http.StripPrefix(apiProxyPrefix, proxy)
} }
if filter != nil {
filter.delegate = server
server = filter
}
proxy.mux.Handle(apiProxyPrefix, server)
proxy.mux.Handle(staticPrefix, newFileHandler(staticPrefix, filebase)) proxy.mux.Handle(staticPrefix, newFileHandler(staticPrefix, filebase))
return proxy, nil return proxy, nil
} }

View File

@ -29,6 +29,157 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
) )
func TestAccept(t *testing.T) {
tests := []struct {
acceptPaths string
rejectPaths string
acceptHosts string
path string
host string
method string
expectAccept bool
}{
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods",
host: "127.0.0.1",
method: "GET",
expectAccept: true,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods",
host: "localhost",
method: "GET",
expectAccept: true,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods/foo/exec",
host: "127.0.0.1",
method: "GET",
expectAccept: false,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods",
host: "evil.com",
method: "GET",
expectAccept: false,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods",
host: "localhost.evil.com",
method: "GET",
expectAccept: false,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods",
host: "127a0b0c1",
method: "GET",
expectAccept: false,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/foo/v1/pods",
host: "localhost",
method: "GET",
expectAccept: false,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods",
host: "localhost",
method: "POST",
expectAccept: false,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods/somepod",
host: "localhost",
method: "PUT",
expectAccept: false,
},
{
acceptPaths: DefaultPathAcceptRE,
rejectPaths: DefaultPathRejectRE,
acceptHosts: DefaultHostAcceptRE,
path: "/api/v1/pods/somepod",
host: "localhost",
method: "PATCH",
expectAccept: false,
},
}
for _, test := range tests {
filter := &FilterServer{
AcceptPaths: MakeRegexpArrayOrDie(test.acceptPaths),
RejectPaths: MakeRegexpArrayOrDie(test.rejectPaths),
AcceptHosts: MakeRegexpArrayOrDie(test.acceptHosts),
RejectMethods: MakeRegexpArrayOrDie(DefaultMethodRejectRE),
}
accept := filter.accept(test.method, test.path, test.host)
if accept != test.expectAccept {
t.Errorf("expected: %v, got %v for %#v", test.expectAccept, accept, test)
}
}
}
func TestRegexpMatch(t *testing.T) {
tests := []struct {
str string
regexps string
expectMatch bool
}{
{
str: "foo",
regexps: "bar,.*",
expectMatch: true,
},
{
str: "foo",
regexps: "bar,fo.*",
expectMatch: true,
},
{
str: "bar",
regexps: "bar,fo.*",
expectMatch: true,
},
{
str: "baz",
regexps: "bar,fo.*",
expectMatch: false,
},
}
for _, test := range tests {
match := matchesRegexp(test.str, MakeRegexpArrayOrDie(test.regexps))
if test.expectMatch != match {
t.Errorf("expected: %v, found: %v, for %s and %v", test.expectMatch, match, test.str, test.regexps)
}
}
}
func TestFileServing(t *testing.T) { func TestFileServing(t *testing.T) {
const ( const (
fname = "test.txt" fname = "test.txt"
@ -136,7 +287,7 @@ func TestPathHandling(t *testing.T) {
for _, item := range table { for _, item := range table {
func() { func() {
p, err := NewProxyServer("", item.prefix, "/not/used/for/this/test", cc) p, err := NewProxyServer("", item.prefix, "/not/used/for/this/test", nil, cc)
if err != nil { if err != nil {
t.Fatalf("%#v: %v", item, err) t.Fatalf("%#v: %v", item, err)
} }