Signing kernel modules with KMM
For more details on using Secure-boot see here or here
Using Signing with KMM
On a secure-boot enabled system all kernel modules (kmods) must be signed with a public/private key-pair enrolled into
the Machine Owner's Key or MOK database.
Drivers distributed as part of a distribution should already be signed by the distributions private key, but for kernel
modules build out-of-tree KMM supports signing kernel modules using the sign
section of the kernel mapping.
To use this functionality you need:
- A public private key pair in the correct (der) format
- At least one secure-boot enabled nodes with the public key enrolled in its MOK database
- Either a pre-built driver container image, or the source code and
Dockerfile
needed to build one in-cluster.
If you have a pre-built image, such as one distributed by a hardware vendor, or already built elsewhere please see the Signing docs for signing with KMM.
Alternatively if you have source code and need to build your image first, please see the Build and Sign docs for deploying with KMM.
If all goes well KMM should load your driver into the node's kernel. If not, see Common Issues.
Adding the Keys for secureboot
To use KMM to sign kernel modules a certificate and private key are required. For details on how to create these see here
For example:
openssl req -x509 -new -nodes -utf8 -sha256 -days 36500 -batch -config configuration_file.config -outform DER -out my_signing_key_pub.der -keyout my_signing_key.priv
The two files created (my_signing_key_pub.der
containing the cert and my_signing_key.priv
containing the private
key) can then be added as secrets either directly by:
kubectl create secret generic my-signing-key --from-file=key=<my_signing_key.priv>
kubectl create secret generic my-signing-key-pub --from-file=key=<my_signing_key_pub.der>
OR
by base64 encoding them:
cat my_signing_key.priv | base64 -w 0 > my_signing_key2.base64
cat my_signing_key_pub.der | base64 -w 0 > my_signing_key_pub.base64
Adding the encoded text in to a yaml file:
apiVersion: v1
kind: Secret
metadata:
name: my-signing-key-pub
namespace: default
type: Opaque
data:
cert: <base64 encoded secureboot public key>
---
apiVersion: v1
kind: Secret
metadata:
name: my-signing-key
namespace: default
type: Opaque
data:
key: <base64 encoded secureboot private key>
and then applying the yaml file using:
kubectl apply -f <yaml filename>
Checking the keys
To check the public key secret is set correctly:
kubectl get secret -o yaml <certificate secret name> | awk '/cert/{print $2; exit}' | base64 -d | openssl x509 -inform der -text
This should display a certificate with a Serial Number, Issuer, Subject etc.
And to check the private key:
kubectl get secret -o yaml <private key secret name> | awk '/key/{print $2; exit}' | base64 -d
Which should display a key, including -----BEGIN PRIVATE KEY-----
and -----END PRIVATE KEY-----
lines
Signing kmods in a pre-built image
The YAML below will add the public/private key-pair as secrets with the required key names (key
for the private key,
cert
for the public key).
It will then pull down the unsignedImage
image, open it up, sign the kernel modules listed in filesToSign
, add them
back and push the resulting image as containerImage
.
KMM should then load the signed kmods onto all the nodes with that match the selector. The kmods should be successfully loaded on any nodes that have the public key in their MOK database, and any nodes that are not secure-boot enabled (which will just ignore the signature). They should fail to load on any that have secure-boot enabled but do not have that key in their MOK database.
Example
Before applying this, ensure that the keySecret
and certSecret
secrets have been created (see
here).
---
apiVersion: kmm.sigs.x-k8s.io/v1beta1
kind: Module
metadata:
name: example-module
spec:
moduleLoader:
serviceAccountName: default
container:
modprobe:
# the name of the kmod to load
moduleName: '<your module name>'
kernelMappings:
# the kmods will be deployed on all nodes in the cluster with a kernel that matches the regexp
- regexp: '^.*\.x86_64$'
# the container to produce containing the signed kmods
containerImage: <image name e.g. quay.io/myuser/my-driver:<kernelversion>-signed>
sign:
# the image containing the unsigned kmods (we need this because we are not building the kmods within the cluster)
unsignedImage: <image name e.g. quay.io/myuser/my-driver:<kernelversion> >
keySecret: # a secret holding the private secureboot key with the key 'key'
name: <private key secret name>
certSecret: # a secret holding the public secureboot key with the key 'cert'
name: <certificate secret name>
filesToSign: # full path within the unsignedImage container to the kmod(s) to sign
- /opt/lib/modules/4.18.0-348.2.1.el8_5.x86_64/kmm_ci_a.ko
imageRepoSecret:
# the name of a secret containing credentials to pull unsignedImage and push containerImage to the registry
name: repo-pull-secret
selector:
kubernetes.io/arch: amd64
Building and signing a kmod image
The YAML below should build a new container image using the
source code from the repo (this
kernel module does nothing useful but provides a good example).
The image produced is saved back in the registry with a temporary name, and this temporary image is then signed using
the parameters in the sign
section.
The temporary image name is based on the final image name and is set to be
<containerImage>:<tag>-<namespace>_<module name>_kmm_unsigned
.
For example, given the YAML below KMM would build an image named
quay.io/chrisp262/minimal-driver:final-default_example-module_kmm_unsigned
containing the build but unsigned kmods,
and push it to the registry.
Then it would create a second image, quay.io/chrisp262/minimal-driver:final
containing the signed kmods.
It is this second image that will be loaded by the DaemonSet and will deploy the kmods to the cluster nodes.
Once it is signed the temporary image can be safely deleted from the registry (it will be rebuilt if needed).
Example
Before applying this, ensure that the keySecret
and certSecret
secrets have been created (see
here).
---
apiVersion: v1
kind: ConfigMap
metadata:
name: example-module-dockerfile
namespace: default
data:
dockerfile: |
ARG DTK_AUTO
ARG KERNEL_VERSION
FROM ${DTK_AUTO} as builder
WORKDIR /build/
RUN git clone -b main --single-branch https://github.com/kubernetes-sigs/kernel-module-management.git
WORKDIR kernel-module-management/ci/kmm-kmod/
RUN make
FROM docker.io/redhat/ubi8:latest
ARG KERNEL_VERSION
RUN yum -y install kmod && yum clean all
RUN mkdir -p /opt/lib/modules/${KERNEL_VERSION}
COPY --from=builder /build/kernel-module-management/ci/kmm-kmod/*.ko /opt/lib/modules/${KERNEL_VERSION}/
RUN /usr/sbin/depmod -b /opt
---
apiVersion: kmm.sigs.x-k8s.io/v1beta1
kind: Module
metadata:
name: example-module
namespace: default
spec:
moduleLoader:
serviceAccountName: default
container:
modprobe:
moduleName: simple_kmod
kernelMappings:
- regexp: '^.*\.x86_64$'
containerImage: < the name of the final driver container to produce>
build:
dockerfileConfigMap:
name: example-module-dockerfile
sign:
keySecret:
name: <private key secret name>
certSecret:
name: <certificate secret name>
filesToSign:
- /opt/lib/modules/4.18.0-348.2.1.el8_5.x86_64/kmm_ci_a.ko
imageRepoSecret: # used as imagePullSecrets in the DaemonSet and to pull / push for the build and sign features
name: repo-pull-secret
selector: # top-level selector
kubernetes.io/arch: amd64
Debugging & troubleshooting
If your worker Pod logs show modprobe: ERROR: could not insert '<your kmod name>': Required key not available
then the
kmods are either not signed, or signed with the wrong key.