mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-29 17:58:15 +08:00
379 lines
14 KiB
Go
379 lines
14 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"google.golang.org/grpc/credentials"
|
|
|
|
"github.com/gravitational/teleport/api/types"
|
|
apievents "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/lib/authz"
|
|
)
|
|
|
|
// UserGetter is responsible for building an authenticated user based on TLS metadata
|
|
type UserGetter interface {
|
|
GetUser(ctx context.Context, connState tls.ConnectionState) (authz.IdentityGetter, error)
|
|
}
|
|
|
|
// ConnectionIdentity contains the identifying properties of a
|
|
// client connection required to enforce connection limits.
|
|
type ConnectionIdentity struct {
|
|
// Username is the name of the user
|
|
Username string
|
|
// MaxConnections the upper limit to number of open connections for a user
|
|
MaxConnections int64
|
|
// LocalAddr is the local address for the connection
|
|
LocalAddr string
|
|
// RemoteAddr is the remote address for the connection
|
|
RemoteAddr string
|
|
// UserMetadata contains metadata for a user
|
|
UserMetadata apievents.UserMetadata
|
|
}
|
|
|
|
// ConnectionEnforcer limits incoming connections based on
|
|
// max connection settings.
|
|
type ConnectionEnforcer interface {
|
|
EnforceConnectionLimits(ctx context.Context, identity ConnectionIdentity, closers ...io.Closer) (context.Context, error)
|
|
}
|
|
|
|
// TransportCredentialsConfig configures the behavior that occurs
|
|
// during the server handshake by the TransportCredentials
|
|
type TransportCredentialsConfig struct {
|
|
// TransportCredentials provide the credentials that are used to perform the TLS
|
|
// server and client handshakes as well as the [credentials.ProtocolInfo]. This
|
|
// **MUST** not be nil, and it must have its [credentials.ProtocolInfo.SecurityProtocol]
|
|
// equal to "tls".
|
|
TransportCredentials credentials.TransportCredentials
|
|
// UserGetter constructs the clients' [tlsca.Identity] from the [tls.ConnectionState]
|
|
// that is received from the TLS handshake. This
|
|
UserGetter UserGetter
|
|
// Authorizer prevents any connections from being established if the user is not
|
|
// authorized due to locks, private key policy, device trust, etc. If not set
|
|
// then no authorization is performed.
|
|
Authorizer authz.Authorizer
|
|
|
|
// ScopedAuthorizer is an additional optional authorizer that can be used to perform
|
|
// authorization of scoped identities. Note that when present, this field is preferred
|
|
// over the above Authorizer, though scoped contexts can be undwrapped into unscoped
|
|
// contexts if not all methods of the underlying service are being ported over to be scope-aware.
|
|
ScopedAuthorizer authz.ScopedAuthorizer
|
|
|
|
// Enforcer prevents any connections from being established if the user would
|
|
// exceed their configured max connection limit. Any connections that are
|
|
// permitted may be terminated if there is an issue determining if the number
|
|
// of active connections is within the limit. If not set then no connection
|
|
// limits are enforced.
|
|
Enforcer ConnectionEnforcer
|
|
// Clock used to tell time.
|
|
Clock clockwork.Clock
|
|
// GetAuthPreference is used to retrieve the auth preference per connection
|
|
// to determine if connections should be terminated as soon as the client
|
|
// certificate has expired.
|
|
GetAuthPreference func(ctx context.Context) (types.AuthPreference, error)
|
|
}
|
|
|
|
// Check validates that the configuration is valid for use and
|
|
// that all supplied parameters are set accordingly.
|
|
func (c *TransportCredentialsConfig) Check() error {
|
|
switch {
|
|
case c.TransportCredentials == nil:
|
|
return trace.BadParameter("parameter TransportCredentials required")
|
|
case c.TransportCredentials.Info().SecurityProtocol != "tls":
|
|
return trace.BadParameter("the TransportCredentials must be a tls security protocol, got %s", c.TransportCredentials.Info().SecurityProtocol)
|
|
case c.UserGetter == nil:
|
|
return trace.BadParameter("parameter UserGetter required")
|
|
case c.Authorizer != nil && c.UserGetter == nil:
|
|
return trace.BadParameter("a UserGetter is required to use validate identities with an Authorizer")
|
|
case c.Enforcer != nil && c.Authorizer == nil:
|
|
return trace.BadParameter("both a UserGetter and an Authorizer are required to enforce connection limits with an Enforcer")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// TransportCredentials is a [credentials.TransportCredentials] that
|
|
// enforces mTLS and retrieves the [IdentityGetter] for use by middleware
|
|
// to perform authorization.
|
|
type TransportCredentials struct {
|
|
credentials.TransportCredentials
|
|
|
|
userGetter UserGetter
|
|
authorizer authz.Authorizer
|
|
scopedAuthorizer authz.ScopedAuthorizer
|
|
enforcer ConnectionEnforcer
|
|
getAuthPreference func(context.Context) (types.AuthPreference, error)
|
|
clock clockwork.Clock
|
|
}
|
|
|
|
// NewTransportCredentials returns a new TransportCredentials
|
|
func NewTransportCredentials(cfg TransportCredentialsConfig) (*TransportCredentials, error) {
|
|
if err := cfg.Check(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
getAuthPreference := func(context.Context) (types.AuthPreference, error) {
|
|
return types.DefaultAuthPreference(), nil
|
|
}
|
|
if cfg.GetAuthPreference != nil {
|
|
getAuthPreference = cfg.GetAuthPreference
|
|
}
|
|
|
|
clock := clockwork.NewRealClock()
|
|
if cfg.Clock != nil {
|
|
clock = cfg.Clock
|
|
}
|
|
|
|
return &TransportCredentials{
|
|
TransportCredentials: cfg.TransportCredentials,
|
|
userGetter: cfg.UserGetter,
|
|
authorizer: cfg.Authorizer,
|
|
scopedAuthorizer: cfg.ScopedAuthorizer,
|
|
enforcer: cfg.Enforcer,
|
|
getAuthPreference: getAuthPreference,
|
|
clock: clock,
|
|
}, nil
|
|
}
|
|
|
|
// IdentityInfo contains the auth information and identity
|
|
// for an authenticated TLS connection. It implements the
|
|
// [credentials.AuthInfo] interface and is returned from
|
|
// [TransportCredentials.ServerHandshake].
|
|
type IdentityInfo struct {
|
|
// TLSInfo contains TLS connection information.
|
|
*credentials.TLSInfo
|
|
// IdentityGetter provides a mechanism to retrieve the
|
|
// identity of the client.
|
|
IdentityGetter authz.IdentityGetter
|
|
// AuthContext contains information about the traits and roles
|
|
// that an identity may have. This will be unset if the
|
|
// [TransportCredentialsConfig.Authorizer] provided to [NewTransportCredentials]
|
|
// was nil.
|
|
AuthContext *authz.Context
|
|
|
|
// ScopedContext contains scoped authorization context. This will be unset if
|
|
// the [TransportCredentialsConfig.ScopedAuthorizer] provided to [NewTransportCredentials]
|
|
// was nil.
|
|
ScopedAuthContext *authz.ScopedContext
|
|
|
|
// Conn is the underlying [net.Conn] of the gRPC connection.
|
|
Conn net.Conn
|
|
}
|
|
|
|
func (i IdentityInfo) AuthzContext() (ctx *authz.Context, ok bool) {
|
|
return i.AuthContext, i.AuthContext != nil
|
|
}
|
|
|
|
func (i IdentityInfo) ScopedAuthzContext() (ctx *authz.ScopedContext, ok bool) {
|
|
return i.ScopedAuthContext, i.ScopedAuthContext != nil
|
|
}
|
|
|
|
// timeoutConn wraps a connection that is to be closed when
|
|
// the timer expires.
|
|
type timeoutConn struct {
|
|
net.Conn // The underlying [net.Conn] of the gRPC connection.
|
|
timer clockwork.Timer
|
|
}
|
|
|
|
// newTimeoutConn creates a [net.Conn] wrapper that closes the rawConn
|
|
// if the timeout is exceeded.
|
|
func newTimeoutConn(conn net.Conn, clock clockwork.Clock, expires time.Time) (net.Conn, error) {
|
|
if expires.IsZero() {
|
|
return conn, nil
|
|
}
|
|
|
|
return &timeoutConn{
|
|
Conn: conn,
|
|
timer: clock.AfterFunc(expires.Sub(clock.Now()), func() {
|
|
logger.DebugContext(context.Background(), "Closing gRPC connection due to certificate expiry")
|
|
conn.Close()
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the wrapped [net.Conn] and stops the timer
|
|
// to prevent leaking it.
|
|
func (c *timeoutConn) Close() error {
|
|
c.timer.Stop()
|
|
return trace.Wrap(c.Conn.Close())
|
|
}
|
|
|
|
// ServerHandshake performs the authentication handshake for servers as per
|
|
// the [credentials.TransportCredentials] interface. It returns the authenticated
|
|
// connection and the corresponding auth information about the connection.
|
|
// At minimum, the TLS handshake is performed and the identity is built from
|
|
// the [tls.ConnectionState]. If the TransportCredentials is configured with
|
|
// an [authz.Authorizer] and a [ConnectionEnforcer], then additional session
|
|
// controls are applied before the handshake completes.
|
|
func (c *TransportCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
|
conn, tlsInfo, err := c.performTLSHandshake(rawConn)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
validatedConn, info, err := c.validateIdentity(conn, tlsInfo)
|
|
if err != nil {
|
|
return nil, nil, trace.NewAggregate(err, conn.Close())
|
|
}
|
|
return validatedConn, info, nil
|
|
}
|
|
|
|
// validateIdentity extracts the identity from the client certificate,
|
|
// authorizes the user, enforces any connection limits, and ensures the
|
|
// connection is terminated at expiry of the client certificate if required.
|
|
func (c *TransportCredentials) validateIdentity(conn net.Conn, tlsInfo *credentials.TLSInfo) (net.Conn, IdentityInfo, error) {
|
|
ctx := context.Background()
|
|
|
|
identityGetter, err := c.userGetter.GetUser(ctx, tlsInfo.State)
|
|
if err != nil {
|
|
return nil, IdentityInfo{}, trace.Wrap(err)
|
|
}
|
|
|
|
authCtx, scopedAuthCtx, err := c.authorize(ctx, conn.RemoteAddr(), identityGetter, &tlsInfo.State)
|
|
if err != nil {
|
|
return nil, IdentityInfo{}, trace.Wrap(err)
|
|
}
|
|
|
|
if authCtx != nil {
|
|
// TODO(fspmarshall/scopes): enforce connection limits for scoped identities, or rework this to have a cleaner
|
|
// abstraction if we decide that there is no sane concept of connection-limits for scopes (likely, there won't
|
|
// be unelss we special-case root role assignments or introduce some additional control mechanism outside of
|
|
// scoped roles).
|
|
if err := c.enforceConnectionLimits(ctx, authCtx, conn); err != nil {
|
|
return nil, IdentityInfo{}, trace.Wrap(err)
|
|
}
|
|
|
|
if authPreference, err := c.getAuthPreference(ctx); err == nil {
|
|
expiry := authCtx.GetDisconnectCertExpiry(authPreference)
|
|
conn, err = newTimeoutConn(conn, c.clock, expiry)
|
|
if err != nil {
|
|
return nil, IdentityInfo{}, trace.Wrap(err)
|
|
}
|
|
}
|
|
} else {
|
|
if authPreference, err := c.getAuthPreference(ctx); err == nil {
|
|
expiry := scopedAuthCtx.GetDisconnectCertExpiry(authPreference)
|
|
conn, err = newTimeoutConn(conn, c.clock, expiry)
|
|
if err != nil {
|
|
return nil, IdentityInfo{}, trace.Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return conn, IdentityInfo{
|
|
TLSInfo: tlsInfo,
|
|
IdentityGetter: identityGetter,
|
|
AuthContext: authCtx,
|
|
ScopedAuthContext: scopedAuthCtx,
|
|
Conn: conn,
|
|
}, nil
|
|
}
|
|
|
|
// performTLSHandshake does the TLS handshake and validates the
|
|
// returned [credentials.AuthInfo] are of type [credentials.TLSInfo].
|
|
func (c *TransportCredentials) performTLSHandshake(rawConn net.Conn) (net.Conn, *credentials.TLSInfo, error) {
|
|
conn, info, err := c.TransportCredentials.ServerHandshake(rawConn)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
tlsInfo, ok := info.(credentials.TLSInfo)
|
|
if !ok {
|
|
return nil, nil, trace.NewAggregate(conn.Close(), trace.BadParameter("unexpected type in tls auth info %T", info))
|
|
}
|
|
|
|
return conn, &tlsInfo, nil
|
|
}
|
|
|
|
// authorize enforces that the identity is not restricted from connecting due
|
|
// to things like locks, private key policy, device trust, etc. If the TransportCredentials
|
|
// was not configured to do authorization then this is a noop and will return nil, nil.
|
|
func (c *TransportCredentials) authorize(ctx context.Context, remoteAddr net.Addr, identityGetter authz.IdentityGetter, connState *tls.ConnectionState) (*authz.Context, *authz.ScopedContext, error) {
|
|
if c.authorizer == nil && c.scopedAuthorizer == nil {
|
|
return &authz.Context{
|
|
Identity: identityGetter,
|
|
}, nil, nil
|
|
}
|
|
|
|
// construct a context with the keys expected by the Authorizer
|
|
ctx = authz.ContextWithUserCertificate(ctx, certFromConnState(connState))
|
|
ctx = authz.ContextWithClientSrcAddr(ctx, remoteAddr)
|
|
ctx = authz.ContextWithUser(ctx, identityGetter)
|
|
|
|
if c.scopedAuthorizer != nil {
|
|
scopedCtx, err := c.scopedAuthorizer.AuthorizeScoped(ctx)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
authCtx, _ := scopedCtx.UnscopedContext()
|
|
return authCtx, scopedCtx, nil
|
|
}
|
|
|
|
authCtx, err := c.authorizer.Authorize(ctx)
|
|
return authCtx, nil, trace.Wrap(err)
|
|
}
|
|
|
|
// enforceConnectionLimits prevents the identity from exceeding any configured
|
|
// connection limits. The provided connection will be closed by the enforcer
|
|
// if connectivity to Auth is interrupted. If the TransportCredentials
|
|
// was not configured to do connection limiting then this is a noop and will return nil.
|
|
func (c *TransportCredentials) enforceConnectionLimits(ctx context.Context, authCtx *authz.Context, conn net.Conn) error {
|
|
if c.enforcer == nil {
|
|
return nil
|
|
}
|
|
|
|
if authCtx == nil || authCtx.Checker == nil {
|
|
return trace.BadParameter("unable to determine connection limits without a valid auth context")
|
|
}
|
|
|
|
_, err := c.enforcer.EnforceConnectionLimits(
|
|
ctx,
|
|
ConnectionIdentity{
|
|
Username: authCtx.User.GetName(),
|
|
MaxConnections: authCtx.Checker.MaxConnections(),
|
|
LocalAddr: conn.LocalAddr().String(),
|
|
RemoteAddr: conn.RemoteAddr().String(),
|
|
UserMetadata: authz.ClientUserMetadata(ctx),
|
|
},
|
|
conn,
|
|
)
|
|
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Clone makes a copy of this TransportCredentials.
|
|
func (c *TransportCredentials) Clone() credentials.TransportCredentials {
|
|
return &TransportCredentials{
|
|
userGetter: c.userGetter,
|
|
authorizer: c.authorizer,
|
|
scopedAuthorizer: c.scopedAuthorizer,
|
|
enforcer: c.enforcer,
|
|
TransportCredentials: c.TransportCredentials.Clone(),
|
|
clock: c.clock,
|
|
}
|
|
}
|