feat: makes mutual TLS optional for postgres, mysql/mariadb and grpc (#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 <martin.abbrent@ufz.de>
This commit is contained in:
Nick Ufer 2022-10-05 21:32:36 +02:00 committed by GitHub
parent a5ca115287
commit 92a9e105cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 757 additions and 113 deletions

3
.gitignore vendored
View File

@ -22,6 +22,9 @@ vendor
.idea/
.vscode/
# generated test certificates, keys and CSRs
test-files/certificates/**/*.csr
test-files/certificates/**/*.pem
# todo
TODO

View File

@ -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
@ -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/cfssl@v1.6.2 && cp ~/go/bin/cfssl /usr/local/bin
RUN export PATH=$PATH:/usr/local/go/bin && go install github.com/cloudflare/cfssl/cmd/cfssljson@v1.6.2 && 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

View File

@ -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 |

View File

@ -5,6 +5,8 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc/credentials/insecure"
"io/ioutil"
"strconv"
"time"
@ -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"
@ -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),
@ -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,
}
nsOpts = append(nsOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
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(tlsConfig)))
}
return nsOpts, nil

View File

@ -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 (
@ -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)
})
})
}

View File

@ -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" {
@ -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.
@ -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)
}
clientCert = append(clientCert, certs)
err = mq.RegisterTLSConfig("custom", &tls.Config{
RootCAs: rootCertPool,
Certificates: clientCert,
})
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)")
}
}
err = mq.RegisterTLSConfig("custom", tlsConfig)
if err != nil {
return mysql, errors.Errorf("Mysql register TLS config error: %s", err)
}

View File

