Skip to content

Commit

Permalink
feat: makes mutual TLS optional for postgres, mysql/mariadb and grpc (#…
Browse files Browse the repository at this point in the history
…244)

* feat: makes mutual TLS optional for postgres and mysql

* feat: makes mutual TLS optional for gRPC

* refactor: replaces deprecated grpc.WithInsecure()

* docs: changes meaning of grpc tls option to client cert

* chore: updates test go version to same as project version (1.18)

* test: adds TLS and mutual TLS support to db and grpc test environments

* chore: adds generated test certificates to .gitignore

* chore: reduces test certificates to minimum key usage

* chore: adds second client certificate which acts as unauthorized

* test: adds mysql tls and mutual tls tests

* refactor: postgres ssl config check

* refactor: change connectTries to 0 for postgres to only have 1 retry by default like mysql

* refactor: postgres sslmode and sslrootcert code

* test: adds postgres tls and mutual tls tests

* fix: treat grpc authOpts grpc_ca_cert, grpc_tls_cert, grpc_tls_key as file paths instead of actual file contents

refactor: improves error logging

* test: adds grpc tls and mutual tls tests

* Fix postgres ssl modes `require`, ``verify-ca` and `verify-full` to work without explicit root certificate.

* refactor: adds warning for unknown pg_sslmode

style: removes empty lines

* style: compress switch case

Co-authored-by: Martin Abbrent <[email protected]>
  • Loading branch information
NickUfer and maab committed Oct 5, 2022
1 parent a5ca115 commit 92a9e10
Show file tree
Hide file tree
Showing 23 changed files with 756 additions and 112 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ vendor
.idea/
.vscode/

# generated test certificates, keys and CSRs
test-files/certificates/**/*.csr
test-files/certificates/**/*.pem

# todo
TODO
9 changes: 8 additions & 1 deletion Dockerfile.runtest
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ FROM debian:stable-slim as builder
#Change them for your needs.
ENV MOSQUITTO_VERSION=1.6.10
ENV PLUGIN_VERSION=0.6.1
ENV GO_VERSION=1.13.8
ENV GO_VERSION=1.18
# Used in run-test-in-docker.sh to check if the script
# is actually run in a container
ENV MOSQUITTO_GO_AUTH_TEST_RUNNING_IN_A_CONTAINER=true

WORKDIR /app

Expand Down Expand Up @@ -68,5 +71,9 @@ RUN wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -
apt-get install -y mongodb-org && \
rm -f /usr/bin/systemctl

# Install CFSSL to generate test certificates required for tests
RUN export PATH=$PATH:/usr/local/go/bin && go install github.com/cloudflare/cfssl/cmd/[email protected] && cp ~/go/bin/cfssl /usr/local/bin
RUN export PATH=$PATH:/usr/local/go/bin && go install github.com/cloudflare/cfssl/cmd/[email protected] && cp ~/go/bin/cfssljson /usr/local/bin

# Pre-compilation of test for speed-up latest re-run
RUN export PATH=$PATH:/usr/local/go/bin && go test -c ./backends -o /dev/null
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1409,8 +1409,8 @@ The following `auth_opt_` options are supported:
| grpc_host | | Y | gRPC server hostname |
| grpc_port | | Y | gRPC server port number |
| grpc_ca_cert | | N | gRPC server CA cert path |
| grpc_tls_cert | | N | gRPC server TLS cert path |
| grpc_tls_key | | N | gRPC server TLS key path |
| grpc_tls_cert | | N | gRPC client TLS cert path |
| grpc_tls_key | | N | gRPC client TLS key path |
| grpc_disable_superuser | false | N | disable superuser checks |
| grpc_fail_on_dial_error | false | N | fail to init on dial error |
| grpc_dial_timeout_ms | 500 | N | dial timeout in ms |
Expand Down
43 changes: 28 additions & 15 deletions backends/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc/credentials/insecure"
"io/ioutil"
"strconv"
"time"

Expand Down Expand Up @@ -53,9 +55,9 @@ func NewGRPC(authOpts map[string]string, logLevel log.Level) (*GRPC, error) {
}
}

caCert := []byte(authOpts["grpc_ca_cert"])
tlsCert := []byte(authOpts["grpc_tls_cert"])
tlsKey := []byte(authOpts["grpc_tls_key"])
caCert := authOpts["grpc_ca_cert"]
tlsCert := authOpts["grpc_tls_cert"]
tlsKey := authOpts["grpc_tls_key"]
addr := fmt.Sprintf("%s:%s", authOpts["grpc_host"], authOpts["grpc_port"])
withBlock := authOpts["grpc_fail_on_dial_error"] == "true"

Expand Down Expand Up @@ -156,7 +158,7 @@ func (o *GRPC) Halt() {
}
}

func setup(hostname string, caCert, tlsCert, tlsKey []byte, withBlock bool) ([]grpc.DialOption, error) {
func setup(hostname string, caCert string, tlsCert string, tlsKey string, withBlock bool) ([]grpc.DialOption, error) {
logrusEntry := log.NewEntry(log.StandardLogger())
logrusOpts := []grpc_logrus.Option{
grpc_logrus.WithLevels(grpc_logrus.DefaultCodeToLevel),
Expand All @@ -172,25 +174,36 @@ func setup(hostname string, caCert, tlsCert, tlsKey []byte, withBlock bool) ([]g
nsOpts = append(nsOpts, grpc.WithBlock())
}

if len(caCert) == 0 && len(tlsCert) == 0 && len(tlsKey) == 0 {
nsOpts = append(nsOpts, grpc.WithInsecure())
if len(caCert) == 0 {
nsOpts = append(nsOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
log.WithField("server", hostname).Warning("creating insecure grpc client")
} else {
log.WithField("server", hostname).Info("creating grpc client")
cert, err := tls.X509KeyPair(tlsCert, tlsKey)

caCertBytes, err := ioutil.ReadFile(caCert)
if err != nil {
return nil, errors.Wrap(err, "load x509 keypair error")
return nil, errors.Wrap(err, fmt.Sprintf("could not load grpc ca certificate (grpc_ca_cert) from file (%s)", caCert))
}

caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, errors.Wrap(err, "append ca cert to pool error")
if !caCertPool.AppendCertsFromPEM(caCertBytes) {
return nil, errors.New("append ca cert to pool error. Maybe the ca file (grpc_ca_cert) does not contain a valid x509 certificate")
}
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}

if len(tlsCert) != 0 && len(tlsKey) != 0 {
cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey)
if err != nil {
return nil, errors.Wrap(err, "load x509 keypair error")
}
certificates := []tls.Certificate{cert}
tlsConfig.Certificates = certificates
} else if len(tlsCert) != 0 || len(tlsKey) != 0 {
log.Warn("gRPC backend warning: mutual TLS was disabled due to missing client certificate (grpc_tls_cert) or client key (grpc_tls_key)")
}

nsOpts = append(nsOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
})))
nsOpts = append(nsOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
}

return nsOpts, nil
Expand Down
128 changes: 125 additions & 3 deletions backends/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package backends

import (
"context"
"net"
"testing"

"crypto/tls"
"crypto/x509"
"github.com/golang/protobuf/ptypes/empty"
gs "github.com/iegomez/mosquitto-go-auth/grpc"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"net"
"testing"
)

const (
Expand Down Expand Up @@ -184,3 +187,122 @@ func TestGRPC(t *testing.T) {
})

}

func TestGRPCTls(t *testing.T) {
Convey("Given a mock grpc server with TLS", t, func(c C) {
serverCert, err := tls.LoadX509KeyPair("/test-files/certificates/grpc/fullchain-server.pem",
"/test-files/certificates/grpc/server-key.pem")
c.So(err, ShouldBeNil)

config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.NoClientCert,
}
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(config)))
gs.RegisterAuthServiceServer(grpcServer, NewAuthServiceAPI())

listen, err := net.Listen("tcp", ":3123")
c.So(err, ShouldBeNil)

go grpcServer.Serve(listen)
defer grpcServer.Stop()

authOpts := make(map[string]string)
authOpts["grpc_host"] = "localhost"
authOpts["grpc_port"] = "3123"
authOpts["grpc_dial_timeout_ms"] = "100"
authOpts["grpc_fail_on_dial_error"] = "true"

Convey("Given client connects without TLS, it should fail", func() {
g, err := NewGRPC(authOpts, log.DebugLevel)
c.So(err, ShouldBeError)
c.So(err.Error(), ShouldEqual, "context deadline exceeded")
c.So(g, ShouldBeNil)
})

authOpts["grpc_ca_cert"] = "/test-files/certificates/db/ca.pem"

Convey("Given client connects with TLS but with wrong CA, it should fail", func() {
g, err := NewGRPC(authOpts, log.DebugLevel)
c.So(err, ShouldBeError)
c.So(err.Error(), ShouldEqual, "context deadline exceeded")
c.So(g, ShouldBeNil)
})

authOpts["grpc_ca_cert"] = "/test-files/certificates/ca.pem"

Convey("Given client connects with TLS, it should work", func() {
g, err := NewGRPC(authOpts, log.DebugLevel)
c.So(err, ShouldBeNil)
c.So(g, ShouldNotBeNil)
})
})
}

func TestGRPCMutualTls(t *testing.T) {
Convey("Given a mock grpc server with TLS", t, func(c C) {
serverCert, err := tls.LoadX509KeyPair("/test-files/certificates/grpc/fullchain-server.pem",
"/test-files/certificates/grpc/server-key.pem")
c.So(err, ShouldBeNil)

clientCaBytes, err := ioutil.ReadFile("/test-files/certificates/grpc/ca.pem")
c.So(err, ShouldBeNil)
clientCaCertPool := x509.NewCertPool()
c.So(clientCaCertPool.AppendCertsFromPEM(clientCaBytes), ShouldBeTrue)

config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: clientCaCertPool,
}
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(config)))
gs.RegisterAuthServiceServer(grpcServer, NewAuthServiceAPI())

listen, err := net.Listen("tcp", ":3123")
c.So(err, ShouldBeNil)

go grpcServer.Serve(listen)
defer grpcServer.Stop()

authOpts := make(map[string]string)
authOpts["grpc_host"] = "localhost"
authOpts["grpc_port"] = "3123"
authOpts["grpc_dial_timeout_ms"] = "100"
authOpts["grpc_fail_on_dial_error"] = "true"

Convey("Given client connects without TLS, it should fail", func() {
g, err := NewGRPC(authOpts, log.DebugLevel)
c.So(err, ShouldBeError)
c.So(err.Error(), ShouldEqual, "context deadline exceeded")
c.So(g, ShouldBeNil)
})

authOpts["grpc_ca_cert"] = "/test-files/certificates/ca.pem"

Convey("Given client connects with TLS but without a client certificate, it should fail", func() {
g, err := NewGRPC(authOpts, log.DebugLevel)
c.So(err, ShouldBeError)
c.So(err.Error(), ShouldEqual, "context deadline exceeded")
c.So(g, ShouldBeNil)
})

authOpts["grpc_tls_cert"] = "/test-files/certificates/db/client.pem"
authOpts["grpc_tls_key"] = "/test-files/certificates/db/client-key.pem"

Convey("Given client connects with mTLS but with client cert from wrong CA, it should fail", func() {
g, err := NewGRPC(authOpts, log.DebugLevel)
c.So(err, ShouldBeError)
c.So(err.Error(), ShouldEqual, "context deadline exceeded")
c.So(g, ShouldBeNil)
})

authOpts["grpc_tls_cert"] = "/test-files/certificates/grpc/client.pem"
authOpts["grpc_tls_key"] = "/test-files/certificates/grpc/client-key.pem"

Convey("Given client connects with mTLS, it should work", func() {
g, err := NewGRPC(authOpts, log.DebugLevel)
c.So(err, ShouldBeNil)
c.So(g, ShouldNotBeNil)
})
})
}
39 changes: 25 additions & 14 deletions backends/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func NewMysql(authOpts map[string]string, logLevel log.Level, hasher hashing.Has
}

customSSL := false
useSslClientCertificate := false

if sslmode, ok := authOpts["mysql_sslmode"]; ok {
if sslmode == "custom" {
Expand All @@ -127,20 +128,21 @@ func NewMysql(authOpts map[string]string, logLevel log.Level, hasher hashing.Has

if sslCert, ok := authOpts["mysql_sslcert"]; ok {
mysql.SSLCert = sslCert
} else {
customSSL = false
useSslClientCertificate = true
}

if sslKey, ok := authOpts["mysql_sslkey"]; ok {
mysql.SSLKey = sslKey
} else {
customSSL = false
useSslClientCertificate = true
}

if sslRootCert, ok := authOpts["mysql_sslrootcert"]; ok {
mysql.SSLRootCert = sslRootCert
} else {
customSSL = false
if customSSL {
log.Warn("MySQL backend warning: TLS was disabled due to missing root certificate (mysql_sslrootcert)")
customSSL = false
}
}

//If the protocol is a unix socket, we need to set the address as the socket path. If it's tcp, then set the address using host and port.
Expand Down Expand Up @@ -179,17 +181,26 @@ func NewMysql(authOpts map[string]string, logLevel log.Level, hasher hashing.Has
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
return mysql, errors.Errorf("Mysql failed to append root CA pem error: %s", err)
}
clientCert := make([]tls.Certificate, 0, 1)
certs, err := tls.LoadX509KeyPair(mysql.SSLCert, mysql.SSLKey)
if err != nil {
return mysql, errors.Errorf("Mysql load key and cert error: %s", err)

tlsConfig := &tls.Config{
RootCAs: rootCertPool,
}

if useSslClientCertificate {
if mysql.SSLCert != "" && mysql.SSLKey != "" {
clientCert := make([]tls.Certificate, 0, 1)
certs, err := tls.LoadX509KeyPair(mysql.SSLCert, mysql.SSLKey)
if err != nil {
return mysql, errors.Errorf("Mysql load key and cert error: %s", err)
}
clientCert = append(clientCert, certs)
tlsConfig.Certificates = clientCert
} else {
log.Warn("MySQL backend warning: mutual TLS was disabled due to missing client certificate (mysql_sslcert) or client key (mysql_sslkey)")
}
}
clientCert = append(clientCert, certs)

err = mq.RegisterTLSConfig("custom", &tls.Config{
RootCAs: rootCertPool,
Certificates: clientCert,
})
err = mq.RegisterTLSConfig("custom", tlsConfig)
if err != nil {
return mysql, errors.Errorf("Mysql register TLS config error: %s", err)
}
Expand Down
Loading

0 comments on commit 92a9e10

Please sign in to comment.