Skip to content

Commit

Permalink
pgwire: support the SCRAM authentication flow
Browse files Browse the repository at this point in the history
Release note (security update): CockroachDB now supports the
SCRAM-SHA-256 authentication method for SQL clients in a way
compatible with PostgreSQL servers and most PostgreSQL-compatible
client drivers.

SCRAM is a standard authentication protocol defined by IETF RFCs
[5802](https://datatracker.ietf.org/doc/html/rfc5802) and
[7677](https://datatracker.ietf.org/doc/html/rfc7677). In contrast to
the cleartext-based mechanism that CockroachDB was previously using,
SCRAM offers:

- computational burden moved to the client: the computational
  cost to compute the authentication hash is borne by the
  client, and thus prevents DoS attacks enabled by forcing the
  server to compute many hashes simultaneously by malicious clients.
- non-replayability: a malicious intermediary cannot reuse
  a password observed in one session to re-gain access later.
- protection against credential stuffing: a malicious
  intermediary cannot reuse a password to gain access to other services.
- credential secrecy: the server never learns the cleartext
  password and cannot impersonate the client to other servers.

As before, the SCRAM credentials are stored on the server in hashed
form, and prevent a malicious attacker from gaining knowledge about
passwords even if they gain access to a copy of the hashes.

To use SCRAM, an operator must take care of the following:

1. the stored credentials for the SQL accounts that want to use SCRAM
   must use the SCRAM password hash format.

   To store SCRAM hashes in CockroachDB, at this time it is necessary
   to pre-compute the SCRAM hash in a SQL client and store it
   pre-hashed using a CREATE/ALTER USER/ROLE WITH PASSWORD statement.

   This was documented in a previous release note already.

   A mechanism to compute the SCRAM hash server-side from a cleartext
   password might be provided at a later date. Note however that such
   a mechanism is generally undesirable: one of the main benefits of
   SCRAM is to remove the need for the server to know the client's
   cleartext password at any time; a SCRAM hash generation server-side
   would defeat this benefit.

   A plan also exists to auto-migrate existing passwords to the new
   format (refer to a later release note).

2. the SCRAM authentication method must be enabled.

   This can be done e.g. explicitly to require SCRAM specifically
   via a HBA configuration via the cluster setting
   `server.host_based_authentication.configuration`.

   For this, two new authentication methods are available:
   `scram-sha-256` `certs-scram-sha-256`. The first one is akin to
   PostgreSQL and requires a SCRAM authentication flow with the
   client. The second one is CockroachDB-specific and allows SQL
   client to authenticate *either* using a TLS client certificate *or*
   a valid SCRAM authentication flow.

   For example, the configuration line `host all all all
   scram-sha-256` will require a SCRAM authentication flow for all
   clients besides the `root` user.

   A plan also exists to automatically opt existing clusters
   into SCRAM (refer to a later release note).

Known limitations:

- HTTP authentication (web UI, HTTP APIs) still uses cleartext
  passwords.

  Security there can be enhanced in two ways:
  - enable and use OIDC authentication for the web UI.
  - use separate user accounts for access to HTTP than those
    used for access to SQL.

- the CockroachDB implementation of SCRAM differs from PostgreSQL in
  two ways:

  - the extended protocol SCRAM-SHA-256-PLUS is not yet supported.
    SCRAM-SHA-256-PLUS adds *channel binding* over TLS, a mechanism that
    offers MITM protection from malicious intermediaries even when these
    have access to well-signed TLS certificates. Without this extension,
    proper MITM protection requires the client to verify the server
    certificate against a known CA and server fingerprint.

    CockroachDB does not yet support SCRAM-SHA-256-PLUS because we
    have observed that support for channel binding is not yet common
    in SQL client drivers besides PostgreSQL's own `libpq` driver.

  - CockroachDB does not yet implement zero-knowledge authentication
    failures like PostgreSQL.  In PostgreSQL, the implementation ensures
    that a SQL client cannot distinguish the causes of an authentication
    failure: whether a password is missing, expired, invalid, or the
    user account does not exist, the SQL client is forced to pay the
    price of the SCRAM handshake and does not learn the exact cause of
    the failure. This ensures that a malicious attacker cannot use the
    type of authentication failure as a mechanism to learn properties of
    a target SQL account.

    This mechanism may be implemented in CockroachDB at a later time.
  • Loading branch information
knz committed Jan 20, 2022
1 parent 6071d6f commit ddddd35
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 51 deletions.
10 changes: 10 additions & 0 deletions pkg/security/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ func (s *scramHash) Size() int {
return int(unsafe.Sizeof(*s)) + len(s.bytes) + len(s.decoded.Salt) + len(s.decoded.StoredKey) + len(s.decoded.ServerKey)
}

// GetSCRAMStoredCredentials retrieves the SCRAM credential parts.
// The caller is responsible for ensuring the hash has method SCRAM-SHA-256.
func GetSCRAMStoredCredentials(hash PasswordHash) (ok bool, creds scram.StoredCredentials) {
h, ok := hash.(*scramHash)
if ok {
return ok, h.decoded
}
return false, creds
}

// LoadPasswordHash decodes a password hash loaded as bytes from a credential store.
func LoadPasswordHash(ctx context.Context, storedHash []byte) (res PasswordHash) {
res = invalidHash(storedHash)
Expand Down
90 changes: 56 additions & 34 deletions pkg/sql/pgwire/auth_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"math"

Expand Down Expand Up @@ -195,37 +194,10 @@ func authScram(
clientConnection bool,
pwRetrieveFn PasswordRetrievalFn,
) error {
// This scram auth credential was generated on pg with:
// create user abc with password 'abc'
// const testPassword = "SCRAM-SHA-256$4096:pAlYy62NTdETKb291V/Wow==$OXMAj9oD53QucEYVMBcdhRnjg2/S/iZY/88ShZnputA=:r8l4c1pk9bmDi+8an059l/nt9Bg1zb1ikkg+DeRv4UQ="
salt, err := base64.StdEncoding.DecodeString("pAlYy62NTdETKb291V/Wow==")
if err != nil {
panic(err)
}
storedKey, err := base64.StdEncoding.DecodeString("OXMAj9oD53QucEYVMBcdhRnjg2/S/iZY/88ShZnputA=")
if err != nil {
panic(err)
}
serverKey, err := base64.StdEncoding.DecodeString("r8l4c1pk9bmDi+8an059l/nt9Bg1zb1ikkg+DeRv4UQ=")
if err != nil {
panic(err)
}
scramServer, _ := scram.SHA256.NewServer(func(user string) (scram.StoredCredentials, error) {
if user == "abc" {
return scram.StoredCredentials{
KeyFactors: scram.KeyFactors{
Salt: string(salt),
Iters: 4096,
},
StoredKey: storedKey,
ServerKey: serverKey,
}, nil
}
return scram.StoredCredentials{}, errors.New("no scram cookie for you")
})

handshake := scramServer.NewConversation()

// First step: send a SCRAM authentication request to the client.
// We do this with an auth request with the request type SASL,
// and a payload containing the list of supported SCRAM methods.
//
// NB: SCRAM-SHA-256-PLUS is not supported, see
// https://github.com/cockroachdb/cockroach/issues/74300
// There is one nul byte to terminate the first string,
Expand All @@ -235,6 +207,44 @@ func authScram(
return err
}

// While waiting for the client response, concurrently
// load the credentials from storage (or cache).
// Note: if this fails, we can't return the error right away,
// because we need to consume the client response first. This
// will be handled below.
expired, hashedPassword, pwRetrievalErr := pwRetrieveFn(ctx)

scramServer, _ := scram.SHA256.NewServer(func(user string) (creds scram.StoredCredentials, err error) {
// NB: the username passed in the SCRAM exchange (the user
// parameter in this callback) is ignored by PostgreSQL servers;
// see auth-scram.c, read_client_first_message().
//
// Therefore, we can't assume that SQL client drivers populate anything
// useful there. So we ignore it too.

// We still need to check whether the credentials loaded above
// are valid. We place this check in this callback because it
// only needs to happen after the SCRAM handshake actually needs
// to know the credentials.
if expired {
c.LogAuthFailed(ctx, eventpb.AuthFailReason_CREDENTIALS_EXPIRED, nil)
return creds, errExpiredPassword
} else if hashedPassword.Method() != security.HashSCRAMSHA256 {
const credentialsNotSCRAM = "user password hash not in SCRAM format"
c.LogAuthInfof(ctx, credentialsNotSCRAM)
return creds, errors.New(credentialsNotSCRAM)
}

// The method check above ensures this cast is always valid.
ok, creds := security.GetSCRAMStoredCredentials(hashedPassword)
if !ok {
return creds, errors.AssertionFailedf("programming error: hash method is SCRAM but no stored credentials")
}
return creds, nil
})

handshake := scramServer.NewConversation()

initial := true
for {
if handshake.Done() {
Expand All @@ -244,8 +254,18 @@ func authScram(
// Receive a response from the client.
resp, err := c.GetPwdData()
if err != nil {
c.LogAuthFailed(ctx, eventpb.AuthFailReason_PRE_HOOK_ERROR, err)
if pwRetrievalErr != nil {
return errors.CombineErrors(err, pwRetrievalErr)
}
return err
}
// Now process the password retrieval error, if any.
if pwRetrievalErr != nil {
c.LogAuthFailed(ctx, eventpb.AuthFailReason_USER_RETRIEVAL_ERROR, pwRetrievalErr)
return pwRetrievalErr
}

var input []byte
if initial {
// Quoth postgres, backend/auth.go:
Expand All @@ -258,6 +278,7 @@ func authScram(
rb := pgwirebase.ReadBuffer{Msg: resp}
reqMethod, err := rb.GetString()
if err != nil {
c.LogAuthFailed(ctx, eventpb.AuthFailReason_PRE_HOOK_ERROR, err)
return err
}
if reqMethod != "SCRAM-SHA-256" {
Expand All @@ -271,13 +292,15 @@ func authScram(
}
inputLen, err := rb.GetUint32()
if err != nil {
c.LogAuthFailed(ctx, eventpb.AuthFailReason_PRE_HOOK_ERROR, err)
return err
}
// PostgreSQL ignores input from clients that pass -1 as length,
// but does not treat it as invalid.
if inputLen < math.MaxUint32 {
input, err = rb.GetBytes(int(inputLen))
if err != nil {
c.LogAuthFailed(ctx, eventpb.AuthFailReason_PRE_HOOK_ERROR, err)
return err
}
}
Expand All @@ -304,12 +327,11 @@ func authScram(
}
}

// Was the authentication for the right username?

// Did authentication succeed?
if !handshake.Valid() {
return security.NewErrPasswordUserAuthFailed(systemIdentity)
}

return nil // auth success!
})
return b, nil
Expand Down
35 changes: 18 additions & 17 deletions pkg/sql/pgwire/testdata/auth/scram
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ config secure

sql
CREATE USER foo WITH PASSWORD 'abc';
CREATE USER abc WITH PASSWORD 'abc';
CREATE USER abc WITH PASSWORD 'SCRAM-SHA-256$4096:pAlYy62NTdETKb291V/Wow==$OXMAj9oD53QucEYVMBcdhRnjg2/S/iZY/88ShZnputA=:r8l4c1pk9bmDi+8an059l/nt9Bg1zb1ikkg+DeRv4UQ=';
----
ok

Expand Down Expand Up @@ -82,15 +82,16 @@ connect user=foo password=abc
----
ERROR: password authentication failed for user foo (SQLSTATE 28000)

authlog 6
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":"scram handshake error: no scram cookie for you","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"foo","Timestamp":"XXX","Transport":"hostssl","User":"foo"}
18 {"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"}
19 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
20 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
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"}

# User abc has SCRAM credentials, but 'mistake' is not its password.
# Expect authn error.
Expand All @@ -101,12 +102,12 @@ ERROR: password authentication failed for user abc (SQLSTATE 28000)
authlog 6
.*client_connection_end
----
21 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
22 {"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"}
23 {"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"}
24 {"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"}
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"}
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"}


connect user=abc password=abc
Expand All @@ -116,11 +117,11 @@ ok defaultdb
authlog 5
.*client_connection_end
----
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_ok","InstanceID":1,"Method":"scram-sha-256","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"abc","Timestamp":"XXX","Transport":"hostssl","User":"abc"}
30 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
31 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
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"}

subtest end

Expand Down

0 comments on commit ddddd35

Please sign in to comment.