3

I am looking to connect a server and a number of clients using mTLS in golang. On my server I would like to be able to generate certificates to put on all the clients so clients can talk to the server but clients cannot talk to each other. My clients however, expose the golang http server on port 80 and my server communicates with the clients through API requests rather than the other way around (technically you could question whether what I am calling a server is in fact a client, but it’s a single source for certificate generation and serves content to the clients so will stick with this naming for simplicity). How could this be set up? Which certificates would need to be generated and is it possible to have the clients listen for requests using certificates generated from a single server?

I will most likely be using SmallStep for certificate generation and SmallStep examples would be useful, but the general approach would be fine too and can then look at how to replicate it with SmallStep separately.

I have looked at some existing golang examples but they tend to be steered at different setups:

https://smallstep.com/hello-mtls/doc/server/go

https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go

Here is the code at the moment which I am quite sure is using the certificates incorrectly :

Client:

step ca certificate "localhost" client.crt client.key

In the below, cert and key variables are client.crt and client.key respectively.

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(cert))

// Read the key pair to create certificate
keyPair, err := tls.X509KeyPair([]byte(cert), []byte(key))
if err != nil {
log.Fatal(err)
}

transport = &http.Transport{
IdleConnTimeout:     transportIdleConnTimeout,
MaxIdleConnsPerHost: transportMaxIdleConnsPerHost,
TLSClientConfig: &tls.Config{
    RootCAs:      caCertPool,
    Certificates: []tls.Certificate{keyPair},
},

Server:

step ca certificate "pm1" server.crt server.key

In the below, cert and key variables are server.crt and server.key respectively.

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(cert))

cert, err := tls.X509KeyPair([]byte(cert), []byte(key))
if err != nil {
    log.Fatal("server: loadkeys: ", err)
}

tlsConfig := tls.Config{
    ClientCAs:    caCertPool,
    ClientAuth:   tls.RequireAndVerifyClientCert,
    Certificates: []tls.Certificate{cert},
    MinVersion:   tls.VersionTLS13,
}

I am not using ca.crt (step ca root ca.crt) at all right now.

Maggie
  • 223
  • 4
  • 8

1 Answers1

3
  1. What mTLS certificates are required when the client is also the server?

From server perspective:

  • each service requires it's own certificate, to serve HTTPS
  • a list of trusted certificates, used to authenticate the incoming requests (the list should include all the certificates used by all the authorised clients).

From client perspective:

  • the list of certificates for all the servers that you intend to communicate with over mTLS; clients won't trust unknown certificates(similar to a web browser); usually the CA that signed certificates for all services is used here, but you may also add them individually if really want to
  • its own certificate which is used to sign the outgoing requests.

  1. clients using mTLS [...] expose the golang http server on port 80

The port( :80 or :443 etc.) and the protocols(http, https, gRPC etc) are different things. However, there is a strong convention to use http on port 80 and https on :443. If you don't care about conventions, nothing can stop you to use https on port :80. Because you want to achieve mTLS, you will have to listen on HTTPS on the servers.


  1. [...]but it’s a single source for certificate generation and serves content to the clients so will stick with this naming for simplicity[...]and is it possible to have the clients listen for requests using certificates generated from a single server?

I am not sure I understand the premise here, but certificate generation isn't usually in the responsibility of the servers or clients that are supposed to use them to establish mutual trust.

I will assume you have a CA that can sign certificates for each of your web-servers and for simplicity will name them:

µA: must communicate with µB and µC
µB: must communicate only with µA
µC: must communicate only with µA

HTTPS and mTLS are again different things: the first is the protocol and the second is an authentication method. I will try to address them individually:

HTTPS:

To start listening on HTTPS, each micro-service which is supposed to accept connections, should start a listener and should load its own .crt and .key(I will revisit this section when configuring mTLS, but for the moment let's solve the HTTPS):

func main() {
    http.HandleFunc("/hello", HelloServer)
    // Replace 443 with 80 if you really don't care about conventions
    err := http.ListenAndServeTLS(":443", "µ<A|B|C>.crt", "µ<A|B|C>.key", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

mTLS:

mTLS authentication is performed mutually between a client(micro-service sending the request) and a server(micro-service receiving the request). Each micro-service can be a client, a server or both.

From client perspective:

  • the certificate presented by the server must be verified and trusted
  • the client must load it's own certificate and sign its outgoing requests

From server perspective:

  • the incoming requests must be signed by a trusted list of client(s) certificates or CAs
  • the response must be signed with a the server certificate, which should be trusted by the client that originated the request.

Client A perspective:

    // Client A perspective
    client := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                // provide the CA that signed certificates that are presented
                // by servers B and C
                RootCAs: caCertPool,
                // provide the certificate for microservice A, which is
                // initiating the request
                Certificates: []tls.Certificate{cert}, 
            },
        },
    }

Server B or C perspective:

tlsConfig := &tls.Config{
    // add only microservice A certificate in this pool; this will allow A 
    // to connect to B and C, but won't allow B to C and C to B 
    ClientCAs: caCertPool, 
    ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsConfig.BuildNameToCertificate()

server := &http.Server{
    Addr:      ":443", // use :80 if you don't care about conventions
    TLSConfig: tlsConfig,
}

server.ListenAndServeTLS("server.crt", "server.key")
Neo Anderson
  • 5,957
  • 2
  • 12
  • 29
  • Incredibly detailed, thanks for the response! So in the context of these SmallStep guides (in terms of the certificate generation steps in the guide), Server B and C uses server.crt, server.key (as shown in your code example) and client.crt (in the pool)? And Client A uses client.crt (in 'Certificates') and ca.crt (as RootCAs)? And only these keys required, no need to regenerate different keys for each server? https://smallstep.com/hello-mtls/doc/server/go https://smallstep.com/hello-mtls/doc/client/go – Maggie Mar 28 '23 at 09:40
  • @Maggie Each server has it's own dedicated certificate (call them A,B,C; a certificate has 2 basic components public and private key, which can be bundled in different formats). If A is just a client: 1. A must load the CA that signed B.pub and C.pub. 2. A must load its own A.private to sign the requests sent towards B or C. On the other side, assuming B is just a server, it must load it's own B.private to expose HTTPs, and it must also load A.pub if it only wants to authenticate requests originating from A. – Neo Anderson Mar 28 '23 at 09:50
  • It is easier to understand if you split the problem in smaller parts and address problems in small steps. Try this with just two services A(client), B(server) 1. Generate certificates for both 2. Implement am HTTPs server in B 3. Implement a client in A which is calling B (you will need to trust B's certificate, like a web browser) 4. Enable mTLS in B (authenticate only requests that are signed by A) - this will break things because A is still not signing its outgoing requests 5. Enable mTLS in A client (load A's private key in the client) And voila..you have mTLS – Neo Anderson Mar 28 '23 at 09:59
  • Going to try and get my head around this. I added some code snippets to the original question in the meantime. – Maggie Mar 28 '23 at 11:24
  • 1
    Some experiments later I think it is starting to make sense. Client A uses ca.crt (the CA root certificate used to sign the others) in its caCertPool and uses a X509KeyPair of client.crt and client.key (public and private). Server A uses serverA.crt and serverA.key as a X509KeyPair (public/private). Server A uses client.crt as the caCertPool limiting the communication between Server A and Server B. Server B uses serverB.crt and serverB.key (its own unique ones) but the same client.crt as Server A as its caCertPool. About right? :S – Maggie Mar 29 '23 at 13:01
  • 1
    @Maggie In big lines yes. I may try to create a minimal viable example in a public gihub repo, and write a gist aside it, because your question has several points to address, as you already noticed from the length of the answer and the length of this discusson. Most likely this weekend, but can't promise :). From SO guidelines on how to ask & how to answer, we're not supposed to write full fledged implementations here :) – Neo Anderson Mar 30 '23 at 07:17
  • 1
    That sounds great, thanks Neo. I think the Gist would be just as helpful as an example for understanding mTLS, keys and certs as it would be to this particular implementation, which would make it useful for many other scenarios and questions too. – Maggie Mar 30 '23 at 15:37