diff --git a/.circleci/config.yml b/.circleci/config.yml index 22c8c30..ff30130 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,11 @@ jobs: # Base test configuration for Go library tests Each distinct version should # inherit this base, and override (at least) the container image used. "test": &test + docker: + - image: "circleci/golang:<< parameters.v >>" + working_directory: /go/src/github.com/elithrar/admission-control + environment: + GO111MODULE: "on" parameters: v: type: string @@ -11,12 +16,12 @@ jobs: latest: type: boolean default: false - docker: - - image: "circleci/golang:<< parameters.v >>" - working_directory: /go/src/github.com/elithrar/admission-control - environment: - GO111MODULE: "on" - LATEST: "<< parameters.latest >>" + golint: + type: boolean + default: true + modules: + type: boolean + default: true steps: - checkout - run: @@ -25,25 +30,31 @@ jobs: go version - run: name: "Fetch dependencies" - command: go mod download + command: > + if [[ << parameters.modules >> = true ]]; then + go mod download + export GO111MODULE=on + else + go get -v ./... + fi # Only run gofmt, vet & lint against the latest Go version - run: name: "Run golint" command: > - if [ "${LATEST}" = true ] && [ -z "${SKIP_GOLINT}" ]; then + if [ << parameters.latest >> = true ] && [ << parameters.golint >> = true ]; then go get -u golang.org/x/lint/golint golint ./... fi - run: name: "Run gofmt" command: > - if [[ "${LATEST}" = true ]]; then + if [[ << parameters.latest >> = true ]]; then diff -u <(echo -n) <(gofmt -d -e .) fi - run: name: "Run go vet" command: > - if [[ "${LATEST}" = true ]]; then + if [[ << parameters.latest >> = true ]]; then go vet -v ./... fi - run: @@ -75,7 +86,7 @@ workflows: v: "latest" latest: true - test: - name: "Go 1.12" + name: "v1.12" v: "1.12" - "build-container": requires: diff --git a/README.md b/README.md index 0bb8710..f6b61a0 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,89 @@ A micro-framework for building Kubernetes [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/). +- Can be used as the target of both [`ValidatingWebhookConfiguration`]() and + [`MutatingWebhookConfiguration`]() - handlers can return simple allow/deny + responses, or patches (mutations) to submitted resources. - Provides an extensible `AdmissionHandler` type that accepts a custom - admission function (or `AdmitFunc`), making it easy for you to add new + admission function (called an `AdmitFunc`), making it easy for you to add new validating or mutating webhook endpoints. -- Makes it easy to set up: -- Includes an example `AdmitFunc` for denying the creation of public Ingress - and Services in GKE. -- Provides example `Deployment`, `Service` and `ValidatingWebhookConfiguration` - definitions for you to build off of. +- Provides sample `Deployment`, `Service` and + `ValidatingWebhookConfiguration` definitions for you to build off of, and an + [`example webhook server`](https://github.com/elithrar/admission-control/tree/master/examples/admissiond) + as additional guidance. -> **Note**: Looking to extend admission-control for your own uses? It's first-and-foremost exposed as a library - simply import it as `github.com/elithrar/admission-control` and reference the example server at `cmd/admissiond`. +--- + +## Using the Framework + +### Built-In AdmitFuncs + +Admission Control provides a number of useful built-in _AdmitFuncs_, including: + +- `DenyPublicLoadBalancers` - prevents exposing `Services` of `type: LoadBalancer` outside of the cluster; instead requiring the LB to be annotated as internal-only. +- `DenyIngresses` - similar to the above, it prevents creating Ingresses (except in the namespaces you allow) + +More built-ins are coming soon! ⏳ -## Setup & Pre-requisites +### Creating Your Own AdmitFunc + +The core type of the library is the [`AdmitFunc`](https://godoc.org/github.com/elithrar/admission-control#AdmitFunc) - a function that takes a k8s `AdmissionReview` object and returns an `(*AdmissionResponse, error)` tuple. You can provide a closure that returns an `AdmitFunc` type if you need to inject additional dependencies into your handler, and/or use a constructor function to do the same. + +The `AdmissionReview` type wraps the [`AdmissionRequest`](https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest), which can be serialized into a concrete type—such as a `Pod` or `Service`—and subsequently validated. + +An example `AdmitFunc` looks like this: + +```go +// DenyDefaultLoadBalancerSourceRanges denies any kind: Service of type: +// LoadBalancer that does not explicitly set .spec.loadBalancerSourceRanges - +// which defaults to 0.0.0.0/0 (e.g. Internet traffic, if routable). +// +// This prevents LoadBalancers from being accidentally exposed to the Internet. +func DenyDefaultLoadBalancerSourceRanges() AdmitFunc { + // Return a function of type AdmitFunc + return func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { + // do work + + // returning a non-nil AdmissionResponse and a nil error will allow admission. + + // returning an error will deny Admission; the error string will be + // provided to the client and should be clear about why we rejected + // them. + } +} +``` + +Tips: + +- Having your `AdmitFunc`s focus on "one" thing is best practice: it allows you to be more granular in how you apply constraints to your cluster +- Returning an `AdmitFunc` from a constructor/closure will allow you to inject dependencies and/or configuration into your handler. + +You can then create an [`AdmissionHandler`](https://godoc.org/github.com/elithrar/admission-control#AdmissionHandler) and pass it the `AdmitFunc`. Use your favorite HTTP router, and associate a path with your handler: + +```go + // We're using "gorilla/mux" as our router here. + r := mux.NewRouter().StrictSlash(true) + admissions := r.PathPrefix("/admission-control").Subrouter() + admissions.Handle("/deny-default-load-balancer-source-ranges", &admissioncontrol.AdmissionHandler{ + AdmitFunc: admissioncontrol.DenyDefaultLoadBalancerSourceRanges(), + Logger: logger, + }).Methods(http.MethodPost) +``` + +The example server [`admissiond`](https://github.com/elithrar/admission-control/tree/master/examples/admissiond) provides a more complete example of how to configure & serve your admission controller endpoints. + +--- + +## Configuring & Deploying a Server + +There are two ways to deploy an admission controller: + +1. Within your Kubernetes cluster ("in-cluster"), where it runs as a Pod and is exposed as a Service to the rest of the cluster. This requires you to provision a TLS keypair, as admission controllers can only be accessed over TLS (HTTPS). +2. Out-of-cluster, where it is accessible over HTTPS by the cluster. The admission controller could be hosted on another cluster, or more commonly, via a serverless platform like [Cloud Run](https://cloud.google.com/run/) or [Azure Container Instances](https://azure.microsoft.com/en-us/services/container-instances/). + +The documentation below covers deploying within a Kubernetes cluster (option 1). + +### Pre-requisites You'll need: @@ -27,9 +98,21 @@ You'll need: - Experience writing Go - for implementing your own `AdmitFuncs` (refer to the example `DenyPublicServices` AdmitFunc included). - Experience building OCI (Docker) containers via `docker build` or similar. +## Setup + +Setting up an Admission Controller in your Kubernetes cluster has three major steps: + +1. Generate a TLS keypair—Kubernetes only allows HTTPS (TLS) communication to Admission Controllers, whether in-cluster or hosted externally—and make the key & certificate available as a `Secret` within your cluster. + +2. Create a `Deployment` with your Admission-Control-based server, mounting the TLS keypair in your `Secret` as a volume in the container. + +3. Configure a `ValidatingWebhookConfiguration` that tells Kubernetes which objects should be validated, and the endpoint (URL) on your `Service` to validate them against. + +Your single server can act as the admission controller for any number of `ValidatingWebhookConfiguration` or `MutatingWebhookConfiguration` - each configuration can point to a specific URL on the same server. + ## Configuring a Server -> ⚠ *Note*: Admission webhooks must support HTTPS (TLS) connections; k8s does not allow webhooks to be reached over plain-text HTTP. If running in-cluster, the Service fronting the controller must be reachable via TCP port 443. External webhooks only need to satisfy the HTTPS requirement, but can be reached on any valid TCP port. +> ⚠ **Reminder**: Admission webhooks must support HTTPS (TLS) connections; k8s does not allow webhooks to be reached over plain-text HTTP. If running in-cluster, the Service fronting the controller must be reachable via TCP port 443. External webhooks only need to satisfy the HTTPS requirement, but can be reached on any valid TCP port. Having your k8s cluster create a TLS certificate for you will dramatically simplify the configuration, as self-signed certificates require you to provide a `.webhooks.clientConfig.caBundle` value for verification. @@ -73,6 +156,7 @@ webhooks: service: # This is the hostname our certificate needs in its Subject Alternative # Name array - name.namespace.svc + # If the certificate does NOT have this name, TLS validation will fail. name: admission-control-service namespace: default path: "/admission-control/deny-public-services" @@ -131,23 +215,6 @@ Error from server (hello-service does not have the cloud.google.com/load-balance Perfect! 🎉 -### Extending Things - -The core type of the library is the [`AdmitFunc`](https://godoc.org/github.com/elithrar/admission-control#AdmitFunc) - a function that takes a k8s `AdmissionReview` object and returns an `(*AdmissionResponse, error)` tuple. - -You can then pass pass the `AdmissionHandler` to your favorite HTTP router, and define a path that the function is avaiable on: - -```go - r := mux.NewRouter().StrictSlash(true) - admissions := r.PathPrefix("/admission-control").Subrouter() - admissions.Handle("/deny-public-services", &admissioncontrol.AdmissionHandler{ - AdmitFunc: admissioncontrol.DenyPublicServices, - Logger: logger, - }).Methods(http.MethodPost) -``` - -The example server [`admissiond`](https://github.com/elithrar/admission-control/tree/master/examples/admissiond) provides a step-by-step of how to configure & serve your admission controller endpoints. - --- ### Troubleshooting diff --git a/admit_funcs.go b/admit_funcs.go index ce825e1..ed24f6f 100644 --- a/admit_funcs.go +++ b/admit_funcs.go @@ -1,117 +1,199 @@ package admissioncontrol import ( - "encoding/json" - "errors" "fmt" admission "k8s.io/api/admission/v1beta1" core "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" ) +// CloudProvider represents supported cloud platforms for provider-specific +// configuration. +type CloudProvider int + const ( - // Docs: https://cloud.google.com/kubernetes-engine/docs/how-to/internal-load-balancing#overview - ilbAnnotationKey = "cloud.google.com/load-balancer-type" - ilbAnnotationVal = "Internal" + // GCP is a constant for Google Cloud Platform specific logic. + GCP CloudProvider = iota + // Azure is a constant for cloud-specific logic. + Azure + // AWS is a constant for Amazon Web Services specific logic. + AWS + // OpenStack is a constant for cloud-specific logic. + OpenStack ) -// DenyPublicServices rejects any Ingress objects, and rejects any Service -// objects of type LoadBalancer without a GCP Internal Load Balancer annotation. -func DenyPublicServices(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { - if admissionReview == nil || admissionReview.Request == nil { - return nil, errors.New("received invalid AdmissionReview") +// ilbAnnotations maps the annotation key:value pairs required to denote an +// internal-only load balancer on the supported cloud platforms. +// +// Docs: https://kubernetes.io/docs/concepts/services-networking/#internal-load-balancer +var ilbAnnotations = map[CloudProvider]map[string]string{ + GCP: {"cloud.google.com/load-balancer-type": "Internal"}, + Azure: {"service.beta.kubernetes.io/azure-load-balancer-internal": "true"}, + AWS: {"service.beta.kubernetes.io/aws-load-balancer-internal": "0.0.0.0/0"}, + OpenStack: {"service.beta.kubernetes.io/openstack-internal-load-balancer": "true"}, +} + +// newDefaultDenyResponse returns an AdmissionResponse with a Result sub-object, +// and defaults to allowed = false. +func newDefaultDenyResponse() *admission.AdmissionResponse { + return &admission.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{}, } +} - kind := admissionReview.Request.Kind.Kind // Base Kind - e.g. "Service" as opposed to "v1/Service" - resp := &admission.AdmissionResponse{ - Allowed: false, // Default deny +// DenyIngresses denies any kind: Ingress from being deployed to the cluster, +// except for any explicitly allowed namespaces (e.g. istio-system). +// +// Providing an empty/nil list of allowedNamespaces will reject Ingress objects +// across all namespaces. Kinds other than Ingress will be allowed. +func DenyIngresses(allowedNamespaces []string) AdmitFunc { + return func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { + kind := admissionReview.Request.Kind.Kind // Base Kind - e.g. "Service" as opposed to "v1/Service" + resp := newDefaultDenyResponse() + + switch kind { + case "Ingress": + ingress := extensionsv1beta1.Ingress{} + deserializer := serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer() + if _, _, err := deserializer.Decode(admissionReview.Request.Object.Raw, nil, &ingress); err != nil { + return nil, err + } + + // Allow Ingresses in whitelisted namespaces. + for _, ns := range allowedNamespaces { + if ingress.Namespace == ns { + resp.Allowed = true + resp.Result.Message = "allowing admission: %s namespace is whitelisted" + return resp, nil + } + } + + return nil, fmt.Errorf("%s objects cannot be deployed to this cluster", kind) + default: + resp.Allowed = true + return resp, nil + } } +} + +// DenyPublicLoadBalancers denies any non-internal public cloud load balancers +// (kind: Service of type: LoadBalancer) by looking for their "internal" load +// balancer annotations. This prevents accidentally exposing Services to the +// Internet for Kubernetes clusters designed to be internal-facing only. +// +// The required annotations are documented at +// https://kubernetes.io/docs/concepts/services-networking/#internal-load-balancer +// +// Services with a .spec.type other than LoadBalancer will NOT be rejected by this handler. +func DenyPublicLoadBalancers(allowedNamespaces []string, provider CloudProvider) AdmitFunc { + return func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { + kind := admissionReview.Request.Kind.Kind + resp := newDefaultDenyResponse() - switch kind { - case "Ingress": - return nil, fmt.Errorf("%s objects cannot be deployed to this cluster", kind) - case "Service": service := core.Service{} - if err := json.Unmarshal(admissionReview.Request.Object.Raw, &service); err != nil { + deserializer := serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer() + if _, _, err := deserializer.Decode(admissionReview.Request.Object.Raw, nil, &service); err != nil { return nil, err } - if service.Spec.Type == "LoadBalancer" { - if val, ok := service.ObjectMeta.Annotations[ilbAnnotationKey]; ok { - if val == ilbAnnotationVal { - resp.Allowed = true - return resp, nil - } + if service.Spec.Type != "LoadBalancer" { + resp.Allowed = true + resp.Result.Message = fmt.Sprintf( + "DenyPublicLoadBalancers received a non-LoadBalancer type (%s)", + service.Spec.Type, + ) + return resp, nil + } - // Not allowed when annotation value doesn't match. - resp.Allowed = false + // Don't deny Services in whitelisted namespaces + for _, ns := range allowedNamespaces { + if service.Namespace == ns { + // this namespace is whitelisted } + } + expectedAnnotations, ok := ilbAnnotations[provider] + if !ok { + return nil, fmt.Errorf("internal load balancer annotations for the given provider (%q) are not supported", provider) + } + + // If we're missing any annotations, provide them in the AdmissionResponse so + // the user can correct them. + if missing, ok := ensureHasAnnotations(expectedAnnotations, service.ObjectMeta.Annotations); !ok { + resp.Result.Message = fmt.Sprintf("%s object is missing the required annotations: %v", kind, missing) return nil, fmt.Errorf("%s objects of type: LoadBalancer without an internal-only annotation cannot be deployed to this cluster", kind) } - fallthrough - default: resp.Allowed = true - } - return resp, nil + return resp, nil + } } -// DenyPodWithoutAnnotations rejects Pods without the provided map of -// annotations (keys, values). The annotations must match exactly -// (case-sensitive). -// func DenyPodWithoutAnnotations(requiredAnnotations map[string]string) func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { -// admitFunc := func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { -// allowed := false +// ensureHasAnnotations checks whether the provided ObjectMeta has the required +// annotations. It returns both a map of missing annotations, and a boolean +// value if the meta had all of the provided annotations. // +// The required annotations are case-sensitive; an empty string for the map +// value will match on key (only) and thus allow any value. +func ensureHasAnnotations(required map[string]string, annotations map[string]string) (map[string]string, bool) { + missing := make(map[string]string) + for requiredKey, requiredVal := range required { + if existingVal, ok := annotations[requiredKey]; !ok { + // Missing a required annotation; add it to the list + missing[requiredKey] = requiredVal + } else { + // The key exists; does the value match? + if existingVal != requiredVal { + missing[requiredKey] = requiredVal + } + } + } + + // If we have any missing annotations, report them to the caller so the user + // can take action. + if len(missing) > 0 { + return missing, false + } + + return nil, true +} + +// func DenyContainersWithMutableTags(allowedNamespaces []string, allowedTags []string) AdmitFunc { +// return func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { // kind := admissionReview.Request.Kind.Kind -// // name := admissionReview.Request.Name -// resp := &admission.AdmissionResponse{ -// Allowed: allowed, -// } -// -// if kind == "Pod" { -// pod := core.Pod{} -// if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil { -// return nil, err -// } +// resp := newDefaultDenyResponse() // -// annotations := pod.ObjectMeta.Annotations -// missing := map[string]string{} -// for requiredKey, requiredVal := range requiredAnnotations { -// if meta.HasAnnotation(pod.ObjectMeta, requiredKey) { -// if annotations[requiredKey] != requiredVal { -// resp.Allowed = false -// // Required value does not match -// // Add to "missing" list to report back on -// } -// // Has key & matching value -// } -// // does not have key at all -// // add to "missing" list to report back on -// } +// TODO(matt): Range over Containers in a Pod spec, parse image URL and inspect tags. // -// if len(missing) == 0 { -// resp.Allowed = true -// } +// return resp, nil +// } +// } + +// func EnforcePodAnnotations(allowedNamespaces []string, matchFunc func(string, string) bool) AdmitFunc { +// return func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { +// kind := admissionReview.Request.Kind.Kind +// resp := newDefaultDenyResponse() // -// // for requiredKey, requiredVal := range requiredAnnotations { -// // if actualVal, ok := annotations[requiredKey]; ok { -// // if actualVal != requiredVal { -// // return nil, fmt.Errorf("the submitted %s (name: %s) is missing required annotations: %#v", kind, name, requiredAnnotations) -// // } -// // } else { -// // return nil, fmt.Errorf("the submitted %s (name: %s) is missing required annotations: %#v", kind, name, requiredAnnotations) -// // } -// // } -// } else { -// resp.Allowed = true -// } +// TODO(matt): enforce annotations on a Pod // // return resp, nil // } -// -// return admitFunc // } + +// func AddAnnotationsToPod(allowedNamespaces []string, newAnnotations map[string]string) AdmitFunc { +// return func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) { +// kind := admissionReview.Request.Kind.Kind +// resp := newDefaultDenyResponse() +// +// TODO(matt): Add annotations to the object's ObjectMeta. // +// return resp, nil +// } +// } diff --git a/admit_funcs_test.go b/admit_funcs_test.go index f14d48f..3599dc5 100644 --- a/admit_funcs_test.go +++ b/admit_funcs_test.go @@ -7,28 +7,125 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// TestDenyPublicServices checks that the correct kind, type & annotation combinations are valid for the AdmitFunc. -func TestDenyPublicServices(t *testing.T) { - var denyTests = []struct { - testName string - kind meta.GroupVersionKind - rawObject []byte - expectedMessage string - shouldAllow bool - }{ +type objectTest struct { + testName string + cloudProvider CloudProvider + kind meta.GroupVersionKind + rawObject []byte + expectedMessage string + shouldAllow bool +} + +func TestDenyIngress(t *testing.T) { + var deniedIngressError = "Ingress objects cannot be deployed to this cluster" + var denyTests = []objectTest{ { - testName: "Reject Ingress", + testName: "Reject Ingress (<= v1.13)", kind: meta.GroupVersionKind{ Group: "extensions", Kind: "Ingress", Version: "v1beta1", }, - rawObject: nil, - expectedMessage: "Ingress objects cannot be deployed to this cluster", + rawObject: []byte(`{"kind":"Ingress","apiVersion":"v1beta1","group":"extensions","metadata":{"name":"hello-ingress","namespace":"default","annotations":{}},"spec":{"rules":[]}}`), + expectedMessage: deniedIngressError, shouldAllow: false, }, { - testName: "Reject Public Service", + testName: "Don't reject Services", + kind: meta.GroupVersionKind{ + Group: "", + Kind: "Service", + Version: "v1", + }, + rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), + expectedMessage: "Service objects of type: LoadBalancer without an internal-only annotation cannot be deployed to this cluster", + shouldAllow: true, + }, + { + testName: "Don't reject Pods", + kind: meta.GroupVersionKind{ + Group: "", + Kind: "Pod", + Version: "v1", + }, + rawObject: nil, + expectedMessage: "", + shouldAllow: true, + }, + { + testName: "Don't reject Deployments", + kind: meta.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }, + rawObject: nil, + expectedMessage: "", + shouldAllow: true, + }, + } + + for _, tt := range denyTests { + t.Run(tt.testName, func(t *testing.T) { + incomingReview := admission.AdmissionReview{ + Request: &admission.AdmissionRequest{}, + } + incomingReview.Request.Kind = tt.kind + incomingReview.Request.Object.Raw = tt.rawObject + + resp, err := DenyIngresses(nil)(&incomingReview) + if err != nil { + if tt.expectedMessage != err.Error() { + t.Fatalf("error message does not match: got %q - expected %q", err.Error(), tt.expectedMessage) + } + + if tt.shouldAllow { + t.Fatalf("incorrectly rejected admission for %s (kind: %v): %s", tt.testName, tt.kind, err.Error()) + } + + t.Logf("correctly rejected admission for %s (kind: %v): %s", tt.testName, tt.kind, err.Error()) + return + } + + if resp.Allowed != tt.shouldAllow { + t.Fatalf("admission mismatch for (kind: %v): got Allowed: %t, wanted %t", tt.kind, resp.Allowed, tt.shouldAllow) + } + }) + } + +} + +// TestDenyPublicServices checks that the correct kind, type & annotation combinations are valid for the AdmitFunc. +func TestDenyPublicLoadBalancers(t *testing.T) { + var missingLBAnnotationsMessage = "Service objects of type: LoadBalancer without an internal-only annotation cannot be deployed to this cluster" + + var denyTests = []objectTest{ + { + testName: "Don't reject Ingress (<= v1.13)", + kind: meta.GroupVersionKind{ + Group: "extensions", + Kind: "Ingress", + Version: "v1beta1", + }, + rawObject: []byte(`{"kind":"Ingress","apiVersion":"v1beta1","group":"extensions","metadata":{"name":"hello-ingress","namespace":"default","annotations":{}},"spec":{"rules":[]}}`), + expectedMessage: "", + shouldAllow: true, + }, + { + + testName: "Don't reject Ingress (>= v1.14)", + kind: meta.GroupVersionKind{ + Group: "networking.k8s.io", + Kind: "Ingress", + Version: "v1beta1", + }, + rawObject: []byte(`{"kind":"Ingress","apiVersion":"v1beta1","group":"networking.k8s.io","metadata":{"name":"hello-ingress","namespace":"default","annotations":{}},"spec":{"rules":[]}}`), + expectedMessage: "", + shouldAllow: true, + }, + { + testName: "Reject Public Service", + cloudProvider: GCP, kind: meta.GroupVersionKind{ Group: "", Kind: "Service", @@ -39,29 +136,79 @@ func TestDenyPublicServices(t *testing.T) { shouldAllow: false, }, { - testName: "Allow Annotated Private Service", + testName: "Allow Annotated Private Service (GCP)", + cloudProvider: GCP, kind: meta.GroupVersionKind{ Group: "", Kind: "Service", Version: "v1", }, - rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{"cloud.google.com/load-balancer-type": "Internal"}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), + rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{"cloud.google.com/load-balancer-type":"Internal"}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), expectedMessage: "", shouldAllow: true, }, { - testName: "Reject Incorrectly Annotated Private Service", + testName: "Allow Annotated Private Service (Azure)", + cloudProvider: Azure, + kind: meta.GroupVersionKind{ + Group: "", + Kind: "Service", + Version: "v1", + }, + rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{"service.beta.kubernetes.io/azure-load-balancer-internal":"true"}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), + expectedMessage: "", + shouldAllow: true, + }, + { + testName: "Allow Annotated Private Service (AWS)", + cloudProvider: AWS, + kind: meta.GroupVersionKind{ + Group: "", + Kind: "Service", + Version: "v1", + }, + rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{"service.beta.kubernetes.io/aws-load-balancer-internal":"0.0.0.0/0"}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), + expectedMessage: missingLBAnnotationsMessage, + shouldAllow: true, + }, + { + testName: "Reject Incorrectly Annotated Private Service (no annotation)", + cloudProvider: GCP, kind: meta.GroupVersionKind{ Group: "", Kind: "Service", Version: "v1", }, rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{"cloud.google.com/load-balancer-type": ""}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), + expectedMessage: missingLBAnnotationsMessage, + shouldAllow: false, + }, + { + testName: "Reject Incorrectly Annotated Private Service (missing annotation val)", + cloudProvider: GCP, + kind: meta.GroupVersionKind{ + Group: "", + Kind: "Service", + Version: "v1", + }, + rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{"cloud.google.com/load-balancer-type": ""}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), + expectedMessage: missingLBAnnotationsMessage, + shouldAllow: false, + }, + { + testName: "Reject Incorrectly Annotated Private Service (Azure provider, AWS annotation)", + cloudProvider: Azure, + kind: meta.GroupVersionKind{ + Group: "", + Kind: "Service", + Version: "v1", + }, + rawObject: []byte(`{"kind":"Service","apiVersion":"v1","metadata":{"name":"hello-service","namespace":"default","annotations":{"service.beta.kubernetes.io/aws-load-balancer-internal": "0.0.0.0/0"}},"spec":{"ports":[{"protocol":"TCP","port":8000,"targetPort":8080,"nodePort":31433}],"selector":{"app":"hello-app"},"type":"LoadBalancer","externalTrafficPolicy":"Cluster"}}`), expectedMessage: "Service objects of type: LoadBalancer without an internal-only annotation cannot be deployed to this cluster", shouldAllow: false, }, { - testName: "Allow Pods", + testName: "Don't reject Pods", kind: meta.GroupVersionKind{ Group: "", Kind: "Pod", @@ -72,7 +219,7 @@ func TestDenyPublicServices(t *testing.T) { shouldAllow: true, }, { - testName: "Allow Deployments", + testName: "Don't reject Deployments", kind: meta.GroupVersionKind{ Group: "apps", Kind: "Deployment", @@ -92,7 +239,7 @@ func TestDenyPublicServices(t *testing.T) { incomingReview.Request.Kind = tt.kind incomingReview.Request.Object.Raw = tt.rawObject - resp, err := DenyPublicServices(&incomingReview) + resp, err := DenyPublicLoadBalancers(nil, tt.cloudProvider)(&incomingReview) if err != nil { if tt.expectedMessage != err.Error() { t.Fatalf("error message does not match: got %q - expected %q", err.Error(), tt.expectedMessage) @@ -107,7 +254,7 @@ func TestDenyPublicServices(t *testing.T) { } if resp.Allowed != tt.shouldAllow { - t.Fatalf("incorrectly allowed admission for %s (kind: %v): %s", tt.testName, tt.kind, resp.String()) + t.Fatalf("admission mismatch for (kind: %v): got Allowed: %t, wanted %t", tt.kind, resp.Allowed, tt.shouldAllow) } }) } diff --git a/examples/admissiond/main.go b/examples/admissiond/main.go index 368aa6a..67fd414 100644 --- a/examples/admissiond/main.go +++ b/examples/admissiond/main.go @@ -27,9 +27,9 @@ func main() { // Get config conf := &conf{} flag.StringVar(&conf.TLSCertPath, "cert-path", "./cert.crt", "The path to the PEM-encoded TLS certificate") - flag.StringVar(&conf.TLSKeyPath, "key-path", "./key.key", "The path to the unencrypted TLS key.") + flag.StringVar(&conf.TLSKeyPath, "key-path", "./key.key", "The path to the unencrypted TLS key") flag.StringVar(&conf.Port, "port", "8443", "The port to listen on (HTTPS).") - flag.StringVar(&conf.Host, "host", "admissiond.questionable.services", "The hostname for the service.") + flag.StringVar(&conf.Host, "host", "admissiond.questionable.services", "The hostname for the service") flag.Parse() // Set up logging @@ -50,26 +50,39 @@ func main() { // Set up the routes & logging middleware. r := mux.NewRouter().StrictSlash(true) - r.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }, ).Methods(http.MethodGet) admissions := r.PathPrefix("/admission-control").Subrouter() - admissions.Handle("/deny-public-services", &admissioncontrol.AdmissionHandler{ - AdmitFunc: admissioncontrol.DenyPublicServices, + admissions.Handle("/deny-ingresses", &admissioncontrol.AdmissionHandler{ + AdmitFunc: admissioncontrol.DenyIngresses(nil), + Logger: logger, + }).Methods(http.MethodPost) + admissions.Handle("/deny-public-services/gcp", &admissioncontrol.AdmissionHandler{ + // nil = don't whitelist any namespace. + AdmitFunc: admissioncontrol.DenyPublicLoadBalancers(nil, admissioncontrol.GCP), + Logger: logger, + }).Methods(http.MethodPost) + admissions.Handle("/deny-public-services/azure", &admissioncontrol.AdmissionHandler{ + AdmitFunc: admissioncontrol.DenyPublicLoadBalancers(nil, admissioncontrol.Azure), + Logger: logger, + }).Methods(http.MethodPost) + admissions.Handle("/deny-public-services/aws", &admissioncontrol.AdmissionHandler{ + AdmitFunc: admissioncontrol.DenyPublicLoadBalancers(nil, admissioncontrol.AWS), Logger: logger, }).Methods(http.MethodPost) // HTTP server + timeout := time.Second * 15 srv := &http.Server{ Handler: admissioncontrol.LoggingMiddleware(logger)(r), TLSConfig: tlsConf, Addr: ":" + conf.Port, - IdleTimeout: time.Second * 15, - ReadTimeout: time.Second * 15, - ReadHeaderTimeout: time.Second * 15, - WriteTimeout: time.Second * 15, + IdleTimeout: timeout, + ReadTimeout: timeout, + ReadHeaderTimeout: timeout, + WriteTimeout: timeout, } admissionServer, err := admissioncontrol.NewServer( diff --git a/go.mod b/go.mod index dc6f4b5..5d18f1c 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,36 @@ module github.com/elithrar/admission-control go 1.12 require ( - github.com/go-kit/kit v0.8.0 + github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect + github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect + github.com/emicklei/go-restful v2.9.6+incompatible // indirect + github.com/evanphx/json-patch v4.5.0+incompatible // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-kit/kit v0.9.0 github.com/go-logfmt/logfmt v0.4.0 // indirect + github.com/go-openapi/spec v0.19.2 // indirect + github.com/go-openapi/swag v0.19.4 // indirect github.com/go-stack/stack v1.8.0 // indirect - github.com/gorilla/mux v1.7.2 - k8s.io/api v0.0.0-20190620073856-dcce3486da33 - k8s.io/apimachinery v0.0.0-20190620073744-d16981aedf33 + github.com/gogo/protobuf v1.2.1 // indirect + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + github.com/golang/protobuf v1.3.2 // indirect + github.com/googleapis/gnostic v0.3.0 // indirect + github.com/gorilla/mux v1.7.3 + github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/kisielk/errcheck v1.2.0 // indirect + github.com/kr/pty v1.1.8 // indirect + github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect + github.com/munnerz/goautoneg v0.0.0-20190414153302-2ae31c8b6b30 // indirect + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect + golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect + golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect + golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/api v0.0.0-20190720062849-3043179095b6 + k8s.io/apimachinery v0.0.0-20190719140911-bfcf53abc9f8 + k8s.io/gengo v0.0.0-20190327210449-e17681d19d3a // indirect + k8s.io/klog v0.3.3 // indirect + k8s.io/kube-openapi v0.0.0-20190718094010-3cf2ea392886 // indirect + sigs.k8s.io/structured-merge-diff v0.0.0-20190719182312-e94e05bfbbe3 // indirect ) diff --git a/go.sum b/go.sum index 5246888..e94ac2f 100644 --- a/go.sum +++ b/go.sum @@ -1,77 +1,177 @@ +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.4/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM= github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20190414153302-2ae31c8b6b30/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI= golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= k8s.io/api v0.0.0-20190620073856-dcce3486da33 h1:aC/EvF9PT1h8NeMEOVwTel8xxbZwq0SZnxXNThEROnE= k8s.io/api v0.0.0-20190620073856-dcce3486da33/go.mod h1:ldk709UQo/iedNLOW7J06V9QSSGY5heETKeWqnPoqF8= +k8s.io/api v0.0.0-20190720062849-3043179095b6 h1:3C9EiZRqH+JKCzZqxuthj5C3q3cw61SlRr2z8XeSfmQ= +k8s.io/api v0.0.0-20190720062849-3043179095b6/go.mod h1:1O0xzX/RAtnm7l+5VEUxZ1ysO2ghatfq/OZED4zM9kA= k8s.io/apimachinery v0.0.0-20190620073744-d16981aedf33 h1:Lkd+QNFOB3DqrDyWo796aodJgFJautn/M+t9IGearPc= k8s.io/apimachinery v0.0.0-20190620073744-d16981aedf33/go.mod h1:9q5NW/mMno/nwbRZd/Ks2TECgi2PTZ9cwarf4q+ze6Q= +k8s.io/apimachinery v0.0.0-20190719140911-bfcf53abc9f8 h1:fVMoqaOPZ6KTeszBSBO8buFmXaR2JlnMn53eEBeganU= +k8s.io/apimachinery v0.0.0-20190719140911-bfcf53abc9f8/go.mod h1:sBJWIJZfxLhp7mRsRyuAE/NfKTr3kXGR1iaqg8O0gJo= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190327210449-e17681d19d3a/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68= k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.3 h1:niceAagH1tzskmaie/icWd7ci1wbG7Bf2c6YGcQv+3c= +k8s.io/klog v0.3.3/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= +k8s.io/kube-openapi v0.0.0-20190718094010-3cf2ea392886/go.mod h1:RZvgC8MSN6DjiMV6oIfEE9pDL9CYXokkfaCKZeHm3nc= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190719182312-e94e05bfbbe3/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/handler.go b/handler.go index 9e53361..613c8c3 100644 --- a/handler.go +++ b/handler.go @@ -2,6 +2,7 @@ package admissioncontrol import ( "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -32,15 +33,15 @@ type AdmissionHandler struct { Logger log.Logger // LimitBytes limits the size of objects the webhook will handle. LimitBytes int64 - // Deserializer supports deserializing k8s objects. It can be left null; the + // deserializer supports deserializing k8s objects. It can be left null; the // ServeHTTP function will lazily instantiate a decoder instance. - Deserializer runtime.Decoder + deserializer runtime.Decoder } func (ah *AdmissionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if ah.Deserializer == nil { + if ah.deserializer == nil { runtimeScheme := runtime.NewScheme() - ah.Deserializer = serializer.NewCodecFactory(runtimeScheme).UniversalDeserializer() + ah.deserializer = serializer.NewCodecFactory(runtimeScheme).UniversalDeserializer() } if ah.LimitBytes <= 0 { @@ -111,10 +112,13 @@ func (ah *AdmissionHandler) handleAdmissionRequest(w http.ResponseWriter, r *htt } incomingReview := admission.AdmissionReview{} - if _, _, err := ah.Deserializer.Decode(body, nil, &incomingReview); err != nil { + if _, _, err := ah.deserializer.Decode(body, nil, &incomingReview); err != nil { return AdmissionError{false, "decoding the review request failed", err.Error()} } + if incomingReview.Request == nil { + return errors.New("received invalid AdmissionReview") + } reviewResponse, err := ah.AdmitFunc(&incomingReview) if err != nil { return AdmissionError{false, err.Error(), "the AdmitFunc returned an error"} diff --git a/samples/admission-control-service.yaml b/samples/admission-control-service.yaml index db701eb..274662b 100644 --- a/samples/admission-control-service.yaml +++ b/samples/admission-control-service.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: admission-controld - image: gcr.io/optimum-rock-145719/admissiond:latest + image: gcr.io/optimum-rock-145719/admissiond-example:latest command: ["/admissiond"] env: - name: HOSTNAME diff --git a/samples/deny-ingress-public-lbs-config.yaml b/samples/deny-ingress-public-lbs-config.yaml new file mode 100644 index 0000000..243ab59 --- /dev/null +++ b/samples/deny-ingress-public-lbs-config.yaml @@ -0,0 +1,64 @@ +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: prevent-exposing-services +webhooks: + - name: deny-ingresses.questionable.services + sideEffects: None + # "Equivalent" provides insurance against API version upgrades/changes - e.g. + # extensions/v1beta1 Ingress -> networking.k8s.io/v1beta1 Ingress + # matchPolicy: Equivalent + rules: + - apiGroups: + - "extensions" + - "networking.k8s.io" + apiVersions: + - "*" + operations: + - "CREATE" + - "UPDATE" + resources: + - "ingresses" + failurePolicy: Fail + clientConfig: + service: + # This is the hostname our certificate needs in its Subject Alternative + # Name array - name.namespace.svc + # If the certificate does NOT have this name, TLS validation will fail. + name: admission-control-service + namespace: default + path: "/admission-control/deny-ingresses" + # This should be the CA certificate from your Kubernetes cluster + # Use the below to generate the certificate in a valid format: + # $ kubectl config view --raw --minify --flatten \ + # -o jsonpath='{.clusters[].cluster.certificate-authority-data}' + caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURERENDQWZTZ0F3SUJBZ0lSQVB3S1JwRkJZdlZEQlJpRXVUNExrcDh3RFFZSktvWklodmNOQVFFTEJRQXcKTHpFdE1Dc0dBMVVFQXhNa05qUXhNMk14T0RndE9UUTVOUzAwT0dJNUxUZzRNVGt0WXpKaFpUTXlPREkzTkRjeQpNQjRYRFRFNU1EWXlNekl3TXpBek9Gb1hEVEkwTURZeU1USXhNekF6T0Zvd0x6RXRNQ3NHQTFVRUF4TWtOalF4Ck0yTXhPRGd0T1RRNU5TMDBPR0k1TFRnNE1Ua3RZekpoWlRNeU9ESTNORGN5TUlJQklqQU5CZ2txaGtpRzl3MEIKQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTdCa1JlM1VRWjNkaWQzaEl3a3ZmaXJNTUVNcCs3eEw4Uytrc3RkVgpVZEwrY0NQb01KeGluS25CZTA0Qmdzckp6bS96TWpMbmIrcTZBV0VFQTUxbk1jSnp2eWFSSFZkQmNsbU1zTlc2Cm95OC9vci9pWDhSNjYxcllUTlVibHFsa3JqSWNxRE9naGJNSzYwbUgzZnlmZlBZcmJUVUs3b3JiQlAvaitPdk8KcEFZYnNQTjh3R2hoZ2Q3K2pGL3JPWlN2amVHZHY0eG9UeUpMZU5QSzJJb3FrWWQ5eENLR2lMNktpaWhXbi9HYgozM3kzWkpJVVZucGFjdTNQODVNanJEVDJSam1vaklIRjgvWHh6VHQ0c2tXQUllZXV1MWhocDJ5WkFuRXRjUVR0CjMvSUIzYm84blJMaUxMRWJmZDJVcnE0ZzlEdkVFK1pOSEZuUFI3dE1POE5GVFFJREFRQUJveU13SVRBT0JnTlYKSFE4QkFmOEVCQU1DQWdRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQQpsL2N5UDROVGVIdkUvT25DcTM3WlBqL2pMbWpsZVlUck1wZ0NrTk1DNG5oQ0c4R3pxTkZtQ3V6TmZWUGliMmYvCi9wSUhRSmZBTUxzWVg3NlpGQzFoMGE1Mm9Db0kzeFpScTVJTGNIWnRZOXAwYm5HQ2EzYUFEcnQ0OUZOVUN3N2QKYTdsOHgrQ3NGQk1lRlZ0dm96RnVaUE1uWnlxNkw1Y2swRnBNd2tQT1VwYjE5bjdQYW1QSGJSbkRVNThVQTlGbApHbkRZdGlBMGZJdFhnbDJwTjVCd1orNlRiOS9FdW1GOU5VQUMvV1ZkUGJ1VTBqK2RPcWwvelQyTHlsRndSVXUzCjVkUWxSRE5LVTgwY2pVSzlqd1Z2U0txVUtReHdFdTRHMWFwM3E2MDdFZFlSTVNuT2NOdzRSeDU2Qm5rdVo2V2oKZmNwQXF4Q2FTYkRqMVEyZk01eERadz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" + # You can alternatively supply a URL to the service, as long as its reachable by the cluster. + # url: "" + - name: deny-public-load-balancers.questionable.services + sideEffects: None + # matchPolicy: Equivalent + rules: + - apiGroups: + - "*" + apiVersions: + - "v1" + operations: + - "CREATE" + - "UPDATE" + resources: + - "services" + failurePolicy: Fail + clientConfig: + service: + # This is the hostname our certificate needs in its Subject Alternative + # Name array - name.namespace.svc + # If the certificate does NOT have this name, TLS validation will fail. + name: admission-control-service + namespace: default + path: "/admission-control/deny-public-services/gcp" + # This should be the CA certificate from your Kubernetes cluster + # Use the below to generate the certificate in a valid format: + # $ kubectl config view --raw --minify --flatten \ + # -o jsonpath='{.clusters[].cluster.certificate-authority-data}' + caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURERENDQWZTZ0F3SUJBZ0lSQVB3S1JwRkJZdlZEQlJpRXVUNExrcDh3RFFZSktvWklodmNOQVFFTEJRQXcKTHpFdE1Dc0dBMVVFQXhNa05qUXhNMk14T0RndE9UUTVOUzAwT0dJNUxUZzRNVGt0WXpKaFpUTXlPREkzTkRjeQpNQjRYRFRFNU1EWXlNekl3TXpBek9Gb1hEVEkwTURZeU1USXhNekF6T0Zvd0x6RXRNQ3NHQTFVRUF4TWtOalF4Ck0yTXhPRGd0T1RRNU5TMDBPR0k1TFRnNE1Ua3RZekpoWlRNeU9ESTNORGN5TUlJQklqQU5CZ2txaGtpRzl3MEIKQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTdCa1JlM1VRWjNkaWQzaEl3a3ZmaXJNTUVNcCs3eEw4Uytrc3RkVgpVZEwrY0NQb01KeGluS25CZTA0Qmdzckp6bS96TWpMbmIrcTZBV0VFQTUxbk1jSnp2eWFSSFZkQmNsbU1zTlc2Cm95OC9vci9pWDhSNjYxcllUTlVibHFsa3JqSWNxRE9naGJNSzYwbUgzZnlmZlBZcmJUVUs3b3JiQlAvaitPdk8KcEFZYnNQTjh3R2hoZ2Q3K2pGL3JPWlN2amVHZHY0eG9UeUpMZU5QSzJJb3FrWWQ5eENLR2lMNktpaWhXbi9HYgozM3kzWkpJVVZucGFjdTNQODVNanJEVDJSam1vaklIRjgvWHh6VHQ0c2tXQUllZXV1MWhocDJ5WkFuRXRjUVR0CjMvSUIzYm84blJMaUxMRWJmZDJVcnE0ZzlEdkVFK1pOSEZuUFI3dE1POE5GVFFJREFRQUJveU13SVRBT0JnTlYKSFE4QkFmOEVCQU1DQWdRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQQpsL2N5UDROVGVIdkUvT25DcTM3WlBqL2pMbWpsZVlUck1wZ0NrTk1DNG5oQ0c4R3pxTkZtQ3V6TmZWUGliMmYvCi9wSUhRSmZBTUxzWVg3NlpGQzFoMGE1Mm9Db0kzeFpScTVJTGNIWnRZOXAwYm5HQ2EzYUFEcnQ0OUZOVUN3N2QKYTdsOHgrQ3NGQk1lRlZ0dm96RnVaUE1uWnlxNkw1Y2swRnBNd2tQT1VwYjE5bjdQYW1QSGJSbkRVNThVQTlGbApHbkRZdGlBMGZJdFhnbDJwTjVCd1orNlRiOS9FdW1GOU5VQUMvV1ZkUGJ1VTBqK2RPcWwvelQyTHlsRndSVXUzCjVkUWxSRE5LVTgwY2pVSzlqd1Z2U0txVUtReHdFdTRHMWFwM3E2MDdFZFlSTVNuT2NOdzRSeDU2Qm5rdVo2V2oKZmNwQXF4Q2FTYkRqMVEyZk01eERadz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" diff --git a/samples/deny-public-webhook-config.yaml b/samples/deny-public-webhook-config.yaml deleted file mode 100644 index 49dea5b..0000000 --- a/samples/deny-public-webhook-config.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: admissionregistration.k8s.io/v1beta1 -kind: ValidatingWebhookConfiguration -metadata: - name: deny-public-services -webhooks: - - name: deny-public-services.questionable.services - sideEffects: None - rules: - # This is an intentionally broad webhook configuration that is designed to - # capture subresources as well, in order to correctly validate all possible - # matches - - apiGroups: - - "*" - apiVersions: - - "*" - operations: - - "CREATE" - - "UPDATE" - resources: - - "services" - - "ingresses" - failurePolicy: Fail - clientConfig: - service: - name: admission-control-service - namespace: default - path: "/admission-control/deny-public-services" - caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURERENDQWZTZ0F3SUJBZ0lSQVB3S1JwRkJZdlZEQlJpRXVUNExrcDh3RFFZSktvWklodmNOQVFFTEJRQXcKTHpFdE1Dc0dBMVVFQXhNa05qUXhNMk14T0RndE9UUTVOUzAwT0dJNUxUZzRNVGt0WXpKaFpUTXlPREkzTkRjeQpNQjRYRFRFNU1EWXlNekl3TXpBek9Gb1hEVEkwTURZeU1USXhNekF6T0Zvd0x6RXRNQ3NHQTFVRUF4TWtOalF4Ck0yTXhPRGd0T1RRNU5TMDBPR0k1TFRnNE1Ua3RZekpoWlRNeU9ESTNORGN5TUlJQklqQU5CZ2txaGtpRzl3MEIKQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTdCa1JlM1VRWjNkaWQzaEl3a3ZmaXJNTUVNcCs3eEw4Uytrc3RkVgpVZEwrY0NQb01KeGluS25CZTA0Qmdzckp6bS96TWpMbmIrcTZBV0VFQTUxbk1jSnp2eWFSSFZkQmNsbU1zTlc2Cm95OC9vci9pWDhSNjYxcllUTlVibHFsa3JqSWNxRE9naGJNSzYwbUgzZnlmZlBZcmJUVUs3b3JiQlAvaitPdk8KcEFZYnNQTjh3R2hoZ2Q3K2pGL3JPWlN2amVHZHY0eG9UeUpMZU5QSzJJb3FrWWQ5eENLR2lMNktpaWhXbi9HYgozM3kzWkpJVVZucGFjdTNQODVNanJEVDJSam1vaklIRjgvWHh6VHQ0c2tXQUllZXV1MWhocDJ5WkFuRXRjUVR0CjMvSUIzYm84blJMaUxMRWJmZDJVcnE0ZzlEdkVFK1pOSEZuUFI3dE1POE5GVFFJREFRQUJveU13SVRBT0JnTlYKSFE4QkFmOEVCQU1DQWdRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQQpsL2N5UDROVGVIdkUvT25DcTM3WlBqL2pMbWpsZVlUck1wZ0NrTk1DNG5oQ0c4R3pxTkZtQ3V6TmZWUGliMmYvCi9wSUhRSmZBTUxzWVg3NlpGQzFoMGE1Mm9Db0kzeFpScTVJTGNIWnRZOXAwYm5HQ2EzYUFEcnQ0OUZOVUN3N2QKYTdsOHgrQ3NGQk1lRlZ0dm96RnVaUE1uWnlxNkw1Y2swRnBNd2tQT1VwYjE5bjdQYW1QSGJSbkRVNThVQTlGbApHbkRZdGlBMGZJdFhnbDJwTjVCd1orNlRiOS9FdW1GOU5VQUMvV1ZkUGJ1VTBqK2RPcWwvelQyTHlsRndSVXUzCjVkUWxSRE5LVTgwY2pVSzlqd1Z2U0txVUtReHdFdTRHMWFwM3E2MDdFZFlSTVNuT2NOdzRSeDU2Qm5rdVo2V2oKZmNwQXF4Q2FTYkRqMVEyZk01eERadz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" - # You can alternatively supply a URL to the service, as long as its reachable by the cluster. - # url: "" diff --git a/samples/ingress-example.yaml b/samples/ingress.yaml similarity index 100% rename from samples/ingress-example.yaml rename to samples/ingress.yaml diff --git a/samples/private-service.yaml b/samples/private-lb-gke.yaml similarity index 90% rename from samples/private-service.yaml rename to samples/private-lb-gke.yaml index 0a9697d..e5060a0 100644 --- a/samples/private-service.yaml +++ b/samples/private-lb-gke.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: hello-service + name: internal-service annotations: "cloud.google.com/load-balancer-type": "Internal" spec: diff --git a/samples/public-service.yaml b/samples/public-lb.yaml similarity index 88% rename from samples/public-service.yaml rename to samples/public-lb.yaml index 2af53fb..ec2f7af 100644 --- a/samples/public-service.yaml +++ b/samples/public-lb.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: hello-service + name: public-service spec: type: LoadBalancer selector: diff --git a/server.go b/server.go index f44315b..e3a58b2 100644 --- a/server.go +++ b/server.go @@ -48,8 +48,12 @@ func NewServer(srv *http.Server, logger log.Logger) (*AdmissionServer, error) { return nil, errors.New("a non-nil *http.Server must be provided") } + // TODO(matt): Should warn here & support plaintext HTTP for proxied environments if srv.TLSConfig == nil { - return nil, errors.New("the provided *http.Server has a nil TLSConfig. Admission webhooks must be served over TLS") + // Warn that TLS termination is required + logger.Log( + "msg", "the provided *http.Server has a nil TLSConfig. Admission webhooks must be served over TLS, or from behind a TLS-terminating proxy", + ) } if logger == nil { @@ -94,15 +98,30 @@ func (as *AdmissionServer) Run(ctx context.Context) error { "msg", fmt.Sprintf("admission control listening on '%s'", as.srv.Addr), ) - if err := as.srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { - errs <- err - as.logger.Log( - "err", err.Error(), - "msg", "the server exited", - ) - return + // Start a plaintext listener if no TLSConfig is provided + switch as.srv.TLSConfig { + case nil: + if err := as.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errs <- err + as.logger.Log( + "err", err.Error(), + "msg", "the server exited", + ) + return + } + default: + if err := as.srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + errs <- err + as.logger.Log( + "err", err.Error(), + "msg", "the server exited", + ) + return + } } + // TODO(matt): Listen as plaintext if no TLSConfig is provided. + return }()