Skip to main content
Redhat Developers  Logo
  • Products

    Featured

    • Red Hat Enterprise Linux
      Red Hat Enterprise Linux Icon
    • Red Hat OpenShift AI
      Red Hat OpenShift AI
    • Red Hat Enterprise Linux AI
      Linux icon inside of a brain
    • Image mode for Red Hat Enterprise Linux
      RHEL image mode
    • Red Hat OpenShift
      Openshift icon
    • Red Hat Ansible Automation Platform
      Ansible icon
    • Red Hat Developer Hub
      Developer Hub
    • View All Red Hat Products
    • Linux

      • Red Hat Enterprise Linux
      • Image mode for Red Hat Enterprise Linux
      • Red Hat Universal Base Images (UBI)
    • Java runtimes & frameworks

      • JBoss Enterprise Application Platform
      • Red Hat build of OpenJDK
    • Kubernetes

      • Red Hat OpenShift
      • Microsoft Azure Red Hat OpenShift
      • Red Hat OpenShift Virtualization
      • Red Hat OpenShift Lightspeed
    • Integration & App Connectivity

      • Red Hat Build of Apache Camel
      • Red Hat Service Interconnect
      • Red Hat Connectivity Link
    • AI/ML

      • Red Hat OpenShift AI
      • Red Hat Enterprise Linux AI
    • Automation

      • Red Hat Ansible Automation Platform
      • Red Hat Ansible Lightspeed
    • Developer tools

      • Red Hat Trusted Software Supply Chain
      • Podman Desktop
      • Red Hat OpenShift Dev Spaces
    • Developer Sandbox

      Developer Sandbox
      Try Red Hat products and technologies without setup or configuration fees for 30 days with this shared Openshift and Kubernetes cluster.
    • Try at no cost
  • Technologies

    Featured

    • AI/ML
      AI/ML Icon
    • Linux
      Linux Icon
    • Kubernetes
      Cloud icon
    • Automation
      Automation Icon showing arrows moving in a circle around a gear
    • View All Technologies
    • Programming Languages & Frameworks

      • Java
      • Python
      • JavaScript
    • System Design & Architecture

      • Red Hat architecture and design patterns
      • Microservices
      • Event-Driven Architecture
      • Databases
    • Developer Productivity

      • Developer productivity
      • Developer Tools
      • GitOps
    • Secure Development & Architectures

      • Security
      • Secure coding
    • Platform Engineering

      • DevOps
      • DevSecOps
      • Ansible automation for applications and services
    • Automated Data Processing

      • AI/ML
      • Data Science
      • Apache Kafka on Kubernetes
      • View All Technologies
    • Start exploring in the Developer Sandbox for free

      sandbox graphic
      Try Red Hat's products and technologies without setup or configuration.
    • Try at no cost
  • Learn

    Featured

    • Kubernetes & Cloud Native
      Openshift icon
    • Linux
      Rhel icon
    • Automation
      Ansible cloud icon
    • Java
      Java icon
    • AI/ML
      AI/ML Icon
    • View All Learning Resources

    E-Books

    • GitOps Cookbook
    • Podman in Action
    • Kubernetes Operators
    • The Path to GitOps
    • View All E-books

    Cheat Sheets

    • Linux Commands
    • Bash Commands
    • Git
    • systemd Commands
    • View All Cheat Sheets

    Documentation

    • API Catalog
    • Product Documentation
    • Legacy Documentation
    • Red Hat Learning

      Learning image
      Boost your technical skills to expert-level with the help of interactive lessons offered by various Red Hat Learning programs.
    • Explore Red Hat Learning
  • Developer Sandbox

    Developer Sandbox

    • Access Red Hat’s products and technologies without setup or configuration, and start developing quicker than ever before with our new, no-cost sandbox environments.
    • Explore Developer Sandbox

    Featured Developer Sandbox activities

    • Get started with your Developer Sandbox
    • OpenShift virtualization and application modernization using the Developer Sandbox
    • Explore all Developer Sandbox activities

    Ready to start developing apps?

    • Try at no cost
  • Blog
  • Events
  • Videos

