From 6dd1efdba0764400bf2705c8ef28d68d0547d586 Mon Sep 17 00:00:00 2001
From: Damien Ciabrini <dciabrin@redhat.com>
Date: Tue, 20 Jun 2023 12:25:32 +0000
Subject: [PATCH] Support TLS for galera

Ability to specify a certificate and a CA to be used for galera
cluster communication (GCOMM, SST).

Updates to the certificate used for galera automatically triggers
a rolling restart of the galera pods, without service disruption.

When the Galera CR is configured to use TLS, the mariadbdatabase
CR creates DB users that still allow connection to the DB without
using TLS. This is because Openstack clients currently cannot be
configured to connect via TLS or via plain TCP. This specific
part will be addressed in a subsequent commit.
---
 api/bases/mariadb.openstack.org_galeras.yaml  |  23 +++
 api/go.mod                                    |  26 +--
 api/go.sum                                    |  52 ++---
 api/v1beta1/conditions.go                     |   3 +
 api/v1beta1/galera_types.go                   |   4 +
 api/v1beta1/mariadbdatabase_funcs.go          |   3 +-
 api/v1beta1/zz_generated.deepcopy.go          |   6 +
 .../bases/mariadb.openstack.org_galeras.yaml  |  23 +++
 config/rbac/role.yaml                         |  12 ++
 config/samples/cert-manager-galera-cert.yaml  |  75 ++++++++
 .../samples/mariadb_v1beta1_galera_tls.yaml   |  14 ++
 controllers/galera_controller.go              | 109 ++++++++++-
 controllers/mariadbdatabase_controller.go     |   9 +-
 go.mod                                        |   4 +-
 go.sum                                        |   8 +-
 pkg/mariadb/database.go                       |  14 +-
 pkg/mariadb/statefulset.go                    | 143 ++------------
 pkg/mariadb/volumes.go                        | 179 ++++++++++++++++++
 templates/database.sh                         |   2 +-
 templates/galera/config/config.json           |  35 ++++
 .../galera/config/galera_external_tls.cnf.in  |   8 +
 templates/galera/config/galera_tls.cnf.in     |  15 ++
 .../common/assert_sample_deployment.yaml      |   4 +
 .../kuttl/tests/galera_deploy/04-assert.yaml  |  65 +++++++
 .../galera_deploy/04-deploy_tls_galera.yaml   |  92 +++++++++
 .../kuttl/tests/galera_deploy/05-assert.yaml  |  63 ++++++
 .../05-deploy_external_tls_galera.yaml        |  22 +++
 .../kuttl/tests/galera_deploy/06-assert.yaml  |  61 ++++++
 .../06-deploy_tls_galera_tls_user.yaml        | 115 +++++++++++
 .../tests/galera_deploy/07-teardown.yaml      |  27 +++
 30 files changed, 1023 insertions(+), 193 deletions(-)
 create mode 100644 config/samples/cert-manager-galera-cert.yaml
 create mode 100644 config/samples/mariadb_v1beta1_galera_tls.yaml
 create mode 100644 templates/galera/config/galera_external_tls.cnf.in
 create mode 100644 templates/galera/config/galera_tls.cnf.in
 create mode 100644 tests/kuttl/tests/galera_deploy/04-assert.yaml
 create mode 100644 tests/kuttl/tests/galera_deploy/04-deploy_tls_galera.yaml
 create mode 100644 tests/kuttl/tests/galera_deploy/05-assert.yaml
 create mode 100644 tests/kuttl/tests/galera_deploy/05-deploy_external_tls_galera.yaml
 create mode 100644 tests/kuttl/tests/galera_deploy/06-assert.yaml
 create mode 100644 tests/kuttl/tests/galera_deploy/06-deploy_tls_galera_tls_user.yaml
 create mode 100644 tests/kuttl/tests/galera_deploy/07-teardown.yaml

diff --git a/api/bases/mariadb.openstack.org_galeras.yaml b/api/bases/mariadb.openstack.org_galeras.yaml
index 564c69d7..39df38ba 100644
--- a/api/bases/mariadb.openstack.org_galeras.yaml
+++ b/api/bases/mariadb.openstack.org_galeras.yaml
@@ -84,6 +84,29 @@ spec:
               storageRequest:
                 description: Storage size allocated for the mariadb databases
                 type: string
