Setting up trusted SSL for IBM Event Streams

A quick how-to for setting up Event Streams with trusted certificates when running a development project.

Problem

You’re working on a project using IBM Event Streams. It’s just a development project, so you’re not using an SSL certificate signed by your real, trusted, corporate signer.

Everything works, but…

You get errors like these every time you access the web tooling – which you have to click through.

And you get errors like these from your Kafka client applications – which you have to configure with a custom truststore to avoid (although, if you do need to do that, I have a guide to help!)

[2021-06-27 23:19:06,048] ERROR [Consumer clientId=consumer-dalegrp-1, groupId=dalegrp] Connection to node -1 (dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com/9.46.199.58:443) failed authentication due to: SSL handshake failed (org.apache.kafka.clients.NetworkClient)
[2021-06-27 23:19:06,049] WARN [Consumer clientId=consumer-dalegrp-1, groupId=dalegrp] Bootstrap broker dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com:443 (id: -1 rack: null) disconnected (org.apache.kafka.clients.NetworkClient)
[2021-06-27 23:19:06,069] ERROR Error processing message, terminating consumer process:  (kafka.tools.ConsoleConsumer$)
org.apache.kafka.common.errors.SslAuthenticationException: SSL handshake failed
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:326)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:269)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:264)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1339)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1214)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1157)
	at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)
	at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1074)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1061)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:770)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask.run(SSLEngineImpl.java:1008)

Explanation

The reason for these errors is because by default, Cloud Pak for Integration – in the absence of a certificate you’d rather use – has generated new self-signed certificates to use.

Your browser and client applications don’t trust this new self-signed certificate by default.

Solution

TLDR: Use a free certificate from Let’s Encrypt. When you set up Event Streams, configure it to use that certificate. That will (almost certainly) be trusted by all of your apps, and you won’t have to go through the annoying workarounds.

The rest of this post is just a guided walkthrough explaining how to do that.

To keep things simple, I’m going to assume that you’re starting from scratch – you have a brand new, vanilla, OpenShift cluster.

Note:

Some of the commands in this post use files that you can find at
github.com/dalelane/eventstreams-valid-ssl.

Commands you should run will be highlighted like this
Commands to see what is happening will be shown like this

Step 1
Get a certificate

ROKS

If you are using a managed OpenShift cluster in IBM Cloud (“ROKS”), a certificate is automatically created for you when you create the cluster. It’s already available as a Secret in the openshift-ingress namespace, and you can use that.

SECRET_NAME=$(oc get secret -n openshift-ingress  -o=jsonpath='{.items[?(@.metadata.annotations.ingress\.cloud\.ibm\.com/cert-source=="ibm")].metadata.name}')

echo "Let's Encrypt certificate is available in the $SECRET_NAME secret in the openshift-ingress namespace"

You can download this to a few files on your computer.

First, the key:

oc get secret -n openshift-ingress $SECRET_NAME -o=jsonpath="{.data['tls\.key']}" | base64 -d > tls.key

Then the full chain, which you’ll need to split into the certificate and the CA’s.

oc get secret -n openshift-ingress $SECRET_NAME -o=jsonpath="{.data['tls\.crt']}" | base64 -d > full-chain.crt

split -p "-----BEGIN CERTIFICATE-----" full-chain.crt cert-

mv cert-aa tls.crt

cat cert-ab cert-ac > ca.crt

rm cert-ab cert-ac

You should now have three files:
– tls.key
– tls.crt
– ca.crt

Other

If you’re running OpenShift somewhere else, you will need to create the certificate yourself. See the documentation for more guidance.

Step 2
Install the Cloud Pak for Integration operators

Add the IBM software catalog to your OpenShift cluster.

oc apply -f 01-ibm-catalog-source.yaml

Wait for the operator catalog to be updated with the IBM operators, then install the Cloud Pak for Integration operator.

oc apply -f 02-operator-platform-navigator.yaml

Wait for the IBM Cloud Pak for Integration operator and it’s pre-reqs (IBM Cloud Pak foundational services, and IBM NamespaceScope Operator) finish installing.

Step 3
Prepare the namespace

I’m going to use a namespace called integration in this walkthrough. You can obviously change this to anything you’d like in all the subsequent commands.

