Skip to content

Commit

Permalink
Add host fields and protos for ssh identities (#51024)
Browse files Browse the repository at this point in the history
* add host fields to sshca.Identity

* add ssh identity proto
  • Loading branch information
fspmarshall authored Jan 27, 2025
1 parent b3904f4 commit 5791a98
Show file tree
Hide file tree
Showing 25 changed files with 1,402 additions and 278 deletions.
674 changes: 654 additions & 20 deletions api/gen/proto/go/teleport/decision/v1alpha1/ssh_identity.pb.go

Large diffs are not rendered by default.

167 changes: 166 additions & 1 deletion api/proto/teleport/decision/v1alpha1/ssh_identity.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,174 @@ syntax = "proto3";

package teleport.decision.v1alpha1;

import "google/protobuf/timestamp.proto";
import "teleport/decision/v1alpha1/tls_identity.proto";
import "teleport/trait/v1/trait.proto";

option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1;decisionpb";

// SSHIdentity is the identity used for SSH connections.
message SSHIdentity {
// TBD
// --- common identity fields ---

// ValidAfter is the unix timestamp that marks the start time for when the certificate should
// be considered valid.
uint64 valid_after = 1;

// ValidBefore is the unix timestamp that marks the end time for when the certificate should
// be considered valid.
uint64 valid_before = 2;

// CertType indicates what type of cert this is (user or host).
uint32 cert_type = 3;

// 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).
repeated string principals = 4;

// --- host identity fields ---

// ClusterName is the name of the cluster within which a node lives
string cluster_name = 5;
// SystemRole identifies the system role of a Teleport instance
string system_role = 6;

// -- user identity fields ---

// Username is teleport username
string username = 7;

// Impersonator is set when a user requests certificate for another user
string impersonator = 8;

// PermitX11Forwarding permits X11 forwarding for this cert
bool permit_x11_forwarding = 9;

// PermitAgentForwarding permits agent forwarding for this cert
bool permit_agent_forwarding = 10;

// PermitPortForwarding permits port forwarding.
bool permit_port_forwarding = 11;

// Roles is a list of roles assigned to this user
repeated string roles = 12;

// RouteToCluster specifies the target cluster
// if present in the certificate, will be used
// to route the requests to
string route_to_cluster = 13;

// Traits hold claim data used to populate a role at runtime.
repeated teleport.trait.v1.Trait traits = 14;

// ActiveRequests tracks privilege escalation requests applied during
// certificate construction.
repeated string active_requests = 15;

// MFAVerified is the UUID of an MFA device when this Identity was
// confirmed immediately after an MFA check.
string mfa_verified = 16;

// 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.
google.protobuf.Timestamp previous_identity_expires = 17;

// LoginIP is an observed IP of the client on the moment of certificate creation.
string login_ip = 18;

// PinnedIP is an IP from which client must communicate with Teleport.
string pinned_ip = 19;

// DisallowReissue flags that any attempt to request new certificates while
// authenticated with this cert should be denied.
bool disallow_reissue = 20;

// 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).
repeated CertExtension certificate_extensions = 21;

// Renewable indicates this certificate is renewable.
bool renewable = 22;

// 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.
uint64 generation = 23;

// BotName is set to the name of the bot, if the user is a Machine ID bot user.
// Empty for human users.
string bot_name = 24;

// BotInstanceID is the unique identifier for the bot instance, if this is a
// Machine ID bot. It is empty for human users.
string bot_instance_id = 25;

// AllowedResourceIDs lists the resources the user should be able to access.
repeated ResourceId allowed_resource_ids = 26;

// ConnectionDiagnosticID references the ConnectionDiagnostic that we should use to append traces when testing a Connection.
string connection_diagnostic_id = 27;

// PrivateKeyPolicy is the private key policy supported by this certificate.
string private_key_policy = 28;

// DeviceID is the trusted device identifier.
string device_id = 29;

// DeviceAssetTag is the device inventory identifier.
string device_asset_tag = 30;

// DeviceCredentialID is the identifier for the credential used by the device
// to authenticate itself.
string device_credential_id = 31;

// GitHubUserID indicates the GitHub user ID identified by the GitHub
// connector.
string github_user_id = 32;

// GitHubUsername indicates the GitHub username identified by the GitHub
// connector.
string github_username = 33;
}

// CertExtensionMode specifies the type of extension to use in the cert. This type
// must be kept up to date with types.CertExtensionMode.
enum CertExtensionMode {
// CERT_EXTENSION_MODE_UNSPECIFIED is the default value and should not be used.
CERT_EXTENSION_MODE_UNSPECIFIED = 0;

// EXTENSION represents a cert extension that may or may not be
// honored by the server.
CERT_EXTENSION_MODE_EXTENSION = 1;
}

// CertExtensionType represents the certificate type the extension is for.
// Currently only ssh is supported. This type must be kept up to date with
// types.CertExtensionType.
enum CertExtensionType {
// CERT_EXTENSION_TYPE_UNSPECIFIED is the default value and should not be used.
CERT_EXTENSION_TYPE_UNSPECIFIED = 0;

// SSH is used when extending an ssh certificate
CERT_EXTENSION_TYPE_SSH = 1;
}

// CertExtension represents a key/value for a certificate extension. This type must
// be kept up to date with types.CertExtension.
message CertExtension {
// Type represents the certificate type being extended, only ssh
// is supported at this time.
// 0 is "ssh".
CertExtensionType type = 1;
// Mode is the type of extension to be used -- currently
// critical-option is not supported.
// 0 is "extension".
CertExtensionMode mode = 2;
// Name specifies the key to be used in the cert extension.
string name = 3;
// Value specifies the value to be used in the cert extension.
string value = 4;
}
9 changes: 6 additions & 3 deletions integration/helpers/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import (
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
Expand Down Expand Up @@ -373,14 +374,16 @@ func NewInstance(t *testing.T, cfg InstanceConfig) *TeleInstance {
fatalIf(err)

keygen := keygen.New(context.TODO())
cert, err := keygen.GenerateHostCert(services.HostCertParams{
cert, err := keygen.GenerateHostCert(sshca.HostCertificateRequest{
CASigner: sshSigner,
PublicHostKey: cfg.Pub,
HostID: cfg.HostID,
NodeName: cfg.NodeName,
ClusterName: cfg.ClusterName,
Role: types.RoleAdmin,
TTL: 24 * time.Hour,
Identity: sshca.Identity{
ClusterName: cfg.ClusterName,
SystemRole: types.RoleAdmin,
},
})
fatalIf(err)
tlsCA, err := tlsca.FromKeys(tlsCACert, cfg.Priv)
Expand Down
45 changes: 22 additions & 23 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2130,28 +2130,30 @@ func (a *Server) GenerateHostCert(ctx context.Context, hostPublicKey []byte, hos
}

// create and sign!
return a.generateHostCert(ctx, services.HostCertParams{
return a.generateHostCert(ctx, sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: hostPublicKey,
HostID: hostID,
NodeName: nodeName,
Principals: principals,
ClusterName: clusterName,
Role: role,
TTL: ttl,
Identity: sshca.Identity{
Principals: principals,
ClusterName: clusterName,
SystemRole: role,
},
})
}

func (a *Server) generateHostCert(
ctx context.Context, p services.HostCertParams,
ctx context.Context, req sshca.HostCertificateRequest,
) ([]byte, error) {
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

var locks []types.LockTarget
switch p.Role {
switch req.Identity.SystemRole {
case types.RoleNode:
// Node role is a special case because it was previously suported as a
// lock target that only locked the `ssh_service`. If the same Teleport server
Expand All @@ -2164,17 +2166,17 @@ func (a *Server) generateHostCert(
// and `Node` fields if the role is `Node` so that the previous behavior
// is preserved.
// This is a legacy behavior that we need to support for backwards compatibility.
locks = []types.LockTarget{{ServerID: p.HostID, Node: p.HostID}, {ServerID: HostFQDN(p.HostID, p.ClusterName), Node: HostFQDN(p.HostID, p.ClusterName)}}
locks = []types.LockTarget{{ServerID: req.HostID, Node: req.HostID}, {ServerID: HostFQDN(req.HostID, req.Identity.ClusterName), Node: HostFQDN(req.HostID, req.Identity.ClusterName)}}
default:
locks = []types.LockTarget{{ServerID: p.HostID}, {ServerID: HostFQDN(p.HostID, p.ClusterName)}}
locks = []types.LockTarget{{ServerID: req.HostID}, {ServerID: HostFQDN(req.HostID, req.Identity.ClusterName)}}
}
if lockErr := a.checkLockInForce(readOnlyAuthPref.GetLockingMode(),
locks,
); lockErr != nil {
return nil, trace.Wrap(lockErr)
}

return a.Authority.GenerateHostCert(p)
return a.Authority.GenerateHostCert(req)
}

// GetKeyStore returns the KeyStore used by the auth server
Expand Down Expand Up @@ -2226,7 +2228,7 @@ type certRequest struct {
traits wrappers.Traits
// activeRequests tracks privilege escalation requests applied
// during the construction of the certificate.
activeRequests services.RequestIDs
activeRequests []string
// appSessionID is the session ID of the application session.
appSessionID string
// appPublicAddr is the public address of the application.
Expand Down Expand Up @@ -3081,7 +3083,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
defaultMode: readOnlyAuthPref.GetLockingMode(),
username: req.user.GetName(),
mfaVerified: req.mfaVerified,
activeAccessRequests: req.activeRequests.AccessRequests,
activeAccessRequests: req.activeRequests,
deviceID: req.deviceExtensions.DeviceID,
}); err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -3210,11 +3212,6 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
// All users have access to this and join RBAC rules are checked after the connection is established.
allowedLogins = append(allowedLogins, teleport.SSHSessionJoinPrincipal)

requestedResourcesStr, err := types.ResourceIDsToString(req.checker.GetAllowedResourceIDs())
if err != nil {
return nil, trace.Wrap(err)
}

pinnedIP := ""
if caType == types.UserCA && (req.checker.PinSourceIP() || req.pinIP) {
if req.loginIP == "" {
Expand Down Expand Up @@ -3254,7 +3251,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
Identity: sshca.Identity{
Username: req.user.GetName(),
Impersonator: req.impersonator,
AllowedLogins: allowedLogins,
Principals: allowedLogins,
Roles: req.checker.RoleNames(),
PermitPortForwarding: req.checker.CanPortForward(),
PermitAgentForwarding: req.checker.CanForwardAgents(),
Expand All @@ -3272,7 +3269,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
BotName: req.botName,
BotInstanceID: req.botInstanceID,
CertificateExtensions: req.checker.CertificateExtensions(),
AllowedResourceIDs: requestedResourcesStr,
AllowedResourceIDs: req.checker.GetAllowedResourceIDs(),
ConnectionDiagnosticID: req.connectionDiagnosticID,
PrivateKeyPolicy: attestedKeyPolicy,
DeviceID: req.deviceExtensions.DeviceID,
Expand Down Expand Up @@ -3367,7 +3364,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
AWSRoleARNs: roleARNs,
AzureIdentities: azureIdentities,
GCPServiceAccounts: gcpAccounts,
ActiveRequests: req.activeRequests.AccessRequests,
ActiveRequests: req.activeRequests,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
Expand Down Expand Up @@ -4734,14 +4731,16 @@ func (a *Server) GenerateHostCerts(ctx context.Context, req *proto.HostCertsRequ
return nil, trace.Wrap(err)
}
// generate host SSH certificate
hostSSHCert, err := a.generateHostCert(ctx, services.HostCertParams{
hostSSHCert, err := a.generateHostCert(ctx, sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: req.PublicSSHKey,
HostID: req.HostID,
NodeName: req.NodeName,
ClusterName: clusterName.GetClusterName(),
Role: req.Role,
Principals: req.AdditionalPrincipals,
Identity: sshca.Identity{
ClusterName: clusterName.GetClusterName(),
SystemRole: req.Role,
Principals: req.AdditionalPrincipals,
},
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2642,7 +2642,7 @@ func TestGenerateUserCertWithLocks(t *testing.T) {
mfaVerified: mfaID,
sshPublicKey: sshPubKey,
tlsPublicKey: tlsPubKey,
activeRequests: services.RequestIDs{AccessRequests: []string{requestID}},
activeRequests: []string{requestID},
deviceExtensions: DeviceExtensions{
DeviceID: deviceID,
AssetTag: "assettag1",
Expand Down
8 changes: 3 additions & 5 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3440,11 +3440,9 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
checker: checker,
// Copy IP from current identity to the generated certificate, if present,
// to avoid generateUserCerts() being used to drop IP pinning in the new certificates.
loginIP: a.context.Identity.GetIdentity().LoginIP,
traits: accessInfo.Traits,
activeRequests: services.RequestIDs{
AccessRequests: req.AccessRequests,
},
loginIP: a.context.Identity.GetIdentity().LoginIP,
traits: accessInfo.Traits,
activeRequests: req.AccessRequests,
connectionDiagnosticID: req.ConnectionDiagnosticID,
botName: getBotName(user),

Expand Down
Loading

0 comments on commit 5791a98

Please sign in to comment.