Skip to content

Commit

Permalink
pgwire: Add dynamic user identity mapping
Browse files Browse the repository at this point in the history
This commit adds support for mapping incoming system identities (e.g.: GSSAPI
or X.509 principals) to database usernames. The implementation follows in the
same pattern as the HBA configuration. Namely, there is a new cluster setting
server.identity_map.configuration which contains data compatible with the
pg_ident.conf file.

When a new connection is made, the relevant HBA configuration line is selected
and the "map" option is used to select an identity-map ruleset. The system
identity is mapped to a database username and authentication proceeds using the
HBA entry method.

CockroachDB explicitly disallows the use of the "map" entry with the "password"
HBA method. When using the "cert-password" method, an identity mapping is
applied only for incoming connections that present a client certificate;
password-based connections will not be subject to any identity remapping.

The root user is never subject to identity remapping and cannot be locked out
by a bad identity-map configuration. The effective HBA configuration always has
a system-defined rule that matches the root user as the first rule. This
baked-in rule does not contain a "map" option, thus bypassing the mapper code
entirely.

Users must already have access to the ADMIN role in order to change cluster
settings, so this change does not, in and of itself, present
privilege-escalation concerns. Futhermore, if the identity mapping results in
the "root", "node", or other reserved usernames, an error will be returned to
the client.

For pedantry's sake, the term "identity" is preferred over "username" within
the code, since not all identities are necessarily what would be considered
usernames.

Fixes: #47196

See also: https://www.postgresql.org/docs/13/auth-username-maps.html

Release note (security update): The server.identity_map.configuration cluster
setting allows a pg_ident.conf file to be uploaded to support dynamically
remapping system identities (e.g.: Kerberos or X.509 principals) to database
usernames. This supports use-cases where X.509 certificates must conform to
organizational standards that mandate the use of Common Names that are not
valid SQL usernames (e.g.: CN=carl@example.com => carl). Mapping rules that
result in the root, node, or other reserved usernames will result in an error
when the client attempts to connect.

Release note (security update): The client_authentication_info structured log
message provides a new "SystemIdentity" field with the client-provided system
identity. The existing "User" field will be populated after any Host-Based
Authentication rules have been selected and applied, which may include a
system-identity to database-username mapping.

Release note (security update): GSSAPI-based authentication can now use either
the HBA "map" option or "include_realm=0" to map the incoming princpal to a
database username. Existing configurations will operate unchanged, however
operators are encouraged to migrate from "include_realm=0" to "map" to avoid
ambiguity in deployments where multiple realms are present.

Release note (security update): Incoming system identities are normalized to
lower-case before they are evaluated against any active identity-mapping HBA
configuration. For example, an incoming GSSAPI principal "carl@EXAMPLE.COM"
would only be matched by rules such as "example carl@example.com carl" or
"example /^(.*)@example.com$ \1".
  • Loading branch information
bobvawter committed Jan 5, 2022
1 parent a3ea612 commit 26fd1f6
Show file tree
Hide file tree
Showing 29 changed files with 1,691 additions and 369 deletions.
12 changes: 8 additions & 4 deletions docs/generated/eventlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -1740,7 +1740,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |

### `client_authentication_info`

Expand All @@ -1767,7 +1768,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |

### `client_authentication_ok`

Expand All @@ -1793,7 +1795,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |

### `client_connection_end`

Expand Down Expand Up @@ -1866,7 +1869,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |

## SQL Slow Query Log

