Skip to content

Commit

Permalink
cli: allow SQL commands to use password authn in more cases
Browse files Browse the repository at this point in the history
Previously, SQL password authn was only allowed over TLS connections.

With this change, password authn is allowed if either TLS is used, or
the flag `--allow-unencrypted-passwords` is passed on the command
line. (i.e. the user tells us they understand the password is passed
in cleartext)

When CockroachDB is extended to support SCRAM auth in the future,
it is expected that this logic will be further relaxed to accept
SCRAM auth without the command-line flag.

This is implemented by also only asking for a password interactively
the first time that the server complains that pw auth has failed. This
way, no password is ever requested interactively if the server
"trusts" the connection (via HBA rules or `--insecure`).

Release justification: low risk, high benefit changes to existing functionality

Release note (cli change): It is now possible to use password
authentication over non-TLS connections with `cockroach` client CLI
commands that use only SQL, e.g. `cockroach sql` or `cockroach node
ls`, as long as the flag `--allow-unencrypted-passwords` is passed
explicitly. Without the flag, password authentication still requires a
TLS connecction.
  • Loading branch information
knz committed Sep 8, 2020
1 parent 6b08822 commit 031ac82
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 28 deletions.
9 changes: 7 additions & 2 deletions pkg/cli/client_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,13 @@ func (u urlParser) setInternal(v string, warn bool) error {

switch sslMode := options.Get("sslmode"); sslMode {
case "", "disable":
if err := fl.Set(cliflags.ClientInsecure.Name, "true"); err != nil {
return errors.Wrapf(err, "setting insecure connection based on --url")
if u.sslStrict {
// For "strict" mode (RPC client commands) we don't support non-TLS
// yet. See https://github.com/cockroachdb/cockroach/issues/54007
// Instead, we see a request for no TLS to imply insecure mode.
if err := fl.Set(cliflags.ClientInsecure.Name, "true"); err != nil {
return errors.Wrapf(err, "setting secure connection based on --url")
}
}
case "require", "verify-ca", "verify-full":
if sslMode != "verify-full" && u.sslStrict {
Expand Down
14 changes: 14 additions & 0 deletions pkg/cli/cliflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,20 @@ apply. This flag is experimental.
`,
}

AllowUnencryptedClientPassword = FlagInfo{
Name: "allow-unencrypted-passwords",
Description: `
When this flag is specified, the CLI client command becomes able
to authentify to a server using passwords, even when the connection
is not protected by TLS. Passwords can be sent in cleartext
to the server. To ensure good security, the system administrator
must provide network-level confidentiality by other means.
It is not safe to use this option when connecting over the internet
or a public network.
`,
}

LocalityAdvertiseAddr = FlagInfo{
Name: "locality-advertise-addr",
Description: `
Expand Down
9 changes: 9 additions & 0 deletions pkg/cli/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ type cliContext struct {
// extraConnURLOptions contains any additional query URL options
// specified in --url that do not have discrete equivalents.
extraConnURLOptions url.Values

// allowUnencryptedClientPassword enables the CLI commands to use
// password authentication over non-TLS TCP connections. This is
// disallowed by default: the user must opt-in and understand that
// CockroachDB does not guarantee confidentiality of a password
// provided this way.
// TODO(knz): Relax this when SCRAM is implemented.
allowUnencryptedClientPassword bool
}

// cliCtx captures the command-line parameters common to most CLI utilities.
Expand Down Expand Up @@ -184,6 +192,7 @@ func setCliContextDefaults() {
cliCtx.sqlConnPasswd = ""
cliCtx.sqlConnDBName = ""
cliCtx.extraConnURLOptions = nil
cliCtx.allowUnencryptedClientPassword = false
}

// sqlCtx captures the command-line parameters of the `sql` command.
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ func init() {
stringFlag(f, &cliCtx.clientConnPort, cliflags.ClientPort)
_ = f.MarkHidden(cliflags.ClientPort.Name)

boolFlag(f, &cliCtx.allowUnencryptedClientPassword, cliflags.AllowUnencryptedClientPassword)
}

if cmd == sqlShellCmd {
Expand Down
33 changes: 25 additions & 8 deletions pkg/cli/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,12 @@ func TestClientURLFlagEquivalence(t *testing.T) {
{anyNonSQL, []string{"--url=postgresql://b:12345"}, []string{"--host=b", "--port=12345"}, "", ""},
{anyNonSQL, []string{"--url=postgresql://b:c"}, nil, `invalid port ":c" after host`, ""},

{anyCmd, []string{"--url=postgresql://foo?application_name=abc"}, []string{"--host=foo", "--insecure"}, "", ""},
{anyCmd, []string{"--url=postgresql://foo?sslmode=disable"}, []string{"--host=foo", "--insecure"}, "", ""},
{anyNonSQL, []string{"--url=postgresql://foo?application_name=abc"}, []string{"--host=foo", "--insecure"}, "", ""},
{anySQL, []string{"--url=postgresql://foo?application_name=abc"}, []string{"--host=foo"}, "", ""},

{anyNonSQL, []string{"--url=postgresql://foo?sslmode=disable"}, []string{"--host=foo", "--insecure"}, "", ""},
{anySQL, []string{"--url=postgresql://foo?sslmode=disable"}, []string{"--host=foo"}, "", ""},

{anySQL, []string{"--url=postgresql://foo?sslmode=require"}, []string{"--host=foo", "--insecure=false"}, "", ""},
{anyNonSQL, []string{"--url=postgresql://foo?sslmode=require"}, nil, "command .* only supports sslmode=disable or sslmode=verify-full", ""},
{anyCmd, []string{"--url=postgresql://foo?sslmode=verify-full"}, []string{"--host=foo", "--insecure=false"}, "", ""},
Expand All @@ -287,17 +291,22 @@ func TestClientURLFlagEquivalence(t *testing.T) {
{anyCmd, []string{"--port=baz", "--url=postgresql://foo"}, []string{"--host=foo", "--port=baz"}, "", `invalid port ":baz" after host`},
{sqlShell, []string{"--database=baz", "--url=postgresql://foo"}, []string{"--host=foo", "--database=baz"}, "", ""},
{anySQL, []string{"--user=baz", "--url=postgresql://foo"}, []string{"--host=foo", "--user=baz"}, "", ""},

{anyCmd, []string{"--insecure=false", "--url=postgresql://foo"}, []string{"--host=foo", "--insecure=false"}, "", ""},
{anyCmd, []string{"--insecure", "--url=postgresql://foo"}, []string{"--host=foo", "--insecure"}, "", ""},
// Only non-SQL lets --insecure bleed into a URL that does not specify sslmode.
{anyNonSQL, []string{"--insecure", "--url=postgresql://foo"}, []string{"--host=foo", "--insecure"}, "", ""},

// URL overrides previous flags if component specified.
{anyCmd, []string{"--host=baz", "--url=postgresql://bar"}, []string{"--host=bar"}, "", ""},
{anyCmd, []string{"--port=baz", "--url=postgresql://foo:12345"}, []string{"--host=foo", "--port=12345"}, "", ""},
{anyCmd, []string{"--port=baz", "--url=postgresql://foo:bar"}, nil, `invalid port ":bar" after host`, ""},
{sqlShell, []string{"--database=baz", "--url=postgresql://foo/bar"}, []string{"--host=foo", "--database=bar"}, "", ""},
{anySQL, []string{"--user=baz", "--url=postgresql://bar@foo"}, []string{"--host=foo", "--user=bar"}, "", ""},
{anyCmd, []string{"--insecure=false", "--url=postgresql://foo?sslmode=disable"}, []string{"--host=foo", "--insecure"}, "", ""},

{anyNonSQL, []string{"--insecure=false", "--url=postgresql://foo?sslmode=disable"}, []string{"--host=foo", "--insecure"}, "", ""},
{anyCmd, []string{"--insecure", "--url=postgresql://foo?sslmode=verify-full"}, []string{"--host=foo", "--insecure=false"}, "", ""},
// SQL is special case: specifying sslmode= does not imply insecure mode. So the insecure bit does not get reset.
{anySQL, []string{"--insecure=false", "--url=postgresql://foo?sslmode=disable"}, []string{"--host=foo"}, "", ""},

// Discrete flag overrides URL if specified afterwards.
{anyCmd, []string{"--url=postgresql://bar", "--host=baz"}, []string{"--host=baz"}, "", ""},
Expand All @@ -306,8 +315,9 @@ func TestClientURLFlagEquivalence(t *testing.T) {
{anyCmd, []string{"--url=postgresql://foo:bar", "--port=baz"}, nil, `invalid port ":bar" after host`, ""},
{sqlShell, []string{"--url=postgresql://foo/bar", "--database=baz"}, []string{"--host=foo", "--database=baz"}, "", ""},
{anySQL, []string{"--url=postgresql://bar@foo", "--user=baz"}, []string{"--host=foo", "--user=baz"}, "", ""},
{anyCmd, []string{"--url=postgresql://foo?sslmode=disable", "--insecure=false"}, []string{"--host=foo", "--insecure=false"}, "", ""},
{anyCmd, []string{"--url=postgresql://foo?sslmode=verify-full", "--insecure"}, []string{"--host=foo", "--insecure"}, "", ""},

{anyNonSQL, []string{"--url=postgresql://foo?sslmode=disable", "--insecure=false"}, []string{"--host=foo", "--insecure=false"}, "", ""},
{anyNonSQL, []string{"--url=postgresql://foo?sslmode=verify-full", "--insecure"}, []string{"--host=foo", "--insecure"}, "", ""},

// Check that the certs dir is extracted properly.
{anyNonSQL, []string{"--url=postgresql://foo?sslmode=verify-full&sslrootcert=" + testCertsDirPath + "/ca.crt"}, []string{"--host=foo", "--certs-dir=" + testCertsDirPath}, "", ""},
Expand Down Expand Up @@ -346,9 +356,9 @@ func TestClientURLFlagEquivalence(t *testing.T) {
}
}

for _, test := range testData {
for testNum, test := range testData {
for _, cmdName := range test.cmds {
t.Run(fmt.Sprintf("%s/%s", cmdName, strings.Join(test.flags, " ")), func(t *testing.T) {
t.Run(fmt.Sprintf("%d/%s/%s", testNum+1, cmdName, strings.Join(test.flags, " ")), func(t *testing.T) {
cmd, _, _ := cockroachCmd.Find([]string{cmdName})

// Parse using the URL.
Expand Down Expand Up @@ -388,6 +398,13 @@ func TestClientURLFlagEquivalence(t *testing.T) {
t.Fatalf("mismatch: URL %q parses\n%+v,\ndiscrete parses\n%+v", resultURL, urlParams, discreteParams)
}

// For SQL commands only, test that reconstructing the URL
// from discrete flags yield equivalent connection parameters.
// (RPC commands never reconstruct a URL.)
if cmdName != anySQL[0] && cmdName != anySQL[1] {
return
}

// Re-parse using the derived URL.
// We'll want to ensure below that the derived URL specifies the same parameters
// (i.e. check makeClientConnURL does its work properly).
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/interactive_tests/test_url_login.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ stop_server $argv

start_test "Check that the unix socket can be used simply."

# The following connection works even if `--insecure` is not passed on the command line.
set ::env(COCKROACH_INSECURE) "false"

# Start a server with a socket listener.
set mywd [pwd]

Expand Down
113 changes: 96 additions & 17 deletions pkg/cli/sql_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ type sqlConn struct {
conn sqlConnI
reconnecting bool

// passwordMissing is true iff the url is missing a password.
passwordMissing bool

// canPassword is true iff password authentication is allowed.
// This is set to false if not using the unix socket, the connection
// is not encrypted, and --allow-unencrypted-password is not set.
canPassword bool

pendingNotices []*pq.Error

// delayNotices, if set, makes notices accumulate for printing
Expand Down Expand Up @@ -152,6 +160,26 @@ func (c *sqlConn) ensureConn() error {
// connections only once instead. The context is only used for dialing.
conn, err := connector.Connect(context.TODO())
if err != nil {
// Connection failed: if the failure is due to a mispresented
// password, we're going to fill the password here.
//
// TODO(knz): CockroachDB servers do not properly fill SQLSTATE
// (28P01) for password auth errors, so we have to "make do"
// with a string match. This should be cleaned up by adding
// the missing code server-side.
errStr := strings.TrimPrefix(err.Error(), "pq: ")
if strings.HasPrefix(errStr, "password authentication failed") && c.passwordMissing {
if pErr := c.fillPassword(); pErr != nil {
return errors.CombineErrors(err, pErr)
}
// Recurse, once. We recurse to ensure that pq.NewConnector
// and ConnectorWithNoticeHandler get called with the new URL.
// The recursion only occurs once because fillPassword()
// resets c.passwordMissing, so we cannot get into this
// conditional a second time.
return c.ensureConn()
}
// Not a password auth error, or password already set. Simply fail.
return wrapConnError(err)
}
if c.reconnecting && c.dbName != "" {
Expand Down Expand Up @@ -665,23 +693,41 @@ func makeSQLClient(appName string, defaultMode defaultSQLDb) (*sqlConn, error) {
return nil, err
}

// Insecure connections are insecure and should never see a password. Reject
// one that may be present in the URL already.
if options.Get("sslmode") == "disable" {
if _, pwdSet := baseURL.User.Password(); pwdSet {
return nil, errors.Errorf("cannot specify a password in URL with an insecure connection")
// tcpConn is true iff the connection is going over the network.
tcpConn := baseURL.Host != ""

// If there is no TLS mode yet, conjure one based on defaults.
if options.Get("sslmode") == "" {
if cliCtx.Insecure {
options.Set("sslmode", "disable")
} else if tcpConn {
options.Set("sslmode", "verify-full")
}
} else {
if options.Get("sslcert") == "" || options.Get("sslkey") == "" {
// If there's no password in the URL yet and we don't have a client
// certificate, ask for it and populate it in the URL.
if _, pwdSet := baseURL.User.Password(); !pwdSet {
pwd, err := security.PromptForPassword()
if err != nil {
return nil, err
}
baseURL.User = url.UserPassword(baseURL.User.Username(), pwd)
}
// (We don't use TLS over unix socket conns.)
}

// Prevent explicit TLS request in insecure mode.
if cliCtx.Insecure && options.Get("sslmode") != "disable" {
return nil, errors.Errorf("cannot use TLS connections in insecure mode")
}

// How we're going to authenticate.
// In non-TLS connections, we don't accept a password unless the
// user tells us that they know what they are doing via
// --allow-unencrypted-password.
canPassword := (options.Get("sslmode") != "disable") || !tcpConn || cliCtx.allowUnencryptedClientPassword
_, pwdSet := baseURL.User.Password()
if pwdSet {
// There's a password already configured.

// In insecure mode, we don't want the user to get the mistaken
// idea that a password is worth anything.
if cliCtx.Insecure {
return nil, errors.Errorf("password authentication not enabled in insecure mode")
}
// Otherwise, check if password auth is allowed for this connection.
if !canPassword {
return nil, errUnencryptedPassword
}
}

Expand All @@ -707,9 +753,42 @@ func makeSQLClient(appName string, defaultMode defaultSQLDb) (*sqlConn, error) {
log.Infof(context.Background(), "connecting with URL: %s", sqlURL)
}

return makeSQLConn(sqlURL), nil
conn := makeSQLConn(sqlURL)

conn.canPassword = canPassword
conn.passwordMissing = !pwdSet

return conn, nil
}

// fillPassword is called the first time the server complains that the
// password authentication has failed, if no password was supplied to
// start with. It asks the user for a password interactively.
func (c *sqlConn) fillPassword() error {
connURL, err := url.Parse(c.url)
if err != nil {
return err
}

// Check first if password auth is allowed for this connection.
if !c.canPassword {
return errUnencryptedPassword
}

// Password can be safely encrypted, or the user opted in
// manually to non-encryption. All good.
pwd, err := security.PromptForPassword()
if err != nil {
return err
}
connURL.User = url.UserPassword(connURL.User.Username(), pwd)
c.url = connURL.String()
c.passwordMissing = false
return nil
}

var errUnencryptedPassword = errors.Errorf("password authentication without TLS is unsafe; combine with --allow-unencrypted-passwords to confirm")

type queryFunc func(conn *sqlConn) (rows *sqlRows, isMultiStatementQuery bool, err error)

func makeQuery(query string, parameters ...driver.Value) queryFunc {
Expand Down
10 changes: 9 additions & 1 deletion pkg/rpc/pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ func (ctx *SecurityContext) LoadSecurityOptions(options url.Values, username str
options.Del("sslkey")
} else {
sslMode := options.Get("sslmode")
if sslMode == "" || sslMode == "disable" {
if sslMode == "disable" {
// TLS explicitly disabled by client. Nothing to do here.
options.Del("sslrootcert")
options.Del("sslcert")
options.Del("sslkey")
return nil
}
// Default is to verify the server's identity.
if sslMode == "" {
options.Set("sslmode", "verify-full")
}

Expand Down

0 comments on commit 031ac82

Please sign in to comment.