In Strimzi 0.31.0, we introduced Pod Security Providers. They provide a pluggable mechanism for configuring the Pod and container security context of the operands managed by Strimzi. In one of our previous blog posts, we explained how they work and introduced the two default implementations which are part of Strimzi itself. And in this blog post, we will focus on how you can write your own custom Pod Security Providers.
Prerequisites
Strimzi is written in Java and the custom Pod Security Providers also use Java. This blog post assumes that you have at least some basic knowledge of Java and the Maven build system, which is used to build the Java code.
If you want to try using custom Pod Security Providers in Strimzi, you can either use the provided example or write your own provider. To do so, you will need to have Java and Maven installed on your computer. Strimzi uses Java 17. To be able to add your custom provider to the Strimzi container image, you will also need to have Docker (or one of the Docker alternatives such as Podman) installed. And finally, you will need to have a container registry to store the newly built container image. It can be a private container registry that is part of your Kubernetes platform or for example a Docker Hub or Quay.io account.
Writing custom providers
The PodSecurityProvider
interface and Strimzi implementations are part of the Strimzi api
module.
This is the same module that you can also use to manage the Strimzi-based infrastructure which we covered in another blog post
We have to add this module to our Maven project and its pom.xml
.
The api
module is available in the central Maven repositories.
So all you need to do is to add it as a dependency:
<dependencies>
<dependency>
<groupId>io.strimzi</groupId>
<artifactId>api</artifactId>
<version>0.34.0</version>
</dependency>
</dependencies>
The version of the module should match the Strimzi version you are using and with which you plan to deploy it.
After you add the api
module as a dependency, you can start coding.
There are two ways you can write your custom provider.
You can start from scratch and implement the PodSecurityProvider
interface.
Or you can take an existing provider and modify it by extending it.
Implementing the PodSecurityProvider
interface
When you decide to implement your custom Pod Security Provider from scratch, the best way to do it is to implement the PodSecurityProvider
interface.
This interface contains several methods that must be implemented in your code.
In this example, we will implement a provider which will configure all containers to use a read-only root filesystem.
This means that the filesystem of the container image will always be read-only, and the containers will only be able to write to folders mounted as volumes in the Pod definition.
To get started, we create a new class CustomPodSecurityProvider
and let it implement the PodSecurityProvider
package cz.scholz.providers;
// Imports
public class CustomPodSecurityProvider implements PodSecurityProvider {
// Implemented interface methods
}
The configure(...)
method is called when the provider is loaded, and can be used to configure and initialize the provider.
This method consumes a single PlatformFeatures
parameter.
You can use this object to find out more about the environment in which the operator is running - in particular the Kubernetes version.
You can use this if the security context should be set differently for different Kubernetes versions.
In many cases, you will not have anything to configure.
In that case, you can just do nothing in this method:
@Override
public void configure(PlatformFeatures platformFeatures) {
// Nothing to configure
}
Next, the interface defines several methods which create the Pod Security Context and (container) Security Context. These methods will exist for every Pod and every container created by Strimzi. For example, the interface defines the following methods for the Kafka pods:
PodSecurityContext kafkaPodSecurityContext(PodSecurityProviderContext context)
for the Pod Security ContextSecurityContext kafkaContainerSecurityContext(ContainerSecurityProviderContext context)
for the Security Context of the init container used for rack awareness or node port listenersSecurityContext kafkaInitContainerSecurityContext(ContainerSecurityProviderContext context)
for the security context of the main Apache Kafka container
When these methods are called by Strimzi, they will always get an object of type PodSecurityProviderContext
or ContainerSecurityProviderContext
as a parameter.
The context contains two types of information:
- The first is storage configuration, which may be needed to configure specific storage settings for stateful operands. However, since this example doesn’t require any special storage considerations, we can ignore it.
- The second type of information is the security context configured by the user directly in the custom resource. In the Pod Security Providers provided by Strimzi, the user-provided security context always takes priority over whatever the provider sets. However, it’s up to you whether your custom provider decides to respect the user-configured security context or ignore it. In this example, we will ignore the user-configured security context for simplicity.
For our example with the read-only root filesystem, we do not care about the Pod Security Context because our configuration is part of the container Security Context.
So we set all the Pod methods to simply return null
, which means that no Pod Security Context should be set.
For example:
@Override
public PodSecurityContext kafkaPodSecurityContext(PodSecurityProviderContext context) {
return null;
}
And in the container methods, we always return the Security Context to enable the read-only root filesystem:
@Override
public SecurityContext kafkaContainerSecurityContext(ContainerSecurityProviderContext context) {
return new SecurityContextBuilder()
.withReadOnlyRootFilesystem()
.build();
}
@Override
public SecurityContext kafkaInitContainerSecurityContext(ContainerSecurityProviderContext context) {
return new SecurityContextBuilder()
.withReadOnlyRootFilesystem()
.build();
}
You can find the full source code in the GitHub repository linked at the end of the blog post.
Extending an existing policy
If you want to do only some small changes to an existing provider, you do not need to implement everything from scratch.
You can just extend the provider and re-implement only the methods you want to change.
Strimzi currently includes two providers: BaselinePodSecurityProvider
and RestrictedPodSecurityProvider
For our example, we are going to extend the RestrictedPodSecurityProvider
.
It configures the security context of the Strimzi operands to match Kubernetes’ restricted security profile.
When you try to use the Kafka Connect Build and its Kaniko builder with this provider, it will throw an exception because the Kaniko container builder does not work under the restricted profile.
Let’s say you want to use the RestrictedPodSecurityProvider
to secure the operand Pods, but you also want to use the Kafka Connect Build feature without any restrictions.
In such a case, you can simply extend the RestrictedPodSecurityProvider
and override the kafkaConnectBuildContainerSecurityContext
method with your implementation, which will just let it run instead of throwing an exception:
package cz.scholz.providers;
import io.fabric8.kubernetes.api.model.SecurityContext;
import io.strimzi.plugin.security.profiles.ContainerSecurityProviderContext;
import io.strimzi.plugin.security.profiles.impl.RestrictedPodSecurityProvider;
public class CustomPodSecurityProvider2 extends RestrictedPodSecurityProvider {
@Override
public SecurityContext kafkaConnectBuildContainerSecurityContext(ContainerSecurityProviderContext context) {
if (context != null
&& context.userSuppliedSecurityContext() != null) {
return context.userSuppliedSecurityContext();
} else {
return null;
}
}
}
This way you achieved what you wanted and you did not need to implement all the methods from scratch.
Service Loader configuration
When Strimzi uses the Pod Security Providers, it is using the Java ServiceLoader to load the implementations.
To allow Strimzi to load your custom provider, you have to create a provider configuration file.
The provider configuration file must be part of the custom provider source code and be packaged into the JAR file.
The file should be named io.strimzi.plugin.security.profiles.PodSecurityProvider
(the name of the interface which it implements) and placed in the resources/META-INF/services/
path.
Alternatively, you can also configure the provider in the module-info.java
file.
It should contain the names of the classes the implementation provides (including the package name).
So in our case, it should contain the following classes:
cz.scholz.providers.CustomPodSecurityProvider
cz.scholz.providers.CustomPodSecurityProvider2
Respect your Kubernetes platform
The Pod Security Providers allow you to customize the Pod and container security context configuration. They define how the Pods created by Strimzi will be defined. One important thing to keep in mind is that your Kubernetes platform might have its requirements for how the security context should be configured and in some cases will even automatically inject it into the Pods. Stateful Pods such as ZooKeeper or Kafka might also require specific configurations to be able to use the persistent volumes and read from them or write to them. So when writing custom policies, you have to make sure that the security context generated by your custom provider does not conflict with the requirements of your Kubernetes platform. Because if they are not aligned, the Pods might be rejected by the Kubernetes cluster or might not work properly.
Deploying the custom policy
When you have the Java code ready, you have to compile and package it into a JAR file. With Maven, you would typically do it using the following command:
mvn clean package
And then you need to add the JAR to a custom container image that extends the Strimzi operator container.
You can do that with the following Dockerfile
:
FROM quay.io/strimzi/operator:0.34.0
USER root:root
COPY ./target/*.jar lib/
USER 1001
The Strimzi version in the FROM
command at the beginning of the Dockerfile
should correspond to the Strimzi version you use.
You also need to build the container image and push it to a registry:
docker build -t <MyContainerRegistry>/<MyUser>/<MyImage>:<MyTag> .
docker push <MyContainerRegistry>/<MyUser>/<MyImage>:<MyTag>
For example:
docker build -t quay.io/scholzj/operator:custom-providers .
docker push quay.io/scholzj/operator:custom-providers
Once the image is pushed there, you have to modify your Strimzi Cluster Operator deployment:
- Change the
image
field from for examplequay.io/strimzi/operator:0.34.0
to the image you just built - Add the JAR with your custom provider to the Java classpath using the
JAVA_CLASSPATH
environment variable. For example:- name: JAVA_CLASSPATH value: lib/custom-pod-security-providers-1.0-SNAPSHOT.jar
(Note: This option is available only from Strimzi 0.34.0)
- Instruct Strimzi to use your custom provider using the
STRIMZI_POD_SECURITY_PROVIDER_CLASS
environment variable. For example:- name: STRIMZI_POD_SECURITY_PROVIDER_CLASS value: cz.scholz.providers.CustomPodSecurityProvider
After you update the Deployment with these changes, it will roll the Cluster Operator pod to activate the new provider. If you have any operands deployed and the new provider caused changes to their security context, the operator will proceed and roll them as well. If not, just deploy a Kafka cluster using one of our provided examples. After it is deployed, you can check its Security Context and you should see that the Pods and containers have the security context defined by your provider. In our example, all containers will have the read-only root filesystem option enabled:
apiVersion: v1
kind: Pod
metadata:
name: my-cluster-kafka-0
# ...
spec:
# ...
containers:
name: kafka
securityContext:
readOnlyRootFilesystem: true
# ...
Conclusion and examples
Hopefully, this blog post helps you to write your own Pod Security Providers and extend your Strimzi installation should you need to. To help you get started, all the code mentioned in this post is also available on GitHub. You can find it in the Custom Strimzi Pod Security Providers repository. The example repository contains the Java classes for custom providers, as well as the required provider configuration file and the Dockerfile.