Files
subhamkrai b70a0c9257 object: add TLS 1.3 cipher suite support for RGW beast frontend
adding new tls ssl_ciphersuites supporting tls 1.3 and the existing,
ssl_cipher supports tls 1.2 and below. Adding, the docs and unit-test
changs as well.

Signed-off-by: subhamkrai <srai@redhat.com>
2026-05-26 20:50:21 +05:30

382 lines
13 KiB
Go

/*
Copyright 2019 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package object
import (
"fmt"
"path"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
cephconfig "github.com/rook/rook/pkg/operator/ceph/config"
"github.com/rook/rook/pkg/operator/ceph/config/keyring"
"github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/rook/rook/pkg/util/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)
const (
keyringTemplate = `
[%s]
key = %s
caps mon = "allow rw"
caps osd = "allow rwx"
`
caBundleVolumeName = "rook-ceph-custom-ca-bundle"
caBundleUpdatedVolumeName = "rook-ceph-ca-bundle-updated"
caBundleTrustedDir = "/etc/pki/ca-trust/"
caBundleSourceCustomDir = caBundleTrustedDir + "source/anchors/"
caBundleExtractedDir = caBundleTrustedDir + "extracted/"
caBundleKeyName = "cabundle"
caBundleFileName = "custom-ca-bundle.crt"
caBundleMountPath = "/etc/ceph/rgw-ca-bundle/"
certVolumeName = "rook-ceph-rgw-cert"
certDir = "/etc/ceph/private"
certKeyName = "cert"
certFilename = "rgw-cert.pem"
certKeyFileName = "rgw-key.pem"
rgwPortInternalPort int32 = 8080
ServiceServingCertCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt"
HttpTimeOut = time.Second * 15
rgwVaultVolumeName = "rgw-vault-volume"
rgwVaultDirName = "/etc/vault/rgw/"
)
var rgwFrontendName = "beast"
func (c *clusterConfig) portString() string {
var portString string
port := c.store.Spec.Gateway.Port
if port != 0 {
if !c.store.Spec.IsHostNetwork(c.clusterSpec) {
port = rgwPortInternalPort
}
portString = fmt.Sprintf("port=%s", strconv.Itoa(int(port)))
}
if c.store.Spec.IsTLSEnabled() {
certPath := path.Join(certDir, certFilename)
// This is the beast backend
// Config is: http://docs.ceph.com/docs/master/radosgw/frontends/#id3
if port != 0 {
portString = fmt.Sprintf("%s ssl_port=%d ssl_certificate=%s",
portString, c.store.Spec.Gateway.SecurePort, certPath)
} else {
portString = fmt.Sprintf("ssl_port=%d ssl_certificate=%s",
c.store.Spec.Gateway.SecurePort, certPath)
}
secretType, _ := c.rgwTLSSecretType(c.store.Spec.Gateway.SSLCertificateRef)
if c.store.Spec.GetServiceServingCert() != "" || secretType == v1.SecretTypeTLS {
privateKey := path.Join(certDir, certKeyFileName)
portString = fmt.Sprintf("%s ssl_private_key=%s", portString, privateKey)
}
}
return portString
}
// rgwFrontendStr builds the "rgw frontends" value including the beast frontend name,
// port/TLS config, SSL options, and cipher settings.
func (c *clusterConfig) rgwFrontendStr() string {
frontendStr := fmt.Sprintf("%s %s", rgwFrontendName, c.portString())
if c.store.Spec.Security == nil {
return frontendStr
}
// Only emit ssl_options when SSLOptions is set.
if c.store.Spec.Security.SslOptions != nil {
if sslOptions := buildSSLOptions(c.store.Spec.Security.SslOptions); sslOptions != "" {
frontendStr = fmt.Sprintf("%s ssl_options=%s", frontendStr, sslOptions)
}
}
if len(c.store.Spec.Security.Ciphers) > 0 {
frontendStr = fmt.Sprintf("%s ssl_ciphers=%s", frontendStr, strings.Join(c.store.Spec.Security.Ciphers, ":"))
}
if len(c.store.Spec.Security.CipherSuites) > 0 {
frontendStr = fmt.Sprintf("%s ssl_ciphersuites=%s", frontendStr, strings.Join(c.store.Spec.Security.CipherSuites, ":"))
}
if len(c.store.Spec.Security.TlsGroups) > 0 {
frontendStr = fmt.Sprintf("%s tls_groups=%s", frontendStr, strings.Join(c.store.Spec.Security.TlsGroups, ":"))
}
return frontendStr
}
// buildSSLOptions constructs the colon-separated ssl_options string for the beast frontend.
// See https://docs.ceph.com/en/latest/radosgw/frontends/
func buildSSLOptions(opts *cephv1.SslOptionsSpec) string {
var options []string
if ptr.Deref(opts.DefaultWorkarounds, false) {
options = append(options, "default_workarounds")
}
if ptr.Deref(opts.NoCompression, false) {
options = append(options, "no_compression")
}
if ptr.Deref(opts.SingleDiffieHellmanUse, false) {
options = append(options, "single_dh_use")
}
// Omitted sslv2, sslv3, tlsv1_0, and tlsv1_1 default to disabled
if !ptr.Deref(opts.SSLv2, false) {
options = append(options, "no_sslv2")
}
if !ptr.Deref(opts.SSLv3, false) {
options = append(options, "no_sslv3")
}
if !ptr.Deref(opts.TLSv1_0, false) {
options = append(options, "no_tlsv1")
}
if !ptr.Deref(opts.TLSv1_1, false) {
options = append(options, "no_tlsv1_1")
}
if !ptr.Deref(opts.TLSv1_2, true) {
options = append(options, "no_tlsv1_2")
}
return strings.Join(options, ":")
}
func generateCephXUser(name string) string {
user := strings.TrimPrefix(name, AppName)
return "client.rgw" + strings.ReplaceAll(user, "-", ".")
}
func (c *clusterConfig) generateKeyring(rgwConfig *rgwConfig) (string, error) {
nsName := controller.NsName(c.clusterInfo.Namespace, c.store.Name)
user := generateCephXUser(rgwConfig.ResourceName)
/* TODO: this says `osd allow rwx` while template says `osd allow *`; which is correct? */
access := []string{"osd", "allow rwx", "mon", "allow rw"}
s := keyring.GetSecretStore(c.context, c.clusterInfo, c.ownerInfo)
key, err := s.GenerateKey(user, access)
if err != nil {
return "", err
}
if c.shouldRotateCephxKeys {
log.NamedInfo(nsName, logger, "rotating cephx key for CephObjectStore %q", c.store.Name)
newKey, err := s.RotateKey(user)
if err != nil {
return "", errors.Wrapf(err, "failed to rotate cephx key for CephObjectStore %q", c.store.Name)
} else {
key = newKey
}
}
keyring := fmt.Sprintf(keyringTemplate, user, key)
return s.CreateOrUpdate(rgwConfig.ResourceName, keyring)
}
func mapKeystoneSecretToConfig(cfg map[string]string, secret *v1.Secret) (map[string]string, error) {
requiredKeys := []string{
"OS_PROJECT_DOMAIN_NAME",
"OS_USER_DOMAIN_NAME",
"OS_PROJECT_DOMAIN_NAME",
"OS_USER_DOMAIN_NAME",
"OS_PROJECT_NAME",
"OS_USERNAME",
"OS_PASSWORD",
}
data := make(map[string]string)
for key, value := range secret.Data {
data[key] = string(value[:])
}
for _, key := range requiredKeys {
if value, ok := data[key]; !ok || value == "" {
return nil, errors.New(fmt.Sprintf("Missing or empty %s", key))
}
}
if authType, ok := data["OS_AUTH_TYPE"]; ok && authType != "password" {
return nil, errors.New(fmt.Sprintf("OS_AUTHTYPE %s is not supported. Only OS_AUTH_TYPE password is supported!", authType))
}
if apiVersion, ok := data["OS_IDENTITY_API_VERSION"]; ok && apiVersion != "3" {
return nil, errors.New(fmt.Sprintf("OS_IDENTITY_API_VERSION %s is not supported! Only OS_IDENTITY_API_VERSION 3 is supported!", apiVersion))
}
if data["OS_PROJECT_DOMAIN_NAME"] != data["OS_USER_DOMAIN_NAME"] {
return nil, errors.New("The user domain name does not match the project domain name.")
}
cfg["rgw_keystone_api_version"] = data["OS_IDENTITY_API_VERSION"]
cfg["rgw_keystone_admin_domain"] = data["OS_PROJECT_DOMAIN_NAME"]
cfg["rgw_keystone_admin_project"] = data["OS_PROJECT_NAME"]
cfg["rgw_keystone_admin_user"] = data["OS_USERNAME"]
cfg["rgw_keystone_admin_password"] = data["OS_PASSWORD"]
return cfg, nil
}
func (c *clusterConfig) setFlagsMonConfigStore(rgwConfig *rgwConfig) error {
monStore := cephconfig.GetMonStore(c.context, c.clusterInfo)
who := generateCephXUser(rgwConfig.ResourceName)
configOptions, err := c.generateMonConfigOptions(rgwConfig)
if err != nil {
return err
}
if err := monStore.SetAll(who, configOptions); err != nil {
return errors.Wrapf(err, "failed to set all RGW configs on %q", who)
}
return nil
}
func (c *clusterConfig) generateMonConfigOptions(rgwConfig *rgwConfig) (map[string]string, error) {
nsName := controller.NsName(c.clusterInfo.Namespace, c.store.Name)
configOptions := make(map[string]string)
configOptions["rgw_run_sync_thread"] = "true"
if c.store.Spec.Gateway.DisableMultisiteSyncTraffic {
configOptions["rgw_run_sync_thread"] = "false"
}
configOptions["rgw_log_nonexistent_bucket"] = "true"
configOptions["rgw_log_object_name_utc"] = "true"
configOptions["rgw_enable_usage_log"] = "true"
configOptions["rgw_zone"] = rgwConfig.Zone
configOptions["rgw_zonegroup"] = rgwConfig.ZoneGroup
configOptions, err := c.configureKeystoneAuthentication(rgwConfig, configOptions)
if err != nil {
return configOptions, err
}
if s3 := rgwConfig.Protocols.S3; s3 != nil {
if s3.AuthUseKeystone != nil {
configOptions["rgw_s3_auth_use_keystone"] = fmt.Sprintf("%t", *s3.AuthUseKeystone)
}
}
if swift := rgwConfig.Protocols.Swift; swift != nil {
if swift.AccountInUrl != nil {
configOptions["rgw_swift_account_in_url"] = fmt.Sprintf("%t", *swift.AccountInUrl)
}
if swift.UrlPrefix != nil {
configOptions["rgw_swift_url_prefix"] = *swift.UrlPrefix
}
if swift.VersioningEnabled != nil {
configOptions["rgw_swift_versioning_enabled"] = fmt.Sprintf("%t", *swift.VersioningEnabled)
}
}
for flag, val := range c.store.Spec.Gateway.RgwConfig {
if currVal, ok := configOptions[flag]; ok {
// RGW might break with some user-specified config overrides; log clearly to help triage
log.NamedInfo(nsName, logger, "overriding RGW config option %q with user-specified rgwConfig", flag)
log.NamedTrace(nsName, logger, "overriding RGW config option %q (current value %q) with user-specified rgwConfig %q", flag, currVal, val)
}
configOptions[flag] = val
}
for flag, val := range c.store.Spec.Gateway.RgwConfigFromSecret {
if val.Key == "" || val.Name == "" {
return nil, fmt.Errorf("invalid object store %q rgwConfigFromSecret value %q=%+v", fmt.Sprintf("%s/%s", c.store.Namespace, c.store.Name), flag, val)
}
secret, err := c.context.Clientset.CoreV1().Secrets(c.clusterInfo.Namespace).Get(c.clusterInfo.Context, val.Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("unable to lookup secret for object store %q rgwConfigFromSecret %q=%+v", fmt.Sprintf("%s/%s", c.store.Namespace, c.store.Name), flag, val)
}
secretVal, ok := secret.Data[val.Key]
if !ok {
return nil, fmt.Errorf("unable to lookup secret for object store %q rgwConfigFromSecret %q: key %q not found in secret %q",
fmt.Sprintf("%s/%s", c.store.Namespace, c.store.Name), flag, val.Key, val.Name)
}
if currVal, ok := configOptions[flag]; ok {
// RGW might break with some user-specified config overrides; log clearly to help triage
log.NamedInfo(nsName, logger, "overriding RGW config option %q with user-specified rgwConfigFromSecret", flag)
log.NamedTrace(nsName, logger, "overriding RGW config option %q (current value %q) with user-specified rgwConfigFromSecret %q", flag, currVal, secretVal)
}
configOptions[flag] = string(secretVal)
}
return configOptions, nil
}
func (c *clusterConfig) configureKeystoneAuthentication(rgwConfig *rgwConfig, configOptions map[string]string) (map[string]string, error) {
nsName := controller.NsName(c.clusterInfo.Namespace, c.store.Name)
keystone := rgwConfig.Auth.Keystone
if keystone == nil {
log.NamedDebug(nsName, logger, "Authentication with keystone is disabled")
return configOptions, nil
}
log.NamedInfo(nsName, logger, "Configuring authentication with keystone")
configOptions["rgw_keystone_url"] = keystone.Url
configOptions["rgw_keystone_accepted_roles"] = strings.Join(keystone.AcceptedRoles, ",")
if keystone.ImplicitTenants != "" {
lc := strings.ToLower(string(keystone.ImplicitTenants))
// only four values are valid here (swift, s3, true and false)
//
// https://docs.ceph.com/en/latest/radosgw/keystone/#integrating-with-openstack-keystone
if lc != "true" &&
lc != "false" &&
lc != "swift" &&
lc != "s3" {
return nil, errors.New(fmt.Sprintf("ImplicitTenantSetting can only be 'swift', 's3', 'true' or 'false', not %q", string(keystone.ImplicitTenants)))
}
configOptions["rgw_keystone_implicit_tenants"] = lc
}
if keystone.TokenCacheSize != nil {
configOptions["rgw_keystone_token_cache_size"] = fmt.Sprintf("%d", *keystone.TokenCacheSize)
}
if rgwConfig.KeystoneSecret == nil {
return nil, errors.New("Cannot find keystone secret")
}
var err error
configOptions, err = mapKeystoneSecretToConfig(configOptions, rgwConfig.KeystoneSecret)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("error mapping keystone secret %s to config", rgwConfig.KeystoneSecret.Name))
}
return configOptions, nil
}
func (c *clusterConfig) deleteFlagsMonConfigStore(rgwName string) error {
nsName := controller.NsName(c.clusterInfo.Namespace, c.store.Name)
monStore := cephconfig.GetMonStore(c.context, c.clusterInfo)
who := generateCephXUser(rgwName)
err := monStore.DeleteDaemon(who)
if err != nil {
return errors.Wrapf(err, "failed to delete rgw config for %q in mon configuration database", who)
}
log.NamedInfo(nsName, logger, "successfully deleted rgw config for %q in mon configuration database", who)
return nil
}