diff --git a/main.go b/main.go index 4399ecfe5..a6e035364 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ import ( "net/url" "os" "os/signal" + "regexp" "strings" "syscall" "time" @@ -60,6 +61,7 @@ type config struct { auth proxy.Config tls tlsConfig kubeconfigLocation string + allowPathsRegex string } type tlsConfig struct { @@ -115,6 +117,7 @@ func main() { flagset.BoolVar(&cfg.upstreamForceH2C, "upstream-force-h2c", false, "Force h2c to communiate with the upstream. This is required when the upstream speaks h2c(http/2 cleartext - insecure variant of http/2) only. For example, go-grpc server in the insecure mode, such as helm's tiller w/o TLS, speaks h2c only") flagset.StringVar(&cfg.upstreamCAFile, "upstream-ca-file", "", "The CA the upstream uses for TLS connection. This is required when the upstream uses TLS and its own CA certificate") flagset.StringVar(&configFileName, "config-file", "", "Configuration file to configure kube-rbac-proxy.") + flagset.StringVar(&cfg.allowPathsRegex, "allow-paths-regex", ".*", "Regular expression against which kube-rbac-proxy matches the incoming request path. If the request doesn't match, kube-rbac-proxy responds with a 404 status code.") // TLS flags flagset.StringVar(&cfg.tls.certFile, "tls-cert-file", "", "File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert)") @@ -148,7 +151,12 @@ func main() { upstreamURL, err := url.Parse(cfg.upstream) if err != nil { - klog.Fatalf("Failed to build parse upstream URL: %v", err) + klog.Fatalf("Failed to parse upstream URL: %v", err) + } + + allowPathsRe, err := regexp.Compile(cfg.allowPathsRegex) + if err != nil { + klog.Fatalf("Failed to compile allow path regex: %v", err) } if configFileName != "" { @@ -215,6 +223,10 @@ func main() { proxy.Transport = upstreamTransport mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if !allowPathsRe.MatchString(req.URL.Path) { + http.NotFound(w, req) + return + } ok := auth.Handle(w, req) if !ok { return diff --git a/test/e2e/allowpaths/clusterRole-client.yaml b/test/e2e/allowpaths/clusterRole-client.yaml new file mode 100644 index 000000000..79c9720b2 --- /dev/null +++ b/test/e2e/allowpaths/clusterRole-client.yaml @@ -0,0 +1,7 @@ +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: metrics +rules: + - nonResourceURLs: ["/metrics"] + verbs: ["get"] diff --git a/test/e2e/allowpaths/clusterRole.yaml b/test/e2e/allowpaths/clusterRole.yaml new file mode 100644 index 000000000..e9bc500b7 --- /dev/null +++ b/test/e2e/allowpaths/clusterRole.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kube-rbac-proxy + namespace: default +rules: + - apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] diff --git a/test/e2e/allowpaths/clusterRoleBinding-client.yaml b/test/e2e/allowpaths/clusterRoleBinding-client.yaml new file mode 100644 index 000000000..2e67fae65 --- /dev/null +++ b/test/e2e/allowpaths/clusterRoleBinding-client.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: metrics +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metrics +subjects: + - kind: ServiceAccount + name: default + namespace: default diff --git a/test/e2e/allowpaths/clusterRoleBinding.yaml b/test/e2e/allowpaths/clusterRoleBinding.yaml new file mode 100644 index 000000000..f7be8fa4e --- /dev/null +++ b/test/e2e/allowpaths/clusterRoleBinding.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kube-rbac-proxy + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kube-rbac-proxy +subjects: + - kind: ServiceAccount + name: kube-rbac-proxy + namespace: default diff --git a/test/e2e/allowpaths/deployment.yaml b/test/e2e/allowpaths/deployment.yaml new file mode 100644 index 000000000..3a285beb1 --- /dev/null +++ b/test/e2e/allowpaths/deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kube-rbac-proxy + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: kube-rbac-proxy + template: + metadata: + labels: + app: kube-rbac-proxy + spec: + serviceAccountName: kube-rbac-proxy + containers: + - name: kube-rbac-proxy + image: quay.io/brancz/kube-rbac-proxy:local + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8081/" + - "--allow-paths-regex=/metrics" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: prometheus-example-app + image: quay.io/brancz/prometheus-example-app:v0.1.0 + args: + - "--bind=127.0.0.1:8081" diff --git a/test/e2e/allowpaths/service.yaml b/test/e2e/allowpaths/service.yaml new file mode 100644 index 000000000..b1ae11686 --- /dev/null +++ b/test/e2e/allowpaths/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: kube-rbac-proxy + name: kube-rbac-proxy + namespace: default +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + app: kube-rbac-proxy diff --git a/test/e2e/allowpaths/serviceAccount.yaml b/test/e2e/allowpaths/serviceAccount.yaml new file mode 100644 index 000000000..45feecc9c --- /dev/null +++ b/test/e2e/allowpaths/serviceAccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kube-rbac-proxy + namespace: default diff --git a/test/e2e/basics.go b/test/e2e/basics.go index 330ce14ec..9f934f204 100644 --- a/test/e2e/basics.go +++ b/test/e2e/basics.go @@ -17,6 +17,7 @@ limitations under the License. package e2e import ( + "fmt" "testing" "github.com/brancz/kube-rbac-proxy/test/kubetest" @@ -190,6 +191,90 @@ func testTokenAudience(s *kubetest.Suite) kubetest.TestSuite { } } +func testAllowPathsRegexp(s *kubetest.Suite) kubetest.TestSuite { + return func(t *testing.T) { + command := `STATUS_CODE=$(curl --connect-timeout 5 -o /dev/null -v -s -k --write-out "%%{http_code}" -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" https://kube-rbac-proxy.default.svc.cluster.local:8443%s); if [[ "$STATUS_CODE" != %d ]]; then echo "expecting %d status code, got $STATUS_CODE instead"; exit 1; fi` + + kubetest.Scenario{ + Name: "WithPathhNotAllowed", + Description: ` + As a client with the correct RBAC rules, + I get a 404 response when requesting a path which isn't allowed by kube-rbac-proxy + `, + + Given: kubetest.Setups( + kubetest.CreatedManifests( + s.KubeClient, + "allowpaths/clusterRole.yaml", + "allowpaths/clusterRoleBinding.yaml", + "allowpaths/deployment.yaml", + "allowpaths/service.yaml", + "allowpaths/serviceAccount.yaml", + "allowpaths/clusterRole-client.yaml", + "allowpaths/clusterRoleBinding-client.yaml", + ), + ), + When: kubetest.Conditions( + kubetest.PodsAreReady( + s.KubeClient, + 1, + "app=kube-rbac-proxy", + ), + kubetest.ServiceIsReady( + s.KubeClient, + "kube-rbac-proxy", + ), + ), + Then: kubetest.Checks( + ClientFails( + s.KubeClient, + fmt.Sprintf(command, "/", 404, 404), + nil, + ), + ), + }.Run(t) + + kubetest.Scenario{ + Name: "WithPathAllowed", + Description: ` + As a client with the correct RBAC rules, + I succeed with my request for a path that is allowed + `, + + Given: kubetest.Setups( + kubetest.CreatedManifests( + s.KubeClient, + "allowpaths/clusterRole.yaml", + "allowpaths/clusterRoleBinding.yaml", + "allowpaths/deployment.yaml", + "allowpaths/service.yaml", + "allowpaths/serviceAccount.yaml", + "allowpaths/clusterRole-client.yaml", + "allowpaths/clusterRoleBinding-client.yaml", + ), + ), + When: kubetest.Conditions( + kubetest.PodsAreReady( + s.KubeClient, + 1, + "app=kube-rbac-proxy", + ), + kubetest.ServiceIsReady( + s.KubeClient, + "kube-rbac-proxy", + ), + ), + Then: kubetest.Checks( + ClientSucceeds( + s.KubeClient, + fmt.Sprintf(command, "/metrics", 200, 200), + nil, + ), + ), + }.Run(t) + } +} + func ClientSucceeds(client kubernetes.Interface, command string, opts *kubetest.RunOptions) kubetest.Check { return func(ctx *kubetest.ScenarioContext) error { return kubetest.RunSucceeds( diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 0da060872..7bb814119 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -51,6 +51,7 @@ func Test(t *testing.T) { tests := map[string]kubetest.TestSuite{ "Basics": testBasics(suite), "TokenAudience": testTokenAudience(suite), + "AllowPath": testAllowPath(suite), } for name, tc := range tests {