Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stable UNIX users: functionality #51200

Open
wants to merge 15 commits into
base: espadolini/stable-unix-user-api
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import (
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
secreportsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/secreports/v1"
stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userloginstatev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
Expand Down Expand Up @@ -4958,6 +4959,11 @@ func (c *Client) GitServerReadOnlyClient() gitserverclient.ReadOnlyClient {
return c.GitServerClient()
}

// StableUNIXUsersClient returns a client for the stable UNIX users API.
func (c *Client) StableUNIXUsersClient() stableunixusersv1.StableUNIXUsersServiceClient {
return stableunixusersv1.NewStableUNIXUsersServiceClient(c.conn)
}

// GetCertAuthority retrieves a CA by type and domain.
func (c *Client) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error) {
ca, err := c.TrustClient().GetCertAuthority(ctx, &trustpb.GetCertAuthorityRequest{
Expand Down
26 changes: 26 additions & 0 deletions api/proto/teleport/legacy/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4549,7 +4549,7 @@
}

// OneOf is a union of one of audit events submitted to the auth service
message OneOf {

Check failure on line 4552 in api/proto/teleport/legacy/types/events/events.proto

View workflow job for this annotation

GitHub Actions / Lint (Proto)

Previously present field "200" with name "StableUNIXUserCreate" on message "OneOf" was deleted without reserving the name "StableUNIXUserCreate".

Check failure on line 4552 in api/proto/teleport/legacy/types/events/events.proto

View workflow job for this annotation

GitHub Actions / Lint (Proto)

Previously present field "200" with name "StableUNIXUserCreate" on message "OneOf" was deleted without reserving the number "200".
// Event is one of the audit events
reserved 185, 186, 187;
reserved "AutoUpdateAgentPlanCreate", "AutoUpdateAgentPlanUpdate", "AutoUpdateAgentPlanDelete";
Expand Down Expand Up @@ -4749,6 +4749,7 @@
events.GitCommand GitCommand = 197;
events.UserLoginAccessListInvalid UserLoginAccessListInvalid = 198;
events.AccessRequestExpire AccessRequestExpire = 199;
events.StableUNIXUserCreate StableUNIXUserCreate = 200;
}
}

Expand Down Expand Up @@ -7950,3 +7951,28 @@
(gogoproto.jsontag) = ""
];
}

// StableUNIXUserCreate is emitted whenever a new stable UNIX user is written in
// the cluster state storage.
message StableUNIXUserCreate {
// Metadata is common event metadata
Metadata Metadata = 1 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];

// User is a common user event metadata
UserMetadata User = 2 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];

StableUNIXUser stable_unix_user = 3;
}