Kubernetes admission control with validating webhooks

September 17, 2021
Ricardo Lourenço
Related topics:
DevOpsDevSecOpsGoKubernetesSecurity
Related products:
Red Hat OpenShift

Share:

    This article describes how to write, configure, and install a simple Kubernetes validating admission webhook. The webhook intercepts and validates PrometheusRule object creation requests to prevent users from creating rules with invalid fields.

    A key benefit of this approach is that your clusters will only contain prevalidated user-defined rules, resulting in uncluttered configuration across environments. Additionally, imagine there is an external alerting system that leverages fields in these customer-provided rules to make alerting decisions. It is important to ensure the rules are properly formatted, so the alerts are forwarded to the appropriate teams with the correct information.

    The example here is quite simple, but it can serve as a starting point to cleaner Prometheus installations with minimal errors.

    Overview of Kubernetes admission control

    When operating clusters on Red Hat OpenShift or Kubernetes, we often perform checks in order to mutate, validate, or convert REST API requests for object creation.

    Such operations occur at admission time—that is, right after the request is authorized, but before the object is registered in etcd. This process is important in order to implement control decisions in the kube-apiserver admission chain.

    A common example used in existing documentation is the LimitRanger admission controller. This is a great example because the end user immediately understands that their configuration must be changed to schedule new workloads. Once pods are denied admission for a specific reason, there is no further need to progress into the pod scheduling phase.

    For our example, we will focus on ValidatingAdmissionWebhook admission controllers and the steps required to write and deploy one using the Go-based controller-runtime webhook package.

    An example validating admission webhook

    Before diving into the code, it might be useful to explain further and visualize our example. We are going to create a validating admission webhook that will be invoked each time a new PrometheusRule is created.

    To configure the API server to forward requests to our validation code, it is necessary to create a ValidatingWebhookConfiguration with a specified service to forward requests to, as defined in webhooks.clientConfig (see Figure 1). This service will be called every time a PrometheusRule is created or updated, as seen in the rules section. Also noteworthy is the namespaceSelector, which only considers AdmissionReview objects for namespaces with the prom-webhook: enabled label.

    The REST handler call flow: validatingwebhook --> service --> pod { /validate-promrule }.
    Figure 1: The REST handler call flow.

    Validating webhooks are executed sequentially and are independent of each other, even when acting on the same resource.

    Because the test cluster has Red Hat OpenShift 4.6 installed, it is important to mention that a webhook for PrometheusRules already exists by default: validatingwebhookconfiguration/prometheusrules.openshift.io, which is part of the cluster monitoring stack. This is useful to know so that we don't choose the same REST endpoint path for our handler function. We also won't be allowed to name our webhook resource the same way.

    Bootstrap with the Operator SDK

    We will use the Operator SDK to initialize our project with a base structure that we can use to start coding.

    To start, we can use the operator-sdk init command, passing in an example domain and a repository as parameters. The goal is to keep things as simple as possible since we won't be using much of the generated code, even though the generated manifests and scaffolding are very useful to have. A go.mod file and a go.sum file are also generated for us in the process:

    operator-sdk init --domain=example.com --repo=github.com/example/promrule-webhook
    Writing kustomize manifests for you to edit...
    Writing scaffold for you to edit...
    Get controller runtime:
    $ go get sigs.k8s.io/controller-runtime@v0.8.3
    go: downloading k8s.io/apimachinery v0.20.2
    go: downloading k8s.io/client-go v0.20.2
    go: downloading k8s.io/api v0.20.2
    go: downloading k8s.io/apiextensions-apiserver v0.20.1
    go: downloading k8s.io/component-base v0.20.2
    Update dependencies:
    $ go mod tidy

    Given that we will be implementing a small REST handler, we don't need to generate any of the additional boilerplate code that is created by the operator-sdk create api and create webhook commands. What matters is that we now have a base starting point to implement our logic.

    Set up a controller manager

    The following code snippet contains the basics for setting up a controller manager, which is then used to instantiate a webhook server:

    package main
    
    import (
    	"os"
    
    	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
    	"sigs.k8s.io/controller-runtime/pkg/client/config"
    	"sigs.k8s.io/controller-runtime/pkg/log"
    	"sigs.k8s.io/controller-runtime/pkg/log/zap"
    	"sigs.k8s.io/controller-runtime/pkg/manager"
    	"sigs.k8s.io/controller-runtime/pkg/manager/signals"
    	"sigs.k8s.io/controller-runtime/pkg/webhook"
    
    	webhookadmission "github.com/rflorenc/prometheusrule-validating-webhook/admission"
    )
    
    func init() {
    	log.SetLogger(zap.New())
    }
    
    func main() {
    	setupLog := log.Log.WithName("entrypoint")
    
    	// setup a manager
    	setupLog.Info("setting up manager")
    	mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
    	if err != nil {
    		setupLog.Error(err, "unable to setup controller manager")
    		os.Exit(1)
    	}
    
    	// +kubebuilder:scaffold:builder
    
    	setupLog.Info("setting up webhook server")
    	hookServer := mgr.GetWebhookServer()
    
    	setupLog.Info("registering PrometheusRule validating webhook endpoint")	
    	hookServer.Register("/validate-promerule", &webhook.Admission{Handler: &webhookadmission.PrometheusRuleValidator{Client: mgr.GetClient()}})
    
    	setupLog.Info("starting manager")
    	if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
    		setupLog.Error(err, "unable to run manager")
    		os.Exit(1)
    	}
    }
    

    Once the hookServer is created, we just have to register the previously configured REST endpoint. We do this by passing it a Handler{} and a client, which the manager provides. The remaining code was generated from the initial bootstrap via the Operator SDK and starts the manager with the default configuration, logging, and signal handling.

    A Prometheus rule validator

    The PrometheusRuleValidator is a helper struct that receives a client to talk with the kube API and (more importantly) defines a pointer to an admission decoder:

    package admission
    
    import (
    	"context"
    	"fmt"
    	"net/http"
    
    	monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
    
    	"sigs.k8s.io/controller-runtime/pkg/client"
    	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    )
    
    // +kubebuilder:webhook:path=/validate-v1-prometheusrule,mutating=false,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=prometheusrule-validating-webhook.example.com
    
    // PrometheusRuleValidator validates PrometheusRules
    type PrometheusRuleValidator struct {
    	Client  client.Client
    	decoder *admission.Decoder
    }
    
    // PrometheusRuleValidator admits a PrometheusRule if a specific set of Rule labels exist
    func (v *PrometheusRuleValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
    	prometheusRule := &monitoringv1.PrometheusRule{}
    
    	err := v.decoder.Decode(req, prometheusRule)
    	if err != nil {
    		return admission.Errored(http.StatusBadRequest, err)
    	}
    
    	for _, group := range prometheusRule.Spec.Groups {
    		for _, rule := range group.Rules {
    			_, found_severity := rule.Labels["severity"]
    			_, found_example_response_code := rule.Labels["example_response_code"]
    			_, found_example_alerting_email := rule.Labels["example_alerting_email"]
    
    			if !found_severity || !found_example_response_code || !found_example_alerting_email {
    				return admission.Denied(fmt.Sprintf("Missing one or more of minimum required labels. severity: %v, example_response_code: %v, example_alerting_email: %v", found_severity, found_example_response_code, found_example_alerting_email))
    			}
    		}
    	}
    	return admission.Allowed("Rule admitted by PrometheusRule validating webhook.")
    }
    
    func (v *PrometheusRuleValidator) InjectDecoder(d *admission.Decoder) error {
    	v.decoder = d
    	return nil
    }
    

    As the name suggests, the InjectDecoder() function injects an admission decoder into the PrometheusRuleValidator. But why do we need decoding in the first place?

    When a target object is received for admission, it is wrapped or referenced in an AdmissionReview object, as shown in the following code. Before this object can be accessed, it has to be decoded so its fields can be accessed for the operations we allow:

    {
        "kind": "AdmissionReview",
        "apiVersion": "admission.k8s.io/v1beta1",
        "request": {
            "uid":"800ab934-1343-01e8-b8cd-45010b400002",
            "resource": {
                "version":"v1",
                "resource":"prometheusrules"
            },
            "name": "foo",
            "namespace": "test",
            "operation": "CREATE",
            "object": {
                "apiVersion": "v1",
                "kind": "PrometheusRule",
                "metadata": {
                    "annotations" : {
                        "abc": "[]"
                    },
                    "labels": {
                        "def": "true"
                    },
                    "name": "foo",
                    "namespace": "test"
                },
                "spec": {
                    ...
                    ...
                }
            }
        }
    }

    So, to decode a PrometheusRule from the "parent" object, we first instantiate it as a raw object and then pass it on to the Decode() function together with the AdmissionReview in the request:

    err := v.decoder.Decode(req, prometheusRule)
    if err != nil {
        return admission.Errored(http.StatusBadRequest, err)
    }

    Once this JSON body decoding step is done, we can access the PrometheusRule fields directly. We then allow or deny object admission based on simple validations. If it is allowed, the object will be persisted to etcd.

    Validating webhook authorization

    Because a ValidatingWebhook is a construct that sits close to the Kubernetes API server, certificate-based authentication and authorization are mandatory.

    In Red Hat OpenShift, we can annotate a ValidatingWebhookConfiguration object with service.beta.openshift.io/inject-cabundle=true to have the webhook's clientConfig.caBundle field populated with the service CA bundle. This allows the Kubernetes API server to validate the service CA certificate used to secure the targeted endpoint.

    Note in the example ValidatingWebhookConfiguration, we specify a dummy clientConfig.caBundle. The value doesn't matter here because it will be injected based on the annotation.

    For the container running our code to access the service CA certificate, it has to mount a ConfigMap object annotated with service.beta.openshift.io/inject-cabundle=true. Once annotated, the cluster automatically injects the service CA certificate into the service-ca.crt key on the ConfigMap:

    volumeMounts:
    # default path if manager.Options.CertDir is not overriden
    # /tmp/k8s-webhook-server/serving-certs/tls.{key,crt} must exist.
      - mountPath: /tmp/k8s-webhook-server/serving-certs
        name: serving-cert

    You can specify an alternate path and mount point by altering the CertDir controller manager Options struct in main.go.

    When writing tests locally, for example, we need to be able to either port-forward to our pod or authenticate to the API with a valid token and a valid TLS key and certificate combination before POSTing our payload with the AdmissionReview and reference the JSON-encoded object.

    Conclusion

    Validating, mutating, and converting webhooks can be of great use in organizations looking for Kubernetes policy enforcement and object validation. On the other hand, having too many registered webhooks can become hard to manage. In edge cases, they can potentially cause scalability issues if not coded correctly, because they will be called every time a new target object is created. Therefore, webhooks should be used in moderation.

    Before the operator-sdk and controller-runtime webhook packages were available, there were multiple nonstandard ways to implement such a project. Requirements such as coding the web server, REST handling routing, JSON object decoding from AdmissionReview, and certificate handling could be done in all sorts of different ways. This article presented an alternative.

    See the GitHub repository associated with this learning project for a full example with a Helm installer.

    Last updated: September 20, 2023

    Related Posts

    • 5 tips for developing Kubernetes Operators with the new Operator SDK

    • Use Skupper to connect multiple Kubernetes clusters

    • Kubernetes configuration patterns, Part 2: Patterns for Kubernetes controllers

    Recent Posts

    • Storage considerations for OpenShift Virtualization

    • Upgrade from OpenShift Service Mesh 2.6 to 3.0 with Kiali

    • EE Builder with Ansible Automation Platform on OpenShift

    • How to debug confidential containers securely

    • Announcing self-service access to Red Hat Enterprise Linux for Business Developers

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Products

    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform

    Build

    • Developer Sandbox
    • Developer Tools
    • Interactive Tutorials
    • API Catalog

    Quicklinks

    • Learning Resources
    • E-books
    • Cheat Sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site Status Dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2025 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Report a website issue