Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.5.1
golang.org/x/mod v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
golang.org/x/tools v0.0.0-20200615222825-6aa8f57aacd9 // indirect
gopkg.in/yaml.v2 v2.2.8
k8s.io/api v0.18.3
Expand Down
6 changes: 5 additions & 1 deletion pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres

return fmt.Sprintf("%s-%s", e.PodName, e.ResourceVersion), nil
})
password_encryption, ok := pgSpec.Spec.PostgresqlParam.Parameters["password_encryption"]
if !ok {
password_encryption = "md5"
}

cluster := &Cluster{
Config: cfg,
Expand All @@ -135,7 +139,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
Secrets: make(map[types.UID]*v1.Secret),
Services: make(map[PostgresRole]*v1.Service),
Endpoints: make(map[PostgresRole]*v1.Endpoints)},
userSyncStrategy: users.DefaultUserSyncStrategy{},
userSyncStrategy: users.DefaultUserSyncStrategy{password_encryption},
deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy},
podEventsQueue: podEventsQueue,
KubeClient: kubeClient,
Expand Down
11 changes: 6 additions & 5 deletions pkg/util/users/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
// an existing roles of another role membership, nor it removes the already assigned flag
// (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
type DefaultUserSyncStrategy struct {
PasswordEncryption string
}

// ProduceSyncRequests figures out the types of changes that need to happen with the given users.
Expand All @@ -45,7 +46,7 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM
}
} else {
r := spec.PgSyncUserRequest{}
newMD5Password := util.PGUserPassword(newUser)
newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser)

