Add IssueTeleportWorkloadIdentity RPC (#66691)

* feat(workloadidentityv1): issue teleport workload identity

* refactor(workloadidentityv1): code review suggestions

* refactor(workloadidentityv1): code review suggestions

* chore(workloadidentityv1): clarify comment

* Apply suggestions from code review

Co-authored-by: Noah Stride <[email protected]>

* chore(workloadidentityv1): code review suggestions and update

---------

Co-authored-by: Noah Stride <[email protected]>
This commit is contained in:
Gabriel Corado
2026-05-20 01:52:10 +00:00
committed by GitHub
co-authored by Noah Stride
parent 54273f91fe
commit c5cb5c8cd6
5 changed files with 1146 additions and 54 deletions
@@ -37,6 +37,55 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// IdentityUsage indicates the generated identity usage.
type IdentityUsage int32
const (
// IDENTITY_USAGE_UNSPECIFIED is the `unset` default value. This should
// never be used in practice.
IdentityUsage_IDENTITY_USAGE_UNSPECIFIED IdentityUsage = 0
IdentityUsage_IDENTITY_USAGE_APP_ACCESS IdentityUsage = 1
)
// Enum value maps for IdentityUsage.
var (
IdentityUsage_name = map[int32]string{
0: "IDENTITY_USAGE_UNSPECIFIED",
1: "IDENTITY_USAGE_APP_ACCESS",
}
IdentityUsage_value = map[string]int32{
"IDENTITY_USAGE_UNSPECIFIED": 0,
"IDENTITY_USAGE_APP_ACCESS": 1,
}
)
func (x IdentityUsage) Enum() *IdentityUsage {
p := new(IdentityUsage)
*p = x
return p
}
func (x IdentityUsage) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (IdentityUsage) Descriptor() protoreflect.EnumDescriptor {
return file_teleport_workloadidentity_v1_issuance_service_proto_enumTypes[0].Descriptor()
}
func (IdentityUsage) Type() protoreflect.EnumType {
return &file_teleport_workloadidentity_v1_issuance_service_proto_enumTypes[0]
}
func (x IdentityUsage) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use IdentityUsage.Descriptor instead.
func (IdentityUsage) EnumDescriptor() ([]byte, []int) {
return file_teleport_workloadidentity_v1_issuance_service_proto_rawDescGZIP(), []int{0}
}
// The parameters for issuing an X509 SVID.
type X509SVIDParams struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -786,6 +835,236 @@ func (x *IssueWorkloadIdentitiesResponse) GetCredentials() []*Credential {
return nil
}
// The request for the IssueTeleportWorkloadIdentity RPC.
type IssueTeleportWorkloadIdentityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The parameters for issuing the credential, varying by credential type.
//
// Types that are valid to be assigned to Credential:
//
// *IssueTeleportWorkloadIdentityRequest_X509SvidParams
// *IssueTeleportWorkloadIdentityRequest_JwtSvidParams
Credential isIssueTeleportWorkloadIdentityRequest_Credential `protobuf_oneof:"credential"`
// The TTL that the client is requesting for the resulting credentials.
// This may be adjusted by the server and therefore the client MUST check the
// returned TTL rather than assuming that the requested TTL was granted.
RequestedTtl *durationpb.Duration `protobuf:"bytes,4,opt,name=requested_ttl,json=requestedTtl,proto3" json:"requested_ttl,omitempty"`
// Usage indicates what the identity will be used for.
//
// Each usage carries the information necessary for authorization and
// credentials issue.
//
// Types that are valid to be assigned to Usage:
//
// *IssueTeleportWorkloadIdentityRequest_AppAccess
Usage isIssueTeleportWorkloadIdentityRequest_Usage `protobuf_oneof:"usage"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IssueTeleportWorkloadIdentityRequest) Reset() {
*x = IssueTeleportWorkloadIdentityRequest{}
mi := &file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IssueTeleportWorkloadIdentityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IssueTeleportWorkloadIdentityRequest) ProtoMessage() {}
func (x *IssueTeleportWorkloadIdentityRequest) ProtoReflect() protoreflect.Message {
mi := &file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IssueTeleportWorkloadIdentityRequest.ProtoReflect.Descriptor instead.
func (*IssueTeleportWorkloadIdentityRequest) Descriptor() ([]byte, []int) {
return file_teleport_workloadidentity_v1_issuance_service_proto_rawDescGZIP(), []int{10}
}
func (x *IssueTeleportWorkloadIdentityRequest) GetCredential() isIssueTeleportWorkloadIdentityRequest_Credential {
if x != nil {
return x.Credential
}
return nil
}
func (x *IssueTeleportWorkloadIdentityRequest) GetX509SvidParams() *X509SVIDParams {
if x != nil {
if x, ok := x.Credential.(*IssueTeleportWorkloadIdentityRequest_X509SvidParams); ok {
return x.X509SvidParams
}
}
return nil
}
func (x *IssueTeleportWorkloadIdentityRequest) GetJwtSvidParams() *JWTSVIDParams {
if x != nil {
if x, ok := x.Credential.(*IssueTeleportWorkloadIdentityRequest_JwtSvidParams); ok {
return x.JwtSvidParams
}
}
return nil
}
func (x *IssueTeleportWorkloadIdentityRequest) GetRequestedTtl() *durationpb.Duration {
if x != nil {
return x.RequestedTtl
}
return nil
}
func (x *IssueTeleportWorkloadIdentityRequest) GetUsage() isIssueTeleportWorkloadIdentityRequest_Usage {
if x != nil {
return x.Usage
}
return nil
}
func (x *IssueTeleportWorkloadIdentityRequest) GetAppAccess() *AppAccessUsage {
if x != nil {
if x, ok := x.Usage.(*IssueTeleportWorkloadIdentityRequest_AppAccess); ok {
return x.AppAccess
}
}
return nil
}
type isIssueTeleportWorkloadIdentityRequest_Credential interface {
isIssueTeleportWorkloadIdentityRequest_Credential()
}
type IssueTeleportWorkloadIdentityRequest_X509SvidParams struct {
// The parameters for issuing an X509 SVID.
X509SvidParams *X509SVIDParams `protobuf:"bytes,1,opt,name=x509_svid_params,json=x509SvidParams,proto3,oneof"`
}
type IssueTeleportWorkloadIdentityRequest_JwtSvidParams struct {
// The parameters for issuing a JWT SVID.
JwtSvidParams *JWTSVIDParams `protobuf:"bytes,2,opt,name=jwt_svid_params,json=jwtSvidParams,proto3,oneof"`
}
func (*IssueTeleportWorkloadIdentityRequest_X509SvidParams) isIssueTeleportWorkloadIdentityRequest_Credential() {
}
func (*IssueTeleportWorkloadIdentityRequest_JwtSvidParams) isIssueTeleportWorkloadIdentityRequest_Credential() {
}
type isIssueTeleportWorkloadIdentityRequest_Usage interface {
isIssueTeleportWorkloadIdentityRequest_Usage()
}
type IssueTeleportWorkloadIdentityRequest_AppAccess struct {
// Used when the identity will be used for accessing apps.
AppAccess *AppAccessUsage `protobuf:"bytes,5,opt,name=app_access,json=appAccess,proto3,oneof"`
}
func (*IssueTeleportWorkloadIdentityRequest_AppAccess) isIssueTeleportWorkloadIdentityRequest_Usage() {
}
// The response for the IssueTeleportWorkloadIdentity RPC.
type IssueTeleportWorkloadIdentityResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The issued credential.
Credential *Credential `protobuf:"bytes,1,opt,name=credential,proto3" json:"credential,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IssueTeleportWorkloadIdentityResponse) Reset() {
*x = IssueTeleportWorkloadIdentityResponse{}
mi := &file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IssueTeleportWorkloadIdentityResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IssueTeleportWorkloadIdentityResponse) ProtoMessage() {}
func (x *IssueTeleportWorkloadIdentityResponse) ProtoReflect() protoreflect.Message {
mi := &file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IssueTeleportWorkloadIdentityResponse.ProtoReflect.Descriptor instead.
func (*IssueTeleportWorkloadIdentityResponse) Descriptor() ([]byte, []int) {
return file_teleport_workloadidentity_v1_issuance_service_proto_rawDescGZIP(), []int{11}
}
func (x *IssueTeleportWorkloadIdentityResponse) GetCredential() *Credential {
if x != nil {
return x.Credential
}
return nil
}
// AppAccessUsage used when identity will be used for accessing apps.
type AppAccessUsage struct {
state protoimpl.MessageState `protogen:"open.v1"`
// ASN.1 DER user identity certificate that the calling Teleport app
// service received from the end user.
UserCertificate []byte `protobuf:"bytes,1,opt,name=user_certificate,json=userCertificate,proto3" json:"user_certificate,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AppAccessUsage) Reset() {
*x = AppAccessUsage{}
mi := &file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AppAccessUsage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AppAccessUsage) ProtoMessage() {}
func (x *AppAccessUsage) ProtoReflect() protoreflect.Message {
mi := &file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AppAccessUsage.ProtoReflect.Descriptor instead.
func (*AppAccessUsage) Descriptor() ([]byte, []int) {
return file_teleport_workloadidentity_v1_issuance_service_proto_rawDescGZIP(), []int{12}
}
func (x *AppAccessUsage) GetUserCertificate() []byte {
if x != nil {
return x.UserCertificate
}
return nil
}
var File_teleport_workloadidentity_v1_issuance_service_proto protoreflect.FileDescriptor
const file_teleport_workloadidentity_v1_issuance_service_proto_rawDesc = "" +
@@ -841,10 +1120,29 @@ const file_teleport_workloadidentity_v1_issuance_service_proto_rawDesc = "" +
"\n" +
"credential\"m\n" +
"\x1fIssueWorkloadIdentitiesResponse\x12J\n" +
"\vcredentials\x18\x01 \x03(\v2(.teleport.workloadidentity.v1.CredentialR\vcredentials2\xcd\x02\n" +
"\vcredentials\x18\x01 \x03(\v2(.teleport.workloadidentity.v1.CredentialR\vcredentials\"\xfd\x02\n" +
"$IssueTeleportWorkloadIdentityRequest\x12X\n" +
"\x10x509_svid_params\x18\x01 \x01(\v2,.teleport.workloadidentity.v1.X509SVIDParamsH\x00R\x0ex509SvidParams\x12U\n" +
"\x0fjwt_svid_params\x18\x02 \x01(\v2+.teleport.workloadidentity.v1.JWTSVIDParamsH\x00R\rjwtSvidParams\x12>\n" +
"\rrequested_ttl\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\frequestedTtl\x12M\n" +
"\n" +
"app_access\x18\x05 \x01(\v2,.teleport.workloadidentity.v1.AppAccessUsageH\x01R\tappAccessB\f\n" +
"\n" +
"credentialB\a\n" +
"\x05usage\"q\n" +
"%IssueTeleportWorkloadIdentityResponse\x12H\n" +
"\n" +
"credential\x18\x01 \x01(\v2(.teleport.workloadidentity.v1.CredentialR\n" +
"credential\";\n" +
"\x0eAppAccessUsage\x12)\n" +
"\x10user_certificate\x18\x01 \x01(\fR\x0fuserCertificate*N\n" +
"\rIdentityUsage\x12\x1e\n" +
"\x1aIDENTITY_USAGE_UNSPECIFIED\x10\x00\x12\x1d\n" +
"\x19IDENTITY_USAGE_APP_ACCESS\x10\x012\xf8\x03\n" +
"\x1fWorkloadIdentityIssuanceService\x12\x90\x01\n" +
"\x15IssueWorkloadIdentity\x12:.teleport.workloadidentity.v1.IssueWorkloadIdentityRequest\x1a;.teleport.workloadidentity.v1.IssueWorkloadIdentityResponse\x12\x96\x01\n" +
"\x17IssueWorkloadIdentities\x12<.teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest\x1a=.teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponseBdZbgithub.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1;workloadidentityv1b\x06proto3"
"\x17IssueWorkloadIdentities\x12<.teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest\x1a=.teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponse\x12\xa8\x01\n" +
"\x1dIssueTeleportWorkloadIdentity\x12B.teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityRequest\x1aC.teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityResponseBdZbgithub.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1;workloadidentityv1b\x06proto3"
var (
file_teleport_workloadidentity_v1_issuance_service_proto_rawDescOnce sync.Once
@@ -858,47 +1156,59 @@ func file_teleport_workloadidentity_v1_issuance_service_proto_rawDescGZIP() []by
return file_teleport_workloadidentity_v1_issuance_service_proto_rawDescData
}
var file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_teleport_workloadidentity_v1_issuance_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_teleport_workloadidentity_v1_issuance_service_proto_goTypes = []any{
(*X509SVIDParams)(nil), // 0: teleport.workloadidentity.v1.X509SVIDParams
(*JWTSVIDParams)(nil), // 1: teleport.workloadidentity.v1.JWTSVIDParams
(*X509SVIDCredential)(nil), // 2: teleport.workloadidentity.v1.X509SVIDCredential
(*JWTSVIDCredential)(nil), // 3: teleport.workloadidentity.v1.JWTSVIDCredential
(*Credential)(nil), // 4: teleport.workloadidentity.v1.Credential
(*IssueWorkloadIdentityRequest)(nil), // 5: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest
(*IssueWorkloadIdentityResponse)(nil), // 6: teleport.workloadidentity.v1.IssueWorkloadIdentityResponse
(*LabelSelector)(nil), // 7: teleport.workloadidentity.v1.LabelSelector
(*IssueWorkloadIdentitiesRequest)(nil), // 8: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest
(*IssueWorkloadIdentitiesResponse)(nil), // 9: teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponse
(*durationpb.Duration)(nil), // 10: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
(*WorkloadAttrs)(nil), // 12: teleport.workloadidentity.v1.WorkloadAttrs
(IdentityUsage)(0), // 0: teleport.workloadidentity.v1.IdentityUsage
(*X509SVIDParams)(nil), // 1: teleport.workloadidentity.v1.X509SVIDParams
(*JWTSVIDParams)(nil), // 2: teleport.workloadidentity.v1.JWTSVIDParams
(*X509SVIDCredential)(nil), // 3: teleport.workloadidentity.v1.X509SVIDCredential
(*JWTSVIDCredential)(nil), // 4: teleport.workloadidentity.v1.JWTSVIDCredential
(*Credential)(nil), // 5: teleport.workloadidentity.v1.Credential
(*IssueWorkloadIdentityRequest)(nil), // 6: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest
(*IssueWorkloadIdentityResponse)(nil), // 7: teleport.workloadidentity.v1.IssueWorkloadIdentityResponse
(*LabelSelector)(nil), // 8: teleport.workloadidentity.v1.LabelSelector
(*IssueWorkloadIdentitiesRequest)(nil), // 9: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest
(*IssueWorkloadIdentitiesResponse)(nil), // 10: teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponse
(*IssueTeleportWorkloadIdentityRequest)(nil), // 11: teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityRequest
(*IssueTeleportWorkloadIdentityResponse)(nil), // 12: teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityResponse
(*AppAccessUsage)(nil), // 13: teleport.workloadidentity.v1.AppAccessUsage
(*durationpb.Duration)(nil), // 14: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp
(*WorkloadAttrs)(nil), // 16: teleport.workloadidentity.v1.WorkloadAttrs
}
var file_teleport_workloadidentity_v1_issuance_service_proto_depIdxs = []int32{
10, // 0: teleport.workloadidentity.v1.Credential.ttl:type_name -> google.protobuf.Duration
11, // 1: teleport.workloadidentity.v1.Credential.expires_at:type_name -> google.protobuf.Timestamp
2, // 2: teleport.workloadidentity.v1.Credential.x509_svid:type_name -> teleport.workloadidentity.v1.X509SVIDCredential
3, // 3: teleport.workloadidentity.v1.Credential.jwt_svid:type_name -> teleport.workloadidentity.v1.JWTSVIDCredential
0, // 4: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.x509_svid_params:type_name -> teleport.workloadidentity.v1.X509SVIDParams
1, // 5: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.jwt_svid_params:type_name -> teleport.workloadidentity.v1.JWTSVIDParams
12, // 6: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.workload_attrs:type_name -> teleport.workloadidentity.v1.WorkloadAttrs
10, // 7: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.requested_ttl:type_name -> google.protobuf.Duration
4, // 8: teleport.workloadidentity.v1.IssueWorkloadIdentityResponse.credential:type_name -> teleport.workloadidentity.v1.Credential
7, // 9: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.label_selectors:type_name -> teleport.workloadidentity.v1.LabelSelector
0, // 10: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.x509_svid_params:type_name -> teleport.workloadidentity.v1.X509SVIDParams
1, // 11: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.jwt_svid_params:type_name -> teleport.workloadidentity.v1.JWTSVIDParams
12, // 12: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.workload_attrs:type_name -> teleport.workloadidentity.v1.WorkloadAttrs
10, // 13: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.requested_ttl:type_name -> google.protobuf.Duration
4, // 14: teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponse.credentials:type_name -> teleport.workloadidentity.v1.Credential
5, // 15: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentity:input_type -> teleport.workloadidentity.v1.IssueWorkloadIdentityRequest
8, // 16: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentities:input_type -> teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest
6, // 17: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentity:output_type -> teleport.workloadidentity.v1.IssueWorkloadIdentityResponse
9, // 18: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentities:output_type -> teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponse
17, // [17:19] is the sub-list for method output_type
15, // [15:17] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
14, // 0: teleport.workloadidentity.v1.Credential.ttl:type_name -> google.protobuf.Duration
15, // 1: teleport.workloadidentity.v1.Credential.expires_at:type_name -> google.protobuf.Timestamp
3, // 2: teleport.workloadidentity.v1.Credential.x509_svid:type_name -> teleport.workloadidentity.v1.X509SVIDCredential
4, // 3: teleport.workloadidentity.v1.Credential.jwt_svid:type_name -> teleport.workloadidentity.v1.JWTSVIDCredential
1, // 4: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.x509_svid_params:type_name -> teleport.workloadidentity.v1.X509SVIDParams
2, // 5: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.jwt_svid_params:type_name -> teleport.workloadidentity.v1.JWTSVIDParams
16, // 6: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.workload_attrs:type_name -> teleport.workloadidentity.v1.WorkloadAttrs
14, // 7: teleport.workloadidentity.v1.IssueWorkloadIdentityRequest.requested_ttl:type_name -> google.protobuf.Duration
5, // 8: teleport.workloadidentity.v1.IssueWorkloadIdentityResponse.credential:type_name -> teleport.workloadidentity.v1.Credential
8, // 9: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.label_selectors:type_name -> teleport.workloadidentity.v1.LabelSelector
1, // 10: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.x509_svid_params:type_name -> teleport.workloadidentity.v1.X509SVIDParams
2, // 11: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.jwt_svid_params:type_name -> teleport.workloadidentity.v1.JWTSVIDParams
16, // 12: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.workload_attrs:type_name -> teleport.workloadidentity.v1.WorkloadAttrs
14, // 13: teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest.requested_ttl:type_name -> google.protobuf.Duration
5, // 14: teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponse.credentials:type_name -> teleport.workloadidentity.v1.Credential
1, // 15: teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityRequest.x509_svid_params:type_name -> teleport.workloadidentity.v1.X509SVIDParams
2, // 16: teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityRequest.jwt_svid_params:type_name -> teleport.workloadidentity.v1.JWTSVIDParams
14, // 17: teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityRequest.requested_ttl:type_name -> google.protobuf.Duration
13, // 18: teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityRequest.app_access:type_name -> teleport.workloadidentity.v1.AppAccessUsage
5, // 19: teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityResponse.credential:type_name -> teleport.workloadidentity.v1.Credential
6, // 20: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentity:input_type -> teleport.workloadidentity.v1.IssueWorkloadIdentityRequest
9, // 21: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentities:input_type -> teleport.workloadidentity.v1.IssueWorkloadIdentitiesRequest
11, // 22: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueTeleportWorkloadIdentity:input_type -> teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityRequest
7, // 23: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentity:output_type -> teleport.workloadidentity.v1.IssueWorkloadIdentityResponse
10, // 24: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueWorkloadIdentities:output_type -> teleport.workloadidentity.v1.IssueWorkloadIdentitiesResponse
12, // 25: teleport.workloadidentity.v1.WorkloadIdentityIssuanceService.IssueTeleportWorkloadIdentity:output_type -> teleport.workloadidentity.v1.IssueTeleportWorkloadIdentityResponse
23, // [23:26] is the sub-list for method output_type
20, // [20:23] is the sub-list for method input_type
20, // [20:20] is the sub-list for extension type_name
20, // [20:20] is the sub-list for extension extendee
0, // [0:20] is the sub-list for field type_name
}
func init() { file_teleport_workloadidentity_v1_issuance_service_proto_init() }
@@ -919,18 +1229,24 @@ func file_teleport_workloadidentity_v1_issuance_service_proto_init() {
(*IssueWorkloadIdentitiesRequest_X509SvidParams)(nil),
(*IssueWorkloadIdentitiesRequest_JwtSvidParams)(nil),
}
file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes[10].OneofWrappers = []any{
(*IssueTeleportWorkloadIdentityRequest_X509SvidParams)(nil),
(*IssueTeleportWorkloadIdentityRequest_JwtSvidParams)(nil),
(*IssueTeleportWorkloadIdentityRequest_AppAccess)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_workloadidentity_v1_issuance_service_proto_rawDesc), len(file_teleport_workloadidentity_v1_issuance_service_proto_rawDesc)),
NumEnums: 0,
NumMessages: 10,
NumEnums: 1,
NumMessages: 13,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_teleport_workloadidentity_v1_issuance_service_proto_goTypes,
DependencyIndexes: file_teleport_workloadidentity_v1_issuance_service_proto_depIdxs,
EnumInfos: file_teleport_workloadidentity_v1_issuance_service_proto_enumTypes,
MessageInfos: file_teleport_workloadidentity_v1_issuance_service_proto_msgTypes,
}.Build()
File_teleport_workloadidentity_v1_issuance_service_proto = out.File
@@ -33,8 +33,9 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
WorkloadIdentityIssuanceService_IssueWorkloadIdentity_FullMethodName = "/teleport.workloadidentity.v1.WorkloadIdentityIssuanceService/IssueWorkloadIdentity"
WorkloadIdentityIssuanceService_IssueWorkloadIdentities_FullMethodName = "/teleport.workloadidentity.v1.WorkloadIdentityIssuanceService/IssueWorkloadIdentities"
WorkloadIdentityIssuanceService_IssueWorkloadIdentity_FullMethodName = "/teleport.workloadidentity.v1.WorkloadIdentityIssuanceService/IssueWorkloadIdentity"
WorkloadIdentityIssuanceService_IssueWorkloadIdentities_FullMethodName = "/teleport.workloadidentity.v1.WorkloadIdentityIssuanceService/IssueWorkloadIdentities"
WorkloadIdentityIssuanceService_IssueTeleportWorkloadIdentity_FullMethodName = "/teleport.workloadidentity.v1.WorkloadIdentityIssuanceService/IssueTeleportWorkloadIdentity"
)
// WorkloadIdentityIssuanceServiceClient is the client API for WorkloadIdentityIssuanceService service.
@@ -51,6 +52,10 @@ type WorkloadIdentityIssuanceServiceClient interface {
// IssueWorkloadIdentities can issue multiple workload identity credentials
// based on label selectors for the WorkloadIdentity resources.
IssueWorkloadIdentities(ctx context.Context, in *IssueWorkloadIdentitiesRequest, opts ...grpc.CallOption) (*IssueWorkloadIdentitiesResponse, error)
// IssueTeleportWorkloadIdentity issues a workload identity credential for the
// requested Teleport usage. This request cannot be performed by users, only
// by Teleport services.
IssueTeleportWorkloadIdentity(ctx context.Context, in *IssueTeleportWorkloadIdentityRequest, opts ...grpc.CallOption) (*IssueTeleportWorkloadIdentityResponse, error)
}
type workloadIdentityIssuanceServiceClient struct {
@@ -81,6 +86,16 @@ func (c *workloadIdentityIssuanceServiceClient) IssueWorkloadIdentities(ctx cont
return out, nil
}
func (c *workloadIdentityIssuanceServiceClient) IssueTeleportWorkloadIdentity(ctx context.Context, in *IssueTeleportWorkloadIdentityRequest, opts ...grpc.CallOption) (*IssueTeleportWorkloadIdentityResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(IssueTeleportWorkloadIdentityResponse)
err := c.cc.Invoke(ctx, WorkloadIdentityIssuanceService_IssueTeleportWorkloadIdentity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// WorkloadIdentityIssuanceServiceServer is the server API for WorkloadIdentityIssuanceService service.
// All implementations must embed UnimplementedWorkloadIdentityIssuanceServiceServer
// for forward compatibility.
@@ -95,6 +110,10 @@ type WorkloadIdentityIssuanceServiceServer interface {
// IssueWorkloadIdentities can issue multiple workload identity credentials
// based on label selectors for the WorkloadIdentity resources.
IssueWorkloadIdentities(context.Context, *IssueWorkloadIdentitiesRequest) (*IssueWorkloadIdentitiesResponse, error)
// IssueTeleportWorkloadIdentity issues a workload identity credential for the
// requested Teleport usage. This request cannot be performed by users, only
// by Teleport services.
IssueTeleportWorkloadIdentity(context.Context, *IssueTeleportWorkloadIdentityRequest) (*IssueTeleportWorkloadIdentityResponse, error)
mustEmbedUnimplementedWorkloadIdentityIssuanceServiceServer()
}
@@ -111,6 +130,9 @@ func (UnimplementedWorkloadIdentityIssuanceServiceServer) IssueWorkloadIdentity(
func (UnimplementedWorkloadIdentityIssuanceServiceServer) IssueWorkloadIdentities(context.Context, *IssueWorkloadIdentitiesRequest) (*IssueWorkloadIdentitiesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method IssueWorkloadIdentities not implemented")
}
func (UnimplementedWorkloadIdentityIssuanceServiceServer) IssueTeleportWorkloadIdentity(context.Context, *IssueTeleportWorkloadIdentityRequest) (*IssueTeleportWorkloadIdentityResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method IssueTeleportWorkloadIdentity not implemented")
}
func (UnimplementedWorkloadIdentityIssuanceServiceServer) mustEmbedUnimplementedWorkloadIdentityIssuanceServiceServer() {
}
func (UnimplementedWorkloadIdentityIssuanceServiceServer) testEmbeddedByValue() {}
@@ -169,6 +191,24 @@ func _WorkloadIdentityIssuanceService_IssueWorkloadIdentities_Handler(srv interf
return interceptor(ctx, in, info, handler)
}
func _WorkloadIdentityIssuanceService_IssueTeleportWorkloadIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(IssueTeleportWorkloadIdentityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WorkloadIdentityIssuanceServiceServer).IssueTeleportWorkloadIdentity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WorkloadIdentityIssuanceService_IssueTeleportWorkloadIdentity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WorkloadIdentityIssuanceServiceServer).IssueTeleportWorkloadIdentity(ctx, req.(*IssueTeleportWorkloadIdentityRequest))
}
return interceptor(ctx, in, info, handler)
}
// WorkloadIdentityIssuanceService_ServiceDesc is the grpc.ServiceDesc for WorkloadIdentityIssuanceService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -184,6 +224,10 @@ var WorkloadIdentityIssuanceService_ServiceDesc = grpc.ServiceDesc{
MethodName: "IssueWorkloadIdentities",
Handler: _WorkloadIdentityIssuanceService_IssueWorkloadIdentities_Handler,
},
{
MethodName: "IssueTeleportWorkloadIdentity",
Handler: _WorkloadIdentityIssuanceService_IssueTeleportWorkloadIdentity_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "teleport/workloadidentity/v1/issuance_service.proto",
@@ -32,6 +32,10 @@ service WorkloadIdentityIssuanceService {
// IssueWorkloadIdentities can issue multiple workload identity credentials
// based on label selectors for the WorkloadIdentity resources.
rpc IssueWorkloadIdentities(IssueWorkloadIdentitiesRequest) returns (IssueWorkloadIdentitiesResponse);
// IssueTeleportWorkloadIdentity issues a workload identity credential for the
// requested Teleport usage. This request cannot be performed by users, only
// by Teleport services.
rpc IssueTeleportWorkloadIdentity(IssueTeleportWorkloadIdentityRequest) returns (IssueTeleportWorkloadIdentityResponse);
}
// The parameters for issuing an X509 SVID.
@@ -158,3 +162,47 @@ message IssueWorkloadIdentitiesResponse {
// The issued credentials.
repeated Credential credentials = 1;
}
// IdentityUsage indicates the generated identity usage.
enum IdentityUsage {
// IDENTITY_USAGE_UNSPECIFIED is the `unset` default value. This should
// never be used in practice.
IDENTITY_USAGE_UNSPECIFIED = 0;
IDENTITY_USAGE_APP_ACCESS = 1;
}
// The request for the IssueTeleportWorkloadIdentity RPC.
message IssueTeleportWorkloadIdentityRequest {
// The parameters for issuing the credential, varying by credential type.
oneof credential {
// The parameters for issuing an X509 SVID.
X509SVIDParams x509_svid_params = 1;
// The parameters for issuing a JWT SVID.
JWTSVIDParams jwt_svid_params = 2;
}
// The TTL that the client is requesting for the resulting credentials.
// This may be adjusted by the server and therefore the client MUST check the
// returned TTL rather than assuming that the requested TTL was granted.
google.protobuf.Duration requested_ttl = 4;
// Usage indicates what the identity will be used for.
//
// Each usage carries the information necessary for authorization and
// credentials issue.
oneof usage {
// Used when the identity will be used for accessing apps.
AppAccessUsage app_access = 5;
}
}
// The response for the IssueTeleportWorkloadIdentity RPC.
message IssueTeleportWorkloadIdentityResponse {
// The issued credential.
Credential credential = 1;
}
// AppAccessUsage used when identity will be used for accessing apps.
message AppAccessUsage {
// ASN.1 DER user identity certificate that the calling Teleport app
// service received from the end user.
bytes user_certificate = 1;
}
@@ -23,6 +23,7 @@ import (
"crypto/rand"
"crypto/x509"
"encoding/json"
"fmt"
"log/slog"
"math/big"
"net/url"
@@ -38,11 +39,15 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/gravitational/teleport"
apiproto "github.com/gravitational/teleport/api/client/proto"
apidefaults "github.com/gravitational/teleport/api/defaults"
traitv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/trait/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/observability/tracing"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/clientutils"
apiworkloadidentity "github.com/gravitational/teleport/api/workloadidentity"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/jwt"
@@ -88,6 +93,7 @@ type issuerCache interface {
GetProxies() ([]types.Server, error)
ListProxyServers(context.Context, int, string) ([]types.Server, string, error)
GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error)
ListResources(ctx context.Context, req apiproto.ListResourcesRequest) (*types.ListResourcesResponse, error)
}
// IssuanceServiceConfig holds configuration options for the IssuanceService.
@@ -134,7 +140,6 @@ func NewIssuanceService(cfg *IssuanceServiceConfig) (*IssuanceService, error) {
return nil, trace.BadParameter("key store is required")
case cfg.OverrideGetter == nil:
return nil, trace.BadParameter("override getter is required")
case cfg.ClusterName == "":
return nil, trace.BadParameter("cluster name is required")
case cfg.GetSigstorePolicyEvaluator == nil:
@@ -239,7 +244,7 @@ func (s *IssuanceService) IssueWorkloadIdentity(
var cred *workloadidentityv1pb.Credential
switch v := req.GetCredential().(type) {
case *workloadidentityv1pb.IssueWorkloadIdentityRequest_X509SvidParams:
ca, chain, err := s.getX509CA(ctx, v.X509SvidParams.GetUseIssuerOverrides())
ca, chain, err := s.getX509CA(ctx, types.SPIFFECA, v.X509SvidParams.GetUseIssuerOverrides())
if err != nil {
return nil, trace.Wrap(err, "fetching X509 SPIFFE CA")
}
@@ -349,7 +354,7 @@ func (s *IssuanceService) IssueWorkloadIdentities(
creds := make([]*workloadidentityv1pb.Credential, 0, len(shouldIssue))
switch v := req.GetCredential().(type) {
case *workloadidentityv1pb.IssueWorkloadIdentitiesRequest_X509SvidParams:
ca, chain, err := s.getX509CA(ctx, v.X509SvidParams.GetUseIssuerOverrides())
ca, chain, err := s.getX509CA(ctx, types.SPIFFECA, v.X509SvidParams.GetUseIssuerOverrides())
if err != nil {
return nil, trace.Wrap(err, "fetching CA to sign X509 SVID")
}
@@ -411,6 +416,281 @@ func (s *IssuanceService) IssueWorkloadIdentities(
}, nil
}
// IssueTeleportWorkloadIdentity issues a workload identity credential for the
// requested Teleport usage. This request cannot be performed by users, only
// by Teleport services.
//
// Optional future work: define the audit semantics for the workload identity
// issued by this RPC. This path currently does not emit per-issuance audit
// events because the app-access usage issues short-lived credentials
// periodically, up to once every 5 minutes per app session.
//
// If explicit audit is needed here, prefer a dedicated lower-volume Teleport
// workload event over reusing SPIFFESVIDIssued, and review event frequency
// before enabling it.
func (s *IssuanceService) IssueTeleportWorkloadIdentity(
ctx context.Context,
req *workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest,
) (*workloadidentityv1pb.IssueTeleportWorkloadIdentityResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
builtin, ok := authCtx.Identity.(authz.BuiltinRole)
if !ok {
return nil, trace.AccessDenied("only Teleport services can execute this request")
}
switch usage := req.Usage.(type) {
case *workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess:
if !authz.HasBuiltinRole(*authCtx, string(types.RoleApp)) {
return nil, trace.AccessDenied("only app services can issue workload identity for app access")
}
switch credParams := req.Credential.(type) {
case *workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams:
return s.issueAppAccessX509Identity(ctx, builtin.GetServerID(), req, usage.AppAccess, credParams)
default:
return nil, trace.BadParameter("app access usage only supports issuing x509 credentials")
}
default:
return nil, trace.BadParameter("invalid identity usage")
}
}
func (s *IssuanceService) issueAppAccessX509Identity(
ctx context.Context,
hostID string,
req *workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest,
appUsage *workloadidentityv1pb.AppAccessUsage,
credParams *workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams,
) (_ *workloadidentityv1pb.IssueTeleportWorkloadIdentityResponse, err error) {
ctx, span := tracer.Start(ctx, "IssuanceService/issueAppAccessX509Identity")
defer func() { tracing.EndSpan(span, err) }()
route, userIdentity, err := s.routeToAppFromCert(ctx, appUsage.GetUserCertificate())
if err != nil {
return nil, trace.Wrap(err)
}
// We cannot rely on RouteToApp.Name because:
// 1. It might not be available.
// 2. The value is not guarateed to be from the correct app, and in some
// flows requestors can provide arbitrary values.
app, err := s.getApp(ctx, hostID, route)
if err != nil {
return nil, trace.Wrap(err, "unable to locate app")
}
pubKey, err := x509.ParsePKIXPublicKey(credParams.X509SvidParams.PublicKey)
if err != nil {
return nil, trace.Wrap(err, "parsing public key")
}
// AppClient CA doesn't support workload identity CA override.
ca, chain, err := s.getX509CA(ctx, types.AppClientCA, false /* useIssuerOverrides */)
if err != nil {
return nil, trace.Wrap(err, "fetching X509 SPIFFE CA")
}
_, notBefore, notAfter, ttl := calculateTTL(
ctx,
s.logger,
s.clock,
req.RequestedTtl.AsDuration(),
// Cap the cert TTL at the session expiry, plus an alloweance for clock
// drift that mirrors [veirfyCertValidityWithSkew]. Without the
// allowance session that are within the skew window would yield app
// certs with a near-zero (or negative) TTL.
s.clock.Until(userIdentity.Expires)+certVerifyClockSkewAllowance,
)
// This intentionally uses cluster name as part of the the SPIFFE trust
// domain, matching regular workload identity issuance behavior. Clusters
// with names that are not valid SPIFFE trust domain are expected to fail
// issuance.
td, err := spiffeid.TrustDomainFromString(
apiworkloadidentity.NewInternalAppTrustDomain(s.clusterName),
)
if err != nil {
return nil, trace.Wrap(err, "cluster name cannot be used as SPIFFE ID trust domain")
}
spiffeID, err := spiffeid.FromSegments(td, "app", app.GetName())
if err != nil {
return nil, trace.Wrap(err, "app name contains invalid format and cannot be used as SPIFFE ID")
}
certSerial, err := generateCertSerial()
if err != nil {
return nil, trace.Wrap(err, "generating certificate serial")
}
certBytes, err := x509.CreateCertificate(
rand.Reader,
x509Template(
certSerial,
notBefore,
notAfter,
spiffeID,
nil, /* dnsSANs */
&workloadidentityv1pb.X509DistinguishedNameTemplate{
CommonName: userIdentity.Username,
},
),
ca.Cert,
pubKey,
ca.Signer,
)
if err != nil {
return nil, trace.Wrap(err)
}
return &workloadidentityv1pb.IssueTeleportWorkloadIdentityResponse{
Credential: &workloadidentityv1pb.Credential{
SpiffeId: spiffeID.String(),
ExpiresAt: timestamppb.New(notAfter),
Ttl: durationpb.New(ttl),
Credential: &workloadidentityv1pb.Credential_X509Svid{
X509Svid: &workloadidentityv1pb.X509SVIDCredential{
Cert: certBytes,
SerialNumber: serialString(certSerial),
Chain: chain,
},
},
},
}, nil
}
// routeToAppFromCert validates the certificate and extracts the RouteToApp info.
//
// We intentionally validate only the user certificate signature and expiration,
// and app routing metadata here. The app service is expected to call this RPC
// only after it has already authenticated the app request and verified that the
// referenced AppSession still exists.
//
// Because it won't look up the AppSession, callers must not use this as the
// AppSession validity check.
func (s *IssuanceService) routeToAppFromCert(ctx context.Context, rawCert []byte) (tlsca.RouteToApp, *tlsca.Identity, error) {
cert, err := x509.ParseCertificate(rawCert)
if err != nil {
return tlsca.RouteToApp{}, nil, trace.Wrap(err)
}
ca, err := s.cache.GetCertAuthority(ctx, types.CertAuthID{
Type: types.UserCA,
DomainName: s.clusterName,
}, false)
if err != nil {
return tlsca.RouteToApp{}, nil, trace.Wrap(err)
}
roots := x509.NewCertPool()
for _, key := range ca.GetTrustedTLSKeyPairs() {
if ok := roots.AppendCertsFromPEM(key.Cert); !ok {
return tlsca.RouteToApp{}, nil, trace.BadParameter("unable to build UserCA pool to validate certificate")
}
}
// cert.Verify applies CurrentTime to every certificate in the chain and
// does not support separate NotBefore/NotAfter leeway for the leaf
// certificate.
//
// For this reason we perform the certificate verify in two steps:
// 1. Verify certificate validity using custom clock-skew policy.
// 2. Perform the remaining verification on the cert using a shallow copy
// of the certificate that will pass the expiry verification.
//
// The shallow copy usage still checks the original TBSCertificate bytes,
// the only effective difference is the Verify's leaf time check.
now := s.clock.Now()
if err := verifyCertValidityWithSkew(cert, now); err != nil {
return tlsca.RouteToApp{}, nil, trace.Wrap(err)
}
verifyCert := *cert
verifyCert.NotAfter = now.Add(time.Second)
verifyCert.NotBefore = now.Add(-time.Second)
if _, err := verifyCert.Verify(x509.VerifyOptions{
Roots: roots,
CurrentTime: now,
KeyUsages: []x509.ExtKeyUsage{
// Extensions added by tlsca.
// See https://github.com/gravitational/teleport/blob/master/lib/tlsca/ca.go
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
}); err != nil {
return tlsca.RouteToApp{}, nil, trace.Wrap(err, "requestor provided an invalid certificate")
}
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
if err != nil {
return tlsca.RouteToApp{}, nil, trace.Wrap(err, "requestor provided a certificate that doesn't contain a Teleport identity")
}
route, err := identity.GetRouteToApp()
if err != nil {
return tlsca.RouteToApp{}, nil, trace.Wrap(err, "identity must be from an app access session")
}
// Ensure the required information is available on the identity.
//
// This acts as a guardrail for any inconsistent or malformed identity and
// shouldn't happen. This validation just prevents generating workload
// identity that is inconsistent as well.
if route.ClusterName != s.clusterName {
return tlsca.RouteToApp{}, nil, trace.BadParameter("cannot request workload identity for a different cluster")
}
return route, identity, nil
}
// getApp retrieves the app definition from an AppServer registered by the
// requesting host. Requiring the requestor (identified by hostID) to be one
// of the app's serving AppServers ensures the app definition we sign comes from
// a service authoritative for the app, not from an arbitrary cached entry.
//
// This is not a strong authorization check: any app service that advertises the
// target app can satisfy this lookup, even if it is not actually serving the
// specific session referenced by the app session certificate.
func (s *IssuanceService) getApp(ctx context.Context, hostID string, route tlsca.RouteToApp) (types.Application, error) {
appServersIter := clientutils.Resources(ctx, func(ctx context.Context, limit int, startKey string) ([]types.ResourceWithLabels, string, error) {
resp, err := s.cache.ListResources(ctx, apiproto.ListResourcesRequest{
Namespace: apidefaults.Namespace,
ResourceType: types.KindAppServer,
Limit: int32(limit),
StartKey: startKey,
PredicateExpression: fmt.Sprintf(`resource.spec.public_addr == %q`, route.PublicAddr),
})
if err != nil {
return nil, "", trace.Wrap(err)
}
return resp.Resources, resp.NextKey, nil
})
for resource, err := range appServersIter {
if err != nil {
return nil, trace.Wrap(err, "unabled to complete app servers list")
}
appServer, ok := resource.(types.AppServer)
if !ok {
return nil, trace.BadParameter("expected types.AppServer, got: %T", resource)
}
// Ensure the requesting app service can serve the app.
if appServer.GetHostID() == hostID {
return appServer.GetApp(), nil
}
}
return nil, trace.NotFound("application at %q not found for app service %q", route.PublicAddr, hostID)
}
func generateCertSerial() (*big.Int, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
return rand.Int(rand.Reader, serialNumberLimit)
@@ -470,6 +750,7 @@ func x509Template(
func (s *IssuanceService) getX509CA(
ctx context.Context,
caType types.CertAuthType,
useIssuerOverrides bool,
) (_ *tlsca.CertAuthority, _ [][]byte, err error) {
ctx, span := tracer.Start(ctx, "IssuanceService/getX509CA")
@@ -477,7 +758,7 @@ func (s *IssuanceService) getX509CA(
const loadKeysTrue = true
ca, err := s.cache.GetCertAuthority(ctx, types.CertAuthID{
Type: types.SPIFFECA,
Type: caType,
DomainName: s.clusterName,
}, loadKeysTrue)
@@ -952,3 +1233,27 @@ func serialString(serial *big.Int) string {
}
return out.String()
}
// certVerifyClockSkewAllowance is the amount of leeway added to the
// certificate's expiration status check to allow for clock drift.
const certVerifyClockSkewAllowance = 1 * time.Minute
func verifyCertValidityWithSkew(cert *x509.Certificate, now time.Time) error {
if now.Add(certVerifyClockSkewAllowance).Before(cert.NotBefore) {
return x509.CertificateInvalidError{
Cert: cert,
Reason: x509.Expired,
Detail: "certificate is not yet valid",
}
}
if now.Add(-certVerifyClockSkewAllowance).After(cert.NotAfter) {
return x509.CertificateInvalidError{
Cert: cert,
Reason: x509.Expired,
Detail: "certificate has expired",
}
}
return nil
}
@@ -19,11 +19,13 @@ package workloadidentityv1_test
import (
"context"
"crypto"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"math/big"
@@ -42,6 +44,7 @@ import (
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
@@ -59,6 +62,8 @@ import (
"github.com/gravitational/teleport/api/types/events"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/tlsutils"
apiworkloadidentity "github.com/gravitational/teleport/api/workloadidentity"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/authtest"
"github.com/gravitational/teleport/lib/auth/keystore"
@@ -116,18 +121,21 @@ func newTestTLSServer(t testing.TB, opts ...authtest.TestTLSServerOption) (*auth
type issuanceTestPack struct {
srv *authtest.TLSServer
eventRecorder *eventstest.MockRecorderEmitter
clock clockwork.Clock
clock *clockwork.FakeClock
sigstorePolicyEvaluator *mockSigstorePolicyEvaluator
issuer string
spiffeX509CAPool *x509.CertPool
spiffeJWTSigner crypto.Signer
spiffeJWTSignerKID string
issuer string
spiffeX509CAPool *x509.CertPool
spiffeJWTSigner crypto.Signer
spiffeJWTSignerKID string
appClientX509CAPool *x509.CertPool
}
func newIssuanceTestPack(t *testing.T, ctx context.Context) *issuanceTestPack {
srv, eventRecorder := newTestTLSServer(t)
clock := srv.Auth().GetClock()
fakeClock, ok := clock.(*clockwork.FakeClock)
require.True(t, ok, "expected to be a clockwork.FakeClock but got %T", clock)
// Upsert a fake proxy to ensure we have a public address to use for the
// issuer.
@@ -157,6 +165,14 @@ func newIssuanceTestPack(t *testing.T, ctx context.Context) *issuanceTestPack {
require.NoError(t, err)
kid, err := libjwt.KeyID(jwtSigner.Public())
require.NoError(t, err)
// Fetch X509 AppClient CA for validation of signature later
appClientX509CA, err := srv.Auth().GetCertAuthority(ctx, types.CertAuthID{
Type: types.AppClientCA,
DomainName: srv.ClusterName(),
}, false)
require.NoError(t, err)
appClientX509CAPool, err := services.CertPool(appClientX509CA)
require.NoError(t, err)
sigstorePolicyEvaluator := newMockSigstorePolicyEvaluator(t)
srv.Auth().SetSigstorePolicyEvaluator(sigstorePolicyEvaluator)
@@ -164,12 +180,13 @@ func newIssuanceTestPack(t *testing.T, ctx context.Context) *issuanceTestPack {
return &issuanceTestPack{
srv: srv,
eventRecorder: eventRecorder,
clock: clock,
clock: fakeClock,
sigstorePolicyEvaluator: sigstorePolicyEvaluator,
issuer: wantIssuer,
spiffeX509CAPool: spiffeX509CAPool,
spiffeJWTSigner: jwtSigner,
spiffeJWTSignerKID: kid,
appClientX509CAPool: appClientX509CAPool,
}
}
@@ -1658,6 +1675,348 @@ func TestIssueWorkloadIdentities(t *testing.T) {
}
}
func TestIssueTeleportWorkloadIdentity(t *testing.T) {
t.Parallel()
const appServiceID = "test-server"
const appName = "panel"
ctx := t.Context()
tp := newIssuanceTestPack(t, ctx)
clusterName := tp.srv.ClusterName()
// Register an application.
app, err := types.NewAppV3(types.Metadata{
Name: appName,
}, types.AppSpecV3{
URI: "localhost",
PublicAddr: appName + "." + clusterName,
})
require.NoError(t, err)
server, err := types.NewAppServerV3FromApp(app, "app", appServiceID)
require.NoError(t, err)
_, err = tp.srv.Auth().UpsertApplicationServer(t.Context(), server)
require.NoError(t, err)
// This alice is the one accessing the Teleport resource.
alice, _, err := authtest.CreateUserAndRole(
tp.srv.Auth(),
"alice",
[]string{},
[]types.Rule{
types.NewRule(
types.KindApp,
[]string{types.VerbRead, types.VerbList},
),
},
authtest.WithRoleMutator(func(role types.Role) {
role.SetAppLabels(types.Allow, types.Labels{
"*": []string{"*"},
})
}),
)
require.NoError(t, err)
// To create an expired session, we issue the cert, and the advance the clock.
aliceExpiredClt, err := tp.srv.NewClient(authtest.TestUser(alice.GetName()))
require.NoError(t, err)
aliceExpiredSessionCertBytes, aliceSessionExpiresAt := createAppSessionCertBytes(t, aliceExpiredClt, appName, alice.GetName())
// Add a bit more time to "escape" the clock skew condition.
tp.clock.Advance(tp.clock.Until(aliceSessionExpiresAt) + 10*time.Minute)
aliceClt, err := tp.srv.NewClient(authtest.TestUser(alice.GetName()))
require.NoError(t, err)
aliceSessionCertBytes, aliceSessionExpirity := createAppSessionCertBytes(t, aliceClt, appName, alice.GetName())
require.True(t, aliceSessionExpirity.After(tp.clock.Now()))
appServiceClt, err := tp.srv.NewClient(authtest.TestServerID(types.RoleApp, appServiceID))
require.NoError(t, err)
// This app service is not serving the requested app.
nonServingAppServiceClt, err := tp.srv.NewClient(authtest.TestServerID(types.RoleApp, "random-host-id"))
require.NoError(t, err)
// Generate a keypair to generate x509 SVIDs for.
workloadKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
require.NoError(t, err)
workloadKeyPubBytes, err := x509.MarshalPKIXPublicKey(workloadKey.Public())
require.NoError(t, err)
// Those clients are used by the test cases.
userClt, err := tp.srv.NewClient(authtest.TestAdmin())
require.NoError(t, err)
otherServiceClt, err := tp.srv.NewClient(authtest.TestBuiltin(types.RoleDatabase))
require.NoError(t, err)
for name, tc := range map[string]struct {
clt *authclient.Client
req *workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest
expectErr require.ErrorAssertionFunc
expectResp require.ValueAssertionFunc
}{
"app service generates workload identity for a session": {
clt: appServiceClt,
req: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest{
Credential: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams{
X509SvidParams: &workloadidentityv1pb.X509SVIDParams{
PublicKey: workloadKeyPubBytes,
},
},
Usage: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess{
AppAccess: &workloadidentityv1pb.AppAccessUsage{
UserCertificate: aliceSessionCertBytes,
},
},
},
expectErr: require.NoError,
expectResp: func(tt require.TestingT, i1 any, i2 ...any) {
resp, _ := i1.(*workloadidentityv1pb.IssueTeleportWorkloadIdentityResponse)
// Check X509 cert actually included and signed.
cert, err := x509.ParseCertificate(resp.Credential.GetX509Svid().GetCert())
require.NoError(tt, err)
// Check included public key matches
require.Equal(tt, workloadKey.Public(), cert.PublicKey)
_, err = cert.Verify(x509.VerifyOptions{
// Must be signed by AppClient CA
Roots: tp.appClientX509CAPool,
CurrentTime: tp.srv.Auth().GetClock().Now(),
})
require.NoError(tt, err)
// Check SPIFFE ID contains the trusted domain, and app name.
require.Len(tt, cert.URIs, 1, "certificate doesn't contain SPIFFE ID")
spiffeID, err := spiffeid.FromString(cert.URIs[0].String())
require.NoError(tt, err)
require.Len(t, cert.URIs, 1)
td, err := spiffeid.TrustDomainFromString(
apiworkloadidentity.NewInternalAppTrustDomain(tp.srv.ClusterName()),
)
require.NoError(tt, err)
require.True(tt, spiffeID.MemberOf(td), "expected the returned SPIFFE ID %q to be part of internal trust domain %q", spiffeID, td)
},
},
"other service cannot generate workload identity for app access": {
clt: otherServiceClt,
req: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest{
Credential: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams{
X509SvidParams: &workloadidentityv1pb.X509SVIDParams{
PublicKey: workloadKeyPubBytes,
},
},
Usage: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess{
AppAccess: &workloadidentityv1pb.AppAccessUsage{
UserCertificate: aliceSessionCertBytes,
},
},
},
expectErr: func(tt require.TestingT, err error, i ...any) {
require.ErrorContains(tt, err, "only app services can issue workload identity for app access", i...)
},
expectResp: require.Nil,
},
"app service not serving app cannot generate workload identity for requested session": {
clt: nonServingAppServiceClt,
req: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest{
Credential: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams{
X509SvidParams: &workloadidentityv1pb.X509SVIDParams{
PublicKey: workloadKeyPubBytes,
},
},
Usage: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess{
AppAccess: &workloadidentityv1pb.AppAccessUsage{
UserCertificate: aliceSessionCertBytes,
},
},
},
expectErr: func(tt require.TestingT, err error, i ...any) {
require.ErrorContains(tt, err, "unable to locate app", i...)
},
expectResp: require.Nil,
},
"app service generates workload identity for an expired session": {
clt: appServiceClt,
req: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest{
Credential: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams{
X509SvidParams: &workloadidentityv1pb.X509SVIDParams{
PublicKey: workloadKeyPubBytes,
},
},
Usage: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess{
AppAccess: &workloadidentityv1pb.AppAccessUsage{
UserCertificate: aliceExpiredSessionCertBytes,
},
},
},
expectErr: func(tt require.TestingT, err error, i ...any) {
require.ErrorContains(tt, err, "certificate has expired", i...)
},
expectResp: require.Nil,
},
"users cannot request the RPC": {
clt: userClt,
req: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest{
Credential: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams{
X509SvidParams: &workloadidentityv1pb.X509SVIDParams{
PublicKey: workloadKeyPubBytes,
},
},
Usage: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess{
AppAccess: &workloadidentityv1pb.AppAccessUsage{
UserCertificate: aliceSessionCertBytes,
},
},
},
expectErr: func(tt require.TestingT, err error, i ...any) {
require.ErrorContains(tt, err, "only app services can issue workload identity for app access", i...)
},
expectResp: require.Nil,
},
} {
t.Run(name, func(t *testing.T) {
c := workloadidentityv1pb.NewWorkloadIdentityIssuanceServiceClient(
tc.clt.GetConnection(),
)
res, err := c.IssueTeleportWorkloadIdentity(ctx, tc.req)
tc.expectErr(t, err)
tc.expectResp(t, res)
})
}
}
// TestIssueTeleportWorkloadIdentityRejectsExpiredUserCA given an app access
// workload identity request with an expired UserCA certificate, the server must
// reject the request.
func TestIssueTeleportWorkloadIdentityRejectsExpiredUserCA(t *testing.T) {
const appServiceID = "test-server"
const appName = "panel"
ctx := t.Context()
tp := newIssuanceTestPack(t, ctx)
clusterName := tp.srv.ClusterName()
app, err := types.NewAppV3(types.Metadata{
Name: appName,
}, types.AppSpecV3{
URI: "localhost",
PublicAddr: appName + "." + clusterName,
})
require.NoError(t, err)
server, err := types.NewAppServerV3FromApp(app, "app", appServiceID)
require.NoError(t, err)
_, err = tp.srv.Auth().UpsertApplicationServer(t.Context(), server)
require.NoError(t, err)
// This alice is the one accessing the Teleport resource.
alice, _, err := authtest.CreateUserAndRole(
tp.srv.Auth(),
"alice",
[]string{},
[]types.Rule{
types.NewRule(
types.KindApp,
[]string{types.VerbRead, types.VerbList},
),
},
authtest.WithRoleMutator(func(role types.Role) {
role.SetAppLabels(types.Allow, types.Labels{
"*": []string{"*"},
})
}),
)
require.NoError(t, err)
aliceClt, err := tp.srv.NewClient(authtest.TestUser(alice.GetName()))
require.NoError(t, err)
// Important: create the app session cert while the User CA is still valid.
aliceSessionCertBytes, _ := createAppSessionCertBytes(t, aliceClt, appName, alice.GetName())
sessionCert, err := x509.ParseCertificate(aliceSessionCertBytes)
require.NoError(t, err)
require.True(t, tp.clock.Now().After(sessionCert.NotBefore))
require.True(t, tp.clock.Now().Before(sessionCert.NotAfter))
appServiceClt, err := tp.srv.NewClient(authtest.TestServerID(types.RoleApp, appServiceID))
require.NoError(t, err)
workloadKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
require.NoError(t, err)
workloadKeyPubBytes, err := x509.MarshalPKIXPublicKey(workloadKey.Public())
require.NoError(t, err)
c := workloadidentityv1pb.NewWorkloadIdentityIssuanceServiceClient(appServiceClt.GetConnection())
// Here we ensure the certificate is OK and returns without errors.
res, err := c.IssueTeleportWorkloadIdentity(ctx, &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest{
Credential: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams{
X509SvidParams: &workloadidentityv1pb.X509SVIDParams{
PublicKey: workloadKeyPubBytes,
},
},
Usage: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess{
AppAccess: &workloadidentityv1pb.AppAccessUsage{
UserCertificate: aliceSessionCertBytes,
},
},
})
require.NoError(t, err)
require.NotNil(t, res)
// Now replace the trusted User CA cert with an expired cert using the same key.
userCA, err := tp.srv.Auth().GetCertAuthority(ctx, types.CertAuthID{
Type: types.UserCA,
DomainName: clusterName,
}, true)
require.NoError(t, err)
activeKeys := userCA.GetActiveKeys()
userCATLSKey := activeKeys.TLS[0]
oldUserCACert, err := tlsca.ParseCertificatePEM(userCATLSKey.Cert)
require.NoError(t, err)
userCASigner, err := keys.ParsePrivateKey(userCATLSKey.Key)
require.NoError(t, err)
expiredUserCACert := *oldUserCACert
expiredUserCACert.NotBefore = tp.clock.Now().Add(-2 * time.Hour)
expiredUserCACert.NotAfter = tp.clock.Now().Add(-time.Minute)
expiredUserCADER, err := x509.CreateCertificate(
rand.Reader,
&expiredUserCACert,
&expiredUserCACert,
userCASigner.Public(),
userCASigner,
)
require.NoError(t, err)
userCATLSKey.Cert = pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: expiredUserCADER,
})
activeKeys.TLS[0] = userCATLSKey
require.NoError(t, userCA.SetActiveKeys(activeKeys))
require.NoError(t, tp.srv.Auth().UpsertCertAuthority(ctx, userCA))
// This time, the UserCA is expected to be expired, meaning it should fail
// the certificate verification.
res, err = c.IssueTeleportWorkloadIdentity(ctx, &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest{
Credential: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_X509SvidParams{
X509SvidParams: &workloadidentityv1pb.X509SVIDParams{
PublicKey: workloadKeyPubBytes,
},
},
Usage: &workloadidentityv1pb.IssueTeleportWorkloadIdentityRequest_AppAccess{
AppAccess: &workloadidentityv1pb.AppAccessUsage{
UserCertificate: aliceSessionCertBytes,
},
},
})
require.ErrorContains(t, err, "requestor provided an invalid certificate")
require.ErrorContains(t, err, "certificate has expired")
require.Nil(t, res)
}
func TestResourceService_CreateWorkloadIdentity(t *testing.T) {
t.Parallel()
srv, eventRecorder := newTestTLSServer(t)
@@ -3717,3 +4076,23 @@ func TestRevocationService_CRL(t *testing.T) {
require.NoError(t, <-rpcErrCh)
})
}
// createAppSessionCertBytes creates an app session and returns the session
// certificate.
func createAppSessionCertBytes(t *testing.T, clt *authclient.Client, appName, username string) ([]byte, time.Time) {
t.Helper()
clusterName, err := clt.GetClusterName(t.Context())
require.NoError(t, err)
aliceSession, err := clt.CreateAppSession(t.Context(), &apiproto.CreateAppSessionRequest{
AppName: appName,
Username: username,
PublicAddr: appName + "." + clusterName.GetClusterName(),
ClusterName: clusterName.GetClusterName(),
})
require.NoError(t, err)
cert, err := tlsutils.ParseCertificatePEM(aliceSession.GetTLSCert())
require.NoError(t, err)
return cert.Raw, aliceSession.Expiry()
}