@ -208,3 +208,110 @@ func TestMysql(t *testing.T) {
})
}
func TestMysqlTls(t *testing.T) {
authOpts := make(map[string]string)
authOpts["mysql_host"] = "localhost"
authOpts["mysql_port"] = "3306"
authOpts["mysql_protocol"] = "tcp"
authOpts["mysql_allow_native_passwords"] = "true"
authOpts["mysql_dbname"] = "go_auth_test"
authOpts["mysql_user"] = "go_auth_test_tls"
authOpts["mysql_password"] = "go_auth_test_tls"
authOpts["mysql_userquery"] = "SELECT password_hash FROM test_user WHERE username = ? limit 1"
authOpts["mysql_superquery"] = "select count(*) from test_user where username = ? and is_admin = true"
authOpts["mysql_aclquery"] = "SELECT test_acl.topic FROM test_acl, test_user WHERE test_user.username = ? AND test_acl.test_user_id = test_user.id AND (rw >= ? or rw = 3)"
Convey("Given custom ssl disabled, it should fail", t, func() {
mysql, err := NewMysql(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "mysql"))
So(err, ShouldBeError)
So(err.Error(), ShouldContainSubstring, "Error 1045: Access denied for user")
So(mysql.DB, ShouldBeNil)
})
authOpts["mysql_sslmode"] = "custom"
authOpts["mysql_sslrootcert"] = "/test-files/certificates/ca.pem"
Convey("Given custom ssl enabled, it should work without a client certificate", t, func() {
mysql, err := NewMysql(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "mysql"))
So(err, ShouldBeNil)
rows, err := mysql.DB.Query("SHOW status like 'Ssl_cipher';")
So(err, ShouldBeNil)
So(rows.Next(), ShouldBeTrue)
var variableName string
var variableValue string
err = rows.Scan(&variableName, &variableValue)
So(err, ShouldBeNil)
So(variableName, ShouldEqual, "Ssl_cipher")
So(variableValue, ShouldNotBeBlank)
})
}
func TestMysqlMutualTls(t *testing.T) {
authOpts := make(map[string]string)
authOpts["mysql_host"] = "localhost"
authOpts["mysql_port"] = "3306"
authOpts["mysql_protocol"] = "tcp"
authOpts["mysql_allow_native_passwords"] = "true"
authOpts["mysql_dbname"] = "go_auth_test"
authOpts["mysql_user"] = "go_auth_test_mutual_tls"
authOpts["mysql_password"] = "go_auth_test_mutual_tls"
authOpts["mysql_userquery"] = "SELECT password_hash FROM test_user WHERE username = ? limit 1"
authOpts["mysql_superquery"] = "select count(*) from test_user where username = ? and is_admin = true"
authOpts["mysql_aclquery"] = "SELECT test_acl.topic FROM test_acl, test_user WHERE test_user.username = ? AND test_acl.test_user_id = test_user.id AND (rw >= ? or rw = 3)"
authOpts["mysql_sslmode"] = "custom"
authOpts["mysql_sslrootcert"] = "/test-files/certificates/ca.pem"
Convey("Given custom ssl enabled and no client certificate is given, it should fail", t, func() {
mysql, err := NewMysql(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "mysql"))
So(err, ShouldBeError)
So(err.Error(), ShouldContainSubstring, "Error 1045: Access denied for user")
So(mysql.DB, ShouldBeNil)
})
authOpts["mysql_sslcert"] = "/test-files/certificates/db/unauthorized-second-client.pem"
authOpts["mysql_sslkey"] = "/test-files/certificates/db/unauthorized-second-client-key.pem"
Convey("Given custom ssl enabled and unauthorized client certificate is given, it should fail", t, func() {
mysql, err := NewMysql(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "mysql"))
So(err, ShouldBeError)
So(err.Error(), ShouldContainSubstring, "Error 1045: Access denied for user")
So(mysql.DB, ShouldBeNil)
})
authOpts["mysql_sslcert"] = "/test-files/certificates/grpc/client.pem"
authOpts["mysql_sslkey"] = "/test-files/certificates/grpc/client-key.pem"
Convey("Given custom ssl enabled and invalid client certificate is given, it should fail", t, func() {
mysql, err := NewMysql(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "mysql"))
So(err, ShouldBeError)
So(err.Error(), ShouldContainSubstring, "invalid connection")
So(mysql.DB, ShouldBeNil)
})
authOpts["mysql_sslcert"] = "/test-files/certificates/db/client.pem"
authOpts["mysql_sslkey"] = "/test-files/certificates/db/client-key.pem"
Convey("Given custom ssl enabled and client certificate is given, it should work", t, func() {
mysql, err := NewMysql(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "mysql"))
So(err, ShouldBeNil)
rows, err := mysql.DB.Query("SHOW status like 'Ssl_cipher';")
So(err, ShouldBeNil)
So(rows.Next(), ShouldBeTrue)
var variableName string
var variableValue string
err = rows.Scan(&variableName, &variableValue)
So(err, ShouldBeNil)
So(variableName, ShouldEqual, "Ssl_cipher")
So(variableValue, ShouldNotBeBlank)
})
}

View File

