简体   繁体   中英

Websocket Autobahn Python client: how to connect to server using server and client certificates?

A websocket client (using Autobahn/Python and Twisted) needs to connect to a websocket server: the client needs to present its client certificate to the server and the client needs to check the server's certificate. These certificates have been created, for instance, during setup of a Kubernetes minikube installation. In particular:

  • server certificate ~/.minikube/ca.crt (in X509 format from what I understand).
  • client certificate ~/.minikube/client.crt with key ~/.minikube/client.key .

I've checked that I can successfully use these certificates+key to issue Kubernetes remote API calls using curl .

From Autobahn's echo_tls/client.py example I understand that I may need to use a ssl.ClientContextFactory() . ssl here refers to the pyopenssl package that twisted automatically imports.

However, I cannot figure out how to pass the certificates to the factory?

  • How do I tell the websocket factor to present the client certificate to the server?
  • How do I tell the websocket to check the server's certificate in order to detect MITM attacks?

After some trial and error I've now arrived at this solution below. To help others I'll not only show code, but also a reference setup to test drive the example code.

First, install minikube, then start a minikube instance; I've tested with minikube 1.0.0, which then runs Kubernetes 1.14 which was current at the time of this writing. Then start a simple websocket server that just shows what is sent to it and will sent back any input you made to the connected websocket client.

minikube start
kubectl run wsserver --generator=run-pod/v1 --rm -i --tty \
  --image ubuntu:disco -- bash -c "\
    apt-get update && apt-get install -y wget && \
    wget https://github.com/vi/websocat/releases/download/v1.4.0/websocat_1.4.0_ssl1.1_amd64.deb && \
    dpkg -i webso*.deb && \
    websocat -vv -s 0.0.0.0:8000"

Next comes the Python code. It attempts to connect to the wsserver we've just started via Kubernetes' remote API from the minikube, using the remote API as its reverse proxy. The minikube setup usually uses mutual SSL/TLS authentication of client and server, so this is a "hard" test here. Please note that there are also other methods, such as server certificate and bearer token (instead of a client certificate).

import kubernetes.client.configuration
from urllib.parse import urlparse
from twisted.internet import reactor
from twisted.internet import ssl
from twisted.python import log
from autobahn.twisted.websocket import WebSocketClientFactory, WebSocketClientProtocol, connectWS
import sys

if __name__ == '__main__':
    log.startLogging(sys.stdout)

    class EchoClientProto(WebSocketClientProtocol):
        def onOpen(self):
            print('onOpen')
            self.sendMessage('testing...\n'.encode('utf8'))
        def onMessage(self, payload, isBinary):
            print('onMessage')
            if not isBinary:
                print('message %s' % payload.decode('utf8'))
        def onClose(self, wasClean, code, reason):
            print('onClose', wasClean, code, reason)
            print('stopping reactor...')
            reactor.stop()

    # Select the Kubernetes cluster context of the minikube instance,
    # and see what client and server certificates need to be used in
    # order to talk to the minikube's remote API instance...
    kubernetes.config.load_kube_config(context='minikube')
    ccfg = kubernetes.client.configuration.Configuration._default
    print('Kubernetes API server CA certificate at %s' % ccfg.ssl_ca_cert)
    with open(ccfg.ssl_ca_cert) as ca_cert:
        trust_root = ssl.Certificate.loadPEM(ca_cert.read())
    print('Kubernetes client key at %s' % ccfg.key_file)
    print('Kubernetes client certificate at %s' % ccfg.cert_file)
    with open(ccfg.key_file) as cl_key:
        with open(ccfg.cert_file) as cl_cert:
            client_cert = ssl.PrivateCertificate.loadPEM(cl_key.read() + cl_cert.read())

    # Now for the real meat: construct the secure websocket URL that connects
    # us with the example wsserver inside the minikube cluster, via the
    # remote API proxy verb.
    ws_url = 'wss://%s/api/v1/namespaces/default/pods/wsserver:8000/proxy/test' % urlparse(ccfg.host).netloc
    print('will contact: %s' % ws_url)
    factory = WebSocketClientFactory(ws_url)
    factory.protocol = EchoClientProto

    # We need to attach the client and server certificates to our websocket
    # factory so it can successfully connect to the remote API.
    context = ssl.optionsForClientTLS(
        trust_root.getSubject().commonName.decode('utf8'),
        trustRoot=trust_root,
        clientCertificate=client_cert
    )

    connectWS(factory, context)
    print('starting reactor...')
    reactor.run()
    print('reactor stopped.')

The tricky part here when attaching the client and server certificates using optionsForClientTLS is that Twisted/SSL expects to be told the server's name we're going to talk to. This is also needed to inform virtual servers which one of their multiple server certificates they need to present -- before there will be any HTTP headers!

Unfortunately, this is now ugly territory -- and I would be glad to get feedback here! Simply using urlparse(ccfg.host).hostname works on some minikube instances, but not on others. I haven't yet figured out why seemingly similar instances behave differently.

My current workaround here is to simply use the CN (common name) of the subject from the server's certificate. Maybe a more robust way might be to only resort to such tactics when the URL for the remote API server uses an IP address literal and not a DNS name (or at least a label).

Alas, run the Python 3 code above python3 wssex.py . If the script correctly connects, then you should see a log message similar to 2019-05-03 12:34:56+9600 [-] {"peer": "tcp4:192.168.99.100:8443", "headers": {"sec-websocket-accept": ...

Additionally, the websocket server that you've started before should show log messages such as [INFO websocat::net_peer] Incoming TCP connection from Some(V4(172.17.0.1:35222)) , and some more.

This then is proof that the client script has successfully connected to minikube's remote API via a secure websocket, passing authentication and access control, and is now connected to the (insecure) websocket demo server inside minikube.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM