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 TargetPort to RouteToApp & use it to route connections to multi-port TCP apps #49047

Open
wants to merge 10 commits into
base: r7s/ports-app-spec
Choose a base branch
from
5 changes: 5 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2161,6 +2161,10 @@ type certRequest struct {
appName string
// appURI is the URI of the app. This is the internal endpoint where the application is running and isn't user-facing.
appURI string
// appTargetPort signifies that the cert should grant access to a specific port in a multi-port
// TCP app, as long as the port is defined in the app spec. Used only for routing, should not be
// used in other contexts (e.g., access requests).
appTargetPort uint16
// awsRoleARN is the role ARN to generate certificate for.
awsRoleARN string
// azureIdentity is the Azure identity to generate certificate for.
Expand Down Expand Up @@ -3245,6 +3249,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
RouteToApp: tlsca.RouteToApp{
SessionID: req.appSessionID,
URI: req.appURI,
TargetPort: req.appTargetPort,
PublicAddr: req.appPublicAddr,
ClusterName: req.appClusterName,
Name: req.appName,
Expand Down
2 changes: 2 additions & 0 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3156,6 +3156,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
DeviceExtensions: DeviceExtensions(a.context.Identity.GetIdentity().DeviceExtensions),
AppName: req.RouteToApp.Name,
AppURI: req.RouteToApp.URI,
AppTargetPort: uint16(req.RouteToApp.TargetPort),
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -3200,6 +3201,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
appName: req.RouteToApp.Name,
appPublicAddr: req.RouteToApp.PublicAddr,
appURI: req.RouteToApp.URI,
appTargetPort: uint16(req.RouteToApp.TargetPort),
appClusterName: req.RouteToApp.ClusterName,
awsRoleARN: req.RouteToApp.AWSRoleARN,
azureIdentity: req.RouteToApp.AzureIdentity,
Expand Down
41 changes: 41 additions & 0 deletions lib/auth/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1719,6 +1719,47 @@ func TestGenerateUserCerts_singleUseCerts(t *testing.T) {
},
},
},
{
desc: "TCP app with target port",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_App,
RouteToApp: proto.RouteToApp{
Name: "app-a",
TargetPort: 1337,
},
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
require.NotEmpty(t, crt)

cert, err := tlsca.ParseCertificatePEM(crt)
require.NoError(t, err)
require.Equal(t, cert.NotAfter, clock.Now().Add(teleport.UserSingleUseCertTTL))

identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.LoginIP).IsLoopback())
require.Equal(t, []string{teleport.UsageAppsOnly}, identity.Usage)
require.Equal(t, "app-a", identity.RouteToApp.Name)
require.Equal(t, uint16(1337), identity.RouteToApp.TargetPort)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copy-pasted the test case above and added a single line which checks TargetPort.

