After creating self-signed wildcard certificates for modern browsers a while back, self-signed certificates came back to haunt me once again. This post describes a solution for creating self-signed keystores (pkcs12 or jks) in OpenShift and Kubernetes to use in Spring Boot applications to re-encrypt traffic. Actually I present 2 solutions, but the second one only works in OpenShift. The first one will leverage the Cert-Manager to create a keystore, the second one relies on OpenShifts service-serving certificate. The goal is to have OpenShift re-encrypt traffic with a route to a Spring Boot application. The keystore will be used as an application facing certificate for OpenShift to validate.
1. Using Cert-Manager
This approach requires an already working cert-manager instance in your cluster.
Create a new issuer for self-signed certificates:
apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: private-cert-issuer namespace: somewhere spec: selfSigned: {}
Next we need a certificate. It’s important to set isCa: true
for OpenShift to accept this certificate as a destination CA. secretName
references the secret which will be created by cert-manager containing the keystore. Another secret needs to exist for this to work which contains the password encrypting the keystore. This secret is referenced at the end of the yaml here. Name
matches the secret name and key
the data entry inside the secret.
apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: app-https namespace: somewhere spec: dnsNames: - "chart-example.local" commonName: my-selfsigned-ca isCA: true issuerRef: group: cert-manager.io kind: Issuer name: private-cert-issuer secretName: app-https keystores: pkcs12: # switch to jks to create a jks keystore create: true passwordSecretRef: # Password used to encrypt the keystore key: password name: keystore-pw
The secret containing the password may look like this:
apiVersion: v1 kind: Secret metadata: name: keystore-pw type: Opaque data: password: somepass
After creating the cert-manager resources a new secret should be created by the cert-manager containing the keystore. This keystore can now be mounted inside the Spring Boot application. The password can be passed as an environment variable referencing the same secret used to create the keystore. In this example the keystore is mounted to the /certs
directory.
# deployment.yaml env: - name: server_ssl_key_store_password valueFrom: secretKeyRef: name: keystore-pw key: password ... volumeMounts: - name: https-certs-volume mountPath: /certs ... volumes: - name: https-certs-volume secret: secretName: app-https defaultMode: 420
Spring Boot needs to pick up the certificate and use it for SSL. This can be achieved using the spring variables, e.g. contained inside the values.yaml when using Helm:
#values.yml server: ssl: key-store: "/certs/keystore.jks" key-alias: "certificate" key-store-type: "PKCS12"
For OpenShift to validate the certificate when re-encrypting traffic, the destinationCACertificate
property in the OpenShift route has to be set with the created certificate from the keystore secret. When using OpenShift 4.11 this can be automated by leveraging an annotation as described here. However using OpenShift there is another way to create the necessary certificates.
2. OpenShift Service Signer
This approach only works in OpenShift as it relies on OpenShift features to create the needed secret. A simple annotation can be used to annotate the service
resource to instruct OpenShift to create a secret containing self-signed certificates.
#service.yaml ... metadata: annotations: service.beta.openshift.io/serving-cert-secret-name: reencrypt-certs ...
This will create a secret named keystore-reencrypt
which contains only a certificate and a key and may look like this:
kind: Secret apiVersion: v1 metadata: name: reencrypt-certs namespace: somewhere annotations: service.alpha.openshift.io/expiry: '2024-11-06T12:10:22Z' service.beta.openshift.io/expiry: '2024-11-06T12:10:22Z' service.beta.openshift.io/originating-service-name: service-name service.beta.openshift.io/originating-service-uid: 0f31e820-b49d-4619-8ef5-65f1c85293e3 data: tls.crt: >- LS0tLS1CRUdJT[...] tls.key: >- LS0tLS1CRUdJT[...] type: kubernetes.io/tls
To use the self-signed certificate within the secret to create a keystore an init container within the deployment
resource can be used. Any image containing openssl
should be possible. The init container reads the created certificate and key from the volume mount, creates a pkcs12 keystore and writes it to an emptyDir
. In this example a secret containing a password is referenced as an environment variable to be used in the init container.
#deployment.yaml ... spec: volumes: - name: reencrypt-certs secret: secretName: reencrypt-certs defaultMode: 420 - name: reencrypt-keystore emptyDir: {} ... initContainers: - name: gen-reencrypt-keystore image: image-with-openssl:tag command: - sh args: - -c - | echo "creating reencrypt keystore" openssl pkcs12 -export -in /reencrypt-certs/tls.crt -inkey /reencrypt-certs/tls.key -name "https" -out /certs/reencrypt.p12 -passout pass:$server_ssl_key_store_password volumeMounts: - name: reencrypt-certs mountPath: /reencrypt-certs - name: reenrcypt-keystore mountPath: /certs env: - name: server_ssl_key_store_password valueFrom: secretKeyRef: name: reencrypt-keystore-pw key: password ...
The actual application receives the same volume mount and the environment variable reference to decrypt the keystore so Spring Boot can start up.
#deployment.yaml ... containers: volumeMounts: - name: reenrcypt-keystore mountPath: /certs ... env: - name: server_ssl_key_store_password valueFrom: secretKeyRef: name: reencrypt-keystore-pw key: password ...
The location of the keystore and the alias can be added as environment variables as well. I would however advise to have these settings within the application config or in the values.yaml when using Helm to achieve a slimmer deployment resource.
#values.yaml server: ssl: key-store: "/certs/reencrypt.p12" key-alias: "https"
After these settings Spring Boot will locate the keystore /certs/reencrypt.p12
on startup, decrypt it using the password passed within the environment variables and run the application. The real neat part here is that no further settings have to be made to the OpenShift route, as the self-signed certificate created by OpenShift already contains the OpenShift signer CA which is automatically trusted as a destination CA. Even though this property is left empty inside the route resource, OpenShift will trust it’s own certificate.
Although the second option to create a keystore is more straightforward in my eyes, using Cert Manager does have it’s benefits if it’s already running in a cluster. If Helm is used to deploy application be aware that the created certificate by the Cert Manager is not recognized in the lifecycle of the deployment triggering the creation of the certificate in Cert Manager. An uninstall in Helm will not remove the keystore secret. This will become an issue if the password is created in a non deterministic way or is just different each time, as the password secret will be created anew and the certificate will not be overwritten, resulting in a mismatch.
If you happen to know a different approach to create self-signed keystores in OpenShift / Kubernetes to re-encrypt traffic, please let me know in the comments below or contacting me directly.