@ -51,7 +51,6 @@ func NewPostgres(authOpts map[string]string, logLevel log.Level, hasher hashing.
SuperuserQuery: "",
AclQuery: "",
hasher: hasher,
connectTries: -1,
}
if host, ok := authOpts["pg_host"]; ok {
@ -98,9 +97,12 @@ func NewPostgres(authOpts map[string]string, logLevel log.Level, hasher hashing.
postgres.AclQuery = aclQuery
}
checkSSL := true
if sslmode, ok := authOpts["pg_sslmode"]; ok {
switch sslmode {
case "verify-full", "verify-ca", "require", "disable":
default:
log.Warnf("PG backend warning: using unknown pg_sslmode: '%s'", sslmode)
}
postgres.SSLMode = sslmode
} else {
postgres.SSLMode = "disable"
@ -108,20 +110,14 @@ func NewPostgres(authOpts map[string]string, logLevel log.Level, hasher hashing.
if sslCert, ok := authOpts["pg_sslcert"]; ok {
postgres.SSLCert = sslCert
} else {
checkSSL = false
}
if sslKey, ok := authOpts["pg_sslkey"]; ok {
postgres.SSLKey = sslKey
} else {
checkSSL = false
}
if sslCert, ok := authOpts["pg_sslrootcert"]; ok {
postgres.SSLCert = sslCert
} else {
checkSSL = false
if sslRootCert, ok := authOpts["pg_sslrootcert"]; ok {
postgres.SSLRootCert = sslRootCert
}
//Exit if any mandatory option is missing.
@ -132,14 +128,31 @@ func NewPostgres(authOpts map[string]string, logLevel log.Level, hasher hashing.
//Build the dsn string and try to connect to the db.
connStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%s", postgres.User, postgres.Password, postgres.DBName, postgres.Host, postgres.Port)
if (postgres.SSLMode == "verify-ca" || postgres.SSLMode == "verify-full") && checkSSL {
connStr = fmt.Sprintf("%s sslmode=verify-ca sslcert=%s sslkey=%s sslrootcert=%s", connStr, postgres.SSLCert, postgres.SSLKey, postgres.SSLRootCert)
} else if postgres.SSLMode == "require" {
switch postgres.SSLMode {
case "require":
connStr = fmt.Sprintf("%s sslmode=require", connStr)
} else {
case "verify-ca":
connStr = fmt.Sprintf("%s sslmode=verify-ca", connStr)
case "verify-full":
connStr = fmt.Sprintf("%s sslmode=verify-full", connStr)
case "disable":
fallthrough
default:
connStr = fmt.Sprintf("%s sslmode=disable", connStr)
}
if postgres.SSLRootCert != "" {
connStr = fmt.Sprintf("%s sslrootcert=%s", connStr, postgres.SSLRootCert)
}
if postgres.SSLKey != "" {
connStr = fmt.Sprintf("%s sslkey=%s", connStr, postgres.SSLKey)
}
if postgres.SSLCert != "" {
connStr = fmt.Sprintf("%s sslcert=%s", connStr, postgres.SSLCert)
}
if tries, ok := authOpts["pg_connect_tries"]; ok {
connectTries, err := strconv.Atoi(tries)

View File

@ -199,3 +199,88 @@ func TestPostgres(t *testing.T) {
})
}
func TestPostgresTls(t *testing.T) {
authOpts := make(map[string]string)
authOpts["pg_host"] = "localhost"
authOpts["pg_port"] = "5432"
authOpts["pg_dbname"] = "go_auth_test"
authOpts["pg_user"] = "go_auth_test_tls"
authOpts["pg_password"] = "go_auth_test_tls"
authOpts["pg_userquery"] = "SELECT password_hash FROM test_user WHERE username = $1 limit 1"
authOpts["pg_superquery"] = "select count(*) from test_user where username = $1 and is_admin = true"
authOpts["pg_aclquery"] = "SELECT test_acl.topic FROM test_acl, test_user WHERE test_user.username = $1 AND test_acl.test_user_id = test_user.id AND (rw = $2 or rw = 3)"
Convey("Given custom ssl disabled, it should fail", t, func() {
postgres, err := NewPostgres(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "postgres"))
So(err, ShouldBeError)
So(err.Error(), ShouldContainSubstring, "pg_hba.conf rejects connection")
So(postgres.DB, ShouldBeNil)
})
authOpts["pg_sslmode"] = "verify-full"
authOpts["pg_sslrootcert"] = "/test-files/certificates/ca.pem"
Convey("Given custom ssl enabled, it should work without a client certificate", t, func() {
postgres, err := NewPostgres(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "postgres"))
So(err, ShouldBeNil)
rows, err := postgres.DB.Query("SELECT cipher FROM pg_stat_activity JOIN pg_stat_ssl USING(pid);")
So(err, ShouldBeNil)
So(rows.Next(), ShouldBeTrue)
var sslCipher string
err = rows.Scan(&sslCipher)
So(err, ShouldBeNil)
So(sslCipher, ShouldNotBeBlank)
})
}
func TestPostgresMutualTls(t *testing.T) {
authOpts := make(map[string]string)
authOpts["pg_host"] = "localhost"
authOpts["pg_port"] = "5432"
authOpts["pg_dbname"] = "go_auth_test"
authOpts["pg_user"] = "go_auth_test_mutual_tls"
authOpts["pg_password"] = "go_auth_test_mutual_tls"
authOpts["pg_userquery"] = "SELECT password_hash FROM test_user WHERE username = $1 limit 1"
authOpts["pg_superquery"] = "select count(*) from test_user where username = $1 and is_admin = true"
authOpts["pg_aclquery"] = "SELECT test_acl.topic FROM test_acl, test_user WHERE test_user.username = $1 AND test_acl.test_user_id = test_user.id AND (rw = $2 or rw = 3)"
authOpts["pg_sslmode"] = "verify-full"
authOpts["pg_sslrootcert"] = "/test-files/certificates/ca.pem"
Convey("Given custom ssl enabled and no client certificate is given, it should fail", t, func() {
postgres, err := NewPostgres(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "postgres"))
So(err, ShouldBeError)
So(err.Error(), ShouldEqual, "PG backend error: couldn't open db: couldn't ping database postgres: pq: connection requires a valid client certificate")
So(postgres.DB, ShouldBeNil)
})
authOpts["pg_sslcert"] = "/test-files/certificates/grpc/client.pem"
authOpts["pg_sslkey"] = "/test-files/certificates/grpc/client-key.pem"
Convey("Given custom ssl enabled and invalid client certificate is given, it should fail", t, func() {
postgres, err := NewPostgres(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "postgres"))
So(err, ShouldBeError)
So(err.Error(), ShouldEqual, "PG backend error: couldn't open db: couldn't ping database postgres: pq: connection requires a valid client certificate")
So(postgres.DB, ShouldBeNil)
})
authOpts["pg_sslcert"] = "/test-files/certificates/db/client.pem"
authOpts["pg_sslkey"] = "/test-files/certificates/db/client-key.pem"
Convey("Given custom ssl enabled and client certificate is given, it should work", t, func() {
postgres, err := NewPostgres(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "postgres"))
So(err, ShouldBeNil)
rows, err := postgres.DB.Query("SELECT cipher FROM pg_stat_activity JOIN pg_stat_ssl USING(pid);")
So(err, ShouldBeNil)
So(rows.Next(), ShouldBeTrue)
var sslCipher string
err = rows.Scan(&sslCipher)
So(err, ShouldBeNil)
So(sslCipher, ShouldNotBeBlank)
})
}