Create the namespace.

oc new-project integration

Create an entitled registry key in the namespace, that you’ll need to deploy IBM software.

oc create secret docker-registry ibm-entitlement-key \
    --docker-username=cp \
    --docker-password=$IBM_ENTITLEMENT_KEY \
    --docker-server=cp.icr.io \
    --namespace=integration --dry-run=client -o yaml | oc apply -f -

Step 4
Start creating Cloud Pak Foundational Services

Cloud Pak Foundational Services provides a common front door for a lot of the Cloud Pak capabilities, including Event Streams.

The next thing to do is to ask Cloud Pak Foundational services for an ingress, that we can then start configuring with our custom certificate.

oc apply -f 03-management-ingress.yaml -n integration

This will take a couple of minutes.

% oc get operandrequest request-management-ingress
NAME                         AGE   PHASE        CREATED AT
request-management-ingress   58s   Installing   2022-10-26T07:58:49Z

Wait for the management ingress operator to finish installing.

oc wait operandrequest request-management-ingress --for=condition=Ready --timeout=15m
% oc get operandrequest request-management-ingress
NAME                         AGE   PHASE     CREATED AT
request-management-ingress   50s   Running   2022-10-26T07:58:49Z

In the background, a variety of work will then start happening under the covers to set up the auth support. You should wait for this to complete. It can take ten minutes or so.

Wait for the pods in the Common Services namespace to finish starting.

oc get pods -n ibm-common-services

And wait for the setup jobs to complete.

% oc get job -n ibm-common-services
NAME                       COMPLETIONS   DURATION   AGE
iam-onboarding             0/1           9s         9s
oidc-client-registration   0/1           7s         7s
security-onboarding        0/1           10s        10s

% oc get job -n ibm-common-services
NAME                       COMPLETIONS   DURATION   AGE
oidc-client-registration   1/1           9m25s      9m25s
iam-onboarding             1/1           10m        10m
security-onboarding        1/1           10m        10m

Step 5
Configure the ingress to use your certificate

The default management ingress will have created, and (using certmanager) will be managing, a route-cert certificate.

You can find it in the Common Services namespace.

% oc get certificates.certmanager.k8s.io route-cert -n ibm-common-services
NAME         READY   SECRET             AGE   EXPIRATION
route-cert   True    route-tls-secret   12m   2023-10-26T08:05:47Z

% oc get secret route-tls-secret -n ibm-common-services
NAME               TYPE                DATA   AGE
route-tls-secret   kubernetes.io/tls   3      6m31s

If you try and replace it, the Operator will immediately revert your changes, so the first step is to edit the management ingress to tell it to stop managing the certificate.

oc patch managementingress default \
    --type merge \
    --patch '{"spec":{"ignoreRouteCert":true}}' \
    -n ibm-common-services

Now that the Operator is ignoring the certificate, you’re free to delete it and replace it with your own custom certificate.

Delete the certificate and secret.

oc delete certificates.certmanager.k8s.io route-cert -n ibm-common-services
oc delete secret route-tls-secret -n ibm-common-services

Replace the secret with a new one using your certificate.

oc create secret generic route-tls-secret -n ibm-common-services \
    --from-file=ca.crt --from-file=tls.crt --from-file=tls.key

There are a couple of things that don’t automatically notice that the certificate has changed, so you need to prompt them to do something.

You should trigger some certificate secrets to be recreated by deleting them.

oc delete secret ibmcloud-cluster-ca-cert  -n ibm-common-services
oc delete secret management-ingress-ibmcloud-cluster-ca-cert  -n integration

And you should restart the auth pods so that they start using the new certificate.

oc delete pod -l app=auth-idp  -n ibm-common-services

They take a little while to restart, so wait for this to complete.

oc wait --for condition=ready --timeout=900s pod -l app=auth-idp -n ibm-common-services

Step 6
Create Cloud Pak for Integration resources using your certificate

Platform Navigator

Create a new secret using your certificate files that the CP4I components can access.

oc create secret generic my-custom-cert -n integration \
    --from-file=ca.crt --from-file=tls.crt --from-file=tls.key

Create the common Platform Navigator UI.

oc apply -f 04-platform-navigator.yaml -n integration

Notice that it has been configured to use the custom certificate by including this:

spec:
  tls:
    secretName: my-custom-cert

This takes a while to install for the first time, so you should wait for it to be ready.

oc wait PlatformNavigator navigator --for=condition=Ready --timeout=1h -n integration

Event Streams

Create a new secret with the certificate that includes the full chain, suitable for use by the Kafka listeners.

oc create secret tls my-kafka-listeners-cert \
    --cert=full-chain.crt \
    --key=tls.key \
    --dry-run=client -o json | \
    oc apply -n integration -f -

Install the Event Streams operator

oc apply -f 05-operator-event-streams.yaml

Create your Event Streams instance

cpconsoleroute="$(oc get route -n ibm-common-services cp-console -o jsonpath='{.spec.host}')" \
    envsubst < 06-event-streams.yaml | \
    oc apply -n integration -f -

Notice that the Kafka listeners are configured to use the full chain certificate, by including:

spec:
  strimziOverrides:
    kafka:
      listeners:
        - tls: true
          type: route
          configuration:
            brokerCertChainAndKey:
              certificate: tls.crt
              key: tls.key
              secretName: my-kafka-listeners-cert
          authentication:
            type: scram-sha-512
          name: external
          port: 9094

And notice that it has been configured to use the custom single certificate for each of the different REST interfaces that Event Streams provides, by including:

spec:
  restProducer:
    endpoints:
      - name: restproducer
        containerPort: 9080
        certOverrides:
          certificate: tls.crt
          key: tls.key
          secretName: my-custom-cert
  apicurioRegistry:
    endpoints:
      - name: apicurio
        containerPort: 9080
        certOverrides:
          certificate: tls.crt
          key: tls.key
          secretName: my-custom-cert
  adminApi:
    endpoints:
      - name: adminapi
        containerPort: 9080
        certOverrides:
          certificate: tls.crt
          key: tls.key
          secretName: my-custom-cert

The admin API also needs some additional configuration to tell it to use the external route (which is where the custom certificate is applied, using an OpenShift route set to reencrypt) instead of the default internal service.

spec:
  adminApi:
    env:
      - name: IAM_SERVER_URL
        value: https://${cpconsoleroute}:443

The value for this comes from the foundational services route:

oc get route -n ibm-common-services cp-console -o jsonpath='{.spec.host}'

Unfortunately, the Event Streams custom resource doesn’t support the normal Kubernetes valueFrom env, which is why I resort to envsubst for this.

You could always edit the yaml file yourself if that is simpler – just remember to:
– add the https:// bit at the start
– include the explicit port number 443 at the end

Step 7
Verify

If it’s all worked, you should see the problems described above have gone.

Verify the Kafka listeners

Try creating a topic and credentials you can use with a Kafka application:

oc apply -f 07-kafka-topic.yaml -n integration
oc apply -f 08-kafka-credentials.yaml -n integration

You can then run your Kafka applications.

kafka-console-producer.sh \
    --bootstrap-server $(oc get eventstreams -n integration my-kafka-cluster -o jsonpath='{.status.kafkaListeners[0].bootstrapServers}') \
    --topic TEST \
    --producer-property "sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username=testuser password=$(oc get secret testuser -nintegration -ojsonpath={.data.password} | base64 -d);" \
    --producer-property 'sasl.mechanism=SCRAM-SHA-512' \
    --producer-property 'security.protocol=SASL_SSL'

No custom truststore or other system configuration changes needed!

Verify the web interfaces

Try visiting the web console:

oc get eventstreams -n integration my-kafka-cluster -o jsonpath='{.status.endpoints[?(@.name=="ui")].uri}'
echo "username : `oc get secret -n ibm-common-services platform-auth-idp-credentials -o jsonpath='{.data.admin_username}' | base64 -d`"
echo "password : `oc get secret -n ibm-common-services platform-auth-idp-credentials -o jsonpath='{.data.admin_password}' | base64 -d`\n"

No browser warnings!

Summary

With a little additional configuration, you can set up your Event Streams cluster using a trusted certificate that was automatically created for you. It means you can avoid the annoying workarounds that I often see developers putting up with in their development clusters.

For more information, check:

Thanks to Tim Mitchell and Piers Walter for help with this post.

Tags: , , , ,

Comments are closed.