+              tls:
+                description: TLS settings for MySQL service and internal Galera replication
+                properties:
+                  ca:
+                    description: Ca contains CA-specific settings, which could be
+                      used both by services (to define their own CA certificates)
+                      and by clients (to verify the server's certificate)
+                    properties:
+                      caSecretName:
+                        type: string
+                    type: object
+                  service:
+                    description: Service contains server-specific TLS secret
+                    properties:
+                      disableNonTLSListeners:
+                        type: boolean
+                      secretName:
+                        type: string
+                    type: object
+                required:
+                - ca
+                - service
+                type: object
             required:
             - containerImage
             - replicas
diff --git a/api/go.mod b/api/go.mod
index 67df8b83..5b88a479 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -4,9 +4,9 @@ go 1.19
 
 require (
 	github.com/go-logr/logr v1.2.4
-	github.com/onsi/ginkgo/v2 v2.12.0
-	github.com/onsi/gomega v1.27.10
-	github.com/openstack-k8s-operators/lib-common/modules/common v0.1.0
+	github.com/onsi/ginkgo/v2 v2.12.1
+	github.com/onsi/gomega v1.28.0
+	github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20231004075925-7a2ccbf0ea0e
 	k8s.io/api v0.26.9
 	k8s.io/apimachinery v0.26.9
 	k8s.io/client-go v0.26.9
@@ -15,7 +15,7 @@ require (
 
 require (
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cespare/xxhash/v2 v2.1.2 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.10.1 // indirect
 	github.com/evanphx/json-patch/v5 v5.6.0 // indirect
@@ -32,7 +32,7 @@ require (
 	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
-	github.com/google/uuid v1.3.0 // indirect
+	github.com/google/uuid v1.3.1 // indirect
 	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -50,14 +50,14 @@ require (
 	github.com/prometheus/procfs v0.8.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	go.uber.org/multierr v1.10.0 // indirect
-	go.uber.org/zap v1.25.0 // indirect
-	golang.org/x/net v0.14.0 // indirect
-	golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect
-	golang.org/x/sys v0.11.0 // indirect
-	golang.org/x/term v0.11.0 // indirect
-	golang.org/x/text v0.12.0 // indirect
+	go.uber.org/zap v1.26.0 // indirect
+	golang.org/x/net v0.15.0 // indirect
+	golang.org/x/oauth2 v0.4.0 // indirect
+	golang.org/x/sys v0.12.0 // indirect
+	golang.org/x/term v0.12.0 // indirect
+	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
-	golang.org/x/tools v0.12.0 // indirect
+	golang.org/x/tools v0.13.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
@@ -66,7 +66,7 @@ require (
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/apiextensions-apiserver v0.26.9 // indirect
 	k8s.io/component-base v0.26.9 // indirect
-	k8s.io/klog/v2 v2.80.1 // indirect
+	k8s.io/klog/v2 v2.100.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect
 	k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
diff --git a/api/go.sum b/api/go.sum
index 97d29d04..d6101fd9 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -41,7 +41,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -50,8 +49,9 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -171,8 +171,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@@ -226,14 +226,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
-github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
-github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
-github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
+github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA=
+github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
+github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c=
+github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8=
 github.com/openshift/api v3.9.0+incompatible h1:fJ/KsefYuZAjmrr3+5U9yZIZbTOpVkDDLDLFresAeYs=
 github.com/openshift/api v3.9.0+incompatible/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY=
-github.com/openstack-k8s-operators/lib-common/modules/common v0.1.0 h1:F1iYRBwa0cZ2VHw8Zs4frqSWQ1B/tiCuSwH/DuHb8VM=
-github.com/openstack-k8s-operators/lib-common/modules/common v0.1.0/go.mod h1:3hAC5Ce0AOSt85BqD6DgTKNkJHmpXwqbwL8mVWRJQqo=
+github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20231004075925-7a2ccbf0ea0e h1:/bKZdCAsu73wscdiMsmctAmh0Jz432WxVQe4h1+ipzQ=
+github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20231004075925-7a2ccbf0ea0e/go.mod h1:Ozg6SxfwOtMkiH553c0XQBWuygZQq4jDQCpR4hZqlxM=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -310,8 +310,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
 go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
 go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
-go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
-go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -383,8 +383,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -392,8 +392,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
-golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
+golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -446,12 +446,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
-golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -460,8 +460,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -510,8 +510,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
-golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -643,8 +643,8 @@ k8s.io/client-go v0.26.9 h1:TGWi/6guEjIgT0Hg871Gsmx0qFuoGyGFjlFedrk7It0=
 k8s.io/client-go v0.26.9/go.mod h1:tU1FZS0bwAmAFyPYpZycUQrQnUMzQ5MHloop7EbX6ow=
 k8s.io/component-base v0.26.9 h1:qQVdQgyEIUe8EUkB3EEuQ9l5sgVlG2KgOB519yWEBGw=
 k8s.io/component-base v0.26.9/go.mod h1:3WmW9lH9tbjpuvpAc22cPF/6C3VxCjMxkOU1j2mpzr8=
-k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
-k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
+k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
 k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg=
 k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
diff --git a/api/v1beta1/conditions.go b/api/v1beta1/conditions.go
index bc055e30..fb72a9f2 100644
--- a/api/v1beta1/conditions.go
+++ b/api/v1beta1/conditions.go
@@ -60,4 +60,7 @@ const (
 
 	// MariaDBInitializedErrorMessage
 	MariaDBInitializedErrorMessage = "MariaDB dbinit error occured %s"
+
+	// MariaDBInputSecretNotFoundMessage
+	MariaDBInputSecretNotFoundMessage = "Input secret not found: %s"
 )
diff --git a/api/v1beta1/galera_types.go b/api/v1beta1/galera_types.go
index da3abaec..a4625301 100644
--- a/api/v1beta1/galera_types.go
+++ b/api/v1beta1/galera_types.go
@@ -18,6 +18,7 @@ package v1beta1
 
 import (
 	condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
+	"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -56,6 +57,9 @@ type GaleraSpec struct {
 	// +kubebuilder:validation:Optional
 	// Adoption configuration
 	AdoptionRedirect AdoptionRedirectSpec `json:"adoptionRedirect"`
+	// +kubebuilder:validation:Optional
+	// TLS settings for MySQL service and internal Galera replication
+	TLS *tls.TLS `json:"tls,omitempty"`
 }
 
 // GaleraAttributes holds startup information for a Galera host
diff --git a/api/v1beta1/mariadbdatabase_funcs.go b/api/v1beta1/mariadbdatabase_funcs.go
index 8f5e818d..7238e6f0 100644
--- a/api/v1beta1/mariadbdatabase_funcs.go
+++ b/api/v1beta1/mariadbdatabase_funcs.go
@@ -106,7 +106,8 @@ func (d *Database) setDatabaseHostname(
 			err,
 		)
 	}
-	d.databaseHostname = serviceList.Items[0].GetName()
+	svc := serviceList.Items[0]
+	d.databaseHostname = svc.GetName() + "." + svc.GetNamespace() + ".svc"
 
 	return nil
 }
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 476fef6c..29c87b0c 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -23,6 +23,7 @@ package v1beta1
 
 import (
 	"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
+	"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -158,6 +159,11 @@ func (in *GaleraSpec) DeepCopyInto(out *GaleraSpec) {
 		}
 	}
 	out.AdoptionRedirect = in.AdoptionRedirect
+	if in.TLS != nil {
+		in, out := &in.TLS, &out.TLS
+		*out = new(tls.TLS)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GaleraSpec.
diff --git a/config/crd/bases/mariadb.openstack.org_galeras.yaml b/config/crd/bases/mariadb.openstack.org_galeras.yaml
index 564c69d7..39df38ba 100644
--- a/config/crd/bases/mariadb.openstack.org_galeras.yaml
+++ b/config/crd/bases/mariadb.openstack.org_galeras.yaml
@@ -84,6 +84,29 @@ spec:
               storageRequest:
                 description: Storage size allocated for the mariadb databases
                 type: string
+              tls:
+                description: TLS settings for MySQL service and internal Galera replication
+                properties:
+                  ca:
+                    description: Ca contains CA-specific settings, which could be
+                      used both by services (to define their own CA certificates)
+                      and by clients (to verify the server's certificate)
+                    properties:
+                      caSecretName:
+                        type: string
+                    type: object
+                  service:
+                    description: Service contains server-specific TLS secret
+                    properties:
+                      disableNonTLSListeners:
+                        type: boolean
+                      secretName:
+                        type: string
+                    type: object
+                required:
+                - ca
+                - service
+                type: object
             required:
             - containerImage
             - replicas
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 0db83485..712dbca5 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -112,6 +112,18 @@ rules:
   - pods/exec
   verbs:
   - create
+- apiGroups:
+  - ""
+  resources:
+  - secrets
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - ""
   resources:
diff --git a/config/samples/cert-manager-galera-cert.yaml b/config/samples/cert-manager-galera-cert.yaml
new file mode 100644
index 00000000..0982b9a9
--- /dev/null
+++ b/config/samples/cert-manager-galera-cert.yaml
@@ -0,0 +1,75 @@
+# the cluster-wide issuer, used to generate a root certificate
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+  name: selfsigned-issuer
+spec:
+  selfSigned: {}
+---
+# The root certificate. they cert/key/ca will be generated in the secret 'root-secret'
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: my-selfsigned-ca
+  namespace: openstack
+spec:
+  isCA: true
+  commonName: my-selfsigned-ca
+  secretName: root-secret
+  privateKey:
+    algorithm: ECDSA
+    size: 256
+  issuerRef:
+    name: selfsigned-issuer
+    kind: ClusterIssuer
+    group: cert-manager.io
+---
+# The CA issuer for galera, uses the certificate from `my-selfsigned-ca`
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+  name: my-ca-issuer
+  namespace: openstack
+spec:
+  ca:
+    secretName: root-secret
+---
+# The certificate used by all galera replicas for GCOMM and SST.
+# The replicas in the galera statefulset all share the same
+# certificate, so the latter requires wildcard in dnsNames for TLS
+# validation.
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: galera-cert
+spec:
+  secretName: galera-tls
+  secretTemplate:
+    labels:
+       mariadb-ref: openstack
+  duration: 6h
+  renewBefore: 1h
+  subject:
+    organizations:
+      - cluster.local
+  commonName: openstack-galera
+  isCA: false
+  privateKey:
+    algorithm: RSA
+    encoding: PKCS8
+    size: 2048
+  usages:
+    - server auth
+    - client auth
+  dnsNames:
+    - "openstack.openstack.svc"
+    - "openstack.openstack.svc.cluster.local"
+    - "*.openstack-galera"
+    - "*.openstack-galera.openstack"
+    - "*.openstack-galera.openstack.svc"
+    - "*.openstack-galera.openstack.svc.cluster"
+    - "*.openstack-galera.openstack.svc.cluster.local"
+  issuerRef:
+    name: my-ca-issuer
+    group: cert-manager.io
+    kind: Issuer
diff --git a/config/samples/mariadb_v1beta1_galera_tls.yaml b/config/samples/mariadb_v1beta1_galera_tls.yaml
new file mode 100644
index 00000000..e92995d9
--- /dev/null
+++ b/config/samples/mariadb_v1beta1_galera_tls.yaml
@@ -0,0 +1,14 @@
+apiVersion: mariadb.openstack.org/v1beta1
+kind: Galera
+metadata:
+  name: openstack
+spec:
+  secret: osp-secret
+  storageClass: local-storage
+  storageRequest: 500M
+  replicas: 3
+  tls:
+    service:
+      secretName: galera-tls
+    ca:
+      caSecretName: galera-tls
diff --git a/controllers/galera_controller.go b/controllers/galera_controller.go
index a51a60d6..eb552826 100644
--- a/controllers/galera_controller.go
+++ b/controllers/galera_controller.go
@@ -22,6 +22,7 @@ import (
 	env "github.com/openstack-k8s-operators/lib-common/modules/common/env"
 	helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
 	common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac"
+	secret "github.com/openstack-k8s-operators/lib-common/modules/common/secret"
 	commonstatefulset "github.com/openstack-k8s-operators/lib-common/modules/common/statefulset"
 	util "github.com/openstack-k8s-operators/lib-common/modules/common/util"
 	appsv1 "k8s.io/api/apps/v1"
@@ -44,15 +45,22 @@ import (
 
 	"github.com/go-logr/logr"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/source"
 
 	mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1"
 	mariadb "github.com/openstack-k8s-operators/mariadb-operator/pkg/mariadb"
 )
 
+// Label used in a k8s secret to reference its corresponding galera CR
+const mariaDBReconcileLabel = "mariadb-ref"
+
 // GaleraReconciler reconciles a Galera object
 type GaleraReconciler struct {
 	client.Client
@@ -91,7 +99,8 @@ func buildGcommURI(instance *mariadbv1.Galera) string {
 	res := []string{}
 
 	for i := 0; i < replicas; i++ {
-		res = append(res, basename+"-"+strconv.Itoa(i)+"."+basename)
+		// Generate Gcomm with FQDN for TLS validation
+		res = append(res, basename+"-"+strconv.Itoa(i)+"."+basename+"."+instance.Namespace+".svc")
 	}
 	uri := "gcomm://" + strings.Join(res, ",")
 	return uri
@@ -250,15 +259,17 @@ func assertPodsAttributesValidity(helper *helper.Helper, instance *mariadbv1.Gal
 		if !found {
 			continue
 		}
-		ci := instance.Status.Attributes[pod.Name].ContainerID
-		pci := pod.Status.ContainerStatuses[0].ContainerID
-		if ci != pci {
+		// A node can have various attributes depending on its known state.
+		// A ContainerID attribute is only present if the node is being started.
+		attrCID := instance.Status.Attributes[pod.Name].ContainerID
+		podCID := pod.Status.ContainerStatuses[0].ContainerID
+		if attrCID != "" && attrCID != podCID {
 			// This gcomm URI was pushed in a pod which was restarted
 			// before the attribute got cleared, which means the pod
 			// failed to start galera. Clear the attribute here, and
 			// reprobe the pod's state in the next reconcile loop
 			clearPodAttributes(instance, pod.Name)
-			util.LogForObject(helper, "Pod restarted while galera was starting", instance, "pod", pod.Name)
+			util.LogForObject(helper, "Pod restarted while galera was starting", instance, "pod", pod.Name, "current pod ID", podCID, "recorded ID", attrCID)
 		}
 	}
 }
@@ -276,6 +287,9 @@ func assertPodsAttributesValidity(helper *helper.Helper, instance *mariadbv1.Gal
 // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
 // +kubebuilder:rbac:groups=core,resources=pods/exec,verbs=create
 
+// RBAC for secrets
+// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete;
+
 // RBAC for services and endpoints
 // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete;
 // +kubebuilder:rbac:groups=core,resources=endpoints,verbs=get;list;watch;create;update;patch;delete;
@@ -352,6 +366,8 @@ func (r *GaleraReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
 		instance.Status.Conditions = condition.Conditions{}
 		// initialize conditions used later as Status=Unknown
 		cl := condition.CreateList(
+			// DB Root password and TLS secrets
+			condition.UnknownCondition(condition.InputReadyCondition, condition.InitReason, condition.InputReadyInitMessage),
 			// endpoint for adoption redirect
 			condition.UnknownCondition(condition.ExposeServiceReadyCondition, condition.InitReason, condition.ExposeServiceReadyInitMessage),
 			// configmap generation
@@ -393,6 +409,40 @@ func (r *GaleraReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
 		return rbacResult, nil
 	}
 
+	//
+	// Check for input resources availability
+	//
+	var secretName, certHash, caHash string
+	secretName = instance.Spec.Secret
+	_, _, err = secret.GetSecret(ctx, helper, secretName, instance.Namespace)
+	tls := instance.Spec.TLS
+	if err == nil && tls != nil && tls.Service.SecretName != "" {
+		secretName = tls.Service.SecretName
+		_, certHash, err = secret.GetSecret(ctx, helper, secretName, instance.Namespace)
+	}
+	if err == nil && tls != nil && tls.Ca.CaSecretName != "" {
+		secretName = tls.Ca.CaSecretName
+		_, caHash, err = secret.GetSecret(ctx, helper, secretName, instance.Namespace)
+	}
+	if k8s_errors.IsNotFound(err) {
+		instance.Status.Conditions.Set(condition.FalseCondition(
+			condition.InputReadyCondition,
+			condition.RequestedReason,
+			condition.SeverityInfo,
+			mariadbv1.MariaDBInputSecretNotFoundMessage,
+			secretName))
+		return ctrl.Result{RequeueAfter: time.Duration(5) * time.Second}, nil
+	}
+	if err != nil {
+		instance.Status.Conditions.Set(condition.FalseCondition(
+			condition.InputReadyCondition,
+			condition.ErrorReason,
+			condition.SeverityWarning,
+			condition.InputReadyErrorMessage,
+			err.Error()))
+	}
+	instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
+
 	//
 	// Create/Update all the resources associated to this galera instance
 	//
@@ -461,7 +511,7 @@ func (r *GaleraReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
 		r.Log.Info(fmt.Sprintf("%s %s database service %s - operation: %s", instance.Kind, instance.Name, service.Name, string(op)))
 	}
 
-	// Generate the config maps for the various services
+	// Generate the config maps
 	configMapVars := make(map[string]env.Setter)
 	err = r.generateConfigMaps(ctx, helper, instance, &configMapVars)
 	if err != nil {
@@ -473,16 +523,40 @@ func (r *GaleraReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
 			err.Error()))
 		return ctrl.Result{}, fmt.Errorf("error calculating configmap hash: %w", err)
 	}
+
+	//
+	// Extend the config maps with hashes for TLS secrets if any
+	//
+	specTLS := instance.Spec.TLS
+	if specTLS != nil && specTLS.Service.SecretName != "" {
+		configMapVars[specTLS.Service.SecretName] = env.SetValue(certHash)
+	}
+	if specTLS != nil && specTLS.Ca.CaSecretName != "" {
+		configMapVars[specTLS.Ca.CaSecretName] = env.SetValue(caHash)
+	}
+
 	// From hereon, configMapVars holds a hash of the config generated for this instance
+	// as well as a hash and of the current TLS certificate and CA used if any.
 	// This is used in an envvar in the statefulset to restart it on config change
-	envHash := &corev1.EnvVar{}
-	configMapVars[configMapNameForConfig(instance)](envHash)
-	instance.Status.ConfigHash = envHash.Value
+
+	keys := maps.Keys(configMapVars)
+	sort.Strings(keys)
+
+	hash := ""
+	for _, k := range keys {
+		envVar := &corev1.EnvVar{}
+		configMapVars[k](envVar)
+		hash = hash + envVar.Value
+	}
+	instance.Status.ConfigHash = hash
 	instance.Status.Conditions.MarkTrue(condition.ServiceConfigReadyCondition, condition.ServiceConfigReadyMessage)
 
 	commonstatefulset := commonstatefulset.NewStatefulSet(mariadb.StatefulSet(instance), 5)
 	sfres, sferr := commonstatefulset.CreateOrPatch(ctx, helper)
 	if sferr != nil {
+		if k8s_errors.IsNotFound(sferr) {
+			return ctrl.Result{RequeueAfter: time.Duration(3) * time.Second}, nil
+		}
 		return sfres, sferr
 	}
 	statefulset := commonstatefulset.GetStatefulSet()
@@ -663,5 +737,22 @@ func (r *GaleraReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		Owns(&corev1.ServiceAccount{}).
 		Owns(&rbacv1.Role{}).
 		Owns(&rbacv1.RoleBinding{}).
+		Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(
+			func(o client.Object) []reconcile.Request {
+				labels := o.GetLabels()
+
+				reconcileCR, hasLabel := labels[mariaDBReconcileLabel]
+				if !hasLabel {
+					return []reconcile.Request{}
+				}
+
+				return []reconcile.Request{
+					{NamespacedName: types.NamespacedName{
+						Name:      reconcileCR,
+						Namespace: o.GetNamespace(),
+					}},
+				}
+			},
+		)).
 		Complete(r)
 }
diff --git a/controllers/mariadbdatabase_controller.go b/controllers/mariadbdatabase_controller.go
index 61bf0d52..999a7ae7 100644
--- a/controllers/mariadbdatabase_controller.go
+++ b/controllers/mariadbdatabase_controller.go
@@ -135,6 +135,7 @@ func (r *MariaDBDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Requ
 	// Non-deletion (normal) flow follows
 	//
 	var dbName, dbSecret, dbContainerImage, serviceAccount string
+	var useTLS bool
 
 	// It is impossible to reach here without either dbGalera or dbMariadb not being nil, due to the checks above
 	if dbGalera != nil {
@@ -147,6 +148,11 @@ func (r *MariaDBDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Requ
 		dbSecret = dbGalera.Spec.Secret
 		dbContainerImage = dbGalera.Spec.ContainerImage
 		serviceAccount = dbGalera.RbacResourceName()
+		// NOTE(dciabrin) When configured to only allow TLS connections, all clients
+		// accessing this DB must support client connection via TLS.
+		useTLS = (dbGalera.Spec.TLS != nil &&
+			dbGalera.Spec.TLS.Service != nil &&
+			dbGalera.Spec.TLS.Service.DisableNonTLSListeners)
 	} else if dbMariadb != nil {
 		if dbMariadb.Status.DbInitHash == "" {
 			r.Log.Info("DB initialization not complete. Requeue...")
@@ -157,10 +163,11 @@ func (r *MariaDBDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Requ
 		dbSecret = dbMariadb.Spec.Secret
 		dbContainerImage = dbMariadb.Spec.ContainerImage
 		serviceAccount = dbMariadb.RbacResourceName()
+		useTLS = false
 	}
 
 	// Define a new Job object (hostname, password, containerImage)
-	jobDef, err := mariadb.DbDatabaseJob(instance, dbName, dbSecret, dbContainerImage, serviceAccount)
+	jobDef, err := mariadb.DbDatabaseJob(instance, dbName, dbSecret, dbContainerImage, serviceAccount, useTLS)
 	if err != nil {
 		return ctrl.Result{}, err
 	}
diff --git a/go.mod b/go.mod
index 7148ddca..ceb984d4 100644
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,8 @@ go 1.19
 require (
 	github.com/go-logr/logr v1.2.4
 	github.com/onsi/ginkgo/v2 v2.12.1
-	github.com/onsi/gomega v1.27.10
-	github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20230927082538-4f614f333d17
+	github.com/onsi/gomega v1.28.0
+	github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20231004075925-7a2ccbf0ea0e
 	github.com/openstack-k8s-operators/mariadb-operator/api v0.1.1-0.20230823144333-b9363c5be8d2
 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9
 	k8s.io/api v0.26.9
diff --git a/go.sum b/go.sum
index ee9951ff..2371b34d 100644
--- a/go.sum
+++ b/go.sum
@@ -231,10 +231,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA=
 github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
-github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
-github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
-github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20230927082538-4f614f333d17 h1:n5QmZLJfPtKbNnPVqqSQkLU1X/NMmW3CbML3yjBUjyY=
-github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20230927082538-4f614f333d17/go.mod h1:kZS5rqVWBZeCyYor2PeQB9IEZ19mGaeL/to3x8F9OJg=
+github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c=
+github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8=
+github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20231004075925-7a2ccbf0ea0e h1:/bKZdCAsu73wscdiMsmctAmh0Jz432WxVQe4h1+ipzQ=
+github.com/openstack-k8s-operators/lib-common/modules/common v0.1.1-0.20231004075925-7a2ccbf0ea0e/go.mod h1:Ozg6SxfwOtMkiH553c0XQBWuygZQq4jDQCpR4hZqlxM=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
diff --git a/pkg/mariadb/database.go b/pkg/mariadb/database.go
index 44a37f12..f56b5c9a 100644
--- a/pkg/mariadb/database.go
+++ b/pkg/mariadb/database.go
@@ -14,12 +14,18 @@ type dbCreateOptions struct {
 	DatabaseName          string
 	DatabaseHostname      string
 	DatabaseAdminUsername string
+	DatabaseUserTLS       string
 }
 
 // DbDatabaseJob -
-func DbDatabaseJob(database *databasev1beta1.MariaDBDatabase, databaseHostName string, databaseSecret string, containerImage string, serviceAccountName string) (*batchv1.Job, error) {
-
-	opts := dbCreateOptions{database.Spec.Name, databaseHostName, "root"}
+func DbDatabaseJob(database *databasev1beta1.MariaDBDatabase, databaseHostName string, databaseSecret string, containerImage string, serviceAccountName string, useTLS bool) (*batchv1.Job, error) {
+	var tlsStatement string
+	if useTLS {
+		tlsStatement = " REQUIRE SSL"
+	} else {
+		tlsStatement = ""
+	}
+	opts := dbCreateOptions{database.Spec.Name, databaseHostName, "root", tlsStatement}
 	dbCmd, err := util.ExecuteTemplateFile("database.sh", &opts)
 	if err != nil {
 		return nil, err
@@ -83,7 +89,7 @@ func DbDatabaseJob(database *databasev1beta1.MariaDBDatabase, databaseHostName s
 // DeleteDbDatabaseJob -
 func DeleteDbDatabaseJob(database *databasev1beta1.MariaDBDatabase, databaseHostName string, databaseSecret string, containerImage string, serviceAccountName string) (*batchv1.Job, error) {
 
-	opts := dbCreateOptions{database.Spec.Name, databaseHostName, "root"}
+	opts := dbCreateOptions{database.Spec.Name, databaseHostName, "root", ""}
 	delCmd, err := util.ExecuteTemplateFile("delete_database.sh", &opts)
 	if err != nil {
 		return nil, err
diff --git a/pkg/mariadb/statefulset.go b/pkg/mariadb/statefulset.go
index 2abcfff8..9c674968 100644
--- a/pkg/mariadb/statefulset.go
+++ b/pkg/mariadb/statefulset.go
@@ -14,6 +14,10 @@ import (
 func StatefulSet(g *mariadbv1.Galera) *appsv1.StatefulSet {
 	ls := StatefulSetLabels(g)
 	name := StatefulSetName(g.Name)
+	replicas := g.Spec.Replicas
+	storage := g.Spec.StorageClass
+	storageRequest := resource.MustParse(g.Spec.StorageRequest)
+	configHash := g.Status.ConfigHash
 	sts := &appsv1.StatefulSet{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
@@ -21,7 +25,7 @@ func StatefulSet(g *mariadbv1.Galera) *appsv1.StatefulSet {
 		},
 		Spec: appsv1.StatefulSetSpec{
 			ServiceName: name,
-			Replicas:    g.Spec.Replicas,
+			Replicas:    replicas,
 			Selector: &metav1.LabelSelector{
 				MatchLabels: ls,
 			},
@@ -53,25 +57,7 @@ func StatefulSet(g *mariadbv1.Galera) *appsv1.StatefulSet {
 								},
 							},
 						}},
-						VolumeMounts: []corev1.VolumeMount{{
-							MountPath: "/var/lib/mysql",
-							Name:      "mysql-db",
-						}, {
-							MountPath: "/var/lib/config-data",
-							ReadOnly:  true,
-							Name:      "config-data",
-						}, {
-							MountPath: "/var/lib/pod-config-data",
-							Name:      "pod-config-data",
-						}, {
-							MountPath: "/var/lib/operator-scripts",
-							ReadOnly:  true,
-							Name:      "operator-scripts",
-						}, {
-							MountPath: "/var/lib/kolla/config_files",
-							ReadOnly:  true,
-							Name:      "kolla-config",
-						}},
+						VolumeMounts: getGaleraInitVolumeMounts(g),
 					}},
 					Containers: []corev1.Container{{
 						Image: g.Spec.ContainerImage,
@@ -80,7 +66,7 @@ func StatefulSet(g *mariadbv1.Galera) *appsv1.StatefulSet {
 						Command: []string{"/usr/bin/dumb-init", "--", "/usr/local/bin/kolla_start"},
 						Env: []corev1.EnvVar{{
 							Name:  "CR_CONFIG_HASH",
-							Value: g.Status.ConfigHash,
+							Value: configHash,
 						}, {
 							Name:  "KOLLA_CONFIG_STRATEGY",
 							Value: "COPY_ALWAYS",
@@ -102,29 +88,7 @@ func StatefulSet(g *mariadbv1.Galera) *appsv1.StatefulSet {
 							ContainerPort: 4567,
 							Name:          "galera",
 						}},
-						VolumeMounts: []corev1.VolumeMount{{
-							MountPath: "/var/lib/mysql",
-							Name:      "mysql-db",
-						}, {
-							MountPath: "/var/lib/config-data",
-							ReadOnly:  true,
-							Name:      "config-data",
-						}, {
-							MountPath: "/var/lib/pod-config-data",
-							Name:      "pod-config-data",
-						}, {
-							MountPath: "/var/lib/secrets",
-							ReadOnly:  true,
-							Name:      "secrets",
-						}, {
-							MountPath: "/var/lib/operator-scripts",
-							ReadOnly:  true,
-							Name:      "operator-scripts",
-						}, {
-							MountPath: "/var/lib/kolla/config_files",
-							ReadOnly:  true,
-							Name:      "kolla-config",
-						}},
+						VolumeMounts: getGaleraVolumeMounts(g),
 						StartupProbe: &corev1.Probe{
 							ProbeHandler: corev1.ProbeHandler{
 								Exec: &corev1.ExecAction{
@@ -149,92 +113,7 @@ func StatefulSet(g *mariadbv1.Galera) *appsv1.StatefulSet {
 							},
 						},
 					}},
-					Volumes: []corev1.Volume{
-						{
-							Name: "secrets",
-							VolumeSource: corev1.VolumeSource{
-								Secret: &corev1.SecretVolumeSource{
-									SecretName: g.Spec.Secret,
-									Items: []corev1.KeyToPath{
-										{
-											Key:  "DbRootPassword",
-											Path: "dbpassword",
-										},
-									},
-								},
-							},
-						},
-						{
-							Name: "kolla-config",
-							VolumeSource: corev1.VolumeSource{
-								ConfigMap: &corev1.ConfigMapVolumeSource{
-									LocalObjectReference: corev1.LocalObjectReference{
-										Name: g.Name + "-config-data",
-									},
-									Items: []corev1.KeyToPath{
-										{
-											Key:  "config.json",
-											Path: "config.json",
-										},
-									},
-								},
-							},
-						},
-						{
-							Name: "pod-config-data",
-							VolumeSource: corev1.VolumeSource{
-								EmptyDir: &corev1.EmptyDirVolumeSource{},
-							},
-						},
-						{
-							Name: "config-data",
-							VolumeSource: corev1.VolumeSource{
-								ConfigMap: &corev1.ConfigMapVolumeSource{
-									LocalObjectReference: corev1.LocalObjectReference{
-										Name: g.Name + "-config-data",
-									},
-									Items: []corev1.KeyToPath{
-										{
-											Key:  "galera.cnf.in",
-											Path: "galera.cnf.in",
-										},
-										{
-											Key:  mariadbv1.CustomServiceConfigFile,
-											Path: mariadbv1.CustomServiceConfigFile,
-										},
-									},
-								},
-							},
-						},
-						{
-							Name: "operator-scripts",
-							VolumeSource: corev1.VolumeSource{
-								ConfigMap: &corev1.ConfigMapVolumeSource{
-									LocalObjectReference: corev1.LocalObjectReference{
-										Name: g.Name + "-scripts",
-									},
-									Items: []corev1.KeyToPath{
-										{
-											Key:  "mysql_bootstrap.sh",
-											Path: "mysql_bootstrap.sh",
-										},
-										{
-											Key:  "mysql_probe.sh",
-											Path: "mysql_probe.sh",
-										},
-										{
-											Key:  "detect_last_commit.sh",
-											Path: "detect_last_commit.sh",
-										},
-										{
-											Key:  "detect_gcomm_and_start.sh",
-											Path: "detect_gcomm_and_start.sh",
-										},
-									},
-								},
-							},
-						},
-					},
+					Volumes: getGaleraVolumes(g),
 				},
 			},
 			VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
@@ -247,10 +126,10 @@ func StatefulSet(g *mariadbv1.Galera) *appsv1.StatefulSet {
 						AccessModes: []corev1.PersistentVolumeAccessMode{
 							"ReadWriteOnce",
 						},
-						StorageClassName: &g.Spec.StorageClass,
+						StorageClassName: &storage,
 						Resources: corev1.ResourceRequirements{
 							Requests: corev1.ResourceList{
-								"storage": resource.MustParse(g.Spec.StorageRequest),
+								"storage": storageRequest,
 							},
 						},
 					},
diff --git a/pkg/mariadb/volumes.go b/pkg/mariadb/volumes.go
index 459131f3..a6f2cbb5 100644
--- a/pkg/mariadb/volumes.go
+++ b/pkg/mariadb/volumes.go
@@ -1,6 +1,7 @@
 package mariadb
 
 import (
+	mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1"
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -113,3 +114,181 @@ func getInitVolumeMounts() []corev1.VolumeMount {
 	}
 
 }
+
+func getGaleraVolumes(g *mariadbv1.Galera) []corev1.Volume {
+	configTemplates := []corev1.KeyToPath{
+		{
+			Key:  "galera.cnf.in",
+			Path: "galera.cnf.in",
+		},
+		{
+			Key:  mariadbv1.CustomServiceConfigFile,
+			Path: mariadbv1.CustomServiceConfigFile,
+		},
+	}
+
+	if g.Spec.TLS != nil && g.Spec.TLS.Service.SecretName != "" {
+		if g.Spec.TLS.Ca.CaSecretName != "" {
+			configTemplates = append(configTemplates, corev1.KeyToPath{
+				Key:  "galera_tls.cnf.in",
+				Path: "galera_tls.cnf.in",
+			})
+		} else {
+			// Without a CA, WSREP is unencrypted. Only SQL traffic is.
+			configTemplates = append(configTemplates, corev1.KeyToPath{
+				Key:  "galera_external_tls.cnf.in",
+				Path: "galera_external_tls.cnf.in",
+			})
+		}
+	}
+
+	volumes := []corev1.Volume{
+		{
+			Name: "secrets",
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: g.Spec.Secret,
+					Items: []corev1.KeyToPath{
+						{
+							Key:  "DbRootPassword",
+							Path: "dbpassword",
+						},
+					},
+				},
+			},
+		},
+		{
+			Name: "kolla-config",
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: g.Name + "-config-data",
+					},
+					Items: []corev1.KeyToPath{
+						{
+							Key:  "config.json",
+							Path: "config.json",
+						},
+					},
+				},
+			},
+		},
+		{
+			Name: "pod-config-data",
+			VolumeSource: corev1.VolumeSource{
+				EmptyDir: &corev1.EmptyDirVolumeSource{},
+			},
+		},
+		{
+			Name: "config-data",
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: g.Name + "-config-data",
+					},
+					Items: configTemplates,
+				},
+			},
+		},
+		{
+			Name: "operator-scripts",
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: g.Name + "-scripts",
+					},
+					Items: []corev1.KeyToPath{
+						{
+							Key:  "mysql_bootstrap.sh",
+							Path: "mysql_bootstrap.sh",
+						},
+						{
+							Key:  "mysql_probe.sh",
+							Path: "mysql_probe.sh",
+						},
+						{
+							Key:  "detect_last_commit.sh",
+							Path: "detect_last_commit.sh",
+						},
+						{
+							Key:  "detect_gcomm_and_start.sh",
+							Path: "detect_gcomm_and_start.sh",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	if g.Spec.TLS != nil {
+		caVolumes := g.Spec.TLS.CreateVolumes()
+		volumes = append(volumes, caVolumes...)
+	}
+
+	return volumes
+}
+
+func getGaleraVolumeMounts(g *mariadbv1.Galera) []corev1.VolumeMount {
+	volumeMounts := []corev1.VolumeMount{
+		{
+			MountPath: "/var/lib/mysql",
+			Name:      "mysql-db",
+		}, {
+			MountPath: "/var/lib/config-data",
+			ReadOnly:  true,
+			Name:      "config-data",
+		}, {
+			MountPath: "/var/lib/pod-config-data",
+			Name:      "pod-config-data",
+		}, {
+			MountPath: "/var/lib/secrets",
+			ReadOnly:  true,
+			Name:      "secrets",
+		}, {
+			MountPath: "/var/lib/operator-scripts",
+			ReadOnly:  true,
+			Name:      "operator-scripts",
+		}, {
+			MountPath: "/var/lib/kolla/config_files",
+			ReadOnly:  true,
+			Name:      "kolla-config",
+		},
+	}
+
+	if g.Spec.TLS != nil {
+		caVolumeMounts := g.Spec.TLS.CreateVolumeMounts()
+		volumeMounts = append(volumeMounts, caVolumeMounts...)
+	}
+
+	return volumeMounts
+}
+
+func getGaleraInitVolumeMounts(g *mariadbv1.Galera) []corev1.VolumeMount {
+	volumeMounts := []corev1.VolumeMount{
+		{
+			MountPath: "/var/lib/mysql",
+			Name:      "mysql-db",
+		}, {
+			MountPath: "/var/lib/config-data",
+			ReadOnly:  true,
+			Name:      "config-data",
+		}, {
+			MountPath: "/var/lib/pod-config-data",
+			Name:      "pod-config-data",
+		}, {
+			MountPath: "/var/lib/secrets",
+			ReadOnly:  true,
+			Name:      "secrets",
+		}, {
+			MountPath: "/var/lib/operator-scripts",
+			ReadOnly:  true,
+			Name:      "operator-scripts",
+		}, {
+			MountPath: "/var/lib/kolla/config_files",
+			ReadOnly:  true,
+			Name:      "kolla-config",
+		},
+	}
+
+	return volumeMounts
+}
diff --git a/templates/database.sh b/templates/database.sh
index 13782839..95dd6bc2 100755
--- a/templates/database.sh
+++ b/templates/database.sh
@@ -1,4 +1,4 @@
 #!/bin/bash
 export DatabasePassword=${DatabasePassword:?"Please specify a DatabasePassword variable."}
 
-mysql -h {{.DatabaseHostname}} -u {{.DatabaseAdminUsername}} -P 3306 -e "CREATE DATABASE IF NOT EXISTS {{.DatabaseName}}; GRANT ALL PRIVILEGES ON {{.DatabaseName}}.* TO '{{.DatabaseName}}'@'localhost' IDENTIFIED BY '$DatabasePassword';GRANT ALL PRIVILEGES ON {{.DatabaseName}}.* TO '{{.DatabaseName}}'@'%' IDENTIFIED BY '$DatabasePassword';"
+mysql -h {{.DatabaseHostname}} -u {{.DatabaseAdminUsername}} -P 3306 -e "CREATE DATABASE IF NOT EXISTS {{.DatabaseName}}; GRANT ALL PRIVILEGES ON {{.DatabaseName}}.* TO '{{.DatabaseName}}'@'localhost' IDENTIFIED BY '$DatabasePassword'{{.DatabaseUserTLS}};GRANT ALL PRIVILEGES ON {{.DatabaseName}}.* TO '{{.DatabaseName}}'@'%' IDENTIFIED BY '$DatabasePassword'{{.DatabaseUserTLS}};"
diff --git a/templates/galera/config/config.json b/templates/galera/config/config.json
index 185e33fa..bc2b6f33 100644
--- a/templates/galera/config/config.json
+++ b/templates/galera/config/config.json
@@ -7,6 +7,20 @@
             "owner": "root",
             "perm": "0644"
         },
+        {
+            "source": "/var/lib/pod-config-data/galera_tls.cnf",
+            "dest": "/etc/my.cnf.d/galera_tls.cnf",
+            "owner": "root",
+            "perm": "0644",
+            "optional": true
+        },
+        {
+            "source": "/var/lib/pod-config-data/galera_external_tls.cnf",
+            "dest": "/etc/my.cnf.d/galera_external_tls.cnf",
+            "owner": "root",
+            "perm": "0644",
+            "optional": true
+        },
         {
             "source": "/var/lib/pod-config-data/galera_custom.cnf",
             "dest": "/etc/my.cnf.d/galera_custom.cnf",
@@ -20,6 +34,27 @@
             "owner": "root",
             "perm": "0755",
             "merge": "true"
+        },
+        {
+            "source": "/var/lib/config-data/tls-certificates/tls.key",
+            "dest": "/etc/pki/tls/private/mysql.key",
+            "owner": "mysql",
+            "perm": "0600",
+            "optional": true
+        },
+        {
+            "source": "/var/lib/config-data/tls-certificates/tls.crt",
+            "dest": "/etc/pki/tls/certs/mysql.crt",
+            "owner": "mysql",
+            "perm": "0755",
+            "optional": true
+        },
+        {
+            "source": "/var/lib/config-data/ca-certificates/ca.crt",
+            "dest": "/etc/ipa/ca.crt",
+            "owner": "mysql",
+            "perm": "0444",
+            "optional": true
         }
     ],
     "permissions": [
diff --git a/templates/galera/config/galera_external_tls.cnf.in b/templates/galera/config/galera_external_tls.cnf.in
new file mode 100644
index 00000000..20a099f0
--- /dev/null
+++ b/templates/galera/config/galera_external_tls.cnf.in
@@ -0,0 +1,8 @@
+[mysqld]
+ssl
+ssl-cert = /etc/pki/tls/certs/mysql.crt
+ssl-key = /etc/pki/tls/private/mysql.key
+ssl-cipher = !SSLv2:kEECDH:kRSA:kEDH:kPSK:+3DES:!aNULL:!eNULL:!MD5:!EXP:!RC4:!SEED:!IDEA:!DES:!SSLv3:!TLSv1
+
+[sst]
+ssl-mode = DISABLED
diff --git a/templates/galera/config/galera_tls.cnf.in b/templates/galera/config/galera_tls.cnf.in
new file mode 100644
index 00000000..040c4e60
--- /dev/null
+++ b/templates/galera/config/galera_tls.cnf.in
@@ -0,0 +1,15 @@
+[mysqld]
+ssl
+ssl-cert = /etc/pki/tls/certs/mysql.crt
+ssl-key = /etc/pki/tls/private/mysql.key
+ssl-ca = /etc/ipa/ca.crt
+ssl-cipher = !SSLv2:kEECDH:kRSA:kEDH:kPSK:+3DES:!aNULL:!eNULL:!MD5:!EXP:!RC4:!SEED:!IDEA:!DES:!SSLv3:!TLSv1
+wsrep_provider_options = gcache.recover=no;gmcast.listen_addr=tcp://{ PODIP }:4567;socket.ssl_key=/etc/pki/tls/private/mysql.key;socket.ssl_cert=/etc/pki/tls/certs/mysql.crt;socket.ssl_cipher=AES128-SHA256;socket.ssl_ca=/etc/ipa/ca.crt;
+
+[sst]
+sockopt = cipher=!SSLv2:kEECDH:kRSA:kEDH:kPSK:+3DES:!aNULL:!eNULL:!MD5:!EXP:!RC4:!SEED:!IDEA:!DES:!SSLv3:!TLSv1
+tcert = /etc/pki/tls/certs/mysql.crt
+tkey = /etc/pki/tls/private/mysql.key
+tca = /etc/ipa/ca.crt
+encrypt = 3
+ssl-mode = REQUIRED
diff --git a/tests/kuttl/common/assert_sample_deployment.yaml b/tests/kuttl/common/assert_sample_deployment.yaml
index 33d9720c..2df58a6c 100644
--- a/tests/kuttl/common/assert_sample_deployment.yaml
+++ b/tests/kuttl/common/assert_sample_deployment.yaml
@@ -28,6 +28,10 @@ status:
     reason: Ready
     status: "True"
     type: ExposeServiceReady
+  - message: Input data complete
+    reason: Ready
+    status: "True"
+    type: InputReady
   - message: RoleBinding created
     reason: Ready
     status: "True"
diff --git a/tests/kuttl/tests/galera_deploy/04-assert.yaml b/tests/kuttl/tests/galera_deploy/04-assert.yaml
new file mode 100644
index 00000000..b129355f
--- /dev/null
+++ b/tests/kuttl/tests/galera_deploy/04-assert.yaml
@@ -0,0 +1,65 @@
+apiVersion: mariadb.openstack.org/v1beta1
+kind: Galera
+metadata:
+  name: openstack
+spec:
+  replicas: 3
+  secret: osp-secret
+  storageRequest: 500M
+  tls:
+    service:
+      secretName: kuttl-galera-tls
+    ca:
+      caSecretName: kuttl-galera-tls
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: openstack-galera
+status:
+  availableReplicas: 3
+  readyReplicas: 3
+  replicas: 3
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-0
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-1
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-2
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: openstack-galera
+spec:
+  ports:
+  - name: mysql
+    port: 3306
+    protocol: TCP
+    targetPort: 3306
+  selector:
+    app: galera
+    cr: galera-openstack
+---
+apiVersion: v1
+kind: Endpoints
+metadata:
+  name: openstack-galera
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+  - script: |
+      # ensure galera/WSREP traffic uses encryption
+      oc rsh -n ${NAMESPACE} -c galera openstack-galera-0 /bin/sh -c 'mysql -uroot -p${DB_ROOT_PASSWORD} -Nse "select @@global.wsrep_provider_options;"' | grep -o -w 'socket.ssl = YES'
+      # ensure mysql/SQL traffic uses encryption
+      oc rsh -n ${NAMESPACE} -c galera openstack-galera-0 /bin/sh -c 'mysql -uroot -p${DB_ROOT_PASSWORD} -Nse "select @@global.ssl_cipher;"' | grep -v '^NULL$'
diff --git a/tests/kuttl/tests/galera_deploy/04-deploy_tls_galera.yaml b/tests/kuttl/tests/galera_deploy/04-deploy_tls_galera.yaml
new file mode 100644
index 00000000..7f5e40c8
--- /dev/null
+++ b/tests/kuttl/tests/galera_deploy/04-deploy_tls_galera.yaml
@@ -0,0 +1,92 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+  - apiVersion: mariadb.openstack.org/v1beta1
+    kind: Galera
+    name: openstack
+---
+# cert-manager CRs to generate TLS certificates
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+  name: kuttl-selfsigned-issuer
+spec:
+  selfSigned: {}
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: kuttl-selfsigned-ca
+  namespace: openstack
+spec:
+  isCA: true
+  commonName: kuttl-selfsigned-ca
+  secretName: kuttl-secret
+  privateKey:
+    algorithm: ECDSA
+    size: 256
+  issuerRef:
+    name: kuttl-selfsigned-issuer
+    kind: ClusterIssuer
+    group: cert-manager.io
+---
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+  name: kuttl-ca-issuer
+  namespace: openstack
+spec:
+  ca:
+    secretName: kuttl-secret
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: kuttl-galera-cert
+spec:
+  secretName: kuttl-galera-tls
+  secretTemplate:
+    labels:
+       mariadb-ref: openstack
+  duration: 6h
+  renewBefore: 1h
+  subject:
+    organizations:
+      - cluster.local
+  commonName: openstack-galera
+  isCA: false
+  privateKey:
+    algorithm: RSA
+    encoding: PKCS8
+    size: 2048
+  usages:
+    - server auth
+    - client auth
+  dnsNames:
+    - "openstack.openstack.svc"
+    - "openstack.openstack.svc.cluster.local"
+    - "*.openstack-galera"
+    - "*.openstack-galera.openstack"
+    - "*.openstack-galera.openstack.svc"
+    - "*.openstack-galera.openstack.svc.cluster"
+    - "*.openstack-galera.openstack.svc.cluster.local"
+  issuerRef:
+    name: kuttl-ca-issuer
+    group: cert-manager.io
+    kind: Issuer
+---
+# galera resource using the TLS certs
+apiVersion: mariadb.openstack.org/v1beta1
+kind: Galera
+metadata:
+  name: openstack
+spec:
+  secret: osp-secret
+  storageClass: local-storage
+  storageRequest: 500M
+  replicas: 3
+  tls:
+    service:
+      secretName: kuttl-galera-tls
+    ca:
+      caSecretName: kuttl-galera-tls
diff --git a/tests/kuttl/tests/galera_deploy/05-assert.yaml b/tests/kuttl/tests/galera_deploy/05-assert.yaml
new file mode 100644
index 00000000..e9efc251
--- /dev/null
+++ b/tests/kuttl/tests/galera_deploy/05-assert.yaml
@@ -0,0 +1,63 @@
+apiVersion: mariadb.openstack.org/v1beta1
+kind: Galera
+metadata:
+  name: openstack
+spec:
+  replicas: 3
+  secret: osp-secret
+  storageRequest: 500M
+  tls:
+    service:
+      secretName: kuttl-galera-tls
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: openstack-galera
+status:
+  availableReplicas: 3
+  readyReplicas: 3
+  replicas: 3
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-0
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-1
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-2
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: openstack-galera
+spec:
+  ports:
+  - name: mysql
+    port: 3306
+    protocol: TCP
+    targetPort: 3306
+  selector:
+    app: galera
+    cr: galera-openstack
+---
+apiVersion: v1
+kind: Endpoints
+metadata:
+  name: openstack-galera
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+  - script: |
+      # ensure galera does not encrypt WSREP traffic
+      oc rsh -n ${NAMESPACE} -c galera openstack-galera-0 /bin/sh -c 'mysql -uroot -p${DB_ROOT_PASSWORD} -Nse "select @@global.wsrep_provider_options;"' | grep -o -w 'gmcast.listen_addr = tcp'
+      # ensure mysql/SQL traffic uses encryption
+      oc rsh -n ${NAMESPACE} -c galera openstack-galera-0 /bin/sh -c 'mysql -uroot -p${DB_ROOT_PASSWORD} -Nse "select @@global.ssl_cipher;"' | grep -v '^NULL$'
diff --git a/tests/kuttl/tests/galera_deploy/05-deploy_external_tls_galera.yaml b/tests/kuttl/tests/galera_deploy/05-deploy_external_tls_galera.yaml
new file mode 100644
index 00000000..902f6d35
--- /dev/null
+++ b/tests/kuttl/tests/galera_deploy/05-deploy_external_tls_galera.yaml
@@ -0,0 +1,22 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+  - apiVersion: mariadb.openstack.org/v1beta1
+    kind: Galera
+    name: openstack
+---
+# galera resource with external TLS only (no TLS for galera WSREP traffic)
+apiVersion: mariadb.openstack.org/v1beta1
+kind: Galera
+metadata:
+  name: openstack
+spec:
+  secret: osp-secret
+  storageClass: local-storage
+  storageRequest: 500M
+  replicas: 3
+  tls:
+    service:
+      secretName: kuttl-galera-tls
+    ca:
+      caSecretName:
diff --git a/tests/kuttl/tests/galera_deploy/06-assert.yaml b/tests/kuttl/tests/galera_deploy/06-assert.yaml
new file mode 100644
index 00000000..558be042
--- /dev/null
+++ b/tests/kuttl/tests/galera_deploy/06-assert.yaml
@@ -0,0 +1,61 @@
+apiVersion: mariadb.openstack.org/v1beta1
+kind: Galera
+metadata:
+  name: openstack
+spec:
+  replicas: 3
+  secret: osp-secret
+  storageRequest: 500M
+  tls:
+    service:
+      secretName: kuttl-galera-tls
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: openstack-galera
+status:
+  availableReplicas: 3
+  readyReplicas: 3
+  replicas: 3
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-0
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-1
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: openstack-galera-2
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: openstack-galera
+spec:
+  ports:
+  - name: mysql
+    port: 3306
+    protocol: TCP
+    targetPort: 3306
+  selector:
+    app: galera
+    cr: galera-openstack
+---
+apiVersion: v1
+kind: Endpoints
+metadata:
+  name: openstack-galera
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+  - script: |
+      # ensure db users are configured to TLS restriction
+      oc rsh -n ${NAMESPACE} -c galera openstack-galera-0 /bin/sh -c 'mysql -uroot -p${DB_ROOT_PASSWORD} -e "show grants for \`kuttldb\`@\`%\`;"' | grep 'REQUIRE SSL'
diff --git a/tests/kuttl/tests/galera_deploy/06-deploy_tls_galera_tls_user.yaml b/tests/kuttl/tests/galera_deploy/06-deploy_tls_galera_tls_user.yaml
new file mode 100644
index 00000000..2de18310
--- /dev/null
+++ b/tests/kuttl/tests/galera_deploy/06-deploy_tls_galera_tls_user.yaml
@@ -0,0 +1,115 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+  - apiVersion: mariadb.openstack.org/v1beta1
+    kind: MariaDBDatabase
+    name: kuttldb
+  - apiVersion: mariadb.openstack.org/v1beta1
+    kind: Galera
+    name: openstack
+---
+# cert-manager CRs to generate TLS certificates
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+  name: kuttl-selfsigned-issuer
+spec:
+  selfSigned: {}
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: kuttl-selfsigned-ca
+  namespace: openstack
+spec:
+  isCA: true
+  commonName: kuttl-selfsigned-ca
+  secretName: kuttl-secret
+  privateKey:
+    algorithm: ECDSA
+    size: 256
+  issuerRef:
+    name: kuttl-selfsigned-issuer
+    kind: ClusterIssuer
+    group: cert-manager.io
+---
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+  name: kuttl-ca-issuer
+  namespace: openstack
+spec:
+  ca:
+    secretName: kuttl-secret
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: kuttl-galera-cert
+spec:
+  secretName: kuttl-galera-tls
+  secretTemplate:
+    labels:
+       mariadb-ref: openstack
+  duration: 6h
+  renewBefore: 1h
+  subject:
+    organizations:
+      - cluster.local
+  commonName: openstack-galera
+  isCA: false
+  privateKey:
+    algorithm: RSA
+    encoding: PKCS8
+    size: 2048
+  usages:
+    - server auth
+    - client auth
+  dnsNames:
+    - "openstack.openstack.svc"
+    - "openstack.openstack.svc.cluster.local"
+    - "*.openstack-galera"
+    - "*.openstack-galera.openstack"
+    - "*.openstack-galera.openstack.svc"
+    - "*.openstack-galera.openstack.svc.cluster"
+    - "*.openstack-galera.openstack.svc.cluster.local"
+  issuerRef:
+    name: kuttl-ca-issuer
+    group: cert-manager.io
+    kind: Issuer
+---
+# galera resource with external TLS only (no TLS for galera WSREP traffic)
+apiVersion: mariadb.openstack.org/v1beta1
+kind: Galera
+metadata:
+  name: openstack
+spec:
+  secret: osp-secret
+  storageClass: local-storage
+  storageRequest: 500M
+  replicas: 3
+  tls:
+    service:
+      secretName: kuttl-galera-tls
+      disableNonTLSListeners: true
+    ca:
+      caSecretName: kuttl-galera-tls
+---
+# a database cr associated with the db above will create users with grants
+# set up to only allow TLS connection
+apiVersion: mariadb.openstack.org/v1beta1
+kind: MariaDBDatabase
+metadata:
+  name: kuttldb
+  labels:
+    dbName: openstack
+spec:
+  secret: kuttldb-secret
+  name: kuttldb
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: kuttldb-secret
+data:
+  DatabasePassword: MTIzNDU2Nzg=
diff --git a/tests/kuttl/tests/galera_deploy/07-teardown.yaml b/tests/kuttl/tests/galera_deploy/07-teardown.yaml
new file mode 100644
index 00000000..04f65e94
--- /dev/null
+++ b/tests/kuttl/tests/galera_deploy/07-teardown.yaml
@@ -0,0 +1,27 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+  - apiVersion: mariadb.openstack.org/v1beta1
+    kind: MariaDBDatabase
+    name: kuttldb
+  - apiVersion: mariadb.openstack.org/v1beta1
+    kind: Galera
+    name: openstack
+  - apiVersion: cert-manager.io/v1
+    kind: ClusterIssuer
+    name: kuttl-selfsigned-issuer
+  - apiVersion: cert-manager.io/v1
+    kind: Certificate
+    name: kuttl-selfsigned-ca
+  - apiVersion: cert-manager.io/v1
+    kind: Issuer
+    name: kuttl-ca-issuer
+  - apiVersion: cert-manager.io/v1
+    kind: Certificate
+    name: kuttl-galera-cert
+  - apiVersion: v1
+    kind: Secret
+    name: kuttldb-secret
+commands:
+  - script: |
+      oc delete -n $NAMESPACE pvc mysql-db-openstack-galera-0 mysql-db-openstack-galera-1 mysql-db-openstack-galera-2