diff --git a/docs/generated/settings/settings-for-tenants.txt b/docs/generated/settings/settings-for-tenants.txt
index 107d07b5c70e..d63bc6b550fe 100644
--- a/docs/generated/settings/settings-for-tenants.txt
+++ b/docs/generated/settings/settings-for-tenants.txt
@@ -69,7 +69,9 @@ server.shutdown.lease_transfer_wait duration 5s the amount of time a server wait
server.shutdown.query_wait duration 10s the server will wait for at least this amount of time for active queries to finish (note that the --drain-wait parameter for cockroach node drain may need adjustment after changing this setting)
server.time_until_store_dead duration 5m0s the time after which if there is no new gossiped information about a store, it is considered dead
server.user_login.min_password_length integer 1 the minimum length accepted for passwords set in cleartext via SQL. Note that a value lower than 1 is ignored: passwords cannot be empty in any case.
+server.user_login.password_encryption enumeration scram-sha-256 which hash method to use to encode cleartext passwords passed via ALTER/CREATE USER/ROLE WITH PASSWORD [crdb-bcrypt = 2, scram-sha-256 = 3]
server.user_login.password_hashes.default_cost.crdb_bcrypt integer 10 the hashing cost to use when storing passwords supplied as cleartext by SQL clients with the hashing method crdb-bcrypt (allowed range: 4-31)
+server.user_login.password_hashes.default_cost.scram_sha_256 integer 119680 the hashing cost to use when storing passwords supplied as cleartext by SQL clients with the hashing method scram-sha-256 (allowed range: 4096-240000000000)
server.user_login.store_client_pre_hashed_passwords.enabled boolean true whether the server accepts to store passwords pre-hashed by clients
server.user_login.timeout duration 10s timeout after which client authentication times out if some system range is unavailable (0 = no timeout)
server.web_session.auto_logout.timeout duration 168h0m0s the duration that web sessions will survive before being periodically purged, since they were last used
diff --git a/docs/generated/settings/settings.html b/docs/generated/settings/settings.html
index f7760d7f155b..c3cd22fe3259 100644
--- a/docs/generated/settings/settings.html
+++ b/docs/generated/settings/settings.html
@@ -76,7 +76,9 @@
server.shutdown.query_wait | duration | 10s | the server will wait for at least this amount of time for active queries to finish (note that the --drain-wait parameter for cockroach node drain may need adjustment after changing this setting) |
server.time_until_store_dead | duration | 5m0s | the time after which if there is no new gossiped information about a store, it is considered dead |
server.user_login.min_password_length | integer | 1 | the minimum length accepted for passwords set in cleartext via SQL. Note that a value lower than 1 is ignored: passwords cannot be empty in any case. |
+server.user_login.password_encryption | enumeration | scram-sha-256 | which hash method to use to encode cleartext passwords passed via ALTER/CREATE USER/ROLE WITH PASSWORD [crdb-bcrypt = 2, scram-sha-256 = 3] |
server.user_login.password_hashes.default_cost.crdb_bcrypt | integer | 10 | the hashing cost to use when storing passwords supplied as cleartext by SQL clients with the hashing method crdb-bcrypt (allowed range: 4-31) |
+server.user_login.password_hashes.default_cost.scram_sha_256 | integer | 119680 | the hashing cost to use when storing passwords supplied as cleartext by SQL clients with the hashing method scram-sha-256 (allowed range: 4096-240000000000) |
server.user_login.store_client_pre_hashed_passwords.enabled | boolean | true | whether the server accepts to store passwords pre-hashed by clients |
server.user_login.timeout | duration | 10s | timeout after which client authentication times out if some system range is unavailable (0 = no timeout) |
server.web_session.auto_logout.timeout | duration | 168h0m0s | the duration that web sessions will survive before being periodically purged, since they were last used |
diff --git a/pkg/security/BUILD.bazel b/pkg/security/BUILD.bazel
index c3ea20abb4b5..5e823f5c4a3c 100644
--- a/pkg/security/BUILD.bazel
+++ b/pkg/security/BUILD.bazel
@@ -22,6 +22,7 @@ go_library(
importpath = "github.com/cockroachdb/cockroach/pkg/security",
visibility = ["//visibility:public"],
deps = [
+ "//pkg/clusterversion",
"//pkg/server/telemetry",
"//pkg/settings",
"//pkg/settings/cluster",
diff --git a/pkg/security/password.go b/pkg/security/password.go
index f596fa8c9ad8..f5fcb4164835 100644
--- a/pkg/security/password.go
+++ b/pkg/security/password.go
@@ -14,15 +14,18 @@ import (
"bytes"
"context"
"crypto/hmac"
+ "crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
+ "io"
"regexp"
"runtime"
"strconv"
"sync"
"unsafe"
+ "github.com/cockroachdb/cockroach/pkg/clusterversion"
"github.com/cockroachdb/cockroach/pkg/settings"
"github.com/cockroachdb/cockroach/pkg/util/envutil"
"github.com/cockroachdb/cockroach/pkg/util/log"
@@ -63,8 +66,37 @@ var BcryptCost = settings.RegisterIntSetting(
// BcryptCostSettingName is the name of the cluster setting BcryptCost.
const BcryptCostSettingName = "server.user_login.password_hashes.default_cost.crdb_bcrypt"
+// SCRAMCost is the cost to use in SCRAM exchanges.
+// The value of 4096 is the minimum value recommended by RFC 5802.
+// It should be increased along with computation power.
+var SCRAMCost = settings.RegisterIntSetting(
+ settings.TenantWritable,
+ SCRAMCostSettingName,
+ fmt.Sprintf(
+ "the hashing cost to use when storing passwords supplied as cleartext by SQL clients "+
+ "with the hashing method scram-sha-256 (allowed range: %d-%d)",
+ scramMinCost, scramMaxCost),
+ // The minimum value 4096 incurs a password check latency of ~2ms on AMD 3950X 3.7GHz.
+ //
+ // The default value 119680 incurs ~60ms latency on the same hw.
+ // This default was calibrated to incur a similar check latency as the
+ // default value for BCryptCost above.
+ //
+ // For reference, value 250000 incurs ~125ms latency on the same hw,
+ // value 1000000 incurs ~500ms.
+ 119680,
+ func(i int64) error {
+ if i < scramMinCost || i > scramMaxCost {
+ return errors.Newf("cost not in allowed range (%d,%d)", scramMinCost, scramMaxCost)
+ }
+ return nil
+ }).WithPublic()
+
const scramMinCost = 4096 // as per RFC 5802.
-const scramMaxCost = 240000000000 // arbitrary value to prevent unreasonably long logins.
+const scramMaxCost = 240000000000 // arbitrary value to prevent unreasonably long logins
+
+// SCRAMCostSettingName is the name of the cluster setting SCRAMCost.
+const SCRAMCostSettingName = "server.user_login.password_hashes.default_cost.scram_sha_256"
// ErrEmptyPassword indicates that an empty password was attempted to be set.
var ErrEmptyPassword = errors.New("empty passwords are not permitted")
@@ -83,19 +115,38 @@ type HashMethod int8
const (
// HashInvalidMethod represents invalid hashes.
// This always fails authentication.
- HashInvalidMethod HashMethod = iota
+ HashInvalidMethod HashMethod = 0
// HashMissingPassword represents a virtual hash when there was
// no password. This too always fails authentication.
// We need a different method here than HashInvalidMethod because
// the authentication code distinguishes the two cases when reporting
// why authentication fails in audit logs.
- HashMissingPassword
+ HashMissingPassword HashMethod = 1
// HashBCrypt indicates CockroachDB's bespoke bcrypt-based method.
- HashBCrypt
+ // NB: Do not renumber this constant; it is used as value
+ // in cluster setting enums.
+ HashBCrypt HashMethod = 2
// HashSCRAMSHA256 indicates SCRAM-SHA-256.
- HashSCRAMSHA256
+ // NB: Do not renumber this constant; it is used as value
+ // in cluster setting enums.
+ HashSCRAMSHA256 HashMethod = 3
)
+func (h HashMethod) String() string {
+ switch h {
+ case HashInvalidMethod:
+ return ""
+ case HashMissingPassword:
+ return ""
+ case HashBCrypt:
+ return "crdb-bcrypt"
+ case HashSCRAMSHA256:
+ return "scram-sha-256"
+ default:
+ panic(errors.AssertionFailedf("programming errof: unknown hash method %d", int(h)))
+ }
+}
+
// PasswordHash represents the type of a password hash loaded from a credential store.
type PasswordHash interface {
fmt.Stringer
@@ -308,15 +359,135 @@ func computeHMAC(hg scram.HashGeneratorFcn, key, data []byte) []byte {
return mac.Sum(nil)
}
-// HashPassword takes a raw password and returns a bcrypt hashed password.
+// PasswordHashMethod is the cluster setting that configures which
+// hash method to use when clients request to store a cleartext password.
+//
+// It is exported for use in tests. Do not use this setting directly
+// to read the current hash method. Instead use the
+// GetConfiguredHashMethod() function.
+var PasswordHashMethod = settings.RegisterEnumSetting(
+ settings.TenantWritable,
+ "server.user_login.password_encryption",
+ "which hash method to use to encode cleartext passwords passed via ALTER/CREATE USER/ROLE WITH PASSWORD",
+ // Note: the default is initially SCRAM, even in mixed-version clusters where
+ // previous-version nodes do not know anything about SCRAM. This is handled
+ // in the GetConfiguredPasswordHashMethod() function.
+ "scram-sha-256",
+ map[int64]string{
+ int64(HashBCrypt): HashBCrypt.String(),
+ int64(HashSCRAMSHA256): HashSCRAMSHA256.String(),
+ },
+).WithPublic()
+
+// hasClusterVersion verifies that all nodes have been upgraded to
+// support the given target version key.
+func hasClusterVersion(
+ ctx context.Context, values *settings.Values, versionkey clusterversion.Key,
+) bool {
+ var vh clusterversion.Handle
+ if values != nil {
+ vh = values.Opaque().(clusterversion.Handle)
+ }
+ return vh != nil && vh.IsActive(ctx, versionkey)
+}
+
+// GetConfiguredPasswordHashMethod returns the configured hash method
+// to use before storing passwords provided in cleartext from clients.
+func GetConfiguredPasswordHashMethod(ctx context.Context, sv *settings.Values) (method HashMethod) {
+ method = HashMethod(PasswordHashMethod.Get(sv))
+ if method == HashSCRAMSHA256 && !hasClusterVersion(ctx, sv, clusterversion.SCRAMAuthentication) {
+ // Not all nodes are upgraded to understand SCRAM yet. Force
+ // Bcrypt for now, otherwise previous-version nodes will get confused.
+ method = HashBCrypt
+ }
+ return method
+}
+
+// HashPassword takes a raw password and returns a hashed password, hashed
+// using the currently configured method.
func HashPassword(ctx context.Context, sv *settings.Values, password string) ([]byte, error) {
- sem := getExpensiveHashComputeSem(ctx)
- alloc, err := sem.Acquire(ctx, 1)
- if err != nil {
- return nil, err
+ method := GetConfiguredPasswordHashMethod(ctx, sv)
+ switch method {
+ case HashBCrypt:
+ sem := getExpensiveHashComputeSem(ctx)
+ alloc, err := sem.Acquire(ctx, 1)
+ if err != nil {
+ return nil, err
+ }
+ defer alloc.Release()
+ return bcrypt.GenerateFromPassword(appendEmptySha256(password), int(BcryptCost.Get(sv)))
+
+ case HashSCRAMSHA256:
+ prepared, err := stringprep.SASLprep.Prepare(password)
+ if err != nil {
+ // Special PostgreSQL case, quoth comment at the top of
+ // auth-scram.c:
+ //
+ // * - If the password isn't valid UTF-8, or contains characters prohibited
+ // * by the SASLprep profile, we skip the SASLprep pre-processing and use
+ // * the raw bytes in calculating the hash.
+ prepared = password
+ }
+
+ // The computation of ServerKey and StoredKey is conveniently provided
+ // to us by xdg/scram in the Client method GetStoredCredentials().
+ // To use it, we need a client.
+ client, err := scram.SHA256.NewClientUnprepped("" /* username: unused */, prepared, "" /* authzID: unused */)
+ if err != nil {
+ return nil, errors.AssertionFailedf("programming error: client construction should never fail")
+ }
+
+ // We also need to generate a random salt ourselves.
+ const scramSaltSize = 16 // postgres: SCRAM_DEFAULT_SALT_LEN.
+ salt := make([]byte, scramSaltSize)
+ if _, err := io.ReadFull(rand.Reader, salt); err != nil {
+ return nil, errors.Wrap(err, "generating random salt")
+ }
+
+ // The computation of the SCRAM hash is expensive. Use the shared
+ // semaphore for it. We reuse the same pattern as the bcrypt case above.
+ sem := getExpensiveHashComputeSem(ctx)
+ alloc, err := sem.Acquire(ctx, 1)
+ if err != nil {
+ return nil, err
+ }
+ defer alloc.Release()
+ // Compute the credentials.
+ cost := int(SCRAMCost.Get(sv))
+ creds := client.GetStoredCredentials(scram.KeyFactors{Iters: cost, Salt: string(salt)})
+ // Encode them in our standard hash format.
+ return encodeScramHash(salt, creds), nil
+
+ default:
+ return nil, errors.Newf("unsupported hash method: %v", method)
}
- defer alloc.Release()
- return bcrypt.GenerateFromPassword(appendEmptySha256(password), int(BcryptCost.Get(sv)))
+}
+
+// encodeScramHash encodes the provided SCRAM credentials using the
+// standard PostgreSQL / RFC5802 representation.
+func encodeScramHash(saltBytes []byte, sc scram.StoredCredentials) []byte {
+ b64enc := base64.StdEncoding
+ saltLen := b64enc.EncodedLen(len(saltBytes))
+ storedKeyLen := b64enc.EncodedLen(len(sc.StoredKey))
+ serverKeyLen := b64enc.EncodedLen(len(sc.ServerKey))
+ // The representation is:
+ // SCRAM-SHA-256$:$:
+ // We use a capacity-based slice extension instead of a size-based fill
+ // so as to automatically support iteration counts with more than 4 digits.
+ res := make([]byte, 0, len(scramPrefix)+1+4 /*iters*/ +1+saltLen+1+storedKeyLen+1+serverKeyLen)
+ res = append(res, scramPrefix...)
+ res = append(res, '$')
+ res = strconv.AppendInt(res, int64(sc.Iters), 10)
+ res = append(res, ':')
+ res = append(res, make([]byte, saltLen)...)
+ b64enc.Encode(res[len(res)-saltLen:], saltBytes)
+ res = append(res, '$')
+ res = append(res, make([]byte, storedKeyLen)...)
+ b64enc.Encode(res[len(res)-storedKeyLen:], sc.StoredKey)
+ res = append(res, ':')
+ res = append(res, make([]byte, serverKeyLen)...)
+ b64enc.Encode(res[len(res)-serverKeyLen:], sc.ServerKey)
+ return res
}
// AutoDetectPasswordHashes is the cluster setting that configures whether
@@ -363,6 +534,8 @@ func checkBcryptHash(inputPassword []byte) (ok bool, hashedPassword []byte, err
return true, hashedPassword, err
}
+const scramPrefix = "SCRAM-SHA-256"
+
// scramHashRe matches the lexical structure of PostgreSQL's
// pre-computed SCRAM hashes.
//
@@ -371,7 +544,7 @@ func checkBcryptHash(inputPassword []byte) (ok bool, hashedPassword []byte, err
// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
// The salt must have size >0; the server key pair is two times 32 bytes,
// which always encode to 44 base64 characters.
-var scramHashRe = regexp.MustCompile(`^SCRAM-SHA-256\$(\d+):([A-Za-z0-9+/]+=*)\$([A-Za-z0-9+/]{43}=):([A-Za-z0-9+/]{43}=)$`)
+var scramHashRe = regexp.MustCompile(`^` + scramPrefix + `\$(\d+):([A-Za-z0-9+/]+=*)\$([A-Za-z0-9+/]{43}=):([A-Za-z0-9+/]{43}=)$`)
// scramParts is an intermediate type to connect the output of
// isSCRAMHash() to makeSCRAMHash(), so that the latter cannot be
diff --git a/pkg/sql/logictest/testdata/logic_test/information_schema b/pkg/sql/logictest/testdata/logic_test/information_schema
index 67edd5c35780..3a703c5a1b19 100644
--- a/pkg/sql/logictest/testdata/logic_test/information_schema
+++ b/pkg/sql/logictest/testdata/logic_test/information_schema
@@ -4691,6 +4691,7 @@ optimizer_use_histograms on
optimizer_use_multicol_stats on
override_multi_region_zone_config off
parallelize_multi_key_lookup_joins_enabled off
+password_encryption scram-sha-256
prefer_lookup_joins_for_fks off
propagate_input_ordering off
reorder_joins_limit 8
diff --git a/pkg/sql/logictest/testdata/logic_test/pg_catalog b/pkg/sql/logictest/testdata/logic_test/pg_catalog
index 2492c151a7ee..61f8f6e7747f 100644
--- a/pkg/sql/logictest/testdata/logic_test/pg_catalog
+++ b/pkg/sql/logictest/testdata/logic_test/pg_catalog
@@ -4097,6 +4097,7 @@ optimizer_use_histograms on NULL
optimizer_use_multicol_stats on NULL NULL NULL string
override_multi_region_zone_config off NULL NULL NULL string
parallelize_multi_key_lookup_joins_enabled off NULL NULL NULL string
+password_encryption scram-sha-256 NULL NULL NULL string
prefer_lookup_joins_for_fks off NULL NULL NULL string
propagate_input_ordering off NULL NULL NULL string
reorder_joins_limit 8 NULL NULL NULL string
@@ -4209,6 +4210,7 @@ optimizer_use_histograms on NULL
optimizer_use_multicol_stats on NULL user NULL on on
override_multi_region_zone_config off NULL user NULL off off
parallelize_multi_key_lookup_joins_enabled off NULL user NULL false false
+password_encryption scram-sha-256 NULL user NULL scram-sha-256 scram-sha-256
prefer_lookup_joins_for_fks off NULL user NULL off off
propagate_input_ordering off NULL user NULL off off
reorder_joins_limit 8 NULL user NULL 8 8
@@ -4317,6 +4319,7 @@ optimizer_use_histograms NULL NULL NULL
optimizer_use_multicol_stats NULL NULL NULL NULL NULL
override_multi_region_zone_config NULL NULL NULL NULL NULL
parallelize_multi_key_lookup_joins_enabled NULL NULL NULL NULL NULL
+password_encryption NULL NULL NULL NULL NULL
prefer_lookup_joins_for_fks NULL NULL NULL NULL NULL
propagate_input_ordering NULL NULL NULL NULL NULL
reorder_joins_limit NULL NULL NULL NULL NULL
diff --git a/pkg/sql/logictest/testdata/logic_test/role b/pkg/sql/logictest/testdata/logic_test/role
index c377b2c6d000..9a2f62c8aa57 100644
--- a/pkg/sql/logictest/testdata/logic_test/role
+++ b/pkg/sql/logictest/testdata/logic_test/role
@@ -1415,6 +1415,9 @@ subtest pw_hashes
user root
+statement ok
+SET CLUSTER SETTING server.user_login.password_encryption = 'crdb-bcrypt'
+
let $bcrypt_pw
SELECT 'CRDB-BCRYPT$'||'2a$'||'10$'||'vcmoIBvgeHjgScVHWRMWI.Z3v03WMixAw2bBS6qZihljSUuwi88Yq'
@@ -1463,7 +1466,7 @@ RESET CLUSTER SETTING server.user_login.store_client_pre_hashed_passwords.enable
subtest bcrypt_cost
statement ok
-SET CLUSTER SETTING server.user_login.password_hashes.default_cost.crdb_bcrypt = 20
+SET CLUSTER SETTING server.user_login.password_hashes.default_cost.crdb_bcrypt = 8
statement ok
CREATE USER hash7 WITH PASSWORD 'hello'
@@ -1472,7 +1475,46 @@ CREATE USER hash7 WITH PASSWORD 'hello'
query TT
SELECT username, substr("hashedPassword", 1, 7) FROM system.users WHERE username = 'hash7'
----
-hash7 $2a$20$
+hash7 $2a$08$
statement ok
RESET CLUSTER SETTING server.user_login.password_hashes.default_cost.crdb_bcrypt;
+
+subtest scram_gen_hash
+
+statement ok
+SET CLUSTER SETTING server.user_login.password_encryption = 'scram-sha-256'
+
+statement ok
+CREATE USER hash8 WITH PASSWORD 'hello world'
+
+query TT
+SELECT username, substr("hashedPassword", 1, 20) FROM system.users WHERE username = 'hash8'
+----
+hash8 SCRAM-SHA-256$119680
+
+statement ok
+ALTER USER hash8 WITH PASSWORD 'hello universe'
+
+query TT
+SELECT username, substr("hashedPassword", 1, 20) FROM system.users WHERE username = 'hash8'
+----
+hash8 SCRAM-SHA-256$119680
+
+subtest scram_cost
+
+statement ok
+SET CLUSTER SETTING server.user_login.password_hashes.default_cost.scram_sha_256 = 200000
+
+statement ok
+ALTER USER hash8 WITH PASSWORD 'hai'
+
+query TT
+SELECT username, substr("hashedPassword", 1, 20) FROM system.users WHERE username = 'hash8'
+----
+hash8 SCRAM-SHA-256$200000
+
+# Reset cluster setting after test completion.
+statement ok
+RESET CLUSTER SETTING server.user_login.password_encryption;
+RESET CLUSTER SETTING server.user_login.password_hashes.default_cost.scram_sha_256
diff --git a/pkg/sql/logictest/testdata/logic_test/scram_221_upgrade b/pkg/sql/logictest/testdata/logic_test/scram_221_upgrade
index 77e3d7fcc66e..8608c7db99a7 100644
--- a/pkg/sql/logictest/testdata/logic_test/scram_221_upgrade
+++ b/pkg/sql/logictest/testdata/logic_test/scram_221_upgrade
@@ -3,3 +3,14 @@
# Check that the new HBA syntax is not accepted during the upgrade.
statement error HBA authentication method "scram-sha-256" requires all nodes to be upgraded to 21.2-
set cluster setting server.host_based_authentication.configuration = 'host all all all scram-sha-256'
+
+# Check that the "password encryption" (hash method) does not change
+# even when the cluster setting is updated, until the cluster is upgraded.
+statement ok
+set cluster setting server.user_login.password_encryption = 'scram-sha-256'
+
+query T
+show password_encryption
+----
+crdb-bcrypt
+
diff --git a/pkg/sql/logictest/testdata/logic_test/show_source b/pkg/sql/logictest/testdata/logic_test/show_source
index 52c0d6f983f5..854905ac7174 100644
--- a/pkg/sql/logictest/testdata/logic_test/show_source
+++ b/pkg/sql/logictest/testdata/logic_test/show_source
@@ -95,6 +95,7 @@ optimizer_use_histograms on
optimizer_use_multicol_stats on
override_multi_region_zone_config off
parallelize_multi_key_lookup_joins_enabled off
+password_encryption scram-sha-256
prefer_lookup_joins_for_fks off
propagate_input_ordering off
reorder_joins_limit 8
diff --git a/pkg/sql/pgwire/testdata/auth/password_change b/pkg/sql/pgwire/testdata/auth/password_change
index 1805a1c22b48..229031aeec1e 100644
--- a/pkg/sql/pgwire/testdata/auth/password_change
+++ b/pkg/sql/pgwire/testdata/auth/password_change
@@ -6,6 +6,55 @@ config secure
subtest regular_user
+subtest regular_user/bcrypt
+
+sql
+SET CLUSTER SETTING server.user_login.password_encryption = 'crdb-bcrypt';
+----
+ok
+
+sql
+CREATE USER userpw WITH PASSWORD 'pass'
+----
+ok
+
+# sanity check: without a password, auth is denied.
+connect user=userpw
+----
+ERROR: password authentication failed for user userpw (SQLSTATE 28000)
+
+# with the proper pass, auth succeeds.
+connect user=userpw password=pass
+----
+ok defaultdb
+
+# Changing the password causes the previous password to fail
+# and the new one to succeed.
+
+sql
+ALTER USER userpw WITH PASSWORD 'pass2'
+----
+ok
+
+connect user=userpw password=pass
+----
+ERROR: password authentication failed for user userpw (SQLSTATE 28000)
+
+connect user=userpw password=pass2
+----
+ok defaultdb
+
+
+subtest end
+
+subtest regular_user/scram
+
+sql
+SET CLUSTER SETTING server.user_login.password_encryption = 'scram-sha-256';
+DROP USER userpw;
+----
+ok
+
sql
CREATE USER userpw WITH PASSWORD 'pass'
----
@@ -38,6 +87,10 @@ connect user=userpw password=pass2
ok defaultdb
+subtest end
+
+subtest regular_user/no_password
+
# Erasing the password forces cert authentication.
sql
@@ -55,6 +108,8 @@ ERROR: password authentication failed for user userpw (SQLSTATE 28000)
subtest end
+subtest end
+
subtest precomputed_hash
subtest precomputed_hash/bcrypt
@@ -153,6 +208,14 @@ ERROR: password authentication failed for user root (SQLSTATE 28000)
# However if we give them a password, they can log in with password.
+
+subtest root_pw/bcrypt
+
+sql
+SET CLUSTER SETTING server.user_login.password_encryption = 'crdb-bcrypt';
+----
+ok
+
sql
ALTER USER root WITH PASSWORD 'secureabc'
----
@@ -168,3 +231,28 @@ connect_unix user=root password=secureabc
ok defaultdb
subtest end
+
+subtest root_pw/scram
+
+sql
+SET CLUSTER SETTING server.user_login.password_encryption = 'scram-sha-256';
+----
+ok
+
+sql
+ALTER USER root WITH PASSWORD 'secureabc'
+----
+ok
+
+# Then they can log in.
+connect user=root password=secureabc sslmode=require sslcert= sslkey=
+----
+ok defaultdb
+
+connect_unix user=root password=secureabc
+----
+ok defaultdb
+
+subtest end
+
+subtest end
diff --git a/pkg/sql/pgwire/testdata/auth/scram b/pkg/sql/pgwire/testdata/auth/scram
index 1b7c6421df8a..a0c49a627e26 100644
--- a/pkg/sql/pgwire/testdata/auth/scram
+++ b/pkg/sql/pgwire/testdata/auth/scram
@@ -2,8 +2,13 @@ config secure
----
sql
-CREATE USER foo WITH PASSWORD 'abc';
+-- Explicit hash.
CREATE USER abc WITH PASSWORD 'SCRAM-SHA-256$4096:pAlYy62NTdETKb291V/Wow==$OXMAj9oD53QucEYVMBcdhRnjg2/S/iZY/88ShZnputA=:r8l4c1pk9bmDi+8an059l/nt9Bg1zb1ikkg+DeRv4UQ=';
+-- Automatic hashes.
+SET CLUSTER SETTING server.user_login.password_encryption = 'crdb-bcrypt';
+CREATE USER foo WITH PASSWORD 'abc';
+SET CLUSTER SETTING server.user_login.password_encryption = 'scram-sha-256';
+CREATE USER abc2 WITH PASSWORD 'abc'
----
ok
@@ -15,16 +20,19 @@ subtest conn_plaintext
set_hba
host all abc all password
+host all abc2 all password
----
# Active authentication configuration on this node:
# Original configuration:
# host all root all cert-password # CockroachDB mandatory rule
# host all abc all password
+# host all abc2 all password
#
# Interpreted configuration:
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
host all root all cert-password
host all abc all password
+host all abc2 all password
# User abc has SCRAM credentials, but 'mistake' is not its password.
# Expect authn error.
@@ -54,6 +62,18 @@ authlog 5
13 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
14 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+connect user=abc2 password=abc
+----
+ok defaultdb
+
+authlog 5
+.*client_connection_end
+----
+15 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+16 {"EventType":"client_authentication_info","Info":"HBA rule: host all abc2 all password","InstanceID":1,"Method":"password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc2","Timestamp":"XXX","Transport":"hostssl"}
+17 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc2","Timestamp":"XXX","Transport":"hostssl","User":"abc2"}
+18 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+19 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
subtest end
@@ -62,18 +82,21 @@ subtest only_scram
set_hba
host all foo all scram-sha-256
host all abc all scram-sha-256
+host all abc2 all scram-sha-256
----
# Active authentication configuration on this node:
# Original configuration:
# host all root all cert-password # CockroachDB mandatory rule
# host all foo all scram-sha-256
# host all abc all scram-sha-256
+# host all abc2 all scram-sha-256
#
# Interpreted configuration:
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
host all root all cert-password
host all foo all scram-sha-256
host all abc all scram-sha-256
+host all abc2 all scram-sha-256
subtest only_scram/conn_scram
@@ -85,13 +108,13 @@ ERROR: password authentication failed for user foo (SQLSTATE 28000)
authlog 7
.*client_connection_end
----
-15 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-16 {"EventType":"client_authentication_info","Info":"HBA rule: host all foo all scram-sha-256","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl"}
-17 {"EventType":"client_authentication_info","Info":"user password hash not in SCRAM format","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl","User":"foo"}
-18 {"EventType":"client_authentication_info","Info":"scram handshake error: user password hash not in SCRAM format","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl","User":"foo"}
-19 {"Detail":"password authentication failed for user foo","EventType":"client_authentication_failed","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl","User":"foo"}
-20 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-21 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+20 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+21 {"EventType":"client_authentication_info","Info":"HBA rule: host all foo all scram-sha-256","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl"}
+22 {"EventType":"client_authentication_info","Info":"user password hash not in SCRAM format","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl","User":"foo"}
+23 {"EventType":"client_authentication_info","Info":"scram handshake error: user password hash not in SCRAM format","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl","User":"foo"}
+24 {"Detail":"password authentication failed for user foo","EventType":"client_authentication_failed","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl","User":"foo"}
+25 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+26 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
# User abc has SCRAM credentials, but 'mistake' is not its password.
# Expect authn error.
@@ -102,12 +125,12 @@ ERROR: password authentication failed for user abc (SQLSTATE 28000)
authlog 6
.*client_connection_end
----
-22 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-23 {"EventType":"client_authentication_info","Info":"HBA rule: host all abc all scram-sha-256","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl"}
-24 {"EventType":"client_authentication_info","Info":"scram handshake error: challenge proof invalid","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl","User":"abc"}
-25 {"Detail":"password authentication failed for user abc","EventType":"client_authentication_failed","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl","User":"abc"}
-26 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-27 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+27 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+28 {"EventType":"client_authentication_info","Info":"HBA rule: host all abc all scram-sha-256","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl"}
+29 {"EventType":"client_authentication_info","Info":"scram handshake error: challenge proof invalid","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl","User":"abc"}
+30 {"Detail":"password authentication failed for user abc","EventType":"client_authentication_failed","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl","User":"abc"}
+31 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+32 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
connect user=abc password=abc
@@ -117,11 +140,24 @@ ok defaultdb
authlog 5
.*client_connection_end
----
-28 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-29 {"EventType":"client_authentication_info","Info":"HBA rule: host all abc all scram-sha-256","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl"}
-30 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl","User":"abc"}
-31 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-32 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+33 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+34 {"EventType":"client_authentication_info","Info":"HBA rule: host all abc all scram-sha-256","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl"}
+35 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl","User":"abc"}
+36 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+37 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+
+connect user=abc2 password=abc
+----
+ok defaultdb
+
+authlog 5
+.*client_connection_end
+----
+38 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+39 {"EventType":"client_authentication_info","Info":"HBA rule: host all abc2 all scram-sha-256","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc2","Timestamp":"XXX","Transport":"hostssl"}
+40 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc2","Timestamp":"XXX","Transport":"hostssl","User":"abc2"}
+41 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+42 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
subtest end
@@ -164,6 +200,10 @@ connect user=abc password=abc
----
ok defaultdb
+connect user=abc2 password=abc
+----
+ok defaultdb
+
subtest end
subtest end
diff --git a/pkg/sql/unsupported_vars.go b/pkg/sql/unsupported_vars.go
index 4bcbc4d60dac..f2325c593bdd 100644
--- a/pkg/sql/unsupported_vars.go
+++ b/pkg/sql/unsupported_vars.go
@@ -148,7 +148,7 @@ var UnsupportedVars = func(ss ...string) map[string]struct{} {
"operator_precedence_warning",
"parallel_setup_cost",
"parallel_tuple_cost",
- "password_encryption",
+ // "password_encryption",
"quote_all_identifiers",
"random_page_cost",
"replacement_sort_tuples",
diff --git a/pkg/sql/vars.go b/pkg/sql/vars.go
index 0a02765e6474..048690076b54 100644
--- a/pkg/sql/vars.go
+++ b/pkg/sql/vars.go
@@ -1262,6 +1262,15 @@ var varGen = map[string]sessionVar{
},
},
+ // See https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-PASSWORD-ENCRYPTION
+ // We only support reading this setting in clients: it is not desirable to let clients choose
+ // their own password hash algorithm.
+ `password_encryption`: {
+ Get: func(evalCtx *extendedEvalContext) (string, error) {
+ return security.GetConfiguredPasswordHashMethod(evalCtx.Ctx(), &evalCtx.Settings.SV).String(), nil
+ },
+ },
+
// Supported for PG compatibility only.
// See https://www.postgresql.org/docs/10/static/runtime-config-compatible.html#GUC-STANDARD-CONFORMING-STRINGS
// If this gets properly implemented, we will need to re-evaluate how escape_string_warning is implemented