Securing a NATS Cluster with cfssl

Secure NATS Cluster in Kubernetes using the NATS Operator

Features

  • Clients TLS setup
  • TLS based auth certs via secret
    • Reloading supported by only updating secret
  • Routes TLS setup
  • Advertising public IP per NATS server for external access

Creating the Certificates

Generating the Root CA Certs

  1. {
  2. "CN": "nats.io",
  3. "key": {
  4. "algo": "rsa",
  5. "size": 2048
  6. },
  7. "names": [
  8. {
  9. "OU": "nats.io"
  10. }
  11. ]
  12. }
  1. (
  2. cd certs
  3. # CA certs
  4. cfssl gencert -initca ca-csr.json | cfssljson -bare ca -
  5. )

Setup the profiles for the Root CA, we will have 3 main profiles: one for the clients connecting, one for the servers, and another one for the full mesh routing connections between the servers.

  1. {
  2. "signing": {
  3. "default": {
  4. "expiry": "43800h"
  5. },
  6. "profiles": {
  7. "server": {
  8. "expiry": "43800h",
  9. "usages": [
  10. "signing",
  11. "key encipherment",
  12. "server auth",
  13. "client auth"
  14. ]
  15. },
  16. "client": {
  17. "expiry": "43800h",
  18. "usages": [
  19. "signing",
  20. "key encipherment",
  21. "client auth"
  22. ]
  23. },
  24. "route": {
  25. "expiry": "43800h",
  26. "usages": [
  27. "signing",
  28. "key encipherment",
  29. "server auth",
  30. "client auth"
  31. ]
  32. }
  33. }
  34. }
  35. }

Generating the NATS server certs

First we generate the certificates for the server.

  1. {
  2. "CN": "nats.io",
  3. "hosts": [
  4. "localhost",
  5. "*.nats-cluster.default.svc",
  6. "*.nats-cluster-mgmt.default.svc",
  7. "nats-cluster",
  8. "nats-cluster-mgmt",
  9. "nats-cluster.default.svc",
  10. "nats-cluster-mgmt.default.svc",
  11. "nats-cluster.default.svc.cluster.local",
  12. "nats-cluster-mgmt.default.svc.cluster.local",
  13. "*.nats-cluster.default.svc.cluster.local",
  14. "*.nats-cluster-mgmt.default.svc.cluster.local"
  15. ],
  16. "key": {
  17. "algo": "rsa",
  18. "size": 2048
  19. },
  20. "names": [
  21. {
  22. "OU": "Operator"
  23. }
  24. ]
  25. }
  1. (
  2. # Generating the peer certificates
  3. cd certs
  4. cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server.json | cfssljson -bare server
  5. )

Generating the NATS server routes certs

We will also be setting up TLS for the full mesh routes.

  1. {
  2. "CN": "nats.io",
  3. "hosts": [
  4. "localhost",
  5. "*.nats-cluster.default.svc",
  6. "*.nats-cluster-mgmt.default.svc",
  7. "nats-cluster",
  8. "nats-cluster-mgmt",
  9. "nats-cluster.default.svc",
  10. "nats-cluster-mgmt.default.svc",
  11. "nats-cluster.default.svc.cluster.local",
  12. "nats-cluster-mgmt.default.svc.cluster.local",
  13. "*.nats-cluster.default.svc.cluster.local",
  14. "*.nats-cluster-mgmt.default.svc.cluster.local"
  15. ],
  16. "key": {
  17. "algo": "rsa",
  18. "size": 2048
  19. },
  20. "names": [
  21. {
  22. "OU": "Operator"
  23. }
  24. ]
  25. }
  1. # Generating the peer certificates
  2. (
  3. cd certs
  4. cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=route route.json | cfssljson -bare route
  5. )

Generating the certs for the clients (CNCF && ACME)

  1. {
  2. "CN": "nats.io",
  3. "hosts": [""],
  4. "key": {
  5. "algo": "rsa",
  6. "size": 2048
  7. },
  8. "names": [
  9. {
  10. "OU": "CNCF"
  11. }
  12. ]
  13. }
  1. (
  2. cd certs
  3. # Generating NATS client certs
  4. cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client
  5. )