message StableUNIXUser {
string username = 1;
int32 uid = 2;
}
5 changes: 5 additions & 0 deletions api/types/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -2434,3 +2434,8 @@ func (m *WorkloadIdentityDelete) TrimToMaxSize(_ int) AuditEvent {
func (m *GitCommand) TrimToMaxSize(_ int) AuditEvent {
return m
}

// TrimToMaxSize implements [AuditEvent].
func (m *StableUNIXUserCreate) TrimToMaxSize(int) AuditEvent {
return m
}
2,816 changes: 1,693 additions & 1,123 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions api/types/events/oneof.go
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,10 @@ func ToOneOf(in AuditEvent) (*OneOf, error) {
out.Event = &OneOf_GitCommand{
GitCommand: e,
}
case *StableUNIXUserCreate:
out.Event = &OneOf_StableUNIXUserCreate{
StableUNIXUserCreate: e,
}
default:
slog.ErrorContext(context.Background(), "Attempted to convert dynamic event of unknown type into protobuf event.", "event_type", in.GetType())
unknown := &Unknown{}
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/authclient/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import (
provisioningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/provisioning/v1"
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
Expand Down Expand Up @@ -1849,6 +1850,9 @@ type ClientI interface {
// will return "not implemented" errors (as per the default gRPC behavior).
StaticHostUserClient() services.StaticHostUser

// StableUNIXUsersClient returns a client for the stable UNIX users API.
StableUNIXUsersClient() stableunixusersv1.StableUNIXUsersServiceClient

// CloneHTTPClient creates a new HTTP client with the same configuration.
CloneHTTPClient(params ...roundtrip.ClientParam) (*HTTPClient, error)

Expand Down
2 changes: 2 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5284,6 +5284,8 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {

stableUNIXUsersServiceServer, err := stableunixusers.New(stableunixusers.Config{
Authorizer: cfg.Authorizer,
Emitter: cfg.Emitter,
Logger: cfg.AuthServer.logger.With(teleport.ComponentKey, "stable_unix_users"),

Backend: cfg.AuthServer.bk,
ReadOnlyCache: cfg.AuthServer.ReadOnlyCache,
Expand Down
36 changes: 32 additions & 4 deletions lib/auth/stableunixusers/stableunixusers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ package stableunixusers
import (
"context"
"errors"
"log/slog"
"time"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"

stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/utils"
Expand All @@ -38,6 +41,8 @@ const uidCacheTTL = 30 * time.Second
// Config contains the parameters for [New].
type Config struct {
Authorizer authz.Authorizer
Emitter apievents.Emitter
Logger *slog.Logger

Backend backend.Backend
ReadOnlyCache *readonly.Cache
Expand Down Expand Up @@ -67,6 +72,8 @@ func New(params Config) (stableunixusersv1.StableUNIXUsersServiceServer, error)

return &server{
authorizer: params.Authorizer,
emitter: params.Emitter,
logger: params.Logger,

backend: params.Backend,
readOnlyCache: params.ReadOnlyCache,
Expand All @@ -86,6 +93,8 @@ type server struct {
stableunixusersv1.UnsafeStableUNIXUsersServiceServer

authorizer authz.Authorizer
emitter apievents.Emitter
logger *slog.Logger

backend backend.Backend
readOnlyCache *readonly.Cache
Expand Down Expand Up @@ -161,9 +170,17 @@ func (s *server) ObtainUIDForUsername(ctx context.Context, req *stableunixusersv
// obtainUIDForUsernameCached calls [*server.obtainUIDForUsernameUncached]
// through the UID FnCache.
func (s *server) obtainUIDForUsernameCached(ctx context.Context, username string) (int32, error) {
idGetter, err := authz.UserFromContext(ctx)
if err != nil {
idGetter = nil
}

uid, err := utils.FnCacheGet(ctx, s.uidCache, username, func(ctx context.Context) (int32, error) {
ctx, cancel := context.WithTimeout(ctx, uidCacheTTL)
defer cancel()
if idGetter != nil {
ctx = authz.ContextWithUser(ctx, idGetter)
}
return s.obtainUIDForUsernameUncached(ctx, username)
})
if err != nil {
Expand Down Expand Up @@ -192,8 +209,6 @@ func (s *server) obtainUIDForUsernameUncached(ctx context.Context, username stri

uid, err := s.stableUNIXUsers.GetUIDForUsername(ctx, username)
if err == nil {
// TODO(espadolini): _potentially_ emit an audit log event with
// username and UID (it might spam the audit log unnecessarily)
return uid, nil
}
if !trace.IsNotFound(err) {
Expand Down Expand Up @@ -233,8 +248,21 @@ func (s *server) obtainUIDForUsernameUncached(ctx context.Context, username stri
return 0, trace.Wrap(err)
}

// TODO(espadolini): emit an audit log event with the username and UID
// that was just created
if s.emitter != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the emitter be required to be non-nil in the New?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added checks for all required fields.

if err := s.emitter.EmitAuditEvent(ctx, &apievents.StableUNIXUserCreate{
Metadata: apievents.Metadata{
Type: events.StableUNIXUserCreateEvent,
Code: events.StableUNIXUserCreateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
StableUnixUser: &apievents.StableUNIXUser{
Username: username,
Uid: uid,
},
}); err != nil {
s.logger.WarnContext(ctx, "Failed to emit stable_unix_user.create", "error", err)
}
}

return uid, nil
}
Expand Down
3 changes: 3 additions & 0 deletions lib/events/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,9 @@ const (

// GitCommandEvent is emitted when a Git command is executed.
GitCommandEvent = "git.command"

// StableUNIXUserCreateEvent is emitted when a stable UNIX user is created.
StableUNIXUserCreateEvent = "stable_unix_user.create"
)

// Add an entry to eventsMap in lib/events/events_test.go when you add
Expand Down
3 changes: 3 additions & 0 deletions lib/events/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,9 @@ const (
// GitCommandFailureCode is the git command feature event code.
GitCommandFailureCode = "TGIT001E"

// StableUNIXUserCreateCode is the stable UNIX user create event code.
StableUNIXUserCreateCode = "TSUU001I"

// UnknownCode is used when an event of unknown type is encountered.
UnknownCode = apievents.UnknownCode
)
Expand Down
3 changes: 3 additions & 0 deletions lib/events/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ func FromEventFields(fields EventFields) (events.AuditEvent, error) {
case WorkloadIdentityDeleteEvent:
e = &events.WorkloadIdentityDelete{}

case StableUNIXUserCreateEvent:
e = &events.StableUNIXUserCreate{}

default:
slog.ErrorContext(context.Background(), "Attempted to convert dynamic event of unknown type into protobuf event.", "event_type", eventType)
unknown := &events.Unknown{}
Expand Down
1 change: 1 addition & 0 deletions lib/events/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ var eventsMap = map[string]apievents.AuditEvent{
WorkloadIdentityUpdateEvent: &apievents.WorkloadIdentityUpdate{},
WorkloadIdentityDeleteEvent: &apievents.WorkloadIdentityDelete{},
AccessRequestExpireEvent: &apievents.AccessRequestExpire{},
StableUNIXUserCreateEvent: &apievents.StableUNIXUserCreate{},
}

// TestJSON tests JSON marshal events
Expand Down
1 change: 1 addition & 0 deletions lib/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3121,6 +3121,7 @@ func (process *TeleportProcess) initSSH() error {
regular.SetTracerProvider(process.TracingProvider),
regular.SetSessionController(sessionController),
regular.SetPublicAddrs(cfg.SSH.PublicAddrs),
regular.SetStableUNIXUsers(conn.Client.StableUNIXUsersClient()),
)
if err != nil {
return trace.Wrap(err)
Expand Down
49 changes: 48 additions & 1 deletion lib/srv/regular/sshserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
"github.com/gravitational/teleport/api/observability/tracing"
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
"github.com/gravitational/teleport/api/types"
Expand Down Expand Up @@ -235,6 +236,10 @@ type Server struct {
// remoteForwardingMap holds the remote port forwarding listeners that need
// to be closed when forwarding finishes, keyed by listen addr.
remoteForwardingMap utils.SyncMap[string, io.Closer]

// stableUnixUsers is used to obtain fallback UIDs for host user
// provisioning from the control plane.
stableUnixUsers stableunixusersv1.StableUNIXUsersServiceClient
}

// TargetMetadata returns metadata about the server.
Expand Down Expand Up @@ -698,6 +703,15 @@ func SetPROXYSigner(proxySigner PROXYHeaderSigner) ServerOption {
}
}

// SetStableUNIXUsers sets the client for the stable UNIX users service, used as
// a fallback to get UIDs for host user creation.
func SetStableUNIXUsers(stableUNIXUsers stableunixusersv1.StableUNIXUsersServiceClient) ServerOption {
return func(s *Server) error {
s.stableUnixUsers = stableUNIXUsers
return nil
}
}

// SetPublicAddrs sets the server's public addresses
func SetPublicAddrs(addrs []utils.NetAddr) ServerOption {
return func(s *Server) error {
Expand Down Expand Up @@ -1258,7 +1272,7 @@ func (s *Server) HandleNewConn(ctx context.Context, ccx *sshutils.ConnectionCont
}

// Create host user.
created, userCloser, err := s.termHandlers.SessionRegistry.UpsertHostUser(identityContext)
created, userCloser, err := s.termHandlers.SessionRegistry.UpsertHostUser(identityContext, s.obtainFallbackUID)
if err != nil {
s.logger.WarnContext(ctx, "error while creating host users", "error", err)
}
Expand All @@ -1281,6 +1295,39 @@ func (s *Server) HandleNewConn(ctx context.Context, ccx *sshutils.ConnectionCont
return ctx, nil
}

// obtainFallbackUID checks if the cluster is configured for stable
// autoprovisioned UNIX user UIDs and, if so, obtains and returns the UID for
// the given username. If the cluster is not configured for stable UIDs, it
// returns (_, false, nil).
func (s *Server) obtainFallbackUID(ctx context.Context, username string) (uid int32, ok bool, _ error) {
authPref, err := s.authService.GetAuthPreference(ctx)
if err != nil {
return 0, false, trace.Wrap(err)
}

cfg := authPref.GetStableUNIXUserConfig()
if cfg == nil || !cfg.Enabled {
return 0, false, nil
}

resp, err := s.stableUnixUsers.ObtainUIDForUsername(ctx, &stableunixusersv1.ObtainUIDForUsernameRequest{
Username: username,
})
if err != nil {
return 0, false, trace.Wrap(err)
}

uid = resp.GetUid()

// see https://github.com/systemd/systemd/blob/cc7300fc5868f6d47f3f47076100b574bf54e58d/docs/UIDS-GIDS.md
const firstUserUID = 1000
if uid < firstUserUID {
return 0, false, trace.BadParameter("received a negative or system UID as fallback UID from the control plane (%v)", uid)
}

return uid, true, nil
}

// HandleNewChan is called when new channel is opened
func (s *Server) HandleNewChan(ctx context.Context, ccx *sshutils.ConnectionContext, nch ssh.NewChannel) {
identityContext, err := s.authHandlers.CreateIdentityContext(ccx.ServerConn)
Expand Down
Loading
Loading