// session ID should be set to a random ID, corresponding to an app session.
require.NotZero(t, identity.RouteToApp.SessionID)
},
},
},
{
desc: "db with ttl limit disabled",
opts: generateUserSingleUseCertsTestOpts{
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@ type NewAppSessionRequest struct {
AppName string
// AppURI is the URI of the app. This is the internal endpoint where the application is running and isn't user-facing.
AppURI string
// AppTargetPort signifies that the session is made to a specific port of a multi-port TCP app.
AppTargetPort uint16
// Identity is the identity of the user.
Identity tlsca.Identity
// ClientAddr is a client (user's) address.
Expand Down Expand Up @@ -577,6 +579,7 @@ func (a *Server) CreateAppSessionFromReq(ctx context.Context, req NewAppSessionR
usage: []string{teleport.UsageAppsOnly},
appPublicAddr: req.PublicAddr,
appClusterName: req.ClusterName,
appTargetPort: req.AppTargetPort,
awsRoleARN: req.AWSRoleARN,
azureIdentity: req.AzureIdentity,
gcpServiceAccount: req.GCPServiceAccount,
Expand Down Expand Up @@ -638,6 +641,7 @@ func (a *Server) CreateAppSessionFromReq(ctx context.Context, req NewAppSessionR
AppURI: req.AppURI,
AppPublicAddr: req.PublicAddr,
AppName: req.AppName,
AppTargetPort: uint32(req.AppTargetPort),
},
})
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions lib/srv/app/common/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func (a *audit) OnSessionStart(ctx context.Context, serverID string, identity *t
AppURI: app.GetURI(),
AppPublicAddr: app.GetPublicAddr(),
AppName: app.GetName(),
AppTargetPort: uint32(identity.RouteToApp.TargetPort),
},
}
return trace.Wrap(a.EmitEvent(ctx, event))
Expand Down Expand Up @@ -146,6 +147,7 @@ func (a *audit) OnSessionEnd(ctx context.Context, serverID string, identity *tls
AppURI: app.GetURI(),
AppPublicAddr: app.GetPublicAddr(),
AppName: app.GetName(),
AppTargetPort: uint32(identity.RouteToApp.TargetPort),
},
}
return trace.Wrap(a.EmitEvent(ctx, event))
Expand All @@ -170,6 +172,7 @@ func (a *audit) OnSessionChunk(ctx context.Context, serverID, chunkID string, id
AppURI: app.GetURI(),
AppPublicAddr: app.GetPublicAddr(),
AppName: app.GetName(),
// Session chunks are not created for TCP apps, so there's no need to pass TargetPort here.
},
SessionChunkID: chunkID,
}
Expand Down
31 changes: 31 additions & 0 deletions lib/tlsca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,14 @@ type RouteToApp struct {
GCPServiceAccount string

// URI is the URI of the app. This is the internal endpoint where the application is running and isn't user-facing.
// Used merely for audit events and mirrors the URI from the app spec. Not used as a source of
// truth when routing connections.
URI string

// TargetPort is the port to which connections should be routed to. Used only for multi-port TCP
// apps. It is appended to the hostname from the URI in the app spec, since the URI from
// RouteToApp is not used as the source of truth for routing.
TargetPort uint16
}

// RouteToDatabase contains routing information for databases.
Expand Down Expand Up @@ -309,6 +316,7 @@ func (id *Identity) GetEventIdentity() events.Identity {
AzureIdentity: id.RouteToApp.AzureIdentity,
GCPServiceAccount: id.RouteToApp.GCPServiceAccount,
URI: id.RouteToApp.URI,
TargetPort: uint32(id.RouteToApp.TargetPort),
}
}
var routeToDatabase *events.RouteToDatabase
Expand Down Expand Up @@ -462,6 +470,10 @@ var (
// Its value is either local or sso.
UserTypeASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 1, 20}

// AppTargetPortASN1ExtensionOID is an extension ID used to encode the application
// target port into a certificate.
AppTargetPortASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 1, 21}

// DatabaseServiceNameASN1ExtensionOID is an extension ID used when encoding/decoding
// database service name into certificates.
DatabaseServiceNameASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 2, 1}
Expand Down Expand Up @@ -630,6 +642,15 @@ func (id *Identity) Subject() (pkix.Name, error) {
Value: id.RouteToApp.PublicAddr,
})
}
if id.RouteToApp.TargetPort != 0 {
subject.ExtraNames = append(subject.ExtraNames,
pkix.AttributeTypeAndValue{
Type: AppTargetPortASN1ExtensionOID,
// asn1 doesn't seem to handle uint16, hence the string.
Value: strconv.Itoa(int(id.RouteToApp.TargetPort)),
},
)
}
if id.RouteToApp.ClusterName != "" {
subject.ExtraNames = append(subject.ExtraNames,
pkix.AttributeTypeAndValue{
Expand Down Expand Up @@ -947,6 +968,16 @@ func FromSubject(subject pkix.Name, expires time.Time) (*Identity, error) {
if ok {
id.RouteToApp.PublicAddr = val
}
case attr.Type.Equal(AppTargetPortASN1ExtensionOID):
// Similar to GenerationASN1ExtensionOID, it has to be cast to a string first and then parsed.
val, ok := attr.Value.(string)
if ok {
targetPort, err := strconv.ParseUint(val, 10, 16)
if err != nil {
return nil, trace.Wrap(err, "parsing target port")
}
id.RouteToApp.TargetPort = uint16(targetPort)
}
case attr.Type.Equal(AppClusterNameASN1ExtensionOID):
val, ok := attr.Value.(string)
if ok {
Expand Down