Skip to content

Commit

Permalink
readonly shared cluster configs (#43755)
Browse files Browse the repository at this point in the history
  • Loading branch information
fspmarshall authored Jul 2, 2024
1 parent 21b1eaf commit 05909ce
Show file tree
Hide file tree
Showing 20 changed files with 606 additions and 95 deletions.
9 changes: 9 additions & 0 deletions api/types/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/tlsutils"
)
Expand Down Expand Up @@ -172,6 +173,9 @@ type AuthPreference interface {

// String represents a human readable version of authentication settings.
String() string

// Clone makes a deep copy of the AuthPreference.
Clone() AuthPreference
}

// NewAuthPreference is a convenience method to to create AuthPreferenceV2.
Expand Down Expand Up @@ -751,6 +755,11 @@ func (c *AuthPreferenceV2) String() string {
return fmt.Sprintf("AuthPreference(Type=%q,SecondFactor=%q)", c.Spec.Type, c.Spec.SecondFactor)
}

// Clone returns a copy of the AuthPreference resource.
func (c *AuthPreferenceV2) Clone() AuthPreference {
return utils.CloneProtoMsg(c)
}

func (u *U2F) Check() error {
if u.AppID == "" {
return trace.BadParameter("u2f configuration missing app_id")
Expand Down
10 changes: 10 additions & 0 deletions api/types/sessionrecording.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"time"

"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/utils"
)

// SessionRecordingConfig defines session recording configuration. This is
Expand All @@ -40,6 +42,9 @@ type SessionRecordingConfig interface {

// SetProxyChecksHostKeys sets if the proxy will check host keys.
SetProxyChecksHostKeys(bool)

// Clone returns a copy of the resource.
Clone() SessionRecordingConfig
}

// NewSessionRecordingConfigFromConfigFile is a convenience method to create
Expand Down Expand Up @@ -158,6 +163,11 @@ func (c *SessionRecordingConfigV2) SetProxyChecksHostKeys(t bool) {
c.Spec.ProxyChecksHostKeys = NewBoolOption(t)
}

// Clone returns a copy of the resource.
func (c *SessionRecordingConfigV2) Clone() SessionRecordingConfig {
return utils.CloneProtoMsg(c)
}

// setStaticFields sets static resource header and metadata fields.
func (c *SessionRecordingConfigV2) setStaticFields() {
c.Kind = KindSessionRecordingConfig
Expand Down
5 changes: 4 additions & 1 deletion integration/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,11 @@ func UpsertAuthPrefAndWaitForCache(
_, err := srv.UpsertAuthPreference(ctx, pref)
require.NoError(t, err)
require.EventuallyWithT(t, func(t *assert.CollectT) {
p, err := srv.GetAuthPreference(ctx)
// we need to wait for the in-memory copy of auth pref to be updated, which
// takes a bit longer than standard cache propagation.
rp, err := srv.GetReadOnlyAuthPreference(ctx)
require.NoError(t, err)
p := rp.Clone()
assert.Empty(t, cmp.Diff(&pref, &p))
}, 5*time.Second, 100*time.Millisecond)
}
55 changes: 40 additions & 15 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import (
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/spacelift"
"github.com/gravitational/teleport/lib/srv/db/common/role"
"github.com/gravitational/teleport/lib/sshca"
Expand Down Expand Up @@ -475,6 +476,20 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
return nil, trace.Wrap(err)
}

_, cacheEnabled := as.getCache()

// cluster config ttl cache *must* be set up after `opts` has been applied to the server because
// the Cache field starts off as a pointer to the local backend services and is only switched
// over to being a proper cache during option processing.
as.ReadOnlyCache, err = readonly.NewCache(readonly.CacheConfig{
Upstream: as.Cache,
Disabled: !cacheEnabled,
ReloadOnErr: true,
})
if err != nil {
return nil, trace.Wrap(err)
}

if as.ghaIDTokenValidator == nil {
as.ghaIDTokenValidator = githubactions.NewIDTokenValidator(
githubactions.IDTokenValidatorConfig{
Expand Down Expand Up @@ -802,6 +817,10 @@ type LoginHook func(context.Context, types.User) error
// the user has no suitable trusted device.
type CreateDeviceWebTokenFunc func(context.Context, *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error)

// ReadOnlyCache is a type alias used to assist with embedding [readonly.Cache] in places
// where it would have a naming conflict with other types named Cache.
type ReadOnlyCache = readonly.Cache

// Server keeps the cluster together. It acts as a certificate authority (CA) for
// a cluster and:
// - generates the keypair for the node it's running on
Expand Down Expand Up @@ -853,6 +872,11 @@ type Server struct {
// method on Services instead.
authclient.Cache

// ReadOnlyCache is a specialized cache that provides read-only shared references
// in certain performance-critical paths where deserialization/cloning may be too
// expensive at scale.
*ReadOnlyCache

// privateKey is used in tests to use pre-generated private keys
privateKey []byte

Expand Down Expand Up @@ -1856,7 +1880,8 @@ func (a *Server) GenerateHostCert(ctx context.Context, hostPublicKey []byte, hos
func (a *Server) generateHostCert(
ctx context.Context, p services.HostCertParams,
) ([]byte, error) {
authPref, err := a.GetAuthPreference(ctx)

readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -1879,7 +1904,7 @@ func (a *Server) generateHostCert(
default:
locks = []types.LockTarget{{ServerID: p.HostID}, {ServerID: HostFQDN(p.HostID, p.ClusterName)}}
}
if lockErr := a.checkLockInForce(authPref.GetLockingMode(),
if lockErr := a.checkLockInForce(readOnlyAuthPref.GetLockingMode(),
locks,
); lockErr != nil {
return nil, trace.Wrap(lockErr)
Expand Down Expand Up @@ -2041,11 +2066,11 @@ func (a *Server) GenerateOpenSSHCert(ctx context.Context, req *proto.OpenSSHCert
return nil, trace.BadParameter("public key is empty")
}
if req.TTL == 0 {
cap, err := a.GetAuthPreference(ctx)
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.BadParameter("cert request does not specify a TTL and the cluster_auth_preference is not available: %v", err)
}
req.TTL = proto.Duration(cap.GetDefaultSessionTTL())
req.TTL = proto.Duration(readOnlyAuthPref.GetDefaultSessionTTL())
}
if req.TTL < 0 {
return nil, trace.BadParameter("TTL must be positive")
Expand Down Expand Up @@ -2593,13 +2618,13 @@ func (a *Server) augmentUserCertificates(
}

// Verify locks right before we re-issue any certificates.
authPref, err := a.GetAuthPreference(ctx)
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{
checker: opts.checker,
defaultMode: authPref.GetLockingMode(),
defaultMode: readOnlyAuthPref.GetLockingMode(),
username: x509Identity.Username,
mfaVerified: x509Identity.MFAVerified,
activeAccessRequests: x509Identity.ActiveRequests,
Expand Down Expand Up @@ -2732,13 +2757,13 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
}

// Reject the cert request if there is a matching lock in force.
authPref, err := a.GetAuthPreference(ctx)
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{
checker: req.checker,
defaultMode: authPref.GetLockingMode(),
defaultMode: readOnlyAuthPref.GetLockingMode(),
username: req.user.GetName(),
mfaVerified: req.mfaVerified,
activeAccessRequests: req.activeRequests.AccessRequests,
Expand Down Expand Up @@ -2767,7 +2792,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
var allowedLogins []string

if req.ttl == 0 {
req.ttl = time.Duration(authPref.GetDefaultSessionTTL())
req.ttl = time.Duration(readOnlyAuthPref.GetDefaultSessionTTL())
}

// If the role TTL is ignored, do not restrict session TTL and allowed logins.
Expand Down Expand Up @@ -2797,7 +2822,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
}

attestedKeyPolicy := keys.PrivateKeyPolicyNone
requiredKeyPolicy, err := req.checker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
requiredKeyPolicy, err := req.checker.PrivateKeyPolicy(readOnlyAuthPref.GetPrivateKeyPolicy())
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -2819,7 +2844,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
}

var validateSerialNumber bool
hksnv, err := authPref.GetHardwareKeySerialNumberValidation()
hksnv, err := readOnlyAuthPref.GetHardwareKeySerialNumberValidation()
if err == nil {
validateSerialNumber = hksnv.Enabled
}
Expand Down Expand Up @@ -3608,7 +3633,7 @@ func (a *Server) deleteMFADeviceSafely(ctx context.Context, user, deviceName str
return nil, trace.Wrap(err)
}

authPref, err := a.GetAuthPreference(ctx)
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -3656,7 +3681,7 @@ func (a *Server) deleteMFADeviceSafely(ctx context.Context, user, deviceName str

// Prevent users from deleting their last device for clusters that require second factors.
const minDevices = 1
switch sf := authPref.GetSecondFactor(); sf {
switch sf := readOnlyAuthPref.GetSecondFactor(); sf {
case constants.SecondFactorOff, constants.SecondFactorOptional: // MFA is not required, allow deletion
case constants.SecondFactorOn:
if knownDevices <= minDevices {
Expand All @@ -3677,7 +3702,7 @@ func (a *Server) deleteMFADeviceSafely(ctx context.Context, user, deviceName str
// It checks whether the credential to delete is a last passkey and whether
// the user has other valid local credentials.
canDeleteLastPasskey := func() (bool, error) {
if !authPref.GetAllowPasswordless() || numResidentKeys > 1 || !isResidentKey(deviceToDelete) {
if !readOnlyAuthPref.GetAllowPasswordless() || numResidentKeys > 1 || !isResidentKey(deviceToDelete) {
return true, nil
}

Expand All @@ -3699,7 +3724,7 @@ func (a *Server) deleteMFADeviceSafely(ctx context.Context, user, deviceName str

// Whether we take TOTPs into consideration or not depends on whether it's
// enabled.
switch sf := authPref.GetSecondFactor(); sf {
switch sf := readOnlyAuthPref.GetSecondFactor(); sf {
case constants.SecondFactorOTP, constants.SecondFactorOn, constants.SecondFactorOptional:
if sfToCount[constants.SecondFactorOTP] >= 1 {
return true, nil
Expand Down
25 changes: 16 additions & 9 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -1558,7 +1558,7 @@ func (a *ServerWithRoles) GetSSHTargets(ctx context.Context, req *proto.GetSSHTa
// try to detect case-insensitive routing setting, but default to false if we can't load
// networking config (equivalent to proxy routing behavior).
var caseInsensitiveRouting bool
if cfg, err := a.authServer.GetClusterNetworkingConfig(ctx); err == nil {
if cfg, err := a.authServer.GetReadOnlyClusterNetworkingConfig(ctx); err == nil {
caseInsensitiveRouting = cfg.GetCaseInsensitiveRouting()
}

Expand Down Expand Up @@ -2951,11 +2951,11 @@ func getBotName(user types.User) string {

func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserCertsRequest, opts ...certRequestOption) (*proto.Certs, error) {
// Device trust: authorize device before issuing certificates.
authPref, err := a.authServer.GetAuthPreference(ctx)
readOnlyAuthPref, err := a.authServer.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.verifyUserDeviceForCertIssuance(req.Usage, authPref.GetDeviceTrust()); err != nil {
if err := a.verifyUserDeviceForCertIssuance(req.Usage, readOnlyAuthPref.GetDeviceTrust()); err != nil {
return nil, trace.Wrap(err)
}

Expand Down Expand Up @@ -3053,7 +3053,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
if err != nil {
return nil, trace.Wrap(err)
}
sessionTTL := roleSet.AdjustSessionTTL(authPref.GetDefaultSessionTTL().Duration())
sessionTTL := roleSet.AdjustSessionTTL(readOnlyAuthPref.GetDefaultSessionTTL().Duration())
req.Expires = a.authServer.GetClock().Now().UTC().Add(sessionTTL)
} else if req.Expires.After(sessionExpires) {
// Standard user impersonation has an expiry limited to the expiry
Expand Down Expand Up @@ -4482,8 +4482,11 @@ func (a *ServerWithRoles) GetAuthPreference(ctx context.Context) (types.AuthPref
if err := a.action(apidefaults.Namespace, types.KindClusterAuthPreference, types.VerbRead); err != nil {
return nil, trace.Wrap(err)
}

return a.authServer.GetAuthPreference(ctx)
cfg, err := a.authServer.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return cfg.Clone(), nil
}

func (a *ServerWithRoles) GetUIConfig(ctx context.Context) (types.UIConfig, error) {
Expand Down Expand Up @@ -4672,7 +4675,11 @@ func (a *ServerWithRoles) GetClusterNetworkingConfig(ctx context.Context) (types
return nil, trace.Wrap(err)
}
}
return a.authServer.GetClusterNetworkingConfig(ctx)
cfg, err := a.authServer.GetReadOnlyClusterNetworkingConfig(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return cfg.Clone(), nil
}

// SetClusterNetworkingConfig sets cluster networking configuration.
Expand Down Expand Up @@ -6771,12 +6778,12 @@ func (a *ServerWithRoles) CreateRegisterChallenge(ctx context.Context, req *prot
// enforceGlobalModeTrustedDevice is used to enforce global device trust requirements
// for key endpoints.
func (a *ServerWithRoles) enforceGlobalModeTrustedDevice(ctx context.Context) error {
authPref, err := a.GetAuthPreference(ctx)
readOnlyAuthPref, err := a.authServer.GetReadOnlyAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}

err = dtauthz.VerifyTLSUser(authPref.GetDeviceTrust(), a.context.Identity.GetIdentity())
err = dtauthz.VerifyTLSUser(readOnlyAuthPref.GetDeviceTrust(), a.context.Identity.GetIdentity())
return trace.Wrap(err)
}

Expand Down
Loading

0 comments on commit 05909ce

Please sign in to comment.