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

Add host fields and protos for ssh identities #51024

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading