VAULT-13614 Support SCRAM-SHA-256 encrypted passwords for PostgreSQL (#19616)
This commit is contained in:
parent
427b4dbd49
commit
96e966e9ef
|
@ -11,5 +11,6 @@ project {
|
|||
"builtin/credential/aws/pkcs7/**",
|
||||
"ui/node_modules/**",
|
||||
"enos/modules/k8s_deploy_vault/raft-config.hcl",
|
||||
"plugins/database/postgresql/scram/**"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -246,6 +246,7 @@ test_packages[13]+=" $base/command/server"
|
|||
test_packages[13]+=" $base/physical/aerospike"
|
||||
test_packages[13]+=" $base/physical/cockroachdb"
|
||||
test_packages[13]+=" $base/plugins/database/postgresql"
|
||||
test_packages[13]+=" $base/plugins/database/postgresql/scram"
|
||||
if [ "${ENTERPRISE:+x}" == "x" ] ; then
|
||||
test_packages[13]+=" $base/vault/external_tests/filteredpathsext"
|
||||
fi
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
secrets/postgresql: Add configuration to scram-sha-256 encrypt passwords on Vault before sending them to PostgreSQL
|
||||
```
|
|
@ -0,0 +1,25 @@
|
|||
package postgresql
|
||||
|
||||
import "fmt"
|
||||
|
||||
// passwordAuthentication determines whether to send passwords in plaintext (password) or hashed (scram-sha-256).
|
||||
type passwordAuthentication string
|
||||
|
||||
var (
|
||||
// passwordAuthenticationPassword is the default. If set, passwords will be sent to PostgreSQL in plain text.
|
||||
passwordAuthenticationPassword passwordAuthentication = "password"
|
||||
passwordAuthenticationSCRAMSHA256 passwordAuthentication = "scram-sha-256"
|
||||
)
|
||||
|
||||
var passwordAuthentications = map[passwordAuthentication]struct{}{
|
||||
passwordAuthenticationSCRAMSHA256: {},
|
||||
passwordAuthenticationPassword: {},
|
||||
}
|
||||
|
||||
func parsePasswordAuthentication(s string) (passwordAuthentication, error) {
|
||||
if _, ok := passwordAuthentications[passwordAuthentication(s)]; !ok {
|
||||
return "", fmt.Errorf("'%s' is not a valid password authentication type", s)
|
||||
}
|
||||
|
||||
return passwordAuthentication(s), nil
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/hashicorp/vault/plugins/database/postgresql/scram"
|
||||
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
||||
"github.com/hashicorp/vault/sdk/database/helper/connutil"
|
||||
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
|
||||
|
@ -69,6 +70,7 @@ func new() *PostgreSQL {
|
|||
|
||||
db := &PostgreSQL{
|
||||
SQLConnectionProducer: connProducer,
|
||||
passwordAuthentication: passwordAuthenticationPassword,
|
||||
}
|
||||
|
||||
return db
|
||||
|
@ -78,6 +80,7 @@ type PostgreSQL struct {
|
|||
*connutil.SQLConnectionProducer
|
||||
|
||||
usernameProducer template.StringTemplate
|
||||
passwordAuthentication passwordAuthentication
|
||||
}
|
||||
|
||||
func (p *PostgreSQL) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) {
|
||||
|
@ -105,6 +108,20 @@ func (p *PostgreSQL) Initialize(ctx context.Context, req dbplugin.InitializeRequ
|
|||
return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err)
|
||||
}
|
||||
|
||||
passwordAuthenticationRaw, err := strutil.GetString(req.Config, "password_authentication")
|
||||
if err != nil {
|
||||
return dbplugin.InitializeResponse{}, fmt.Errorf("failed to retrieve password_authentication: %w", err)
|
||||
}
|
||||
|
||||
if passwordAuthenticationRaw != "" {
|
||||
pwAuthentication, err := parsePasswordAuthentication(passwordAuthenticationRaw)
|
||||
if err != nil {
|
||||
return dbplugin.InitializeResponse{}, err
|
||||
}
|
||||
|
||||
p.passwordAuthentication = pwAuthentication
|
||||
}
|
||||
|
||||
resp := dbplugin.InitializeResponse{
|
||||
Config: newConf,
|
||||
}
|
||||
|
@ -188,6 +205,15 @@ func (p *PostgreSQL) changeUserPassword(ctx context.Context, username string, ch
|
|||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
if p.passwordAuthentication == passwordAuthenticationSCRAMSHA256 {
|
||||
hashedPassword, err := scram.Hash(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to scram-sha256 password: %w", err)
|
||||
}
|
||||
m["password"] = hashedPassword
|
||||
}
|
||||
|
||||
if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil {
|
||||
return fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
|
@ -272,15 +298,24 @@ func (p *PostgreSQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (
|
|||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
for _, stmt := range req.Statements.Commands {
|
||||
if containsMultilineStatement(stmt) {
|
||||
// Execute it as-is.
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"username": username,
|
||||
"password": req.Password,
|
||||
"expiration": expirationStr,
|
||||
}
|
||||
|
||||
if p.passwordAuthentication == passwordAuthenticationSCRAMSHA256 {
|
||||
hashedPassword, err := scram.Hash(req.Password)
|
||||
if err != nil {
|
||||
return dbplugin.NewUserResponse{}, fmt.Errorf("unable to scram-sha256 password: %w", err)
|
||||
}
|
||||
m["password"] = hashedPassword
|
||||
}
|
||||
|
||||
for _, stmt := range req.Statements.Commands {
|
||||
if containsMultilineStatement(stmt) {
|
||||
// Execute it as-is.
|
||||
if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, stmt); err != nil {
|
||||
return dbplugin.NewUserResponse{}, fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
|
@ -293,12 +328,6 @@ func (p *PostgreSQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (
|
|||
continue
|
||||
}
|
||||
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"username": username,
|
||||
"password": req.Password,
|
||||
"expiration": expirationStr,
|
||||
}
|
||||
if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil {
|
||||
return dbplugin.NewUserResponse{}, fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
|
||||
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
|
||||
"github.com/hashicorp/vault/sdk/helper/template"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -93,6 +94,97 @@ func TestPostgreSQL_Initialize_ConnURLWithDSNFormat(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestPostgreSQL_PasswordAuthentication tests that the default "password_authentication" is "none", and that
|
||||
// an error is returned if an invalid "password_authentication" is provided.
|
||||
func TestPostgreSQL_PasswordAuthentication(t *testing.T) {
|
||||
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
|
||||
defer cleanup()
|
||||
|
||||
dsnConnURL, err := dbutil.ParseURL(connURL)
|
||||
assert.NoError(t, err)
|
||||
db := new()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("invalid-password-authentication", func(t *testing.T) {
|
||||
connectionDetails := map[string]interface{}{
|
||||
"connection_url": dsnConnURL,
|
||||
"password_authentication": "invalid-password-authentication",
|
||||
}
|
||||
|
||||
req := dbplugin.InitializeRequest{
|
||||
Config: connectionDetails,
|
||||
VerifyConnection: true,
|
||||
}
|
||||
|
||||
_, err := db.Initialize(ctx, req)
|
||||
assert.EqualError(t, err, "'invalid-password-authentication' is not a valid password authentication type")
|
||||
})
|
||||
|
||||
t.Run("default-is-none", func(t *testing.T) {
|
||||
connectionDetails := map[string]interface{}{
|
||||
"connection_url": dsnConnURL,
|
||||
}
|
||||
|
||||
req := dbplugin.InitializeRequest{
|
||||
Config: connectionDetails,
|
||||
VerifyConnection: true,
|
||||
}
|
||||
|
||||
_ = dbtesting.AssertInitialize(t, db, req)
|
||||
assert.Equal(t, passwordAuthenticationPassword, db.passwordAuthentication)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPostgreSQL_PasswordAuthentication_SCRAMSHA256 tests that password_authentication works when set to scram-sha-256.
|
||||
// When sending an encrypted password, the raw password should still successfully authenticate the user.
|
||||
func TestPostgreSQL_PasswordAuthentication_SCRAMSHA256(t *testing.T) {
|
||||
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
|
||||
defer cleanup()
|
||||
|
||||
dsnConnURL, err := dbutil.ParseURL(connURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
connectionDetails := map[string]interface{}{
|
||||
"connection_url": dsnConnURL,
|
||||
"password_authentication": string(passwordAuthenticationSCRAMSHA256),
|
||||
}
|
||||
|
||||
req := dbplugin.InitializeRequest{
|
||||
Config: connectionDetails,
|
||||
VerifyConnection: true,
|
||||
}
|
||||
|
||||
db := new()
|
||||
resp := dbtesting.AssertInitialize(t, db, req)
|
||||
assert.Equal(t, string(passwordAuthenticationSCRAMSHA256), resp.Config["password_authentication"])
|
||||
|
||||
if !db.Initialized {
|
||||
t.Fatal("Database should be initialized")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
newUserRequest := dbplugin.NewUserRequest{
|
||||
Statements: dbplugin.Statements{
|
||||
Commands: []string{
|
||||
`
|
||||
CREATE ROLE "{{name}}" WITH
|
||||
LOGIN
|
||||
PASSWORD '{{password}}'
|
||||
VALID UNTIL '{{expiration}}';
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`,
|
||||
},
|
||||
},
|
||||
Password: "somesecurepassword",
|
||||
Expiration: time.Now().Add(1 * time.Minute),
|
||||
}
|
||||
newUserResponse, err := db.NewUser(ctx, newUserRequest)
|
||||
|
||||
assertCredsExist(t, db.ConnectionURL, newUserResponse.Username, newUserRequest.Password)
|
||||
}
|
||||
|
||||
func TestPostgreSQL_NewUser(t *testing.T) {
|
||||
type testCase struct {
|
||||
req dbplugin.NewUserRequest
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Taishi Kasuga
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,86 @@
|
|||
package scram
|
||||
|
||||
//
|
||||
// @see https://github.com/postgres/postgres/blob/c30f54ad732ca5c8762bb68bbe0f51de9137dd72/src/interfaces/libpq/fe-auth.c#L1167-L1285
|
||||
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/interfaces/libpq/fe-auth-scram.c#L868-L905
|
||||
// @see https://github.com/postgres/postgres/blob/c30f54ad732ca5c8762bb68bbe0f51de9137dd72/src/port/pg_strong_random.c#L66-L96
|
||||
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/common/scram-common.c#L160-L274
|
||||
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/common/scram-common.c#L27-L85
|
||||
|
||||
// Implementation from https://github.com/supercaracal/scram-sha-256/blob/d3c05cd927770a11c6e12de3e3a99c3446a1f78d/main.go
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const (
|
||||
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/include/common/scram-common.h#L36-L41
|
||||
saltSize = 16
|
||||
|
||||
// @see https://github.com/postgres/postgres/blob/c30f54ad732ca5c8762bb68bbe0f51de9137dd72/src/include/common/sha2.h#L22
|
||||
digestLen = 32
|
||||
|
||||
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/include/common/scram-common.h#L43-L47
|
||||
iterationCnt = 4096
|
||||
)
|
||||
|
||||
var (
|
||||
clientRawKey = []byte("Client Key")
|
||||
serverRawKey = []byte("Server Key")
|
||||
)
|
||||
|
||||
func genSalt(size int) ([]byte, error) {
|
||||
salt := make([]byte, size)
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return salt, nil
|
||||
}
|
||||
|
||||
func encodeB64(src []byte) (dst []byte) {
|
||||
dst = make([]byte, base64.StdEncoding.EncodedLen(len(src)))
|
||||
base64.StdEncoding.Encode(dst, src)
|
||||
return
|
||||
}
|
||||
|
||||
func getHMACSum(key, msg []byte) []byte {
|
||||
h := hmac.New(sha256.New, key)
|
||||
_, _ = h.Write(msg)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func getSHA256Sum(key []byte) []byte {
|
||||
h := sha256.New()
|
||||
_, _ = h.Write(key)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func hashPassword(rawPassword, salt []byte, iter, keyLen int) string {
|
||||
digestKey := pbkdf2.Key(rawPassword, salt, iter, keyLen, sha256.New)
|
||||
clientKey := getHMACSum(digestKey, clientRawKey)
|
||||
storedKey := getSHA256Sum(clientKey)
|
||||
serverKey := getHMACSum(digestKey, serverRawKey)
|
||||
|
||||
return fmt.Sprintf("SCRAM-SHA-256$%d:%s$%s:%s",
|
||||
iter,
|
||||
string(encodeB64(salt)),
|
||||
string(encodeB64(storedKey)),
|
||||
string(encodeB64(serverKey)),
|
||||
)
|
||||
}
|
||||
|
||||
func Hash(password string) (string, error) {
|
||||
salt, err := genSalt(saltSize)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hashedPassword := hashPassword([]byte(password), salt, iterationCnt, digestLen)
|
||||
return hashedPassword, nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package scram
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestScram tests the Hash method. The hashed password string should have a SCRAM-SHA-256 prefix.
|
||||
func TestScram(t *testing.T) {
|
||||
tcs := map[string]struct {
|
||||
Password string
|
||||
}{
|
||||
"empty-password": {Password: ""},
|
||||
"simple-password": {Password: "password"},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := Hash(tc.Password)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(got, "SCRAM-SHA-256$4096:"))
|
||||
assert.Len(t, got, 133)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -55,6 +55,12 @@ has a number of parameters to further configure a connection.
|
|||
and password fields. See the [databases secrets engine docs](/vault/docs/secrets/databases#disable-character-escaping)
|
||||
for more information. Defaults to `false`.
|
||||
|
||||
- `password_authentication` `(string: "password")` - When set to "scram-sha-256", passwords will be hashed by Vault and stored as-is by PostgreSQL.
|
||||
Using "scram-sha-256" requires a minimum version of PostgreSQL 10. Available options are "scram-sha-256" and "password". The default is "password".
|
||||
When set to "password", passwords will be sent to PostgresSQL in plaintext format and may appear in PostgreSQL logs as-is.
|
||||
For more information, please refer to the [https://www.postgresql.org/docs/current/sql-createrole.html#password](PostgreSQL documentation).
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>Default Username Template</b></summary>
|
||||
|
||||
|
|
|
@ -48,7 +48,8 @@ options, including SSL options, can be found in the [pgx][pgxlib] and
|
|||
allowed_roles="my-role" \
|
||||
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/database-name" \
|
||||
username="vaultuser" \
|
||||
password="vaultpass"
|
||||
password="vaultpass" \
|
||||
password_authentication="scram-sha-256"
|
||||
```
|
||||
|
||||
1. Configure a role that maps a name in Vault to an SQL statement to execute to
|
||||
|
|
Loading…
Reference in New Issue