Skip to content

Commit dcbea26

Browse files
authored
CLOUDP-59109: Configure SCRAM (#78)
1 parent d93a7bc commit dcbea26

File tree

15 files changed

+438
-41
lines changed

15 files changed

+438
-41
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: mongodb.com/v1
2+
kind: MongoDB
3+
metadata:
4+
name: example-scram-mongodb
5+
spec:
6+
members: 3
7+
type: ReplicaSet
8+
version: "4.2.6"
9+
security:
10+
authentication:
11+
enabled: true
12+
modes: ["SCRAM"]

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/golang/protobuf v1.3.5 // indirect
1111
github.com/hashicorp/go-multierror v1.0.0
1212
github.com/hashicorp/golang-lru v0.5.4 // indirect
13-
github.com/imdario/mergo v0.3.9
13+
github.com/imdario/mergo v0.3.9 // indirect
1414
github.com/json-iterator/go v1.1.9 // indirect
1515
github.com/klauspost/compress v1.9.8 // indirect
1616
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect

pkg/apis/mongodb/v1/mongodb_types.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ type MongoDBSpec struct {
3737
// +optional
3838
FeatureCompatibilityVersion string `json:"featureCompatibilityVersion,omitempty"`
3939

40-
// Security configures security features, such as TLS, for a deployment
40+
// Security configures security features, such as TLS, and authentication settings for a deployment
4141
// +optional
4242
Security Security `json:"security"`
4343
}
4444

4545
type Security struct {
46+
// +optional
47+
Authentication Authentication `json:"authentication"`
4648
// TLS configuration for both client-server and server-server communication
4749
// +optional
4850
TLS TLS `json:"tls"`
@@ -67,6 +69,17 @@ type TLS struct {
6769
CAConfigMapName string `json:"caConfigMapName"`
6870
}
6971

72+
type Authentication struct {
73+
// Enabled specifies if authentication should be enabled
74+
Enabled bool `json:"enabled"`
75+
76+
// Modes is an array specifying which authentication methods should be enabled
77+
Modes []AuthMode `json:"modes"`
78+
}
79+
80+
// +kubebuilder:validation:Enum=SCRAM
81+
type AuthMode string
82+
7083
// MongoDBStatus defines the observed state of MongoDB
7184
type MongoDBStatus struct {
7285
MongoURI string `json:"mongoUri"`
@@ -129,6 +142,10 @@ func (m MongoDB) NamespacedName() types.NamespacedName {
129142
return types.NamespacedName{Name: m.Name, Namespace: m.Namespace}
130143
}
131144

145+
func (m *MongoDB) ScramCredentialsNamespacedName() types.NamespacedName {
146+
return types.NamespacedName{Name: "agent-scram-credentials", Namespace: m.Namespace}
147+
}
148+
132149
// GetFCV returns the feature compatibility version. If no FeatureCompatibilityVersion is specified.
133150
// It uses the major and minor version for whichever version of MongoDB is configured.
134151
func (m MongoDB) GetFCV() string {

pkg/authentication/scram/scram.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package scram
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/automationconfig"
7+
8+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/secret"
9+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/util/generate"
10+
"k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/apimachinery/pkg/types"
12+
)
13+
14+
// EnsureAgentSecret make sure that the agent password and keyfile exist in the secret and returns
15+
// the scram authEnabler configured with this values
16+
func EnsureAgentSecret(getUpdateCreator secret.GetUpdateCreator, secretNsName types.NamespacedName) (automationconfig.AuthEnabler, error) {
17+
generatedPassword, err := generate.RandomFixedLengthStringOfSize(20)
18+
if err != nil {
19+
return authEnabler{}, fmt.Errorf("error generating password: %s", err)
20+
}
21+
22+
generatedContents, err := generate.KeyFileContents()
23+
if err != nil {
24+
return authEnabler{}, fmt.Errorf("error generating keyfile contents: %s", err)
25+
}
26+
27+
agentSecret, err := getUpdateCreator.GetSecret(secretNsName)
28+
if err != nil {
29+
if errors.IsNotFound(err) {
30+
s := secret.Builder().
31+
SetNamespace(secretNsName.Namespace).
32+
SetName(secretNsName.Name).
33+
SetField(AgentPasswordKey, generatedPassword).
34+
SetField(AgentKeyfileKey, generatedContents).
35+
Build()
36+
return authEnabler{
37+
agentPassword: generatedPassword,
38+
agentKeyFile: generatedContents,
39+
}, getUpdateCreator.CreateSecret(s)
40+
}
41+
return authEnabler{}, err
42+
}
43+
44+
if _, ok := agentSecret.Data[AgentPasswordKey]; !ok {
45+
agentSecret.Data[AgentPasswordKey] = []byte(generatedPassword)
46+
}
47+
48+
if _, ok := agentSecret.Data[AgentKeyfileKey]; !ok {
49+
agentSecret.Data[AgentKeyfileKey] = []byte(generatedContents)
50+
}
51+
52+
return authEnabler{
53+
agentPassword: string(agentSecret.Data[AgentPasswordKey]),
54+
agentKeyFile: string(agentSecret.Data[AgentKeyfileKey]),
55+
}, getUpdateCreator.UpdateSecret(agentSecret)
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package scram
2+
3+
import (
4+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/automationconfig"
5+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/util/contains"
6+
)
7+
8+
const (
9+
scram256 = "SCRAM-SHA-256"
10+
automationAgentKeyFilePathInContainer = "/var/lib/mongodb-mms-automation/authentication/keyfile"
11+
automationAgentWindowsKeyFilePath = "%SystemDrive%\\MMSAutomation\\versions\\keyfile"
12+
AgentName = "mms-automation"
13+
AgentPasswordKey = "password"
14+
AgentKeyfileKey = "keyfile"
15+
)
16+
17+
type authEnabler struct {
18+
agentPassword string
19+
agentKeyFile string
20+
}
21+
22+
func (s authEnabler) EnableAuth(auth automationconfig.Auth) automationconfig.Auth {
23+
enableAgentAuthentication(&auth, s.agentPassword, s.agentKeyFile)
24+
enableDeploymentMechanisms(&auth)
25+
return auth
26+
}
27+
28+
func enableAgentAuthentication(auth *automationconfig.Auth, agentPassword, agentKeyFileContents string) {
29+
auth.Disabled = false
30+
auth.AuthoritativeSet = true
31+
auth.KeyFile = automationAgentKeyFilePathInContainer
32+
33+
// windows file is specified to pass validation, this will never be used
34+
auth.KeyFileWindows = automationAgentWindowsKeyFilePath
35+
auth.AutoAuthMechanisms = []string{scram256}
36+
37+
// the username of the MongoDB Agent
38+
auth.AutoUser = AgentName
39+
40+
// the mechanism used by the Agent
41+
auth.AutoAuthMechanism = scram256
42+
43+
// the password for the Agent user
44+
auth.AutoPwd = agentPassword
45+
46+
// the contents the keyfile should have, this file is owned and managed
47+
// by the agent
48+
auth.Key = agentKeyFileContents
49+
}
50+
51+
func enableDeploymentMechanisms(auth *automationconfig.Auth) {
52+
if contains.String(auth.DeploymentAuthMechanisms, scram256) {
53+
return
54+
}
55+
auth.DeploymentAuthMechanisms = append(auth.DeploymentAuthMechanisms, scram256)
56+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package scram
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/automationconfig"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestScramEnabler(t *testing.T) {
11+
enabler := authEnabler{
12+
agentPassword: "password",
13+
agentKeyFile: "keyfilecontents",
14+
}
15+
auth := enabler.EnableAuth(automationconfig.Auth{})
16+
t.Run("Authentication is correctly configured", func(t *testing.T) {
17+
assert.Equal(t, AgentName, auth.AutoUser)
18+
assert.Equal(t, "keyfilecontents", auth.Key)
19+
assert.Equal(t, "password", auth.AutoPwd)
20+
assert.Equal(t, scram256, auth.AutoAuthMechanism)
21+
assert.Len(t, auth.DeploymentAuthMechanisms, 1)
22+
assert.Len(t, auth.AutoAuthMechanisms, 1)
23+
assert.Equal(t, []string{scram256}, auth.DeploymentAuthMechanisms)
24+
assert.Equal(t, []string{scram256}, auth.AutoAuthMechanisms)
25+
assert.Equal(t, automationAgentKeyFilePathInContainer, auth.KeyFile)
26+
assert.Equal(t, automationAgentWindowsKeyFilePath, auth.KeyFileWindows)
27+
})
28+
29+
t.Run("Subsequent configuration doesn't add to deployment auth mechanisms", func(t *testing.T) {
30+
auth = enabler.EnableAuth(auth)
31+
assert.Equal(t, []string{scram256}, auth.DeploymentAuthMechanisms)
32+
})
33+
34+
}

pkg/automationconfig/automation_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type Auth struct {
3838
AutoPwd string `json:"autoPwd,omitempty"`
3939
}
4040

41-
func DisabledAuth() Auth {
41+
func disabledAuth() Auth {
4242
return Auth{
4343
Users: make([]MongoDBUser, 0),
4444
AutoAuthMechanisms: make([]string, 0),

pkg/automationconfig/automation_config_builder.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ const (
1212
ReplicaSetTopology Topology = "ReplicaSet"
1313
)
1414

15+
// AuthEnabler is an interface which can configure authentication settings
16+
type AuthEnabler interface {
17+
EnableAuth(auth Auth) Auth
18+
}
19+
1520
type Builder struct {
21+
enabler AuthEnabler
1622
processes []Process
1723
replicaSets []ReplicaSet
1824
members int
@@ -38,6 +44,11 @@ func NewBuilder() *Builder {
3844
}
3945
}
4046

47+
func (b *Builder) SetAuthEnabler(enabler AuthEnabler) *Builder {
48+
b.enabler = enabler
49+
return b
50+
}
51+
4152
func (b *Builder) SetTopology(topology Topology) *Builder {
4253
b.topology = topology
4354
return b
@@ -121,6 +132,11 @@ func (b *Builder) Build() (AutomationConfig, error) {
121132
members[i] = newReplicaSetMember(process, i)
122133
}
123134

135+
auth := disabledAuth()
136+
if b.enabler != nil {
137+
auth = b.enabler.EnableAuth(auth)
138+
}
139+
124140
currentAc := AutomationConfig{
125141
Version: b.previousAC.Version,
126142
Processes: processes,
@@ -134,7 +150,7 @@ func (b *Builder) Build() (AutomationConfig, error) {
134150
Versions: b.versions,
135151
ToolsVersion: b.toolsVersion,
136152
Options: Options{DownloadBase: "/var/lib/mongodb-mms-automation"},
137-
Auth: DisabledAuth(),
153+
Auth: auth,
138154
TLS: TLS{
139155
ClientCertificateMode: ClientCertificateModeOptional,
140156
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package mongodb
2+
3+
import (
4+
mdbv1 "github.com/mongodb/mongodb-kubernetes-operator/pkg/apis/mongodb/v1"
5+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/authentication/scram"
6+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/automationconfig"
7+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/podtemplatespec"
8+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/secret"
9+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/statefulset"
10+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/util/contains"
11+
)
12+
13+
const (
14+
scramShaOption = "SCRAM"
15+
)
16+
17+
// noOpAuthEnabler performs no changes, leaving authentication settings untouched
18+
type noOpAuthEnabler struct{}
19+
20+
func (n noOpAuthEnabler) EnableAuth(auth automationconfig.Auth) automationconfig.Auth {
21+
return auth
22+
}
23+
24+
// getAuthenticationEnabler returns a type that is able to configure the automation config's
25+
// authentication settings
26+
func getAuthenticationEnabler(getUpdateCreator secret.GetUpdateCreator, mdb mdbv1.MongoDB) (automationconfig.AuthEnabler, error) {
27+
if !mdb.Spec.Security.Authentication.Enabled {
28+
return noOpAuthEnabler{}, nil
29+
}
30+
31+
// currently, just enable auth if it's in the list as there is only one option
32+
if contains.AuthMode(mdb.Spec.Security.Authentication.Modes, scramShaOption) {
33+
enabler, err := scram.EnsureAgentSecret(getUpdateCreator, mdb.ScramCredentialsNamespacedName())
34+
if err != nil {
35+
return noOpAuthEnabler{}, err
36+
}
37+
return enabler, nil
38+
}
39+
return noOpAuthEnabler{}, nil
40+
}
41+
42+
// buildScramPodSpecModification will add the keyfile volume to the podTemplateSpec
43+
// the keyfile is owned by the agent, and is required to have 0600 permissions.
44+
func buildScramPodSpecModification(mdb mdbv1.MongoDB) podtemplatespec.Modification {
45+
if !mdb.Spec.Security.Authentication.Enabled {
46+
return podtemplatespec.NOOP()
47+
}
48+
49+
mode := int32(0600)
50+
scramSecretNsName := mdb.ScramCredentialsNamespacedName()
51+
keyFileVolume := statefulset.CreateVolumeFromSecret(scramSecretNsName.Name, scramSecretNsName.Name, statefulset.WithSecretDefaultMode(&mode))
52+
keyFileVolumeVolumeMount := statefulset.CreateVolumeMount(keyFileVolume.Name, "/var/lib/mongodb-mms-automation/authentication", statefulset.WithReadOnly(false))
53+
keyFileVolumeVolumeMountMongod := statefulset.CreateVolumeMount(keyFileVolume.Name, "/var/lib/mongodb-mms-automation/authentication", statefulset.WithReadOnly(false))
54+
55+
return podtemplatespec.Apply(
56+
podtemplatespec.WithVolume(keyFileVolume),
57+
podtemplatespec.WithVolumeMounts(agentName, keyFileVolumeVolumeMount),
58+
podtemplatespec.WithVolumeMounts(mongodbName, keyFileVolumeVolumeMountMongod),
59+
)
60+
}

0 commit comments

Comments
 (0)