205
run-test-in-docker.sh Normal file → Executable file
View File

@ -1,17 +1,127 @@
#!/bin/sh
#!/bin/bash
# This script is make to be run in Docker image build by Dockerfile.test
service postgresql start
service mariadb start
service redis-server start
function checkIfContainer {
if [[ $MOSQUITTO_GO_AUTH_TEST_RUNNING_IN_A_CONTAINER != "true" ]]; then
echo "This script is only supposed run in a container as it modifies the system and databases."
exit 1
fi
}
sudo -u mongodb mongod --config /etc/mongod.conf &
function prepareAndStartPostgres {
local POSTGRES_MAJOR_VERSION=$(sudo find /usr/lib/postgresql -wholename '/usr/lib/postgresql/*/bin/postgres' | grep -Eo '[0-9]+')
local POSTGRES_POSTGRESQL_CONF_FILE="/etc/postgresql/$POSTGRES_MAJOR_VERSION/main/postgresql.conf"
local POSTGRES_PG_HBA_FILE="/etc/postgresql/$POSTGRES_MAJOR_VERSION/main/pg_hba.conf"
mkdir /tmp/cluster-test
cd /tmp/cluster-test
mkdir 7000 7001 7002 7003 7004 7005
cat > 7000/redis.conf << EOF
# Postgres requires 'postgres' to be owner of the server key
mkdir -p /etc/ssl/private/postgresql
cp -r /test-files/certificates/db/server-key.pem /etc/ssl/private/postgresql/server-key.pem
chown postgres:postgres -R /etc/ssl/private/postgresql
usermod -aG ssl-cert postgres
sed -i "/^ssl_(ca|cert|key)_file)/d" $POSTGRES_POSTGRESQL_CONF_FILE
cat >> $POSTGRES_POSTGRESQL_CONF_FILE <<- EOF
ssl_ca_file = '/test-files/certificates/db/fullchain-server.pem'
ssl_cert_file = '/test-files/certificates/db/server.pem'
ssl_key_file = '/etc/ssl/private/postgresql/server-key.pem'
EOF
local PG_HBA_TLS_ENTRIES=$(cat <<- EOF
hostssl all go_auth_test_tls 0.0.0.0/0 md5
hostnossl all go_auth_test_tls 0.0.0.0/0 reject
hostssl all go_auth_test_mutual_tls 0.0.0.0/0 md5 clientcert=verify-ca
hostnossl all go_auth_test_mutual_tls 0.0.0.0/0 reject
EOF)
# Add the tls entries to the beginning of the file, because entry order is important
echo "${PG_HBA_TLS_ENTRIES}$(cat $POSTGRES_PG_HBA_FILE)" > $POSTGRES_PG_HBA_FILE
service postgresql stop && service postgresql start
sudo -u postgres psql <<- "EOF"
create user go_auth_test with login password 'go_auth_test';
create database go_auth_test with owner go_auth_test;
create user go_auth_test_tls with login password 'go_auth_test_tls';
grant all privileges on database go_auth_test TO go_auth_test_tls;
create user go_auth_test_mutual_tls with login password 'go_auth_test_mutual_tls';
grant all privileges on database go_auth_test TO go_auth_test_mutual_tls;
EOF
psql "user=go_auth_test password=go_auth_test host=127.0.0.1" <<- "EOF"
create table test_user(
id bigserial primary key,
username character varying (100) not null,
password_hash character varying (200) not null,
is_admin boolean not null);
create table test_acl(
id bigserial primary key,
test_user_id bigint not null references test_user on delete cascade,
topic character varying (200) not null,
rw int not null);
EOF
}
function prepareAndStartMariaDb {
# Mariadb requires 'mysql' to be owner of the server key
mkdir -p /etc/ssl/private/mariadb
cp -r /test-files/certificates/db/server-key.pem /etc/ssl/private/mariadb/server-key.pem
chown mysql:mysql -R /etc/ssl/private/mariadb
usermod -aG ssl-cert mysql
cat > /etc/mysql/mariadb.conf.d/100-server-ssl-config.cnf <<- EOF
[mysqld]
ssl-ca=/test-files/certificates/db/fullchain-server.pem
ssl-cert=/test-files/certificates/db/server.pem
ssl-key=/etc/ssl/private/mariadb/server-key.pem
EOF
service mariadb stop && service mariadb start
mysql <<- "EOF"
create database go_auth_test;
create user 'go_auth_test'@'localhost' identified by 'go_auth_test';
grant all privileges on go_auth_test.* to 'go_auth_test'@'localhost';
create user 'go_auth_test_tls'@'localhost' identified by 'go_auth_test_tls' REQUIRE SSL;
grant all privileges on go_auth_test.* to 'go_auth_test_tls'@'localhost';
create user 'go_auth_test_mutual_tls'@'localhost' identified by 'go_auth_test_mutual_tls' REQUIRE SUBJECT '/CN=Mosquitto Go Auth Test DB Client';
grant all privileges on go_auth_test.* to 'go_auth_test_mutual_tls'@'localhost';
flush privileges;
EOF
mysql go_auth_test <<- "EOF"
create table test_user(
id mediumint not null auto_increment,
username varchar(100) not null,
password_hash varchar(200) not null,
is_admin boolean not null,
primary key(id)
);
create table test_acl(
id mediumint not null auto_increment,
test_user_id mediumint not null,
topic varchar(200) not null,
rw int not null,
primary key(id),
foreign key(test_user_id) references test_user(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
EOF
}
function prepareAndStartRedis() {
service redis-server start
mkdir /tmp/cluster-test
cd /tmp/cluster-test
mkdir 7000 7001 7002 7003 7004 7005
cat > 7000/redis.conf <<- EOF
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
@ -19,65 +129,36 @@ cluster-node-timeout 5000
appendonly yes
EOF
for i in 7001 7002 7003 7004 7005; do
sed s/7000/$i/ < 7000/redis.conf > $i/redis.conf
done
for i in 7001 7002 7003 7004 7005; do
sed s/7000/$i/ < 7000/redis.conf > $i/redis.conf
done
for i in 7000 7001 7002 7003 7004 7005; do
(cd $i; redis-server redis.conf > server.log 2>&1 &)
done
for i in 7000 7001 7002 7003 7004 7005; do
(cd $i; redis-server redis.conf > server.log 2>&1 &)
done
sudo -u postgres psql << "EOF"
create user go_auth_test with login password 'go_auth_test';
create database go_auth_test with owner go_auth_test;
EOF
sleep 3
psql "user=go_auth_test password=go_auth_test host=127.0.0.1" << "EOF"
yes yes | redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
}
create table test_user(
id bigserial primary key,
username character varying (100) not null,
password_hash character varying (200) not null,
is_admin boolean not null);
checkIfContainer
create table test_acl(
id bigserial primary key,
test_user_id bigint not null references test_user on delete cascade,
topic character varying (200) not null,
rw int not null);
EOF
# Copy certificates structure to container so we
# don't overwrite anything
mkdir -p /test-files/certificates
cp -r /app/test-files/certificates/* /test-files/certificates
# Remove all generated certificates because the generator does not delete already existing files
rm -rf /test-files/certificates/*.pem && rm -rf /test-files/certificates/*.csr
rm -rf /test-files/certificates/**/*.pem && rm -rf /test-files/certificates/**/*.csr
/test-files/certificates/generate-all.sh
mysql << "EOF"
create user 'go_auth_test'@'localhost' identified by 'go_auth_test';
create database go_auth_test;
grant all privileges on go_auth_test.* to 'go_auth_test'@'localhost';
EOF
mysql go_auth_test << "EOF"
create table test_user(
id mediumint not null auto_increment,
username varchar(100) not null,
password_hash varchar(200) not null,
is_admin boolean not null,
primary key(id)
);
create table test_acl(
id mediumint not null auto_increment,
test_user_id mediumint not null,
topic varchar(200) not null,
rw int not null,
primary key(id),
foreign key(test_user_id) references test_user(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
EOF
yes yes | redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
prepareAndStartPostgres
prepareAndStartMariaDb
prepareAndStartRedis
sudo -u mongodb mongod --config /etc/mongod.conf &
cd /app
export PATH=$PATH:/usr/local/go/bin

View File

@ -0,0 +1,11 @@
{
"CN": "Mosquitto Go Auth Test Root CA",
"CA": {
"expiry": "1h",
"pathlen": 1
},
"key": {
"algo": "rsa",
"size": 2048
}
}

View File

@ -0,0 +1,11 @@
{
"CN": "Mosquitto Go Auth Test DB Intermediate CA",
"CA": {
"expiry": "1h",
"pathlen": 0
},
"key": {
"algo": "rsa",
"size": 2048
}
}

View File

@ -0,0 +1,8 @@
{
"CN": "Mosquitto Go Auth Test DB Client",
"key": {
"algo": "rsa",
"size": 2048
},
"hosts": [""]
}

View File

@ -0,0 +1,20 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd $SCRIPT_DIR
cfssl genkey -initca ca.json | cfssljson -bare ca
cfssl sign -ca ../ca.pem -ca-key ../ca-key.pem -config=profiles.json -profile=ca ca.csr | cfssljson -bare ca
cfssl gencert -ca ca.pem -ca-key ca-key.pem -config=profiles.json -profile=server server.json | cfssljson -bare server
cfssl gencert -ca ca.pem -ca-key ca-key.pem -config=profiles.json -profile=client client.json | cfssljson -bare client
cfssl gencert -ca ca.pem -ca-key ca-key.pem -config=profiles.json -profile=client unauthorized-second-client.json | cfssljson -bare unauthorized-second-client
cat server.pem > fullchain-server.pem
cat ca.pem >> fullchain-server.pem
cat ../ca.pem >> fullchain-server.pem
cat client.pem > fullchain-client.pem
cat ca.pem >> fullchain-client.pem
cat ../ca.pem >> fullchain-client.pem
cd -

View File

@ -0,0 +1,35 @@
{
"signing": {
"default": {
"expiry": "1h"
},
"profiles": {
"ca": {
"usages": [
"cert sign"
],
"expiry": "1h",
"ca_constraint": {
"is_ca": true,
"max_path_len": 0,
"max_path_len_zero": true
}
},
"server": {
"usages": [
"key encipherment",
"server auth"
],
"expiry": "1h"
},
"client": {
"usages": [
"signing",
"key encipherment",
"client auth"
],
"expiry": "1h"
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"CN": "Mosquitto Go Auth Test DB Server",
"key": {
"algo": "rsa",
"size": 2048
},
"hosts": [
"localhost",
"127.0.0.1",
"db.mosquitto-go-auth.invalid"
]
}

View File

@ -0,0 +1,8 @@
{
"CN": "Mosquitto Go Auth Test DB Second Client",
"key": {
"algo": "rsa",
"size": 2048
},
"hosts": [""]
}

View File

@ -0,0 +1,12 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd $SCRIPT_DIR
cfssl genkey -initca ca.json | cfssljson -bare ca
# New subcommand so we don't mess up our last cd location
bash -c "./db/generate.sh"
bash -c "./grpc/generate.sh"
cd -

View File

@ -0,0 +1,11 @@
{
"CN": "Mosquitto Go Auth Test gRPC Intermediate CA",
"CA": {
"expiry": "1h",
"pathlen": 0
},
"key": {
"algo": "rsa",
"size": 2048
}
}

View File

@ -0,0 +1,8 @@
{
"CN": "Mosquitto Go Auth Test gRPC Client",
"key": {
"algo": "rsa",
"size": 2048
},
"hosts": [""]
}

View File

@ -0,0 +1,19 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd $SCRIPT_DIR
cfssl genkey -initca ca.json | cfssljson -bare ca
cfssl sign -ca ../ca.pem -ca-key ../ca-key.pem -config=profiles.json -profile=ca ca.csr | cfssljson -bare ca
cfssl gencert -ca ca.pem -ca-key ca-key.pem -config=profiles.json -profile=server server.json | cfssljson -bare server
cfssl gencert -ca ca.pem -ca-key ca-key.pem -config=profiles.json -profile=client client.json | cfssljson -bare client
cat server.pem > fullchain-server.pem
cat ca.pem >> fullchain-server.pem
cat ../ca.pem >> fullchain-server.pem
cat client.pem > fullchain-client.pem
cat ca.pem >> fullchain-client.pem
cat ../ca.pem >> fullchain-client.pem
cd -

View File

@ -0,0 +1,35 @@
{
"signing": {
"default": {
"expiry": "1h"
},
"profiles": {
"ca": {
"usages": [
"cert sign"
],
"expiry": "1h",
"ca_constraint": {
"is_ca": true,
"max_path_len": 0,
"max_path_len_zero": true
}
},
"server": {
"usages": [
"key encipherment",
"server auth"
],
"expiry": "1h"
},
"client": {
"usages": [
"signing",
"key encipherment",
"client auth"
],
"expiry": "1h"
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"CN": "Mosquitto Go Auth Test gRPC Server",
"key": {
"algo": "rsa",
"size": 2048
},
"hosts": [
"localhost",
"127.0.0.1",
"grpc.mosquitto-go-auth.invalid"
]
}