Expand Down
1 change: 1 addition & 0 deletions docs/generated/settings/settings-for-tenants.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ server.consistency_check.max_rate byte size 8.0 MiB the rate limit (bytes/sec) t
server.eventlog.enabled boolean true if set, logged notable events are also stored in the table system.eventlog
server.eventlog.ttl duration 2160h0m0s if nonzero, entries in system.eventlog older than this duration are deleted every 10m0s. Should not be lowered below 24 hours.
server.host_based_authentication.configuration string host-based authentication configuration to use during connection authentication
server.identity_map.configuration string system-identity to database-username mappings
server.oidc_authentication.autologin boolean false if true, logged-out visitors to the DB Console will be automatically redirected to the OIDC login endpoint (this feature is experimental)
server.oidc_authentication.button_text string Login with your OIDC provider text to show on button on DB Console login page to login with your OIDC provider (only shown if OIDC is enabled) (this feature is experimental)
server.oidc_authentication.claim_json_key string sets JSON key of principal to extract from payload after OIDC authentication completes (usually email or sid) (this feature is experimental)
Expand Down
1 change: 1 addition & 0 deletions docs/generated/settings/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<tr><td><code>server.eventlog.enabled</code></td><td>boolean</td><td><code>true</code></td><td>if set, logged notable events are also stored in the table system.eventlog</td></tr>
<tr><td><code>server.eventlog.ttl</code></td><td>duration</td><td><code>2160h0m0s</code></td><td>if nonzero, entries in system.eventlog older than this duration are deleted every 10m0s. Should not be lowered below 24 hours.</td></tr>
<tr><td><code>server.host_based_authentication.configuration</code></td><td>string</td><td><code></code></td><td>host-based authentication configuration to use during connection authentication</td></tr>
<tr><td><code>server.identity_map.configuration</code></td><td>string</td><td><code></code></td><td>system-identity to database-username mappings</td></tr>
<tr><td><code>server.oidc_authentication.autologin</code></td><td>boolean</td><td><code>false</code></td><td>if true, logged-out visitors to the DB Console will be automatically redirected to the OIDC login endpoint (this feature is experimental)</td></tr>
<tr><td><code>server.oidc_authentication.button_text</code></td><td>string</td><td><code>Login with your OIDC provider</code></td><td>text to show on button on DB Console login page to login with your OIDC provider (only shown if OIDC is enabled) (this feature is experimental)</td></tr>
<tr><td><code>server.oidc_authentication.claim_json_key</code></td><td>string</td><td><code></code></td><td>sets JSON key of principal to extract from payload after OIDC authentication completes (usually email or sid) (this feature is experimental)</td></tr>
Expand Down
1 change: 1 addition & 0 deletions pkg/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ ALL_TESTS = [
"//pkg/sql/opt:opt_test",
"//pkg/sql/parser:parser_test",
"//pkg/sql/pgwire/hba:hba_test",
"//pkg/sql/pgwire/identmap:identmap_test",
"//pkg/sql/pgwire/pgerror:pgerror_test",
"//pkg/sql/pgwire:pgwire_test",
"//pkg/sql/physicalplan/replicaoracle:replicaoracle_test",
Expand Down
43 changes: 42 additions & 1 deletion pkg/acceptance/compose/gss/psql/gss_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func TestGSS(t *testing.T) {
hbaErr string
// Error message of gss login.
gssErr string
// Optionally inject an HBA identity map.
identMap string
}{
{
conf: `host all all all gss include_realm=0 nope=1`,
Expand All @@ -59,9 +61,13 @@ func TestGSS(t *testing.T) {
conf: `host all all all gss include_realm=1`,
hbaErr: `include_realm must be set to 0`,
},
{
conf: `host all all all gss map=ignored include_realm=1`,
hbaErr: `include_realm must be set to 0`,
},
{
conf: `host all all all gss`,
hbaErr: `missing "include_realm=0"`,
hbaErr: `at least one of "include_realm=0" or "map" options required`,
},
{
conf: `host all all all gss include_realm=0`,
Expand Down Expand Up @@ -93,6 +99,36 @@ func TestGSS(t *testing.T) {
user: "tester",
gssErr: `GSS authentication requires an enterprise license`,
},
// Validate that we can use the "map" option to strip the realm
// data. Note that the system-identity value will have been
// normalized into a lower-case value.
{
conf: `host all all all gss map=demo`,
identMap: `demo /^(.*)@my.ex$ \1`,
user: "tester",
gssErr: `GSS authentication requires an enterprise license`,
},
// Verify case-sensitivity.
{
conf: `host all all all gss map=demo`,
identMap: `demo /^(.*)@MY.EX$ \1`,
user: "tester",
gssErr: `system identity "tester@my.ex" did not map to a database role`,
},
// Validating the use of "map" as a filter.
{
conf: `host all all all gss map=demo`,
identMap: `demo /^(.*)@NOPE.EX$ \1`,
user: "tester",
gssErr: `system identity "tester@my.ex" did not map to a database role`,
},
// Check map+include_realm=0 case.
{
conf: `host all all all gss include_realm=0 map=demo`,
identMap: `demo tester remapped`,
user: "remapped",
gssErr: `GSS authentication requires an enterprise license`,
},
}
for i, tc := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
Expand All @@ -102,6 +138,11 @@ func TestGSS(t *testing.T) {
if tc.hbaErr != "" {
return
}
if tc.identMap != "" {
if _, err := db.Exec(`SET CLUSTER SETTING server.identity_map.configuration = $1`, tc.identMap); err != nil {
t.Fatalf("bad identity_map: %v", err)
}
}
if _, err := db.Exec(fmt.Sprintf(`CREATE USER IF NOT EXISTS '%s'`, tc.user)); err != nil {
t.Fatal(err)
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/ccl/gssapiccl/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "gssapiccl",
srcs = select({
"@io_bazel_rules_go//go/platform:linux_amd64": ["gssapi.go"],
"@io_bazel_rules_go//go/platform:linux_amd64": ["gssapi.go", "get_user.go"],
"//conditions:default": ["empty.go"],
}),
cdeps = select({
Expand All @@ -26,6 +26,7 @@ go_library(
"//pkg/sql/sem/tree",
"//pkg/sql/pgwire",
"//pkg/sql/pgwire/hba",
"//pkg/sql/pgwire/identmap",
"@com_github_cockroachdb_errors//:errors",
],
"//conditions:default": [],
Expand Down
128 changes: 128 additions & 0 deletions pkg/ccl/gssapiccl/get_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2021 The Cockroach Authors.
//
// Licensed as a CockroachDB Enterprise file under the Cockroach Community
// License (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt

// See comment on build tag in gssapi.go.

//go:build gss
// +build gss

package gssapiccl

// This file contains the code that calls out to the GSSAPI library
// to retrieve the current user.

import (
"unsafe"

"github.com/cockroachdb/cockroach/pkg/sql/pgwire"
"github.com/cockroachdb/errors"
)

// #cgo LDFLAGS: -lgssapi_krb5 -lcom_err -lkrb5 -lkrb5support -ldl -lk5crypto -lresolv
//
// #include <gssapi/gssapi.h>
// #include <stdlib.h>
import "C"

func getGssUser(c pgwire.AuthConn) (connClose func(), gssUser string, _ error) {
var (
majStat, minStat, lminS, gflags C.OM_uint32
gbuf C.gss_buffer_desc
contextHandle C.gss_ctx_id_t = C.GSS_C_NO_CONTEXT
acceptorCredHandle C.gss_cred_id_t = C.GSS_C_NO_CREDENTIAL
srcName C.gss_name_t
outputToken C.gss_buffer_desc
)

if err := c.SendAuthRequest(authTypeGSS, nil); err != nil {
return nil, "", err
}

// This cleanup function must be called at the
// "completion of a communications session", not
// merely at the end of an authentication init. See
// https://tools.ietf.org/html/rfc2744.html, section
// `1. Introduction`, stage `d`:
//
// At the completion of a communications session (which
// may extend across several transport connections),
// each application calls a GSS-API routine to delete
// the security context.
//
// See https://github.com/postgres/postgres/blob/f4d59369d2ddf0ad7850112752ec42fd115825d4/src/backend/libpq/pqcomm.c#L269
connClose = func() {
C.gss_delete_sec_context(&lminS, &contextHandle, C.GSS_C_NO_BUFFER)
}

for {
token, err := c.GetPwdData()
if err != nil {
return connClose, "", err
}

gbuf.length = C.ulong(len(token))
gbuf.value = C.CBytes([]byte(token))

majStat = C.gss_accept_sec_context(
&minStat,
&contextHandle,
acceptorCredHandle,
&gbuf,
C.GSS_C_NO_CHANNEL_BINDINGS,
&srcName,
nil,
&outputToken,
&gflags,
nil,
nil,
)
C.free(unsafe.Pointer(gbuf.value))

if outputToken.length != 0 {
outputBytes := C.GoBytes(outputToken.value, C.int(outputToken.length))
C.gss_release_buffer(&lminS, &outputToken)
if err := c.SendAuthRequest(authTypeGSSContinue, outputBytes); err != nil {
return connClose, "", err
}
}
if majStat != C.GSS_S_COMPLETE && majStat != C.GSS_S_CONTINUE_NEEDED {
return connClose, "", gssError("accepting GSS security context failed", majStat, minStat)
}
if majStat != C.GSS_S_CONTINUE_NEEDED {
break
}
}

majStat = C.gss_display_name(&minStat, srcName, &gbuf, nil)
if majStat != C.GSS_S_COMPLETE {
return connClose, "", gssError("retrieving GSS user name failed", majStat, minStat)
}
gssUser = C.GoStringN((*C.char)(gbuf.value), C.int(gbuf.length))
C.gss_release_buffer(&lminS, &gbuf)

return connClose, gssUser, nil
}

func gssError(msg string, majStat, minStat C.OM_uint32) error {
var (
gmsg C.gss_buffer_desc
lminS, msgCtx C.OM_uint32
)

msgCtx = 0
C.gss_display_status(&lminS, majStat, C.GSS_C_GSS_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
msgMajor := C.GoString((*C.char)(gmsg.value))
C.gss_release_buffer(&lminS, &gmsg)

msgCtx = 0
C.gss_display_status(&lminS, minStat, C.GSS_C_MECH_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
msgMinor := C.GoString((*C.char)(gmsg.value))
C.gss_release_buffer(&lminS, &gmsg)

return errors.Errorf("%s: %s: %s", msg, msgMajor, msgMinor)
}
Loading

0 comments on commit 26fd1f6

Please sign in to comment.