This is an automated email from the ASF dual-hosted git repository.
thelabdude pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git
The following commit(s) were added to refs/heads/main by this push:
new 3d14af0 Option to watch for updates to the mTLS cert used by the
operator to call Solr pods (#318)
3d14af0 is described below
commit 3d14af0808a3453fb98dd3ff30c033b22e0e7a07
Author: Timothy Potter <[email protected]>
AuthorDate: Wed Sep 1 10:15:25 2021 -0600
Option to watch for updates to the mTLS cert used by the operator to call
Solr pods (#318)
Co-authored-by: Houston Putman <[email protected]>
---
build/NOTICE-ADDITION | 35 +++++++
docs/running-the-operator.md | 5 +-
go.mod | 1 +
helm/solr-operator/Chart.yaml | 7 ++
helm/solr-operator/README.md | 1 +
helm/solr-operator/templates/deployment.yaml | 2 +
helm/solr-operator/values.yaml | 1 +
main.go | 144 ++++++++++++++++++++++-----
8 files changed, 170 insertions(+), 26 deletions(-)
diff --git a/build/NOTICE-ADDITION b/build/NOTICE-ADDITION
index f1ec400..62bd388 100644
--- a/build/NOTICE-ADDITION
+++ b/build/NOTICE-ADDITION
@@ -1,2 +1,37 @@
This project uses the hashicorp/golang-lru project, which is MPL 2.0 licensed.
The source code can be found at
https://github.com/hashicorp/golang-lru
+
+This project uses the runtime dependency https://github.com/fsnotify/fsnotify,
which is BSD-3 licensed.
+See the below notice, provided by the project.
+
+=================================================
+== FSNotify Notice ==
+=================================================
+Copyright (c) 2012 The Go Authors. All rights reserved.
+Copyright (c) 2012-2019 fsnotify Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/running-the-operator.md b/docs/running-the-operator.md
index 3ada501..41e90e9 100644
--- a/docs/running-the-operator.md
+++ b/docs/running-the-operator.md
@@ -140,4 +140,7 @@ The CA certificate needs to be stored in Kubernetes secret
in PEM format and pro
```
In most cases, you'll also want to configure the operator with
`mTLS.insecureSkipVerify=true` (the default) as you'll want the operator to
skip hostname verification for Solr pods.
-Setting `mTLS.insecureSkipVerify` to `false` means the operator will enforce
hostname verification for the certificate provided by Solr pods.
\ No newline at end of file
+Setting `mTLS.insecureSkipVerify` to `false` means the operator will enforce
hostname verification for the certificate provided by Solr pods.
+
+By default, the operator watches for updates to the mTLS client certificate
(mounted from the `mTLS.clientCertSecret` secret) and then refreshes the HTTP
client to use the updated certificate.
+To disable this behavior, configure the operator using: `--set
mTLS.watchForUpdates=false`.
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 4760908..c6dfc02 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.16
require (
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c //
indirect
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 //
indirect
+ github.com/fsnotify/fsnotify v1.4.9
github.com/go-logr/logr v0.4.0
github.com/go-logr/zapr v0.2.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 860dc5d..c0f80e2 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -156,6 +156,13 @@ annotations:
url: https://github.com/apache/solr-operator/issues/290
- name: Github PR
url: https://github.com/apache/solr-operator/pull/311
+ - kind: added
+ description: Option to watch for updates to the mTLS client certificate
used by the operator to call Solr pods.
+ links:
+ - name: Github Issue
+ url: https://github.com/apache/solr-operator/issues/317
+ - name: Github PR
+ url: https://github.com/apache/solr-operator/pull/318
artifacthub.io/images: |
- name: solr-operator
image: apache/solr-operator:v0.4.0-prerelease
diff --git a/helm/solr-operator/README.md b/helm/solr-operator/README.md
index 841e9a5..50870ca 100644
--- a/helm/solr-operator/README.md
+++ b/helm/solr-operator/README.md
@@ -163,6 +163,7 @@ The command removes all the Kubernetes components
associated with the chart and
| mTLS.caCertSecretKey | string | `""` | Name of a Kubernetes secret, in the
same namespace, that contains PEM encoded Root CA Certificate to use when
connecting to Solr with Client Auth. |
| mTLS.caCertSecret | string | `""` | Name of the key in the `caCertSecret`
that contains the Root CA Cert as a value. |
| mTLS.insecureSkipVerify | boolean | `true` | Skip server certificate and
hostname verification when connecting to Solr with ClientAuth. |
+| mTLS.watchForUpdates | boolean | `true` | Watch for updates to the mTLS
certificate to reload the HTTP client used to call Solr pods with an updated
client certificate. |
### Running the Solr Operator
diff --git a/helm/solr-operator/templates/deployment.yaml
b/helm/solr-operator/templates/deployment.yaml
index e6f2991..946c81c 100644
--- a/helm/solr-operator/templates/deployment.yaml
+++ b/helm/solr-operator/templates/deployment.yaml
@@ -64,6 +64,8 @@ spec:
{{- if .Values.mTLS.insecureSkipVerify }}
- --tls-skip-verify-server={{ .Values.mTLS.insecureSkipVerify }}
{{- end }}
+ - --tls-watch-cert={{ .Values.mTLS.watchForUpdates }}
+
env:
- name: POD_NAMESPACE
valueFrom:
diff --git a/helm/solr-operator/values.yaml b/helm/solr-operator/values.yaml
index d8acfdf..c165b21 100644
--- a/helm/solr-operator/values.yaml
+++ b/helm/solr-operator/values.yaml
@@ -76,3 +76,4 @@ mTLS:
caCertSecret: ""
caCertSecretKey: ca-cert.pem
insecureSkipVerify: true
+ watchForUpdates: true
diff --git a/main.go b/main.go
index a87b695..adc4321 100644
--- a/main.go
+++ b/main.go
@@ -26,9 +26,11 @@ import (
"github.com/apache/solr-operator/controllers"
"github.com/apache/solr-operator/controllers/util/solr_api"
"github.com/apache/solr-operator/version"
+ "github.com/fsnotify/fsnotify"
"io/ioutil"
"net/http"
"os"
+ "path/filepath"
"runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"strings"
@@ -65,6 +67,10 @@ var (
clientCertPath string
clientCertKeyPath string
caCertPath string
+ clientCertWatch bool
+
+ // Ref to the active client certificate, will get updated when the
secret changes if watch enabled
+ clientCertificate *tls.Certificate
)
func init() {
@@ -82,6 +88,7 @@ func init() {
flag.StringVar(&clientCertKeyPath, "tls-client-cert-key-path", "",
"Path where a TLS client cert key can be found")
flag.StringVar(&caCertPath, "tls-ca-cert-path", "", "Path where a
Certificate Authority (CA) cert in PEM format can be found")
+ flag.BoolVar(&clientCertWatch, "tls-watch-cert", true, "Controls
whether the operator performs a hot reload of the mTLS when it gets updated;
set to false to disable watching for updates to the TLS cert.")
flag.Parse()
}
@@ -147,8 +154,21 @@ func main() {
controllers.UseZkCRD(useZookeeperCRD)
- if err = initMTLSConfig(); err != nil {
- os.Exit(1)
+ // watch TLS files for update
+ if clientCertPath != "" {
+ var watcher *fsnotify.Watcher
+ if clientCertWatch {
+ watcher, err = fsnotify.NewWatcher()
+ if err != nil {
+ setupLog.Error(err, "Create new file watcher
failed")
+ os.Exit(1)
+ }
+ defer watcher.Close()
+ }
+
+ if err = initMTLSConfig(watcher); err != nil {
+ os.Exit(1)
+ }
}
if err = (&controllers.SolrCloudReconciler{
@@ -181,36 +201,110 @@ func main() {
}
}
-func initMTLSConfig() error {
- if clientCertPath != "" {
- setupLog.Info("mTLS config", "clientSkipVerify",
clientSkipVerify, "clientCertPath", clientCertPath,
- "clientCertKeyPath", clientCertKeyPath, "caCertPath",
caCertPath)
+// Setup for mTLS with Solr pods with hot reload support using the fsnotify
Watcher
+func initMTLSConfig(watcher *fsnotify.Watcher) error {
+ setupLog.Info("mTLS config", "clientSkipVerify", clientSkipVerify,
"clientCertPath", clientCertPath,
+ "clientCertKeyPath", clientCertKeyPath, "caCertPath",
caCertPath, "clientCertWatch", clientCertWatch)
- // Load client cert information from files
- clientCert, err := tls.LoadX509KeyPair(clientCertPath,
clientCertKeyPath)
- if err != nil {
- setupLog.Error(err, "Error loading clientCert pair for
mTLS transport", "certPath", clientCertPath, "keyPath", clientCertKeyPath)
- return err
+ // Load client cert information from files
+ clientCert, err := tls.LoadX509KeyPair(clientCertPath,
clientCertKeyPath)
+ if err != nil {
+ setupLog.Error(err, "Error loading clientCert pair for mTLS
transport", "certPath", clientCertPath, "keyPath", clientCertKeyPath)
+ return err
+ }
+
+ if watcher != nil {
+ // If the cert file is a symlink (which is the case when loaded
from a secret), then we need to re-add the watch after the
+ // Right now, it will always be a symlink, so this is just
future proofing when the cert gets loaded from a CSI driver
+ isSymlink := false
+ clientCertFile, _ := filepath.EvalSymlinks(clientCertPath)
+ if clientCertFile != clientCertPath {
+ isSymlink = true
}
- mTLSTransport := http.DefaultTransport.(*http.Transport).Clone()
- mTLSTransport.TLSClientConfig = &tls.Config{Certificates:
[]tls.Certificate{clientCert}, InsecureSkipVerify: clientSkipVerify}
-
- // Add the rootCA if one is provided
- if caCertPath != "" {
- if caCertBytes, err := ioutil.ReadFile(caCertPath); err
== nil {
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCertBytes)
- mTLSTransport.TLSClientConfig.ClientCAs =
caCertPool
- setupLog.Info("Configured the custom CA pem for
the mTLS transport", "path", caCertPath)
- } else {
- setupLog.Error(err, "Cannot read provided CA
pem for mTLS transport", "path", caCertPath)
- return err
+ // Watch cert files for updates
+ go func() {
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return
+ }
+ // If the cert was loaded from a
secret, then the path will be for a symlink and an update comes in as a REMOVE
event
+ // otherwise, look for a write to the
real file
+ if ((isSymlink &&
event.Op&fsnotify.Remove == fsnotify.Remove) || (event.Op&fsnotify.Write ==
fsnotify.Write)) && event.Name == clientCertPath {
+ clientCertFile, _ :=
filepath.EvalSymlinks(clientCertPath)
+ setupLog.Info("mTLS cert file
updated", "certPath", clientCertPath, "certFile", clientCertFile)
+
+ clientCert, err =
tls.LoadX509KeyPair(clientCertPath, clientCertKeyPath)
+ if err == nil {
+ // update our global
client certificate to the new one
+ clientCertificate =
&clientCert
+ setupLog.Info("Updated
mTLS Http Client after update to cert", "certPath", clientCertPath)
+ } else {
+ // will keep using the
old cert, which eventually will cause failures when the old cert expires
+ setupLog.Error(err,
"Error loading clientCert pair (after update) for mTLS transport", "certPath",
clientCertPath, "keyPath", clientCertKeyPath)
+ }
+
+ // If the symlink we were
watching was removed, re-add the watch
+ if event.Op&fsnotify.Remove ==
fsnotify.Remove {
+ err =
watcher.Add(clientCertPath)
+ if err != nil {
+
setupLog.Error(err, "Re-add fsnotify watch for cert failed", "certPath",
clientCertPath)
+ } else {
+
setupLog.Info("Re-added watch for symlink to mTLS cert file", "certPath",
clientCertPath, "certFile", clientCertFile)
+ }
+ }
+ }
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return
+ }
+ setupLog.Error(err, "fsnotify error")
+ }
}
+ }()
+
+ err = watcher.Add(clientCertPath)
+ if err != nil {
+ setupLog.Error(err, "Add fsnotify watch for cert
failed", "certPath", clientCertPath)
+ return err
}
+ setupLog.Info("Added fsnotify watch for mTLS cert file",
"certPath", clientCertPath, "certFile", clientCertFile, "isSymlink", isSymlink)
+ } else {
+ setupLog.Info("Watch for mTLS cert updates disabled",
"certPath", clientCertPath)
+ }
- solr_api.SetMTLSHttpClient(&http.Client{Transport:
mTLSTransport})
+ clientCertificate = &clientCert
+ mTLSTransport, err := buildTLSTransport()
+ if err != nil {
+ return err
}
+ solr_api.SetMTLSHttpClient(&http.Client{Transport: mTLSTransport})
return nil
}
+
+func buildTLSTransport() (*http.Transport, error) {
+ mTLSTransport := http.DefaultTransport.(*http.Transport).Clone()
+ mTLSTransport.TLSClientConfig = &tls.Config{GetClientCertificate:
getClientCertificate, InsecureSkipVerify: clientSkipVerify}
+
+ // Add the rootCA if one is provided
+ if caCertPath != "" {
+ if caCertBytes, err := ioutil.ReadFile(caCertPath); err == nil {
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCertBytes)
+ mTLSTransport.TLSClientConfig.ClientCAs = caCertPool
+ setupLog.Info("Configured the custom CA pem for the
mTLS transport", "path", caCertPath)
+ } else {
+ setupLog.Error(err, "Cannot read provided CA pem for
mTLS transport", "path", caCertPath)
+ return nil, err
+ }
+ }
+
+ return mTLSTransport, nil
+}
+
+func getClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate,
error) {
+ return clientCertificate, nil
+}