diff --git a/.gitignore b/.gitignore index 701e121d..b13c9f08 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,8 @@ bin/ # e2e logs test/e2e/logs_* +# vscode local +.devcontainer +# vs debug files +__debug_* diff --git a/api/v1alpha1/mysqlcluster_types.go b/api/v1alpha1/mysqlcluster_types.go index 6a8037f0..71c9e681 100644 --- a/api/v1alpha1/mysqlcluster_types.go +++ b/api/v1alpha1/mysqlcluster_types.go @@ -96,6 +96,9 @@ type MysqlClusterSpec struct { // +optional // +kubebuilder:default:=6 BackupScheduleJobsHistoryLimit *int `json:"backupScheduleJobsHistoryLimit,omitempty"` + // Containing CA (ca.crt) and server cert (tls.crt) ,server private key (tls.key) for SSL + //+optional + TlsSecretName string `json:"tlsSecretName,omitempty"` } // MysqlOpts defines the options of MySQL container. diff --git a/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml b/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml index de7fc65c..1ba82b4d 100644 --- a/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml +++ b/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml @@ -1264,6 +1264,10 @@ spec: description: Represents the name of the cluster restore from backup path. type: string + tlsSecretName: + description: Containing CA (ca.crt) and server cert (tls.crt) ,server + private key (tls.key) for SSL + type: string xenonOpts: default: admitDefeatHearbeatCount: 5 diff --git a/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml b/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml index de7fc65c..1ba82b4d 100644 --- a/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml +++ b/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml @@ -1264,6 +1264,10 @@ spec: description: Represents the name of the cluster restore from backup path. type: string + tlsSecretName: + description: Containing CA (ca.crt) and server cert (tls.crt) ,server + private key (tls.key) for SSL + type: string xenonOpts: default: admitDefeatHearbeatCount: 5 diff --git a/docs/zh-cn/how_to_use_tls.md b/docs/zh-cn/how_to_use_tls.md new file mode 100644 index 00000000..831aa41e --- /dev/null +++ b/docs/zh-cn/how_to_use_tls.md @@ -0,0 +1,115 @@ +[TOC] + +# 为MySQL客户端开启加密连接 + +# `TLS`(传输层加密)简介 + +RadonDB MySQL Operator 默认采用非加密连接,如果具备网络嗅探及监视的第三方工具可能截获服务端与客户端之间的数据,容易造成信息泄露,因此建议开启加密连接来确保数据安全。 + +RadonDB MySQL Operator 服务端支持`TLS`,协议为MySQL支持的加密协议,如`5.7`版本支持`TLS 1.0、TLS 1.1、TLS 1.2`、`8.0`版本支持`TLS 1.0、TLS 1.1、TLS 1.2、TLS 1.3`。 + +使用加密连接需要满足两个条件: + +* MySQL Operator 服务端开启加密连接支持 +* 客户端使用加密连接 + +# 配置`MySQL Operator`使用加密连接 + +## 准备证书 + +* `ca.crt` - 服务端`CA`证书 +* `tls.key` - 服务端证书私钥 +* `tls.crt` - 服务端证书 + +可以用`OpenSSL`生成,也可以用`MySQL`自带的`mysql_ssl_rsa_setup`快捷生成: + +`mysql_ssl_rsa_setup --datadir=/tmp/certs` + +运行该命令后会生成如下文件: + +```shell +certs +├── ca-key.pem +├── ca.pem +├── client-cert.pem +├── client-key.pem +├── private_key.pem +├── public_key.pem +├── server-cert.pem +└── server-key.pem +``` + + + +### 根据证书文件创建secret + +```shell +kubectl create secret generic sample-ssl --from-file=tls.crt=server.pem -- +from-file=tls.key=server-key.pem --from-file=ca.crt=ca.pem -- +type=kubernetes.io/tls +``` + +### 配置RadonDB MySQL 集群使用`TLS` + +```shell +kubectl patch mysqlclusters.mysql.radondb.com sample --type=merge -p '{"spec":{"tlsSecretName":"sample-ssl"}}' +``` + +> 配置之后会触发`rolling update`即集群会重启 + +### 验证测试 + +* 不使用`SSL`连接 + + ```shell + kubectl exec -it sample-mysql-0 -c mysql -- mysql -uradondb_usr -p"RadonDB@123" -e "\s" + mysql Ver 14.14 Distrib 5.7.34-37, for Linux (x86_64) using 7.0 + Connection id: 7940 + Current database: + Current user: radondb_usr@localhost + SSL: Not in use + Current pager: stdout + Using outfile: '' + Using delimiter: ; + Server version: 5.7.34-37-log Percona Server (GPL), Release 37, Revision 7c516e9 + Protocol version: 10 + Connection: Localhost via UNIX socket + Server characterset: utf8mb4 + Db characterset: utf8mb4 + Client characterset: latin1 + Conn. characterset: latin1 + UNIX socket: /var/lib/mysql/mysql.sock + Uptime: 21 hours 49 min 36 sec + + Threads: 5 Questions: 181006 Slow queries: 0 Opens: 127 Flush tables: 1 Open tables: 120 Queries per second avg: 2.303 + ``` + + + +* 使用`SSL`连接 + +```shell + kubectl exec -it sample-mysql-0 -c mysql -- mysql -uradondb_usr -p"RadonDB@123" --ssl-mode=REQUIRED -e "\s" +mysql: [Warning] Using a password on the command line interface can be insecure. +-------------- +mysql Ver 14.14 Distrib 5.7.34-37, for Linux (x86_64) using 7.0 + +Connection id: 7938 +Current database: +Current user: radondb_usr@localhost +SSL: Cipher in use is ECDHE-RSA-AES128-GCM-SHA256 +Current pager: stdout +Using outfile: '' +Using delimiter: ; +Server version: 5.7.34-37-log Percona Server (GPL), Release 37, Revision 7c516e9 +Protocol version: 10 +Connection: Localhost via UNIX socket +Server characterset: utf8mb4 +Db characterset: utf8mb4 +Client characterset: latin1 +Conn. characterset: latin1 +UNIX socket: /var/lib/mysql/mysql.sock +Uptime: 21 hours 49 min 26 sec + +Threads: 5 Questions: 180985 Slow queries: 0 Opens: 127 Flush tables: 1 Open tables: 120 Queries per second avg: 2.303 +``` diff --git a/mysqlcluster/container/init_sidecar.go b/mysqlcluster/container/init_sidecar.go index fa00c424..c590d5e3 100644 --- a/mysqlcluster/container/init_sidecar.go +++ b/mysqlcluster/container/init_sidecar.go @@ -200,7 +200,17 @@ func (c *initSidecar) getVolumeMounts() []corev1.VolumeMount { MountPath: utils.SysLocalTimeZoneMountPath, }, } - + if c.Spec.TlsSecretName != "" { + volumeMounts = append(volumeMounts, + corev1.VolumeMount{ + Name: utils.TlsVolumeName + "-sidecar", + MountPath: "/tmp/mysql-ssl", + }, corev1.VolumeMount{ + Name: utils.TlsVolumeName, + MountPath: utils.TlsMountPath, + }, + ) + } if c.Spec.MysqlOpts.InitTokuDB { volumeMounts = append(volumeMounts, corev1.VolumeMount{ diff --git a/mysqlcluster/container/mysql.go b/mysqlcluster/container/mysql.go index acef63c6..79c20cf8 100644 --- a/mysqlcluster/container/mysql.go +++ b/mysqlcluster/container/mysql.go @@ -133,7 +133,7 @@ func (c *mysql) getReadinessProbe() *corev1.Probe { // getVolumeMounts get the container volumeMounts. func (c *mysql) getVolumeMounts() []corev1.VolumeMount { - return []corev1.VolumeMount{ + volumeMounts := []corev1.VolumeMount{ { Name: utils.MysqlConfVolumeName, MountPath: utils.MysqlConfVolumeMountPath, @@ -151,4 +151,13 @@ func (c *mysql) getVolumeMounts() []corev1.VolumeMount { MountPath: utils.SysLocalTimeZoneMountPath, }, } + if c.Spec.TlsSecretName != "" { + volumeMounts = append(volumeMounts, + corev1.VolumeMount{ + Name: utils.TlsVolumeName, + MountPath: utils.TlsMountPath, + }, + ) + } + return volumeMounts } diff --git a/mysqlcluster/mysqlcluster.go b/mysqlcluster/mysqlcluster.go index 6cf04e75..d85e7715 100644 --- a/mysqlcluster/mysqlcluster.go +++ b/mysqlcluster/mysqlcluster.go @@ -270,6 +270,22 @@ func (c *MysqlCluster) EnsureVolumes() []corev1.Volume { }, }) } + // Add the ssl secret mounts. + if len(c.Spec.TlsSecretName) != 0 { + volumes = append(volumes, corev1.Volume{ + Name: utils.TlsVolumeName + "-sidecar", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: c.Spec.TlsSecretName, + }, + }, + }, corev1.Volume{ + Name: utils.TlsVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + } return volumes } diff --git a/mysqlcluster/syncer/mysql_cm.go b/mysqlcluster/syncer/mysql_cm.go index 3dd5ee2f..1335ab4b 100644 --- a/mysqlcluster/syncer/mysql_cm.go +++ b/mysqlcluster/syncer/mysql_cm.go @@ -92,7 +92,9 @@ func buildMysqlConf(c *mysqlcluster.MysqlCluster) (string, error) { log.Error(err, "failed to add boolean key to config section", "key", key) } } - + if len(c.Spec.TlsSecretName) != 0 { + addKVConfigsToSection(sec, mysqlSSLConfigs) + } data, err := writeConfigs(cfg) if err != nil { return "", err diff --git a/mysqlcluster/syncer/mysql_configs.go b/mysqlcluster/syncer/mysql_configs.go index 8849c1b2..8d530a89 100644 --- a/mysqlcluster/syncer/mysql_configs.go +++ b/mysqlcluster/syncer/mysql_configs.go @@ -152,3 +152,10 @@ var mysqlBooleanConfigs = []string{ "log-slave-updates", "!includedir /etc/mysql/conf.d", } + +// mysqlSSLConfigs is the ist of the mysql ssl configs. +var mysqlSSLConfigs = map[string]string{ + "ssl_ca": "/etc/mysql-ssl/ca.crt", + "ssl_cert": "/etc/mysql-ssl/tls.crt", + "ssl_key": "/etc/mysql-ssl/tls.key", +} diff --git a/sidecar/init.go b/sidecar/init.go index 2b8a1aad..594af847 100644 --- a/sidecar/init.go +++ b/sidecar/init.go @@ -141,6 +141,10 @@ func runInitCommand(cfg *Config) error { return fmt.Errorf("failed to copy my.cnf: %s", err) } + // SSL settings. + if exists, _ := checkIfPathExists(utils.TlsMountPath); exists { + buildSSLdata() + } buildDefaultXenonMeta(uid, gid) // build client.conf. @@ -163,7 +167,6 @@ func runInitCommand(cfg *Config) error { if err = os.Chown(extraConfPath, uid, gid); err != nil { return fmt.Errorf("failed to chown %s: %s", dataPath, err) } - // Run reset master in init-mysql container. if err = ioutil.WriteFile(initFilePath+"/reset.sql", []byte("reset master;"), 0644); err != nil { return fmt.Errorf("failed to write reset.sql: %s", err) @@ -315,3 +318,19 @@ func buildDefaultXenonMeta(uid, gid int) error { } return nil } + +func buildSSLdata() error { + // cp -rp /tmp/myssl/* /etc/mysql/ssl/ Refer https://stackoverflow.com/questions/31467153/golang-failed-exec-command-that-works-in-terminal + shellCmd := "cp /tmp/mysql-ssl/* " + utils.TlsMountPath + cmd := exec.Command("sh", "-c", shellCmd) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to copy ssl: %s", err) + } + + cronCmd := "chown -R mysql.mysql " + utils.TlsMountPath + cmd = exec.Command("sh", "-c", cronCmd) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to copy ssl: %s", err) + } + return nil +} diff --git a/utils/constants.go b/utils/constants.go index df31d393..d8a298a4 100644 --- a/utils/constants.go +++ b/utils/constants.go @@ -139,6 +139,10 @@ const ( // PluginConfigs is the alias for mysql plugin config. PluginConfigs = "plugin.cnf" + // TlsVolumeName is the volume name for tls + TlsVolumeName = "tls" + // TlsMountPath is the volume mount path for tls + TlsMountPath = "/etc/mysql-ssl" ) // ResourceName is the type for aliasing resources that will be created.