if dbUser.Password != newMD5Password {
r.User.Password = newMD5Password
Expand Down Expand Up @@ -140,7 +141,7 @@ func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.D
if user.Password == "" {
userPassword = "PASSWORD NULL"
} else {
userPassword = fmt.Sprintf(passwordTemplate, util.PGUserPassword(user))
userPassword = fmt.Sprintf(passwordTemplate, util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(user))
}
query := fmt.Sprintf(createUserSQL, user.Name, strings.Join(userFlags, " "), userPassword)

Expand All @@ -155,7 +156,7 @@ func (strategy DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB
var resultStmt []string

if user.Password != "" || len(user.Flags) > 0 {
alterStmt := produceAlterStmt(user)
alterStmt := produceAlterStmt(user, strategy.PasswordEncryption)
resultStmt = append(resultStmt, alterStmt)
}
if len(user.MemberOf) > 0 {
Expand All @@ -174,14 +175,14 @@ func (strategy DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB
return nil
}

func produceAlterStmt(user spec.PgUser) string {
func produceAlterStmt(user spec.PgUser, encryption string) string {
// ALTER ROLE ... LOGIN ENCRYPTED PASSWORD ..
result := make([]string, 0)
password := user.Password
flags := user.Flags

if password != "" {
result = append(result, fmt.Sprintf(passwordTemplate, util.PGUserPassword(user)))
result = append(result, fmt.Sprintf(passwordTemplate, util.NewEncryptor(encryption).PGUserPassword(user)))
}
if len(flags) != 0 {
result = append(result, strings.Join(flags, " "))
Expand Down
61 changes: 57 additions & 4 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package util

import (
"crypto/hmac"
"crypto/md5" // #nosec we need it to for PostgreSQL md5 passwords
cryptoRand "crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
Expand All @@ -16,10 +19,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/zalando/postgres-operator/pkg/spec"
"golang.org/x/crypto/pbkdf2"
)

const (
md5prefix = "md5"
md5prefix = "md5"
scramsha256prefix = "SCRAM-SHA-256"
saltlength = 16
iterations = 4096
)

var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
Expand Down Expand Up @@ -61,16 +68,62 @@ func NameFromMeta(meta metav1.ObjectMeta) spec.NamespacedName {
}
}

// PGUserPassword is used to generate md5 password hash for a given user. It does nothing for already hashed passwords.
func PGUserPassword(user spec.PgUser) string {
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) || user.Password == "" {
type Hasher func(user spec.PgUser) string
type Random func(n int) string

type Encryptor struct {
encrypt Hasher
random Random
}

func NewEncryptor(encryption string) *Encryptor {
e := Encryptor{random: RandomPassword}
m := map[string]Hasher{
"md5": e.PGUserPasswordMD5,
"scram-sha-256": e.PGUserPasswordScramSHA256,
}
hasher, ok := m[encryption]
if !ok {
hasher = e.PGUserPasswordMD5
}
e.encrypt = hasher
return &e
}

func (e *Encryptor) PGUserPassword(user spec.PgUser) string {
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) ||
(len(user.Password) > len(scramsha256prefix) && user.Password[:len(scramsha256prefix)] == scramsha256prefix) || user.Password == "" {
// Avoid processing already encrypted or empty passwords
return user.Password
}
return e.encrypt(user)
}

func (e *Encryptor) PGUserPasswordMD5(user spec.PgUser) string {
s := md5.Sum([]byte(user.Password + user.Name)) // #nosec, using md5 since PostgreSQL uses it for hashing passwords.
return md5prefix + hex.EncodeToString(s[:])
}

func (e *Encryptor) PGUserPasswordScramSHA256(user spec.PgUser) string {
salt := []byte(e.random(saltlength))
key := pbkdf2.Key([]byte(user.Password), salt, iterations, 32, sha256.New)
mac := hmac.New(sha256.New, key)
mac.Write([]byte("Server Key"))
serverKey := mac.Sum(nil)
mac = hmac.New(sha256.New, key)
mac.Write([]byte("Client Key"))
clientKey := mac.Sum(nil)
storedKey := sha256.Sum256(clientKey)
pass := fmt.Sprintf("%s$%v:%s$%s:%s",
scramsha256prefix,
iterations,
base64.StdEncoding.EncodeToString(salt),
base64.StdEncoding.EncodeToString(storedKey[:]),
base64.StdEncoding.EncodeToString(serverKey),
)
return pass
}

// Diff returns diffs between 2 objects
func Diff(a, b interface{}) []string {
return pretty.Diff(a, b)
Expand Down
28 changes: 21 additions & 7 deletions pkg/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,27 @@ import (
)

var pgUsers = []struct {
in spec.PgUser
out string
in spec.PgUser
outmd5 string
outscramsha256 string
}{{spec.PgUser{
Name: "test",
Password: "password",
Flags: []string{},
MemberOf: []string{}},
"md587f77988ccb5aa917c93201ba314fcd4"},
"md587f77988ccb5aa917c93201ba314fcd4", "SCRAM-SHA-256$4096:c2FsdA==$lF4cRm/Jky763CN4HtxdHnjV4Q8AWTNlKvGmEFFU8IQ=:ub8OgRsftnk2ccDMOt7ffHXNcikRkQkq1lh4xaAqrSw="},
{spec.PgUser{
Name: "test",
Password: "md592f413f3974bdf3799bb6fecb5f9f2c6",
Flags: []string{},
MemberOf: []string{}},
"md592f413f3974bdf3799bb6fecb5f9f2c6"}}
"md592f413f3974bdf3799bb6fecb5f9f2c6", "md592f413f3974bdf3799bb6fecb5f9f2c6"},
{spec.PgUser{
Name: "test",
Password: "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=",
Flags: []string{},
MemberOf: []string{}},
"SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=", "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs="}}

var prettyDiffTest = []struct {
inA interface{}
Expand Down Expand Up @@ -107,9 +114,16 @@ func TestNameFromMeta(t *testing.T) {

func TestPGUserPassword(t *testing.T) {
for _, tt := range pgUsers {
pwd := PGUserPassword(tt.in)
if pwd != tt.out {
t.Errorf("PgUserPassword expected: %q, got: %q", tt.out, pwd)
e := NewEncryptor("md5")
pwd := e.PGUserPassword(tt.in)
if pwd != tt.outmd5 {
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outmd5, pwd)
}
e = NewEncryptor("scram-sha-256")
e.random = func(n int) string { return "salt" }
pwd = e.PGUserPassword(tt.in)
if pwd != tt.outscramsha256 {
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outscramsha256, pwd)
}
}
}
Expand Down