Kubectl Create

  1. cd certs kubectl create secret generic nats-tls-example --from-file=ca.pem --from-file=server-key.pem --from-file=server.pem kubectl create secret generic nats-tls-routes-example --from-file=ca.pem --from-file=route-key.pem --from-file=route.pem kubectl create secret generic nats-tls-client-example --from-file=ca.pem --from-file=client-key.pem --from-file=client.pem

Create the Auth secret

  1. {
  2. "users": [
  3. { "username": "CN=nats.io,OU=ACME" },
  4. { "username": "CN=nats.io,OU=CNCF",
  5. "permissions": {
  6. "publish": ["hello.*"],
  7. "subscribe": ["hello.world"]
  8. }
  9. }
  10. ],
  11. "default_permissions": {
  12. "publish": ["SANDBOX.*"],
  13. "subscribe": ["PUBLIC.>"]
  14. }
  15. }
  1. kubectl create secret generic nats-tls-users --from-file=users.json

Create a cluster with TLS

  1. echo '
  2. apiVersion: "nats.io/v1alpha2"
  3. kind: "NatsCluster"
  4. metadata:
  5. name: "nats-cluster"
  6. spec:
  7. size: 3
  8. # Using custom edge nats server image for TLS verify and map support.
  9. serverImage: "wallyqs/nats-server"
  10. version: "edge-2.0.0-RC5"
  11. tls:
  12. enableHttps: true
  13. # Certificates to secure the NATS client connections:
  14. serverSecret: "nats-tls-example"
  15. # Certificates to secure the routes.
  16. routesSecret: "nats-tls-routes-example"
  17. auth:
  18. tlsVerifyAndMap: true
  19. clientsAuthSecret: "nats-tls-users"
  20. # How long to wait for authentication
  21. clientsAuthTimeout: 5
  22. pod:
  23. # To be able to reload the secret changes
  24. enableConfigReload: true
  25. reloaderImage: connecteverything/nats-server-config-reloader
  26. # Bind the port 4222 as the host port to allow external access.
  27. enableClientsHostPort: true
  28. # Initializer container that resolves the external IP from the
  29. # container where it is running.
  30. advertiseExternalIP: true
  31. # Image of container that resolves external IP from K8S API
  32. bootconfigImage: "wallyqs/nats-boot-config"
  33. bootconfigImageTag: "0.5.0"
  34. # Service account required to be able to find the external IP
  35. template:
  36. spec:
  37. serviceAccountName: "nats-server"
  38. ' | kubectl apply -f -

Create APP using certs

Adding a new pod which uses the certificates

