Kubernetes mTLS
Otterize can automatically provision mTLS credentials using Kubernetes pod identities and integrating with SPIFFE/SPIRE. Here we document how to generate mTLS credentials, how to consume them in a variety of languages, and how to verify them if needed.
Provisioning mTLS credentials
To provision mTLS credentials for a client pod with Otterize, make the following 3 simple changes to its Kubernetes spec:
- Generate credentials: add the
credentials-operator.otterize.com/tls-secret-name
annotation, which tells the Otterize credentials operator to generate mTLS credentials, and to store them in a Kubernetes secret whose name is the value of this annotation. - Expose credentials in a volume: add a volume containing this secret to the pod.
- Mount the volume: mount the volume in every container in the pod.
Here is the general structure for such a spec:
spec:
template:
metadata:
annotations:
# 1. Generate credentials as a secret called "client-credentials-secret":
credentials-operator.otterize.com/tls-secret-name: client-credentials-secret
...
spec:
volumes:
# 2. Create a volume containing this secret:
- name: otterize-credentials
secret:
secretName: client-credentials-secret
...
containers:
- name: client
...
volumeMounts:
# 3. Mount volume into container
- name: otterize-credentials
mountPath: /var/otterize/credentials
readOnly: true
For the complete list of annotation parameters please consult the Otterize credentials operator documentation.
Certificates are automatically refreshed before expiring. We recommend loading certificates each time before using them, to avoid using expired certificates.
Using mTLS credentials
The generated mTLS credentials can be used by services directly in their native programming languages via common SDKs. The following examples showcase how to use the generated mTLS credentials for mTLS between:
- HTTP servers and their clients
- Kafka brokers and their clients
HTTP
Client
- JavaScript
- Go
- Python
- cURL
const fs = require('fs');
const https = require('https');
const options = {
hostname: 'server.otterize-tutorial-mtls/hello',
port: 443,
path: '/hello',
method: 'GET',
cert: fs.readFileSync('/var/otterize/credentials/cert.pem'),
key: fs.readFileSync('/var/otterize/credentials/key.pem'),
ca: fs.readFileSync('/var/otterize/credentials/ca.pem')
}
const req = https.request(
options,
res => {
res.on('data', function (data) {
console.log(data)
});
}
);
req.end();
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
// Read the key pair to create certificate
cert, err := tls.LoadX509KeyPair("/var/otterize/credentials/cert.pem", "/var/otterize/credentials/key.pem")
if err != nil {
log.Fatal(err)
}
// Create a CA certificate pool and add bundle.pem to it
caCert, err := ioutil.ReadFile("/var/otterize/credentials/ca.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Create an HTTPS client and supply the created CA pool and certificate
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
},
},
}
r, err := client.Get("https://server.otterize-tutorial-mtls/hello")
if err != nil {
log.Fatal(err)
}
// Read the response body
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
}
// Print the response body to stdout
fmt.Printf("%s\n", body)
}
import requests
resp = requests.get("https://server.otterize-tutorial-mtls/hello",
cert=('/var/otterize/credentials/cert.pem', '/var/otterize/credentials/key.pem'),
verify="/var/otterize/credentials/ca.pem")
curl --cert /var/otterize/credentials/cert.pem \
--key /var/otterize/credentials/key.pem \
--cacert /var/otterize/credentials/ca.pem https://server.otterize-tutorial-mtls/hello
Server
- JavaScript
- Go
- Python
const https = require(`https`);
const fs = require(`fs`);
const options = {
key: fs.readFileSync('/var/otterize/credentials/key.pem'),
cert: fs.readFileSync('/var/otterize/credentials/cert.pem'),
ca: fs.readFileSync('/var/otterize/credentials/ca.pem'),
requestCert: true
};
https.createServer(
options,
(req, res) => {
const peerCert = req.connection.getPeerCertificate();
const ownCert = req.connection.getCertificate();
console.log("Received request:");
console.log(peerCert.subject.CN + ":\t" + req.method + " " + req.url);
if (req.url === '/hello') {
res.writeHead(200);
res.end('mTLS hello world\nfrom: ' + ownCert.subject.CN + '\nto client: ' + peerCert.subject.CN);
} else {
res.end();
}
}).listen(443);
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
// Write "Hello, world!" to the response body
io.WriteString(w, "Hello, world over mTLS!\\n")
fmt.Println("GET /hello mTLS")
}
func main() {
// Set up a /hello resource handler
http.HandleFunc("/hello", helloHandler)
// Create a CA certificate pool and add bundle.pem to it
caCert, err := ioutil.ReadFile("/var/otterize/credentials/ca.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Create the TLS Config with the CA pool and enable Client certificate validation
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsConfig.BuildNameToCertificate()
// Create a Server instance to listen on port 8443 with the TLS config
server := &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
}
// Listen to HTTPS connections with the server certificate and wait
log.Fatal(server.ListenAndServeTLS("/var/otterize/credentials/cert.pem", "/var/otterize/credentials/key.pem"))
}
from flask import Flask
import ssl
app = Flask(__name__)
@app.route("/hello")
def hello():
return "Hello World!"
if __name__ == "__main__":
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations('/var/otterize/credentials/ca.pem')
context.load_cert_chain('/var/otterize/credentials/cert.pem', '/var/otterize/credentials/key.pem')
app.run(port=443, ssl_context=context)
Kafka
- JavaScript
- Go
- Python
- Bash
const fs = require('fs')
const {Kafka} = require('kafkajs')
const kafka = new Kafka({
brokers: ['kafka.kafka:9092'],
ssl: {
ca: [fs.readFileSync('/var/otterize/credentials/ca.pem', 'utf-8')],
key: fs.readFileSync('/var/otterize/credentials/key.pem', 'utf-8'),
cert: fs.readFileSync('/var/otterize/credentials/cert.pem', 'utf-8')
},
})
const consumer = kafka.consumer({groupId: 'test-group'})
consumer.connect().then(
consumer.subscribe({topic: 'mytopic', fromBeginning: true}).then(
consumer.run({
eachMessage: async ({
topic,
partition,
message
}) => {
console.log({
value: message.value.toString(),
})
},
})
)
)
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/Shopify/sarama"
"github.com/sirupsen/logrus"
"io/ioutil"
"time"
)
const (
kafkaAddr = "kafka.kafka:9092"
testTopicName = "mytopic"
certFile = "/var/otterize/credentials/cert.pem"
keyFile = "/var/otterize/credentials/key.pem"
rootCAFile = "/var/otterize/credentials/ca.pem"
)
func getTLSConfig() (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("failed loading x509 key pair: %w", err)
}
pool := x509.NewCertPool()
rootCAPEM, err := ioutil.ReadFile(rootCAFile)
if err != nil {
return nil, fmt.Errorf("failed loading root CA PEM file: %w ", err)
}
pool.AppendCertsFromPEM(rootCAPEM)
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: pool,
}, nil
}
func send_messages(producer sarama.SyncProducer) {
i := 1
for {
msg := fmt.Sprintf("Message %d [sent by client]", i)
_, _, err := producer.SendMessage(&sarama.ProducerMessage{
Topic: testTopicName,
Partition: -1,
Value: sarama.StringEncoder(msg),
})
if err != nil {
return
}
fmt.Printf("Sent message - %s\n", msg)
time.Sleep(1 * time.Second)
i++
}
}
func loop_kafka() error {
addrs := []string{kafkaAddr}
config := sarama.NewConfig()
fmt.Println("Loading mTLS certificates")
config.Net.TLS.Enable = true
tlsConfig, err := getTLSConfig()
if err != nil {
return err
}
config.Net.TLS.Config = tlsConfig
fmt.Println("Connecting to Kafka")
config.Net.DialTimeout = 5 * time.Second
config.Net.ReadTimeout = 5 * time.Second
config.Net.WriteTimeout = 5 * time.Second
client, err := sarama.NewClient(addrs, config)
if err != nil {
return err
}
fmt.Println("Creating a producer and a consumer for -", testTopicName)
config.Producer.Return.Successes = true
config.Producer.Timeout = 5 * time.Second
config.Consumer.MaxWaitTime = 5 * time.Second
config.Producer.Return.Errors = true
config.Consumer.Return.Errors = true
producer, err := sarama.NewSyncProducerFromClient(client)
if err != nil {
return err
}
consumer, err := sarama.NewConsumerFromClient(client)
if err != nil {
return err
}
fmt.Println("Sending messages")
go send_messages(producer)
partConsumer, err := consumer.ConsumePartition(testTopicName, 0, 0)
if err != nil {
return err
}
for msg := range partConsumer.Messages() {
fmt.Printf("Read message - %s\n", msg.Value)
}
return nil
}
func main() {
for {
err := loop_kafka()
logrus.WithError(err).Println()
fmt.Println("Loop exited")
time.Sleep(2 * time.Second)
}
}
from kafka import KafkaConsumer
ssl_kwargs = dict(
security_protocol='SSL',
ssl_cafile="/var/otterize/credentials/ca.pem",
ssl_keyfile="/var/otterize/credentials/key.pem",
ssl_certfile="/var/otterize/credentials/cert.pem",
)
consumer = KafkaConsumer(
"test",
bootstrap_servers=["kafka.kafka:9092"],
**ssl_kwargs)
print("Connected to kafka consumer")
for message in consumer:
print("Read Kafka message: " + str(message))
- Bash
- client.properties
kafka-console-consumer.sh \
--bootstrap-server kafka.kafka:9092 \
-topic mytopic \
--consumer.config client.properties
security.protocol=SSL
ssl.keystore.location=/var/otterize/credentials/kafka.keystore.jks
ssl.keystore.password=password
ssl.truststore.location=/var/otterize/credentials/kafka.truststore.jks
ssl.truststore.password=password
Verify the generated mTLS credentials
We can use openssl
to inspect the generated certificates that were stored as Kubernetes secrets
and mounted as volumes into pod containers.
To retrieve the credentials directly from the Kubernetes secrets store, fetch them via kubectl get secret
:
kubectl get secret -n otterize-tutorial-mtls client-credentials-secret -o jsonpath='{.data.cert\.pem}' | base64 -d > svid.pem
Alternatively, retrieve the credentials from the container's mounted volume, e.g. by executing a shell command:
kubectl exec -n otterize-tutorial-mtls -it deploy/client -- cat /var/otterize/credentials/cert.pem > svid.pem
The retrieved credentials can be inspected with:
openssl x509 -in svid.pem -text | head -n 15
You should see a similar output to
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
0b:eb:eb:4d:0e:02:7e:28:93:30:1c:55:26:22:8b:c7
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = SPIRE
Validity
Not Before: Aug 24 12:19:57 2022 GMT
Not After : Sep 23 12:20:07 2022 GMT
Subject: C = US, O = SPIRE, CN = client.otterize-tutorial-mtls # the client's name
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
You can see that Otterize generated an X.509 keypair using the pod's name ("client")
and namespace ("otterize-tutorial-mtls"): client.otterize-tutorial-mtls
.
The certificate belongs to a chain of trust rooted at the SPIRE server.
What's next
- Configure secure access between pods and Kafka running within the same Kubernetes cluster with this guide.
- Read more about the Otterize credentials operator