Files
Noah Stride 25347ffd67 Encode Bot Scope into TLS/SSH identity (#68385)
* Encode Bot Scope into TLS/SSH identity

* fix
2026-07-06 17:27:36 +00:00

669 lines
26 KiB
Go

/*
* Teleport
* Copyright (C) 2025 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 sshca specifies interfaces for SSH certificate authorities
package sshca
import (
"fmt"
"maps"
"strconv"
"strings"
"time"
"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/wrappers"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/scopes/pinning"
"github.com/gravitational/teleport/lib/utils"
)
// Identity is a user identity. All identity fields map directly to an ssh certificate field.
type Identity struct {
// --- common identity fields ---
// ValidAfter is the unix timestamp that marks the start time for when the certificate should
// be considered valid.
ValidAfter uint64
// ValidBefore is the unix timestamp that marks the end time for when the certificate should
// be considered valid.
ValidBefore uint64
// CertType indicates what type of cert this is (user or host).
CertType uint32
// Principals is the list of SSH principals associated with the certificate (this means the
// list of allowed unix logins in the case of user certs).
Principals []string
// --- host identity fields ---
// ClusterName is the name of the cluster within which a node lives
ClusterName string
// SystemRole identifies the system role of a Teleport instance
SystemRole types.SystemRole
// -- user identity fields ---
// Username is teleport username
Username string
// ScopePin is an optional pin that ties the certificate to a specific scope and set of scoped roles. When
// set, the Roles field must not be set.
ScopePin *scopesv1.Pin
// Impersonator is set when a user requests certificate for another user
Impersonator string
// PermitX11Forwarding permits X11 forwarding for this cert
PermitX11Forwarding bool
// PermitAgentForwarding permits agent forwarding for this cert
PermitAgentForwarding bool
// PermitPortForwarding permits port forwarding.
PermitPortForwarding bool
// Roles is a list of roles assigned to this user
Roles []string
// RouteToCluster specifies the target cluster
// if present in the certificate, will be used
// to route the requests to
RouteToCluster string
// Traits hold claim data used to populate a role at runtime.
Traits wrappers.Traits
// ActiveRequests tracks privilege escalation requests applied during
// certificate construction.
ActiveRequests []string
// MFAVerified is the UUID of an MFA device when this Identity was
// confirmed immediately after an MFA check.
MFAVerified string
// PreviousIdentityExpires is the expiry time of the identity/cert that this
// identity/cert was derived from. It is used to determine a session's hard
// deadline in cases where both require_session_mfa and disconnect_expired_cert
// are enabled. See https://github.com/gravitational/teleport/issues/18544.
PreviousIdentityExpires time.Time
// LoginIP is an observed IP of the client on the moment of certificate creation.
LoginIP string
// PinnedIP is an IP from which client must communicate with Teleport. If set,
// this implies that IP Pinning was required at the time of certificate creation.
PinnedIP string
// DisallowReissue flags that any attempt to request new certificates while
// authenticated with this cert should be denied.
DisallowReissue bool
// CertificateExtensions are user configured ssh key extensions (note: this field also
// ends up aggregating all *unknown* extensions during cert parsing, meaning that this
// can sometimes contain fields that were inserted by a newer version of teleport).
CertificateExtensions []*types.CertExtension
// Renewable indicates this certificate is renewable.
Renewable bool
// Generation counts the number of times a certificate has been renewed, with a generation of 1
// meaning the cert has never been renewed. A generation of zero means the cert's generation is
// not being tracked.
Generation uint64
// BotName is set to the name of the bot, if the user is a Machine ID bot user.
// Empty for human users.
BotName string
// BotInstanceID is the unique identifier for the bot instance, if this is a
// Machine ID bot. It is empty for human users.
BotInstanceID string
// BotScope is the scope of the Machine ID bot this identity was issued to,
// if any. Empty for unscoped bots and human users.
BotScope string
// JoinToken is the name of the join token used by the bot to join, if any.
JoinToken string
// AllowedResourceIDs lists the resources the user should be able to access.
//
// Deprecated: Use [Identity.AllowedResourceAccessIDs].
AllowedResourceIDs []types.ResourceID
// AllowedResourceAccessIDs lists the resources the user should be able to access,
// paired with ResourceConstraints or additional information.
AllowedResourceAccessIDs []types.ResourceAccessID
// ConnectionDiagnosticID references the ConnectionDiagnostic that we should use to append traces when testing a Connection.
ConnectionDiagnosticID string
// PrivateKeyPolicy is the private key policy supported by this certificate.
PrivateKeyPolicy keys.PrivateKeyPolicy
// DeviceID is the trusted device identifier.
DeviceID string
// DeviceAssetTag is the device inventory identifier.
DeviceAssetTag string
// DeviceCredentialID is the identifier for the credential used by the device
// to authenticate itself.
DeviceCredentialID string
// GitHubUserID indicates the GitHub user ID identified by the GitHub
// connector.
GitHubUserID string
// GitHubUsername indicates the GitHub username identified by the GitHub
// connector.
GitHubUsername string
// DelegationSessionID is the identifier of the Delegation Session this
// certificate was created for.
DelegationSessionID string
// BeamID is the identifier of the Beam this certificate was created for,
// derived from the delegation session's types.BeamIDLabel label.
BeamID string
// HeadlessAuthenticationID is the ID of the headless authentication
// resource this certificate is being generated for.
HeadlessAuthenticationID string
// AgentScope is the scope this identity belongs to.
AgentScope string
// ImmutableLabelHash is the immutable label hash used to verify
// immutable labels against the identity.
ImmutableLabelHash string
}
// Encode encodes the identity into an ssh certificate. Note that the returned certificate is incomplete
// and must be have its public key set before signing.
func (i *Identity) Encode(certFormat string) (*ssh.Certificate, error) {
validBefore := i.ValidBefore
if validBefore == 0 {
validBefore = uint64(ssh.CertTimeInfinity)
}
validAfter := i.ValidAfter
if validAfter == 0 {
validAfter = uint64(time.Now().UTC().Add(-1 * time.Minute).Unix())
}
if i.CertType == 0 {
return nil, trace.BadParameter("cannot encode ssh identity missing required field CertType")
}
// Agent scope pins encode role and scope exclusively via the ScopePin field.
// Mixing in the legacy SystemRole or AgentScope fields would allow outdated
// infrastructure to misinterpret a scoped-agent cert as an unscoped one.
if i.ScopePin.GetKind() == scopesv1.PinKind_PIN_KIND_AGENT {
if i.SystemRole != "" {
return nil, trace.BadParameter("cannot encode ssh certificate for agent scope pin with SystemRole set; encode roles via ScopePin.SystemRoles")
}
if i.AgentScope != "" {
return nil, trace.BadParameter("cannot encode ssh certificate for agent scope pin with AgentScope set; encode scope via ScopePin.Scope")
}
}
cert := &ssh.Certificate{
// we have to use key id to identify teleport user
KeyId: i.Username,
ValidPrincipals: i.Principals,
ValidAfter: validAfter,
ValidBefore: validBefore,
CertType: i.CertType,
}
cert.Permissions.Extensions = make(map[string]string)
if i.CertType == ssh.UserCert {
cert.Permissions.Extensions[teleport.CertExtensionPermitPTY] = ""
}
// --- host extensions ---
if sr := i.SystemRole.String(); sr != "" {
cert.Permissions.Extensions[utils.CertExtensionRole] = sr
}
if i.ClusterName != "" {
cert.Permissions.Extensions[utils.CertExtensionAuthority] = i.ClusterName
}
if i.AgentScope != "" {
cert.Permissions.Extensions[teleport.CertExtensionAgentScope] = i.AgentScope
}
if i.ImmutableLabelHash != "" {
cert.Permissions.Extensions[teleport.CertExtensionImmutableLabelHash] = i.ImmutableLabelHash
}
// --- user extensions ---
if i.ScopePin != nil {
encoded, err := pinning.Encode(i.ScopePin)
if err != nil {
return nil, trace.Errorf("failed to encode scope pin for ssh cert: %w", err)
}
var ext string
switch i.ScopePin.GetKind() {
case scopesv1.PinKind_PIN_KIND_USER:
ext = teleport.CertExtensionScopePin
case scopesv1.PinKind_PIN_KIND_AGENT:
ext = teleport.CertExtensionAgentScopePin
default:
return nil, trace.BadParameter("cannot encode scope pin with unknown or unspecified kind %v", i.ScopePin.GetKind())
}
cert.Permissions.Extensions[ext] = encoded
}
if i.PermitX11Forwarding {
cert.Permissions.Extensions[teleport.CertExtensionPermitX11Forwarding] = ""
}
if i.PermitAgentForwarding {
cert.Permissions.Extensions[teleport.CertExtensionPermitAgentForwarding] = ""
}
if i.PermitPortForwarding {
cert.Permissions.Extensions[teleport.CertExtensionPermitPortForwarding] = ""
}
if i.MFAVerified != "" {
cert.Permissions.Extensions[teleport.CertExtensionMFAVerified] = i.MFAVerified
}
if !i.PreviousIdentityExpires.IsZero() {
cert.Permissions.Extensions[teleport.CertExtensionPreviousIdentityExpires] = i.PreviousIdentityExpires.Format(time.RFC3339)
}
if i.LoginIP != "" {
cert.Permissions.Extensions[teleport.CertExtensionLoginIP] = i.LoginIP
}
if i.Impersonator != "" {
cert.Permissions.Extensions[teleport.CertExtensionImpersonator] = i.Impersonator
}
if i.DisallowReissue {
cert.Permissions.Extensions[teleport.CertExtensionDisallowReissue] = ""
}
if i.Renewable {
cert.Permissions.Extensions[teleport.CertExtensionRenewable] = ""
}
if i.Generation > 0 {
cert.Permissions.Extensions[teleport.CertExtensionGeneration] = fmt.Sprint(i.Generation)
}
if i.BotName != "" {
cert.Permissions.Extensions[teleport.CertExtensionBotName] = i.BotName
}
if i.BotInstanceID != "" {
cert.Permissions.Extensions[teleport.CertExtensionBotInstanceID] = i.BotInstanceID
}
if i.BotScope != "" {
cert.Permissions.Extensions[teleport.CertExtensionBotScope] = i.BotScope
}
if i.JoinToken != "" {
cert.Permissions.Extensions[teleport.CertExtensionJoinToken] = i.JoinToken
}
//nolint:staticcheck // TODO(kiosion): deprecated, to be removed in v21
if len(i.AllowedResourceIDs) != 0 {
//nolint:staticcheck // TODO(kiosion): deprecated, to be removed in v21
requestedResourcesStr, err := types.ResourceIDsToString(i.AllowedResourceIDs)
if err != nil {
return nil, trace.Wrap(err)
}
cert.Permissions.Extensions[teleport.CertExtensionAllowedResources] = requestedResourcesStr
} else if len(i.AllowedResourceAccessIDs) != 0 {
// If an identity is resource-constrained exclusively via AllowedResourceAccessIDs,
// we add a non-matching sentinel ResourceID into AllowedResourceIDs.
// This prevents authorization paths that only parse AllowedResourceIDs and ignore AllowedResourceAccessIDs
// (e.g., older Auths in mixed-version clusters) from interpreting an empty AllowedResourceIDs slice as
// "no resource-specific restrictions".
// TODO(kiosion): DELETE in 21.0.0
sentinelResourceIDStr, err := types.ResourceIDsToString([]types.ResourceID{types.CreateSentinelResourceID()})
if err != nil {
return nil, trace.Wrap(err)
}
cert.Permissions.Extensions[teleport.CertExtensionAllowedResources] = sentinelResourceIDStr
}
if len(i.AllowedResourceAccessIDs) != 0 {
allowedResourceAccessIDsStr, err := types.ResourceAccessIDsToString(i.AllowedResourceAccessIDs)
if err != nil {
return nil, trace.Wrap(err)
}
cert.Permissions.Extensions[teleport.CertExtensionAllowedResourceAccessIDs] = allowedResourceAccessIDsStr
}
if i.ConnectionDiagnosticID != "" {
cert.Permissions.Extensions[teleport.CertExtensionConnectionDiagnosticID] = i.ConnectionDiagnosticID
}
if i.PrivateKeyPolicy != "" {
cert.Permissions.Extensions[teleport.CertExtensionPrivateKeyPolicy] = string(i.PrivateKeyPolicy)
}
if devID := i.DeviceID; devID != "" {
cert.Permissions.Extensions[teleport.CertExtensionDeviceID] = devID
}
if assetTag := i.DeviceAssetTag; assetTag != "" {
cert.Permissions.Extensions[teleport.CertExtensionDeviceAssetTag] = assetTag
}
if credID := i.DeviceCredentialID; credID != "" {
cert.Permissions.Extensions[teleport.CertExtensionDeviceCredentialID] = credID
}
if i.DelegationSessionID != "" {
cert.Permissions.Extensions[teleport.CertExtensionDelegationSessionID] = i.DelegationSessionID
}
if i.BeamID != "" {
cert.Permissions.Extensions[teleport.CertExtensionBeamID] = i.BeamID
}
if i.GitHubUserID != "" {
cert.Permissions.Extensions[teleport.CertExtensionGitHubUserID] = i.GitHubUserID
}
if i.GitHubUsername != "" {
cert.Permissions.Extensions[teleport.CertExtensionGitHubUsername] = i.GitHubUsername
}
if i.HeadlessAuthenticationID != "" {
cert.Permissions.Extensions[teleport.CertExtensionHeadlessAuthenticationID] = i.HeadlessAuthenticationID
}
if i.PinnedIP != "" {
if cert.CriticalOptions == nil {
cert.CriticalOptions = make(map[string]string)
}
// IPv4, all bits matter
ip := i.PinnedIP + "/32"
if strings.Contains(i.PinnedIP, ":") {
// IPv6
ip = i.PinnedIP + "/128"
}
cert.CriticalOptions[teleport.CertCriticalOptionSourceAddress] = ip
}
for _, extension := range i.CertificateExtensions {
// TODO(lxea): update behavior when non ssh, non extensions are supported.
if extension.Mode != types.CertExtensionMode_EXTENSION ||
extension.Type != types.CertExtensionType_SSH {
continue
}
// Beam IDs are server-derived from delegation sessions and must not be
// spoofed or overwritten by role-supplied certificate extensions.
if extension.Name == teleport.CertExtensionBeamID {
continue
}
cert.Extensions[extension.Name] = extension.Value
}
// Add roles, traits, and route to cluster in the certificate extensions if
// the standard format was requested. Certificate extensions are not included
// legacy SSH certificates due to a bug in OpenSSH <= OpenSSH 7.1:
// https://bugzilla.mindrot.org/show_bug.cgi?id=2387
if certFormat == constants.CertificateFormatStandard {
traits, err := wrappers.MarshalTraits(&i.Traits)
if err != nil {
return nil, trace.Wrap(err)
}
if len(traits) > 0 {
cert.Permissions.Extensions[teleport.CertExtensionTeleportTraits] = string(traits)
}
if len(i.Roles) != 0 {
roles := certRoles{
Roles: i.Roles,
}
rolesExt, err := roles.Marshal()
if err != nil {
return nil, trace.Wrap(err)
}
cert.Permissions.Extensions[teleport.CertExtensionTeleportRoles] = string(rolesExt)
}
if i.RouteToCluster != "" {
cert.Permissions.Extensions[teleport.CertExtensionTeleportRouteToCluster] = i.RouteToCluster
}
if len(i.ActiveRequests) != 0 {
reqs := requestIDs{
AccessRequests: i.ActiveRequests,
}
reqsExt, err := reqs.Marshal()
if err != nil {
return nil, trace.Wrap(err)
}
cert.Permissions.Extensions[teleport.CertExtensionTeleportActiveRequests] = string(reqsExt)
}
}
return cert, nil
}
// GetDeviceMetadata returns information about user's trusted device.
func (i *Identity) GetDeviceMetadata() *apievents.DeviceMetadata {
if i == nil {
return nil
}
if i.DeviceID == "" && i.DeviceAssetTag == "" && i.DeviceCredentialID == "" {
return nil
}
return &apievents.DeviceMetadata{
DeviceId: i.DeviceID,
AssetTag: i.DeviceAssetTag,
CredentialId: i.DeviceCredentialID,
}
}
// GetValidBefore gets the ValidBefore time as a time.Time, preserving "zeroness" across the conversion.
func (i *Identity) GetValidBefore() time.Time {
var validBefore time.Time
if i.ValidBefore != 0 {
validBefore = time.Unix(int64(i.ValidBefore), 0)
}
return validBefore
}
// IsBot returns whether this identity belongs to a bot.
func (id *Identity) IsBot() bool {
return id.BotName != ""
}
// IsDelegationSession returns whether this identity was created for a Delegation Session.
func (id *Identity) IsDelegationSession() bool {
return id.DelegationSessionID != ""
}
// DecodeIdentity decodes an ssh certificate into an identity.
func DecodeIdentity(cert *ssh.Certificate) (*Identity, error) {
ident := &Identity{
Username: cert.KeyId,
Principals: cert.ValidPrincipals,
ValidAfter: cert.ValidAfter,
ValidBefore: cert.ValidBefore,
CertType: cert.CertType,
}
// clone the extension map and remove entries from the clone as they are processed so
// that we can easily aggregate the remainder into the CertificateExtensions field.
extensions := maps.Clone(cert.Extensions)
takeExtension := func(name string) (value string, ok bool) {
v, ok := extensions[name]
if !ok {
return "", false
}
delete(extensions, name)
return v, true
}
takeValue := func(name string) string {
value, _ := takeExtension(name)
return value
}
takeBool := func(name string) bool {
_, ok := takeExtension(name)
return ok
}
// ignore the permit pty extension, teleport considers this permission implied for all users
_, _ = takeExtension(teleport.CertExtensionPermitPTY)
// --- host extensions ---
if v, ok := takeExtension(utils.CertExtensionRole); ok {
ident.SystemRole = types.SystemRole(v)
}
ident.ClusterName = takeValue(utils.CertExtensionAuthority)
// --- user extensions ---
if v, ok := takeExtension(teleport.CertExtensionScopePin); ok {
pin, err := pinning.Decode(v)
if err != nil {
return nil, trace.BadParameter("failed to decode value %q for extension %q as scope pin: %v", v, teleport.CertExtensionScopePin, err)
}
// Certs issued before PinKind was introduced will have UNSPECIFIED here.
// Pins decoded from the user OID are always user pins.
if pin.GetKind() == scopesv1.PinKind_PIN_KIND_UNSPECIFIED {
pin.SetKind(scopesv1.PinKind_PIN_KIND_USER)
}
ident.ScopePin = pin
}
if v, ok := takeExtension(teleport.CertExtensionAgentScopePin); ok {
pin, err := pinning.Decode(v)
if err != nil {
return nil, trace.BadParameter("failed to decode value %q for extension %q as agent scope pin: %v", v, teleport.CertExtensionAgentScopePin, err)
}
ident.ScopePin = pin
}
ident.AgentScope = takeValue(teleport.CertExtensionAgentScope)
ident.PermitX11Forwarding = takeBool(teleport.CertExtensionPermitX11Forwarding)
ident.PermitAgentForwarding = takeBool(teleport.CertExtensionPermitAgentForwarding)
ident.PermitPortForwarding = takeBool(teleport.CertExtensionPermitPortForwarding)
ident.MFAVerified = takeValue(teleport.CertExtensionMFAVerified)
if v, ok := takeExtension(teleport.CertExtensionPreviousIdentityExpires); ok {
t, err := time.Parse(time.RFC3339, v)
if err != nil {
return nil, trace.BadParameter("failed to parse value %q for extension %q as RFC3339 timestamp: %v", v, teleport.CertExtensionPreviousIdentityExpires, err)
}
ident.PreviousIdentityExpires = t
}
ident.LoginIP = takeValue(teleport.CertExtensionLoginIP)
ident.Impersonator = takeValue(teleport.CertExtensionImpersonator)
ident.DisallowReissue = takeBool(teleport.CertExtensionDisallowReissue)
ident.Renewable = takeBool(teleport.CertExtensionRenewable)
if v, ok := takeExtension(teleport.CertExtensionGeneration); ok {
i, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return nil, trace.BadParameter("failed to parse value %q for extension %q as uint64: %v", v, teleport.CertExtensionGeneration, err)
}
ident.Generation = i
}
ident.BotName = takeValue(teleport.CertExtensionBotName)
ident.BotInstanceID = takeValue(teleport.CertExtensionBotInstanceID)
ident.BotScope = takeValue(teleport.CertExtensionBotScope)
ident.JoinToken = takeValue(teleport.CertExtensionJoinToken)
var (
allowedResourceIDs []types.ResourceID
allowedResourceAccessIDs []types.ResourceAccessID
)
if v, ok := takeExtension(teleport.CertExtensionAllowedResources); ok {
resourceIDs, err := types.ResourceIDsFromString(v)
if err != nil {
return nil, trace.BadParameter("failed to parse value %q for extension %q as resource IDs: %v", v, teleport.CertExtensionAllowedResources, err)
}
filteredResourceIDs := make([]types.ResourceID, 0, len(resourceIDs))
for _, rid := range resourceIDs {
// AllowedResourceIDs may contain a non-matching sentinel whose sole purpose is to prevent
// authorization paths (e.g., older versions operating in mixed-version clusters) that ignore
// AllowedResourceAccessIDs from treating an otherwise resource-scoped identity as unconstrained.
//
// It should be filtered out at decoding here, as it's not a real "requested resource".
if types.IsSentinelResourceID(rid) {
continue
}
filteredResourceIDs = append(filteredResourceIDs, rid)
}
allowedResourceIDs = filteredResourceIDs
}
if v, ok := takeExtension(teleport.CertExtensionAllowedResourceAccessIDs); ok {
resourceAccessIDs, err := types.ResourceAccessIDsFromString(v)
if err != nil {
return nil, trace.BadParameter("failed to parse value %q for extension %q as resourceAccessIDs: %v", v, teleport.CertExtensionAllowedResourceAccessIDs, err)
}
allowedResourceAccessIDs = resourceAccessIDs
}
if len(allowedResourceAccessIDs) > 0 {
// Prefer new extension when present.
ident.AllowedResourceAccessIDs = allowedResourceAccessIDs
// Populate AllowedResourceIDs with any present unconstrained resources,
// so any path re-encoding this identity persists the resourceIDs
// to the legacy extension instead of adding a sentinel.
//
// TODO(kiosion): DELETE in 20.0.0
ident.AllowedResourceIDs, _ = types.UnwrapResourceAccessIDs(allowedResourceAccessIDs)
} else if len(allowedResourceIDs) > 0 {
// Fallback for certs from older Auths that don't write the new extension.
ident.AllowedResourceAccessIDs = types.CombineAsResourceAccessIDs(allowedResourceIDs, nil)
//nolint:staticcheck // TODO(kiosion): deprecated, to be removed in v20
ident.AllowedResourceIDs = allowedResourceIDs
}
ident.ConnectionDiagnosticID = takeValue(teleport.CertExtensionConnectionDiagnosticID)
ident.PrivateKeyPolicy = keys.PrivateKeyPolicy(takeValue(teleport.CertExtensionPrivateKeyPolicy))
ident.DeviceID = takeValue(teleport.CertExtensionDeviceID)
ident.DeviceAssetTag = takeValue(teleport.CertExtensionDeviceAssetTag)
ident.DeviceCredentialID = takeValue(teleport.CertExtensionDeviceCredentialID)
ident.DelegationSessionID = takeValue(teleport.CertExtensionDelegationSessionID)
ident.BeamID = takeValue(teleport.CertExtensionBeamID)
ident.GitHubUserID = takeValue(teleport.CertExtensionGitHubUserID)
ident.GitHubUsername = takeValue(teleport.CertExtensionGitHubUsername)
ident.HeadlessAuthenticationID = takeValue(teleport.CertExtensionHeadlessAuthenticationID)
if v, ok := cert.CriticalOptions[teleport.CertCriticalOptionSourceAddress]; ok {
parts := strings.Split(v, "/")
if len(parts) != 2 {
return nil, trace.BadParameter("failed to parse value %q for critical option %q as CIDR", v, teleport.CertCriticalOptionSourceAddress)
}
ident.PinnedIP = parts[0]
}
if v, ok := takeExtension(teleport.CertExtensionTeleportTraits); ok {
var traits wrappers.Traits
if err := wrappers.UnmarshalTraits([]byte(v), &traits); err != nil {
return nil, trace.BadParameter("failed to unmarshal value %q for extension %q as traits: %v", v, teleport.CertExtensionTeleportTraits, err)
}
ident.Traits = traits
}
if v, ok := takeExtension(teleport.CertExtensionTeleportRoles); ok {
var roles certRoles
if err := roles.Unmarshal([]byte(v)); err != nil {
return nil, trace.BadParameter("failed to unmarshal value %q for extension %q as roles: %v", v, teleport.CertExtensionTeleportRoles, err)
}
ident.Roles = roles.Roles
}
ident.RouteToCluster = takeValue(teleport.CertExtensionTeleportRouteToCluster)
if v, ok := takeExtension(teleport.CertExtensionTeleportActiveRequests); ok {
var reqs requestIDs
if err := reqs.Unmarshal([]byte(v)); err != nil {
return nil, trace.BadParameter("failed to unmarshal value %q for extension %q as active requests: %v", v, teleport.CertExtensionTeleportActiveRequests, err)
}
ident.ActiveRequests = reqs.AccessRequests
}
ident.ImmutableLabelHash = takeValue(teleport.CertExtensionImmutableLabelHash)
// aggregate all remaining extensions into the CertificateExtensions field
for name, value := range extensions {
ident.CertificateExtensions = append(ident.CertificateExtensions, &types.CertExtension{
Name: name,
Value: value,
Type: types.CertExtensionType_SSH,
Mode: types.CertExtensionMode_EXTENSION,
})
}
return ident, nil
}
// GetPrimaryRole returns the primary role for the identity regardless of
// whether or not it is scope pinned.
func (i *Identity) GetPrimaryRole() types.SystemRole {
if i.ScopePin.GetKind() == scopesv1.PinKind_PIN_KIND_AGENT {
return types.SystemRole(i.ScopePin.GetSystemRoles().GetPrimary())
}
return i.SystemRole
}