From fad450b4ea536901270a259be8bc9feed0610da4 Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Fri, 22 May 2026 11:26:19 -0600 Subject: [PATCH] Prevent users from creating apps for the beam app service (#66937) Beams runs an app_service, configured to look for apps with the "teleport.internal/beams/app-type" label. Users should not be able to create apps with this label - we expect these apps are only created via static beams config or via the `tsh beams publish` command. It's safe to block these requests at the RPC layer because beam apps are written directly to storage and don't go through these auth RPCs. --- api/types/constants.go | 11 +++++++---- lib/auth/auth_with_roles.go | 23 +++++++++++++++++++++++ lib/auth/auth_with_roles_test.go | 20 +++++++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/api/types/constants.go b/api/types/constants.go index 8ee39170e18..f2f0b74a7dc 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1355,20 +1355,23 @@ const ( // AppSubKindLabel is the label that has the same value of "app.sub_kind". AppSubKindLabel = TeleportInternalLabelPrefix + "app-sub-kind" + // BeamsInternalLabelPrefix is the prefix used by internal beams labels. + BeamsInternalLabelPrefix = TeleportInternalLabelPrefix + "beams/" + // BeamIDLabel is the label used to track which Beam a resource belongs to. - BeamIDLabel = TeleportInternalLabelPrefix + "beams/id" + BeamIDLabel = BeamsInternalLabelPrefix + "id" // BeamOwnerLabel is the label used to track which user's Beam a resource // belongs to. - BeamOwnerLabel = TeleportInternalLabelPrefix + "beams/owner" + BeamOwnerLabel = BeamsInternalLabelPrefix + "owner" // BeamAliasLabel is the label used to track the alias of the Beam a // resource belongs to. - BeamAliasLabel = TeleportInternalLabelPrefix + "beams/alias" + BeamAliasLabel = BeamsInternalLabelPrefix + "alias" // BeamAppTypeLabel is the label used to denote the type of app created for // Beams. Valid values: "ingress" and "llm". - BeamAppTypeLabel = TeleportInternalLabelPrefix + "beams/app-type" + BeamAppTypeLabel = BeamsInternalLabelPrefix + "app-type" ) const ( diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 17eb54f2515..c3905426b89 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -6957,6 +6957,15 @@ func sessionTypeFromStartEvent(sessionStart apievents.AuditEvent) types.SessionK } } +func hasInternalBeamsLabel(labels map[string]string) bool { + for key := range maps.Keys(labels) { + if strings.HasPrefix(key, types.BeamsInternalLabelPrefix) { + return true + } + } + return false +} + // CreateApp creates a new application resource. func (a *ServerWithRoles) CreateApp(ctx context.Context, app types.Application) error { if err := a.authorizeAction(types.KindApp, types.VerbCreate); err != nil { @@ -6973,6 +6982,13 @@ func (a *ServerWithRoles) CreateApp(ctx context.Context, app types.Application) if err := a.checkAccessToApp(app); err != nil { return trace.Wrap(err) } + + // Users are not allowed to create apps that would be picked up + // by the beams app_service. + if hasInternalBeamsLabel(app.GetAllLabels()) { + return trace.AccessDenied("access denied") + } + return trace.Wrap(a.authServer.CreateApp(ctx, app)) } @@ -6998,6 +7014,13 @@ func (a *ServerWithRoles) UpdateApp(ctx context.Context, app types.Application) if err := a.checkAccessToApp(app); err != nil { return trace.Wrap(err) } + + // Users are not allowed to create apps that would be picked up + // by the beams app_service. + if hasInternalBeamsLabel(app.GetAllLabels()) { + return trace.AccessDenied("access denied") + } + return trace.Wrap(a.authServer.UpdateApp(ctx, app)) } diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index 69e32d53a21..cdc3f1af480 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -4085,7 +4085,7 @@ func TestListSAMLIdPServiceProviderAndListResources(t *testing.T) { // TestApps verifies RBAC is applied to app resources. func TestApps(t *testing.T) { t.Parallel() - ctx := context.Background() + ctx := t.Context() srv := newTestTLSServer(t) // Setup a couple of users: @@ -4093,19 +4093,25 @@ func TestApps(t *testing.T) { // - "admin" has access to all apps dev, devRole, err := authtest.CreateUserAndRole(srv.Auth(), "dev", nil, nil) require.NoError(t, err) + devRole.SetAppLabels(types.Allow, types.Labels{"env": {"dev"}}) _, err = srv.Auth().UpsertRole(ctx, devRole) require.NoError(t, err) + devClt, err := srv.NewClient(authtest.TestUser(dev.GetName())) require.NoError(t, err) + t.Cleanup(func() { devClt.Close() }) admin, adminRole, err := authtest.CreateUserAndRole(srv.Auth(), "admin", nil, nil) require.NoError(t, err) + adminRole.SetAppLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}}) _, err = srv.Auth().UpsertRole(ctx, adminRole) require.NoError(t, err) + adminClt, err := srv.NewClient(authtest.TestUser(admin.GetName())) require.NoError(t, err) + t.Cleanup(func() { adminClt.Close() }) // Prepare a couple of app resources. devApp, err := types.NewAppV3(types.Metadata{ @@ -4115,6 +4121,7 @@ func TestApps(t *testing.T) { URI: "localhost1", }) require.NoError(t, err) + adminApp, err := types.NewAppV3(types.Metadata{ Name: "admin", Labels: map[string]string{"env": "prod", types.OriginLabel: types.OriginDynamic}, @@ -4172,6 +4179,17 @@ func TestApps(t *testing.T) { cmpopts.IgnoreFields(types.Metadata{}, "Revision"), )) + // Clients can't create/update apps for the beams service. + adminApp.GetMetadata().Labels["teleport.internal/beams/app-type"] = "llm" + err = adminClt.UpdateApp(ctx, adminApp) + require.True(t, trace.IsAccessDenied(err)) + delete(adminApp.GetMetadata().Labels, "teleport.internal/beams/app-type") + + beamApp := devApp.Copy() + beamApp.GetMetadata().Labels["teleport.internal/beams/app-type"] = "llm" + err = devClt.CreateApp(ctx, beamApp) + require.True(t, trace.IsAccessDenied(err)) + // When listing apps, dev should only see one. apps, err := devClt.GetApps(ctx) require.NoError(t, err)