Development

  1. package main
  2. import (
  3. "encoding/json"
  4. "flag"
  5. "fmt"
  6. "log"
  7. "time"
  8. "github.com/nats-io/go-nats"
  9. "github.com/nats-io/nuid"
  10. )
  11. func main() {
  12. var (
  13. serverList string
  14. rootCACertFile string
  15. clientCertFile string
  16. clientKeyFile string
  17. )
  18. flag.StringVar(&serverList, "s", "tls://nats-1.nats-cluster.default.svc:4222", "List of NATS of servers available")
  19. flag.StringVar(&rootCACertFile, "cacert", "./certs/ca.pem", "Root CA Certificate File")
  20. flag.StringVar(&clientCertFile, "cert", "./certs/client.pem", "Client Certificate File")
  21. flag.StringVar(&clientKeyFile, "key", "./certs/client-key.pem", "Client Private key")
  22. flag.Parse()
  23. log.Println("NATS endpoint:", serverList)
  24. log.Println("Root CA:", rootCACertFile)
  25. log.Println("Client Cert:", clientCertFile)
  26. log.Println("Client Key:", clientKeyFile)
  27. // Connect options
  28. rootCA := nats.RootCAs(rootCACertFile)
  29. clientCert := nats.ClientCert(clientCertFile, clientKeyFile)
  30. alwaysReconnect := nats.MaxReconnects(-1)
  31. var nc *nats.Conn
  32. var err error
  33. for {
  34. nc, err = nats.Connect(serverList, rootCA, clientCert, alwaysReconnect)
  35. if err != nil {
  36. log.Printf("Error while connecting to NATS, backing off for a sec... (error: %s)", err)
  37. time.Sleep(1 * time.Second)
  38. continue
  39. }
  40. break
  41. }
  42. nc.Subscribe("discovery.*.status", func(m *nats.Msg) {
  43. log.Printf("[Received on %q] %s", m.Subject, string(m.Data))
  44. })
  45. discoverySubject := fmt.Sprintf("discovery.%s.status", nuid.Next())
  46. info := struct {
  47. InMsgs uint64 `json:"in_msgs"`
  48. OutMsgs uint64 `json:"out_msgs"`
  49. Reconnects uint64 `json:"reconnects"`
  50. CurrentServer string `json:"current_server"`
  51. Servers []string `json:"servers"`
  52. }{}
  53. for range time.NewTicker(1 * time.Second).C {
  54. stats := nc.Stats()
  55. info.InMsgs = stats.InMsgs
  56. info.OutMsgs = stats.OutMsgs
  57. info.Reconnects = stats.Reconnects
  58. info.CurrentServer = nc.ConnectedUrl()
  59. info.Servers = nc.Servers()
  60. payload, err := json.Marshal(info)
  61. if err != nil {
  62. log.Printf("Error marshalling data: %s", err)
  63. }
  64. err = nc.Publish(discoverySubject, payload)
  65. if err != nil {
  66. log.Printf("Error during publishing: %s", err)
  67. }
  68. nc.Flush()
  69. }
  70. }
  1. FROM golang:1.11.0-alpine3.8 AS builder
  2. COPY . /go/src/github.com/nats-io/nats-kubernetes/examples/nats-cluster-routes-tls/app
  3. WORKDIR /go/src/github.com/nats-io/nats-kubernetes/examples/nats-cluster-routes-tls/app
  4. RUN apk add --update git
  5. RUN go get -u github.com/nats-io/go-nats
  6. RUN go get -u github.com/nats-io/nuid
  7. RUN CGO_ENABLED=0 go build -o nats-client-app -v -a ./client.go
  8. FROM scratch
  9. COPY --from=builder /go/src/github.com/nats-io/nats-kubernetes/examples/nats-cluster-routes-tls/app/nats-client-app /nats-client-app
  10. ENTRYPOINT ["/nats-client-app"]
  1. docker build . -t wallyqs/nats-client-app
  2. docker run wallyqs/nats-client-app
  3. docker push wallyqs/nats-client-app

Pod spec

  1. echo ' apiVersion: apps/v1beta2 kind: Deployment
  2. ## The name of the deployment
  3. metadata: name: nats-client-app
  4. spec:
  5. ## This selector has to match the template.metadata.labels section
  6. ## which is below in the PodSpec
  7. selector: matchLabels: name: nats-client-app
  8. ## Number of instances
  9. replicas: 1
  10. ## PodSpec
  11. template: metadata: labels: name: nats-client-app spec: volumes:
  12. * name: "client-tls-certs"
  13. secret:
  14. secretName: "nats-tls-client-example"
  15. containers:
  16. * name: nats-client-app
  17. command: \["/nats-client-app", "-s", "tls://nats-cluster.default.svc:4222", "-cacert", '/etc/nats-client-tls-certs/ca.pem', '-cert', '/etc/nats-client-tls-certs/client.pem', '-key', '/etc/nats-client-tls-certs/client-key.pem'\]
  18. image: wallyqs/nats-client-app:latest
  19. imagePullPolicy: Always
  20. volumeMounts:
  21. * name: "client-tls-certs"
  22. mountPath: "/etc/nats-client-tls-certs/"
  23. ' \| kubectl apply -f -