Add beam-user, beam-admin, and beam roles (#66708)

* Add `beam-user`, `beam-admin`, and `beam` roles

Migrates the beam roles from the cloud tenant controller, adds descriptions, and
marks `beam` as a "system" role so it does not show in the role selector.

Fixes https://github.com/gravitational/beams/issues/137

* Make `host_cert` permissions more restrictive

* Disable beams roles in OSS

* Extract beams login into a constant

* Fix stray quote

* Further lock down the host cert permissions

* Fix role preset tests

* Using `host_cert.role` in expressions doesn't work
This commit is contained in:
Dan Upton
2026-05-14 11:50:52 +00:00
committed by GitHub
parent f3f66bd585
commit ce46e109d0
8 changed files with 160 additions and 4 deletions
+7
View File
@@ -1365,6 +1365,10 @@ const (
// BeamAliasLabel is the label used to track the alias of the Beam a
// resource belongs to.
BeamAliasLabel = TeleportInternalLabelPrefix + "beams/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"
)
const (
@@ -1950,3 +1954,6 @@ const (
// BuiltInAutomaticReview is used within access monitoring rules and indicates
// that the automatic_review rule should be monitored by Teleport.
const BuiltInAutomaticReview = "builtin"
// BeamsLogin is the login that should be used when SSHing into beams.
const BeamsLogin = "beams"
+12
View File
@@ -747,6 +747,10 @@ const (
// Access Requests.
SystemIdentityCenterAccessRoleName = "aws-ic-access"
// SystemBeamRoleName specifies the name of a system role that grants the
// beam bot permission to issue itself credentials.
SystemBeamRoleName = "beam"
// PresetWildcardWorkloadIdentityIssuerRoleName is a name of a preset role
// that includes the permissions necessary to issue workload identity
// credentials using any workload_identity resource. This exists to simplify
@@ -764,6 +768,14 @@ const (
// PresetMCPUserRoleName is a name of a preset role that allows
// accessing MCP servers.
PresetMCPUserRoleName = "mcp-user"
// PresetBeamUserRoleName is a name of a preset role that allows users to use
// the Beams feature.
PresetBeamUserRoleName = "beam-user"
// PresetBeamAdminRoleName is a name of a preset role that allows users to
// administer beams belonging to other users.
PresetBeamAdminRoleName = "beam-admin"
)
var PresetRoles = []string{PresetEditorRoleName, PresetAccessRoleName, PresetAuditorRoleName}
+3
View File
@@ -1432,6 +1432,9 @@ func GetPresetRoles(buildType string) []types.Role {
services.NewPresetAccessPluginRole(),
services.NewPresetListAccessRequestResourcesRole(),
services.NewPresetMCPUserRole(),
services.NewSystemBeamRole(buildType),
services.NewPresetBeamUserRole(buildType),
services.NewPresetBeamAdminRole(buildType),
}
// Certain `New$FooRole()` functions will return a nil role if the
+3
View File
@@ -1293,12 +1293,15 @@ func TestPresets(t *testing.T) {
teleport.PresetDeviceEnrollRoleName,
teleport.PresetRequireTrustedDeviceRoleName,
teleport.SystemOktaRequesterRoleName, // This is treated as a preset
teleport.PresetBeamUserRoleName,
teleport.PresetBeamAdminRoleName,
}, presetRoleNames...)
enterpriseSystemRoleNames := []string{
teleport.SystemAutomaticAccessApprovalRoleName,
teleport.SystemOktaAccessRoleName,
teleport.SystemIdentityCenterAccessRoleName,
teleport.SystemBeamRoleName,
}
enterpriseUsers := []types.User{
+131
View File
@@ -20,8 +20,10 @@ package services
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"github.com/gravitational/trace"
@@ -882,6 +884,135 @@ func NewPresetMCPUserRole() types.Role {
return role
}
// NewPresetBeamUserRole returns a new pre-defined role for accessing your own
// beam resources.
func NewPresetBeamUserRole(buildType string) types.Role {
if buildType != modules.BuildEnterprise {
return nil
}
allowLLMApps := fmt.Sprintf(`labels[%q] == %q`, types.BeamAppTypeLabel, types.SubKindLLM)
allowBeamApps := fmt.Sprintf(`labels[%q] == user.metadata.name`, types.BeamOwnerLabel)
role := &types.RoleV6{
Kind: types.KindRole,
Version: types.V8,
Metadata: types.Metadata{
Name: teleport.PresetBeamUserRoleName,
Namespace: apidefaults.Namespace,
Description: "Use the Beams feature",
Labels: map[string]string{
types.TeleportInternalResourceType: types.PresetResource,
},
},
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{types.BeamsLogin},
AppLabelsExpression: strings.Join([]string{allowLLMApps, allowBeamApps}, " || "),
NodeLabels: types.Labels{
types.BeamOwnerLabel: {"{{user.metadata.name}}"},
},
BeamLabels: types.Labels{
types.BeamOwnerLabel: {"{{user.metadata.name}}"},
},
Rules: []types.Rule{
{
Resources: []string{types.KindBeam},
Verbs: []string{types.Wildcard},
},
},
},
},
}
return role
}
// NewPresetBeamAdminRole returns a new pre-defined role for administering beams
// belonging to other users.
func NewPresetBeamAdminRole(buildType string) types.Role {
if buildType != modules.BuildEnterprise {
return nil
}
role := &types.RoleV6{
Kind: types.KindRole,
Version: types.V8,
Metadata: types.Metadata{
Name: teleport.PresetBeamAdminRoleName,
Namespace: apidefaults.Namespace,
Description: "Administer beams belonging to other users",
Labels: map[string]string{
types.TeleportInternalResourceType: types.PresetResource,
},
},
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
BeamLabels: types.Labels{
types.BeamOwnerLabel: {types.Wildcard},
},
Rules: []types.Rule{
{
Resources: []string{types.KindBeam},
Verbs: []string{types.Wildcard},
},
},
},
},
}
return role
}
// NewSystemBeamRole returns a new pre-defined role for the beam to issue itself
// credentials.
func NewSystemBeamRole(buildType string) types.Role {
if buildType != modules.BuildEnterprise {
return nil
}
// Only allow the bot to generate host certificates for the Beam's OpenSSH
// server. tbot explicitly sends a blank HostID, HostName and the Node role.
hostCertConstraints := strings.Join([]string{
fmt.Sprintf(`contains_all(user.spec.traits[%q], host_cert.principals)`, types.BeamIDLabel),
`host_cert.host_id == ""`,
`host_cert.node_name == ""`,
}, " && ")
role := &types.RoleV6{
Kind: types.KindRole,
Version: types.V8,
Metadata: types.Metadata{
Name: teleport.SystemBeamRoleName,
Namespace: apidefaults.Namespace,
Description: "Used by a beam to issue itself credentials",
Labels: map[string]string{
types.TeleportInternalResourceType: types.SystemResource,
},
},
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
{
Resources: []string{types.KindHostCert},
Verbs: []string{types.VerbCreate},
Where: hostCertConstraints,
},
{
Resources: []string{types.KindWorkloadIdentity},
Verbs: []string{
types.VerbList,
types.VerbRead,
},
},
},
WorkloadIdentityLabels: types.Labels{
types.BeamIDLabel: []string{fmt.Sprintf(`{{external[%q]}}`, types.BeamIDLabel)},
},
},
},
}
return role
}
// VirtualDefaultHealthCheckConfigDB returns a health_check_config enabling
// health checks for all databases resources, and is intended to be used as a
// virtual default resource. Its name is "default" for historical reasons.
-2
View File
@@ -33,8 +33,6 @@ import (
"github.com/gravitational/teleport/lib/utils"
)
const beamsLogin = "beams"
type beamsCommands struct {
ls *beamsLSCommand
add *beamsAddCommand
+2 -1
View File
@@ -28,6 +28,7 @@ import (
"github.com/gravitational/teleport"
beamsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/beams/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
@@ -158,7 +159,7 @@ func (t *beamCopyTarget) toSFTP() string {
}
return fmt.Sprintf(
"%s@%s:%s",
beamsLogin,
types.BeamsLogin,
t.beam.GetStatus().GetNodeId(),
t.path,
)
+2 -1
View File
@@ -26,6 +26,7 @@ import (
"github.com/gravitational/trace"
beamsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/beams/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/lib/client"
)
@@ -85,7 +86,7 @@ func sshBeam(cf *CLIConf, tc *client.TeleportClient, beam *beamsv1.Beam, command
}
target := fmt.Sprintf("%s:0", beam.GetStatus().GetNodeId())
tc.HostLogin = beamsLogin
tc.HostLogin = types.BeamsLogin
tc.Stdin = cf.Stdin()
retry, err := retryutils.NewLinear(retryutils.LinearConfig{