support keying the cert on disk by scope qualified subdomain (#68721)

Proliferate Scope Qualified Name in tsh app commands

Relax naming length constraints for qualified names
This commit is contained in:
williamong-tel
2026-07-24 21:22:15 +00:00
committed by GitHub
parent 8b35ba4db7
commit 552b86b65a
22 changed files with 230 additions and 127 deletions
+4 -3
View File
@@ -93,6 +93,7 @@ import (
"github.com/gravitational/teleport/lib/multiplexer"
"github.com/gravitational/teleport/lib/observability/tracing"
libplayer "github.com/gravitational/teleport/lib/player"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/scopes/pinning"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
@@ -3511,17 +3512,17 @@ func (tc *TeleportClient) LogoutDatabase(dbName string) error {
}
// LogoutApp removes key and cert for the specified app.
func (tc *TeleportClient) LogoutApp(appName string) error {
func (tc *TeleportClient) LogoutApp(appSQN scopes.QualifiedName) error {
if tc.localAgent == nil {
return nil
}
if tc.SiteName == "" {
return trace.BadParameter("cluster name must be set for app logout")
}
if appName == "" {
if appSQN.Name == "" {
return trace.BadParameter("please specify app name to log out of")
}
return tc.localAgent.DeleteUserCerts(tc.SiteName, WithAppCerts{appName})
return tc.localAgent.DeleteUserCerts(tc.SiteName, WithAppCerts{ScopedAppName(appSQN)})
}
// LogoutAllApps removes keys and certs for all apps in the cluster.
+3 -2
View File
@@ -43,6 +43,7 @@ import (
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/resumption"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/services"
)
@@ -262,7 +263,7 @@ func (c *ClusterClient) generateUserCerts(ctx context.Context, cachePolicy CertC
keyRing.SSHPrivateKey = newUserKeys.ssh
keyRing.Cert = certs.SSH
case proto.UserCertsRequest_App:
keyRing.AppTLSCredentials[params.RouteToApp.Name] = TLSCredential{
keyRing.AppTLSCredentials[ScopedAppName(scopes.QualifiedName{Name: params.RouteToApp.Name, Scope: params.RouteToApp.Scope})] = TLSCredential{
PrivateKey: newUserKeys.app,
Cert: certs.TLS,
}
@@ -935,7 +936,7 @@ func PerformSessionMFACeremony(ctx context.Context, params PerformSessionMFACere
if keyRing.AppTLSCredentials == nil {
keyRing.AppTLSCredentials = make(map[string]TLSCredential)
}
keyRing.AppTLSCredentials[certsReq.RouteToApp.Name] = TLSCredential{
keyRing.AppTLSCredentials[ScopedAppName(scopes.QualifiedName{Name: certsReq.RouteToApp.Name, Scope: certsReq.RouteToApp.Scope})] = TLSCredential{
Cert: newCerts.TLS,
PrivateKey: params.newUserKeys.app,
}
+2 -1
View File
@@ -45,6 +45,7 @@ import (
"github.com/gravitational/teleport/api/utils/prompt"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/kube/kubeconfig"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
@@ -805,7 +806,7 @@ func KeyRingFromIdentityFile(identityPath, proxyHost, clusterName string, opts .
// Similarly, if this identity has any app certs, copy them in.
if parsedIdent.RouteToApp.Name != "" {
keyRing.AppTLSCredentials[parsedIdent.RouteToApp.Name] = client.TLSCredential{
keyRing.AppTLSCredentials[client.ScopedAppName(scopes.QualifiedName{Name: parsedIdent.RouteToApp.Name, Scope: parsedIdent.RouteToApp.Scope})] = client.TLSCredential{
// Identity files only have room for one private key and TLS
// cert, it must match the app cert.
PrivateKey: priv,
+17 -3
View File
@@ -45,6 +45,8 @@ import (
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/scopes"
scopedapp "github.com/gravitational/teleport/lib/scopes/app"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
@@ -580,11 +582,23 @@ func (k *KeyRing) DBTLSCertificates() (certs []x509.Certificate, err error) {
return certs, nil
}
// ScopedAppName returns the name that keys an app's TLS credential.
// Unscoped apps are keyed by their plain name. Scoped apps are keyed by their
// scope-qualified subdomain with a leading "@" prefix: app names must begin
// with an alphanumeric character, so the prefix keeps scoped keys separate
// from unscoped app names.
func ScopedAppName(appSQN scopes.QualifiedName) string {
if appSQN.Scope == "" {
return appSQN.Name
}
return "@" + scopedapp.ScopedSubdomain(appSQN.Name, appSQN.Scope)
}
// AppTLSCert returns the tls.Certificate for authentication against a named app.
func (k *KeyRing) AppTLSCert(appName string) (tls.Certificate, error) {
cred, ok := k.AppTLSCredentials[appName]
func (k *KeyRing) AppTLSCert(appSQN scopes.QualifiedName) (tls.Certificate, error) {
cred, ok := k.AppTLSCredentials[ScopedAppName(appSQN)]
if !ok {
return tls.Certificate{}, trace.NotFound("TLS certificate for application %q not found", appName)
return tls.Certificate{}, trace.NotFound("TLS certificate for application %q not found", appSQN.Name)
}
return cred.TLSCertificate()
}
+2 -1
View File
@@ -36,6 +36,7 @@ import (
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
@@ -318,7 +319,7 @@ func (c *AppCertIssuer) IssueCert(ctx context.Context) (tls.Certificate, error)
return tls.Certificate{}, trace.Wrap(err)
}
appCert, err := keyRing.AppTLSCert(c.RouteToApp.Name)
appCert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: c.RouteToApp.Name, Scope: c.RouteToApp.Scope})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
+2 -1
View File
@@ -34,6 +34,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/scopes"
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/utils"
)
@@ -167,7 +168,7 @@ func (d *MCPServerDialer) getCertLocked(ctx context.Context, mcpServer types.App
return tls.Certificate{}, trace.Wrap(err)
}
cert, err := keyRing.AppTLSCert(mcpServer.GetName())
cert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: appCertParams.RouteToApp.Name, Scope: appCertParams.RouteToApp.Scope})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
+18 -6
View File
@@ -37,6 +37,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/wrappers"
"github.com/gravitational/teleport/api/utils/keypaths"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/tlsca"
@@ -609,11 +610,15 @@ func (p *ProfileStatus) DatabaseLocalCAPath() string {
// AppCertPath returns path to the specified app access certificate
// for this profile.
//
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.crt
func (p *ProfileStatus) AppCertPath(cluster, name string) string {
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.crt for
// unscoped apps.
// Scoped apps are kept in
// <profile-dir>/keys/<proxy>/<user>-app/<cluster>/@<scope-qualified-subdomain>.crt.
func (p *ProfileStatus) AppCertPath(cluster string, appSQN scopes.QualifiedName) string {
if cluster == "" {
cluster = p.Cluster
}
name := ScopedAppName(appSQN)
if path, ok := p.virtualPathFromEnv(VirtualPathAppCert, VirtualPathAppCertParams(name)); ok {
return path
}
@@ -624,11 +629,15 @@ func (p *ProfileStatus) AppCertPath(cluster, name string) string {
// AppKeyPath returns path to the specified app access private key for this
// profile.
//
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.key
func (p *ProfileStatus) AppKeyPath(cluster, name string) string {
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.key for
// unscoped apps.
// Scoped apps are kept in
// <profile-dir>/keys/<proxy>/<user>-app/<cluster>/@<scope-qualified-subdomain>.key
func (p *ProfileStatus) AppKeyPath(cluster string, appSQN scopes.QualifiedName) string {
if cluster == "" {
cluster = p.Cluster
}
name := ScopedAppName(appSQN)
if path, ok := p.virtualPathFromEnv(VirtualPathKey, VirtualPathAppKeyParams(name)); ok {
return path
}
@@ -640,11 +649,14 @@ func (p *ProfileStatus) AppKeyPath(cluster, name string) string {
// this profile.
//
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>-localca.pem
func (p *ProfileStatus) AppLocalCAPath(cluster, name string) string {
// for unscoped apps.
// Scoped apps are kept in
// <profile-dir>/keys/<proxy>/<user>-app/<cluster>/@<scope-qualified-subdomain>-localca.pem.
func (p *ProfileStatus) AppLocalCAPath(cluster string, appSQN scopes.QualifiedName) string {
if cluster == "" {
cluster = p.Cluster
}
return keypaths.AppLocalCAPath(p.Dir, p.Name, p.Username, cluster, name)
return keypaths.AppLocalCAPath(p.Dir, p.Name, p.Username, cluster, ScopedAppName(appSQN))
}
// KubeConfigPath returns path to the specified kubeconfig for this profile.
+5
View File
@@ -57,6 +57,11 @@ func scopeSubdomainOk(subdomain string) bool {
return true
}
// ScopedSubdomain returns the scope-qualified subdomain for a scoped app.
func ScopedSubdomain(appName, scope string) string {
return generateScopedSubDomain(appName, scope)
}
// ScopedAppPublicAddr returns the derived public address for a scoped app (Scope Qualified SubDomain):
// "<ScopedSubdomain(name, scope)>.<proxy>".
// The trailing port on the proxy is stripped and the host is lowercased so the result
+35 -1
View File
@@ -19,6 +19,7 @@
package scopes
import (
"regexp"
"strings"
"github.com/gravitational/trace"
@@ -55,6 +56,25 @@ func (q QualifiedName) String() string {
return q.Scope + QualifiedNameSeparator + q.Name
}
// Set sets a possible scope qualified name. If the "::" separator is not present,
// then the scope qualified name will have an empty scope. This implements
// the flag/kingping Value interface.
func (q *QualifiedName) Set(val string) error {
if !strings.Contains(val, QualifiedNameSeparator) {
*q = QualifiedName{Name: val}
return nil
}
sqn, err := ParseQualifiedName(val)
if err != nil {
return err
}
if err := sqn.StrongValidate(); err != nil {
return err
}
*q = sqn
return nil
}
// StrongValidate validates this QualifiedName using strong validation rules. This method
// *must* be called on all QualifiedName values derived from user input and/or cluster-external
// sources. Use [QualifiedName.WeakValidate] when checking values from the control plane in
@@ -64,7 +84,7 @@ func (q QualifiedName) StrongValidate() error {
return trace.BadParameter("scope-qualified name %q has invalid scope: %v", q, err)
}
if err := StrongValidateSegment(q.Name); err != nil {
if err := strongValidateName(q.Name); err != nil {
return trace.BadParameter("scope-qualified name %q has invalid name: %v", q, err)
}
@@ -93,6 +113,20 @@ func (q QualifiedName) WeakValidate() error {
return nil
}
// nameRegexp is the regular expression used to validate scoped resource names. It enforces
// the same character rules as segmentRegexp, but allows for single character names.
var nameRegexp = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-\_\.]*[a-z0-9])?$`)
// strongValidateName checks if a scoped resource name is valid according to scope character
// formatting rules. Unlike scope segments, names carry no length constraints.
func strongValidateName(name string) error {
if name == "" {
return trace.BadParameter("name is empty")
}
return trace.Wrap(strongValidateFormat(name, nameRegexp))
}
// ParseQualifiedName parses a scope-qualified name string into its scope and name
// components by splitting on the first occurrence of "::". Returns an error if the
// separator is absent or either component is empty. This function does not validate
+52 -2
View File
@@ -252,9 +252,9 @@ func TestValidateQualifiedName(t *testing.T) {
weakOk: true,
},
{
name: "name too short",
name: "single-character name",
sqn: "/staging::x",
strongOk: false,
strongOk: true,
weakOk: true,
},
{
@@ -297,3 +297,53 @@ func TestValidateQualifiedName(t *testing.T) {
})
}
}
func TestSet(t *testing.T) {
t.Parallel()
tts := []struct {
name string
val string
ok bool
expected QualifiedName
}{
{
name: "only name provided",
val: "bare-name",
ok: true,
expected: QualifiedName{Name: "bare-name"},
},
{
name: "valid scope and name",
val: "/scope::name",
ok: true,
expected: QualifiedName{Name: "name", Scope: "/scope"},
},
{
name: "invalid name",
val: "/scope::!!name",
ok: false,
},
{
name: "invalid scope",
val: "!bad::test",
ok: false,
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var sqn QualifiedName
err := sqn.Set(tt.val)
if tt.ok {
require.NoError(t, err)
} else {
require.Error(t, err)
return
}
require.Equal(t, tt.expected, sqn)
})
}
}
+17 -5
View File
@@ -144,6 +144,22 @@ func StrongValidateSegment(segment string) error {
return trace.BadParameter("segment %q is too short (min characters %d)", segment, minSegmentSize)
}
if err := strongValidateFormat(segment, segmentRegexp); err != nil {
return trace.Wrap(err)
}
if len(segment) > maxSegmentSize {
return trace.BadParameter("segment %q is too long (max characters %d)", segment, maxSegmentSize)
}
return nil
}
// strongValidateFormat applies the strong formatting checks:
// - no uppercase characters
// - the provided shape regexp
// - weak checks as a defensive backstop
func strongValidateFormat(segment string, shape *regexp.Regexp) error {
// check for uppercase characters separately. this would be caught by the regex, but its better
// UX to call out uppercase characters specifically since its a common mistake.
for _, r := range segment {
@@ -152,14 +168,10 @@ func StrongValidateSegment(segment string) error {
}
}
if !segmentRegexp.MatchString(segment) {
if !shape.MatchString(segment) {
return trace.BadParameter("segment %q is malformed", segment)
}
if len(segment) > maxSegmentSize {
return trace.BadParameter("segment %q is too long (max characters %d)", segment, maxSegmentSize)
}
// as an extra precaution, also run all weak checks just to be certain we didn't accidentally
// construct a weak check that rejects something that would otherwise pass a strong check. strong
// validation is not used in perf-critical paths, so there isn't any real downside to a little
+2 -1
View File
@@ -29,6 +29,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/aws"
@@ -92,7 +93,7 @@ func (c *Cluster) ReissueAppCert(ctx context.Context, clusterClient *client.Clus
return tls.Certificate{}, trace.Wrap(err)
}
appCert, err := result.KeyRing.AppTLSCert(routeToApp.Name)
appCert, err := result.KeyRing.AppTLSCert(scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope})
return appCert, trace.Wrap(err)
}
+28 -57
View File
@@ -48,31 +48,8 @@ import (
logutils "github.com/gravitational/teleport/lib/utils/log"
)
// parseScopeQualifiedAppName splits a scope-qualified app name argument
// ("/scope::name") into cf.AppScope and cf.AppName. Bare names are left
// untouched and refer to unscoped apps.
func parseScopeQualifiedAppName(cf *CLIConf) error {
if !strings.Contains(cf.AppName, scopes.QualifiedNameSeparator) {
return nil
}
qn, err := scopes.ParseQualifiedName(cf.AppName)
if err != nil {
return trace.Wrap(err)
}
if err := scopes.StrongValidate(qn.Scope); err != nil {
return trace.Wrap(err)
}
cf.AppScope = qn.Scope
cf.AppName = qn.Name
return nil
}
// onAppLogin implements "tsh apps login" command.
func onAppLogin(cf *CLIConf) error {
if err := parseScopeQualifiedAppName(cf); err != nil {
return trace.Wrap(err)
}
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
@@ -457,9 +434,6 @@ Example command: tsh gcloud compute instances list
// onAppLogout implements "tsh apps logout" command.
func onAppLogout(cf *CLIConf) error {
if err := parseScopeQualifiedAppName(cf); err != nil {
return trace.Wrap(err)
}
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
@@ -477,19 +451,19 @@ func onAppLogout(cf *CLIConf) error {
// If a specific app name was specified, just log out of that app.
// Otherwise, log out of all apps.
var logout []tlsca.RouteToApp
if cf.AppName != "" {
if cf.AppSQN.Name != "" {
for _, app := range activeRoutes {
if app.Name == cf.AppName && app.Scope == cf.AppScope {
if app.Name == cf.AppSQN.Name && app.Scope == cf.AppSQN.Scope {
logout = append(logout, app)
}
}
if len(logout) == 0 {
// Not logged in but still try to delete any dangling files.
if err := tc.LogoutApp(cf.AppName); err != nil {
if err := tc.LogoutApp(cf.AppSQN); err != nil {
return trace.Wrap(err)
}
return trace.BadParameter("not logged into app %q", cf.AppName)
return trace.BadParameter("not logged into app %q", cf.AppSQN)
}
} else {
logout = activeRoutes
@@ -501,27 +475,27 @@ func onAppLogout(cf *CLIConf) error {
return trace.Wrap(err)
}
err = tc.LogoutApp(app.Name)
err = tc.LogoutApp(scopes.QualifiedName{Name: app.Name, Scope: app.Scope})
if err != nil {
return trace.Wrap(err)
}
// remove generated local files for the provided app.
err := utils.RemoveFileIfExist(profile.AppLocalCAPath(tc.SiteName, app.Name))
err := utils.RemoveFileIfExist(profile.AppLocalCAPath(tc.SiteName, scopes.QualifiedName{Name: app.Name, Scope: app.Scope}))
if err != nil {
logger.WarnContext(cf.Context, "Failed to clean up app session",
"error", err,
"profile", profile.AppLocalCAPath(tc.SiteName, app.Name))
"profile", profile.AppLocalCAPath(tc.SiteName, scopes.QualifiedName{Name: app.Name, Scope: app.Scope}))
}
if err := removeExternalFilesForApp(app); err != nil {
logger.WarnContext(cf.Context, "Failed to clean up app external files",
"error", err,
"app", cf.AppName)
"app", cf.AppSQN)
}
}
if cf.AppName == "" {
if cf.AppSQN.Name == "" {
// Try to delete any dangling files even if the app sessions are expired.
if err := tc.LogoutAllApps(); err != nil {
return trace.Wrap(err)
@@ -534,7 +508,7 @@ func onAppLogout(cf *CLIConf) error {
}
if len(logout) == 1 {
fmt.Printf("Logged out of app %q\n", logout[0].Name)
fmt.Printf("Logged out of app %q\n", scopes.QualifiedName{Name: logout[0].Name, Scope: logout[0].Scope}.String())
} else {
fmt.Println("Logged out of all apps")
}
@@ -543,9 +517,6 @@ func onAppLogout(cf *CLIConf) error {
// onAppConfig implements "tsh apps config" command.
func onAppConfig(cf *CLIConf) error {
if err := parseScopeQualifiedAppName(cf); err != nil {
return trace.Wrap(err)
}
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
@@ -602,10 +573,10 @@ func formatAppConfig(tc *client.TeleportClient, profile *client.ProfileStatus, r
if tc.InsecureSkipVerify {
curlInsecureFlag = "--insecure "
}
certPath := profile.AppCertPath(tc.SiteName, routeToApp.Name)
keyPath := profile.AppKeyPath(tc.SiteName, routeToApp.Name)
appName := scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope}.String()
appSQN := scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope}
certPath := profile.AppCertPath(tc.SiteName, appSQN)
keyPath := profile.AppKeyPath(tc.SiteName, appSQN)
appName := appSQN.String()
curlCmd := fmt.Sprintf(`curl %s\
--cert %q \
@@ -734,7 +705,7 @@ func getAppInfo(cf *CLIConf, clt authclient.ClientI, profile *client.ProfileStat
}
// If we didn't find an active profile for the app, get info from server.
app, logins, err := getApp(cf.Context, clt, cf.AppName, cf.AppScope)
app, logins, err := getApp(cf.Context, clt, cf.AppSQN)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -822,7 +793,7 @@ func (a *appInfo) GetApp(ctx context.Context, clt apiclient.GetResourcesClient)
return a.app.Copy(), nil
}
// holding mutex across the api call to avoid multiple redundant api calls.
app, _, err := getApp(ctx, clt, a.Name, a.RouteToApp.Scope)
app, _, err := getApp(ctx, clt, scopes.QualifiedName{Name: a.GetName(), Scope: a.GetScope()})
if err != nil {
return nil, trace.Wrap(err)
}
@@ -833,7 +804,7 @@ func (a *appInfo) GetApp(ctx context.Context, clt apiclient.GetResourcesClient)
// getApp returns the registered application with the specified name and scope.
// A bare name (empty scope) only ever resolves to an unscoped app; scoped apps
// must be referenced by their scope-qualified name.
func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope string) (app types.Application, logins []string, err error) {
func getApp(ctx context.Context, clt apiclient.GetResourcesClient, appSQN scopes.QualifiedName) (app types.Application, logins []string, err error) {
// A (name, scope) pair identifies a single logical app.
// if a user does not supply a scope, we need to further parse the results to make sure
// that the results do not contain scopes - disallowing cross scope app access.
@@ -842,10 +813,10 @@ func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope s
//
// TODO (williamo/scopes): eventually revert back to retrieving just 1 result and pass the scope expression by
// default, and delete the client side filtering.
predicate := fmt.Sprintf("name == %q", name)
predicate := fmt.Sprintf("name == %q", appSQN.Name)
limit := apidefaults.DefaultChunkSize
if scope != "" {
predicate = fmt.Sprintf("name == %q && resource.scope == %q", name, scope)
if appSQN.Scope != "" {
predicate = fmt.Sprintf("name == %q && resource.scope == %q", appSQN.Name, appSQN.Scope)
limit = 1
}
res, err := apiclient.GetEnrichedResourcePage(ctx, clt, &proto.ListResourcesRequest{
@@ -868,12 +839,12 @@ func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope s
}
a := server.GetApp()
if a.GetScope() == scope {
if a.GetScope() == appSQN.Scope {
return a, r.Logins, nil
}
}
return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name)
return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", appSQN.String())
}
// pickActiveApp returns the app the current profile is logged into.
@@ -881,7 +852,7 @@ func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope s
// If logged into multiple apps, returns an error unless one was specified
// explicitly on CLI.
func pickActiveApp(cf *CLIConf, activeRoutes []tlsca.RouteToApp) (proto.RouteToApp, error) {
if cf.AppName == "" {
if cf.AppSQN.Name == "" {
switch len(activeRoutes) {
case 0:
return proto.RouteToApp{}, trace.NotFound("please login using 'tsh apps login' first")
@@ -898,14 +869,14 @@ func pickActiveApp(cf *CLIConf, activeRoutes []tlsca.RouteToApp) (proto.RouteToA
}
for _, r := range activeRoutes {
if r.Name == cf.AppName && r.Scope == cf.AppScope {
if r.Name == cf.AppSQN.Name && r.Scope == cf.AppSQN.Scope {
return tlscaRouteToAppToProto(r), nil
}
}
if cf.AppScope != "" {
return proto.RouteToApp{}, trace.NotFound("not logged into app %q in scope %q", cf.AppName, cf.AppScope)
if cf.AppSQN.Scope != "" {
return proto.RouteToApp{}, trace.NotFound("not logged into app %q in scope %q", cf.AppSQN.Name, cf.AppSQN.Scope)
}
return proto.RouteToApp{}, trace.NotFound("not logged into app %q", cf.AppName)
return proto.RouteToApp{}, trace.NotFound("not logged into app %q", cf.AppSQN.Name)
}
func tlscaRouteToAppToProto(route tlsca.RouteToApp) proto.RouteToApp {
@@ -940,7 +911,7 @@ func onAppLogins(cf *CLIConf) error {
}
defer clusterClient.Close()
app, logins, err = getApp(cf.Context, clusterClient.AuthClient, cf.AppName, cf.AppScope)
app, logins, err = getApp(cf.Context, clusterClient.AuthClient, cf.AppSQN)
return trace.Wrap(err)
}); err != nil {
return trace.Wrap(err)
+2 -1
View File
@@ -37,6 +37,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
"github.com/gravitational/teleport/lib/tlsca"
@@ -169,7 +170,7 @@ func (a *awsApp) GetEnvVars() (map[string]string, error) {
// https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
"AWS_ACCESS_KEY_ID": cred.AccessKeyID,
"AWS_SECRET_ACCESS_KEY": cred.SecretAccessKey,
"AWS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, a.routeToApp.Name),
"AWS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, scopes.QualifiedName{Name: a.routeToApp.Name, Scope: a.routeToApp.Scope}),
// Proxy settings.
"HTTPS_PROXY": "http://" + a.localForwardProxy.GetAddr(),
"https_proxy": "http://" + a.localForwardProxy.GetAddr(),
+2 -1
View File
@@ -40,6 +40,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
@@ -245,7 +246,7 @@ func (a *azureApp) GetEnvVars() (map[string]string, error) {
// This isn't portable and applications other than az CLI may have to set different env variables,
// add the application cert to system root store (not recommended, ultimate fallback)
// or use equivalent of --insecure flag.
"REQUESTS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, a.routeToApp.Name),
"REQUESTS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, scopes.QualifiedName{Name: a.routeToApp.Name, Scope: a.routeToApp.Scope}),
}
if a.usingMSAL() {
+2 -1
View File
@@ -35,6 +35,7 @@ import (
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
@@ -241,7 +242,7 @@ func (a *gcpApp) GetEnvVars() (map[string]string, error) {
// Set core.custom_ca_certs_file via env variable, customizing the path to CA certs file.
// https://cloud.google.com/sdk/gcloud/reference/config/set#:~:text=custom_ca_certs_file
"CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE": a.profile.AppLocalCAPath(a.cf.SiteName, a.routeToApp.Name),
"CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE": a.profile.AppLocalCAPath(a.cf.SiteName, scopes.QualifiedName{Name: a.routeToApp.Name, Scope: a.routeToApp.Scope}),
// We need to set project ID. This is sourced from the account name.
// https://cloud.google.com/sdk/gcloud/reference/config#GROUP:~:text=authentication%20to%20gsutil.-,project,-Project%20ID%20of
+3 -2
View File
@@ -31,6 +31,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
)
@@ -159,7 +160,7 @@ func (a *localProxyApp) startLocalALPNProxy(ctx context.Context, portMapping cli
// If a stored cert is found for the app, try using it.
// Otherwise, let the checker reissue one as needed.
cert, err := loadAppCertificate(a.tc, routeToAppWithTargetPort.Name)
cert, err := loadAppCertificate(a.tc, routeToAppWithTargetPort.Name, routeToAppWithTargetPort.Scope)
if err == nil {
if a.app != nil && len(a.app.GetTCPPorts()) > 0 {
// There are too many cases to cover when dealing with a multi-port app and an existing cert.
@@ -176,7 +177,7 @@ func (a *localProxyApp) startLocalALPNProxy(ctx context.Context, portMapping cli
var listener net.Listener
if withTLS {
appLocalCAPath := a.profile.AppLocalCAPath(a.tc.SiteName, routeToAppWithTargetPort.Name)
appLocalCAPath := a.profile.AppLocalCAPath(a.tc.SiteName, scopes.QualifiedName{Name: routeToAppWithTargetPort.Name, Scope: routeToAppWithTargetPort.Scope})
localCertGenerator, err := client.NewLocalCertGenerator(ctx, appCertChecker, appLocalCAPath)
if err != nil {
return trace.Wrap(err)
+6 -6
View File
@@ -51,7 +51,7 @@ func newMCPConnectCommand(parent *kingpin.CmdClause, cf *CLIConf) *mcpConnectCom
cf: cf,
}
cmd.Arg("name", "Name of the MCP server.").Required().StringVar(&cf.AppName)
cmd.Arg("name", "Name of the MCP server.").Required().SetValue(&cf.AppSQN)
cmd.Flag("auto-reconnect", mcpAutoReconnectHelp).Default("true").BoolVar(&cmd.autoReconnect)
cmd.Flag("header", "Extra custom headers used for streamable HTTP MCP servers.").Short('H').StringsVar(&cmd.httpHeaders)
return cmd
@@ -81,7 +81,7 @@ func newMCPConfigCommand(parent *kingpin.CmdClause, cf *CLIConf) *mcpConfigComma
cmd.Flag("labels", labelHelp).StringVar(&cf.Labels)
cmd.Flag("query", queryHelp).StringVar(&cf.PredicateExpression)
cmd.Flag("auto-reconnect", mcpAutoReconnectHelp).IsSetByUser(&cmd.autoReconnectSetByUser).BoolVar(&cmd.autoReconnect)
cmd.Arg("name", "Name of the MCP server.").StringVar(&cf.AppName)
cmd.Arg("name", "Name of the MCP server.").SetValue(&cf.AppSQN)
cmd.clientConfig.addToCmd(cmd.CmdClause)
cmd.Alias(mcpConfigHelp)
cmd.Flag("header", "Extra custom headers used for streamable HTTP MCP servers.").Short('H').StringsVar(&cmd.httpHeaders)
@@ -323,7 +323,7 @@ func (c *mcpConfigCommand) checkSelectorFlags() error {
var mutuallyExclusiveSelectors int
for _, selectorEnabled := range []bool{
c.cf.ListAll,
c.cf.AppName != "",
c.cf.AppSQN.Name != "",
c.cf.PredicateExpression != "",
c.cf.Labels != "",
} {
@@ -356,8 +356,8 @@ func (c *mcpConfigCommand) fetchAndPrintResult() error {
}
func (c *mcpConfigCommand) fetch() error {
if c.cf.AppName != "" {
c.cf.PredicateExpression = makeNamePredicate(c.cf.AppName)
if c.cf.AppSQN.Name != "" {
c.cf.PredicateExpression = makeNamePredicate(c.cf.AppSQN.Name)
}
if c.fetchFunc == nil {
c.fetchFunc = fetchMCPServers
@@ -488,7 +488,7 @@ func (c *mcpConnectCommand) run() error {
return trace.Wrap(err)
}
dialer := client.NewMCPServerDialer(tc, c.cf.AppName)
dialer := client.NewMCPServerDialer(tc, c.cf.AppSQN.Name)
return clientmcp.ProxyStdioConn(
c.cf.Context,
clientmcp.ProxyStdioConnConfig{
+7 -6
View File
@@ -36,6 +36,7 @@ import (
"github.com/gravitational/teleport/lib/client"
mcpconfig "github.com/gravitational/teleport/lib/client/mcp/config"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)
@@ -225,7 +226,7 @@ func Test_mcpConfigCommand(t *testing.T) {
{
name: "not found",
cf: &CLIConf{
AppName: "not found",
AppSQN: scopes.QualifiedName{Name: "not found"},
},
checkError: require.Error,
// "local-everything" was already in the config. Double-check we
@@ -235,7 +236,7 @@ func Test_mcpConfigCommand(t *testing.T) {
{
name: "single",
cf: &CLIConf{
AppName: "dev2",
AppSQN: scopes.QualifiedName{Name: "dev2"},
},
checkError: require.NoError,
wantNamesInConfig: []string{"teleport-mcp-dev2", "local-everything"},
@@ -269,7 +270,7 @@ func Test_mcpConfigCommand(t *testing.T) {
name: "too many selectors",
cf: &CLIConf{
ListAll: true,
AppName: "dev2",
AppSQN: scopes.QualifiedName{Name: "dev2"},
},
checkError: require.Error,
wantNamesInConfig: []string{"local-everything"},
@@ -277,7 +278,7 @@ func Test_mcpConfigCommand(t *testing.T) {
{
name: "print json",
cf: &CLIConf{
AppName: "dev2",
AppSQN: scopes.QualifiedName{Name: "dev2"},
},
disableConfigFile: true,
checkError: require.NoError,
@@ -288,7 +289,7 @@ func Test_mcpConfigCommand(t *testing.T) {
{
name: "custom headers",
cf: &CLIConf{
AppName: "streamable-http",
AppSQN: scopes.QualifiedName{Name: "streamable-http"},
},
inputHeaderArgs: []string{
"H1: v1",
@@ -305,7 +306,7 @@ func Test_mcpConfigCommand(t *testing.T) {
{
name: "invalid header",
cf: &CLIConf{
AppName: "streamable-http",
AppSQN: scopes.QualifiedName{Name: "streamable-http"},
},
inputHeaderArgs: []string{
"H1=v1",
+4 -6
View File
@@ -48,6 +48,7 @@ import (
libclient "github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/db/dbcmd"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/tlsca"
@@ -613,9 +614,6 @@ func alpnProtocolForApp(app types.Application, appHTTPSTunnel bool) (alpncommon.
}
func onProxyCommandApp(cf *CLIConf) error {
if err := parseScopeQualifiedAppName(cf); err != nil {
return trace.Wrap(err)
}
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
@@ -671,7 +669,7 @@ func onProxyCommandApp(cf *CLIConf) error {
return trace.Wrap(err)
}
appName := cf.AppName
appName := cf.AppSQN.Name
if portMapping.TargetPort != 0 {
appName = net.JoinHostPort(appName, strconv.Itoa(portMapping.TargetPort))
}
@@ -874,13 +872,13 @@ func onProxyCommandGCloud(cf *CLIConf) error {
return nil
}
func loadAppCertificate(tc *libclient.TeleportClient, appName string) (tls.Certificate, error) {
func loadAppCertificate(tc *libclient.TeleportClient, appName, scope string) (tls.Certificate, error) {
keyRing, err := tc.LocalAgent().GetKeyRing(tc.SiteName, libclient.WithAppCerts{})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
appCert, err := keyRing.AppTLSCert(appName)
appCert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: appName, Scope: scope})
if trace.IsNotFound(err) {
return tls.Certificate{}, trace.NotFound("please login into the application first: 'tsh apps login %v'", appName)
} else if err != nil {
+15 -20
View File
@@ -280,13 +280,8 @@ type CLIConf struct {
DatabaseRoles string
// DatabaseCommand specifies the command to execute.
DatabaseCommand string
// AppName specifies proxied application name.
AppName string
// AppScope is the scope of the app to log into, parsed from a scope-qualified
// app name argument ("/scope::name"). Scoped apps must be referenced by their
// scope-qualified name; bare names refer to unscoped apps.
AppScope string
// AppSQN specifies proxied application name and scope.
AppSQN scopes.QualifiedName
// AppHTTPSTunnel enables a special mode to tunnel https for HTTP apps.
// Mainly used for debugging purpose so the flag should be hidden.
AppHTTPSTunnel bool
@@ -1088,7 +1083,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
// Use Interspersed(false) to forward all flags to AWS CLI.
aws := app.Command("aws", "Access AWS API.").Interspersed(false)
aws.Arg("command", "AWS command and subcommands arguments that are going to be forwarded to AWS CLI.").StringsVar(&cf.AWSCommandArgs)
aws.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").StringVar(&cf.AppName)
aws.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").SetValue(&cf.AppSQN)
aws.Flag("endpoint-url", "Run local proxy to serve as an AWS endpoint URL. If not specified, local proxy serves as an HTTPS proxy.").
Short('e').Hidden().BoolVar(&cf.AWSEndpointURLMode)
aws.Flag("exec", "Execute different commands (e.g. terraform) under Teleport credentials.").StringVar(&cf.Exec)
@@ -1101,18 +1096,18 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
azure := app.Command("az", "Access Azure API.").Interspersed(false)
azure.Arg("command", "`az` command and subcommands arguments that are going to be forwarded to Azure CLI.").StringsVar(&cf.AzureCommandArgs)
azure.Flag("app", "Optional name of the Azure application to use if logged into multiple.").StringVar(&cf.AppName)
azure.Flag("app", "Optional name of the Azure application to use if logged into multiple.").SetValue(&cf.AppSQN)
azure.Flag("azure-identity", "(For Azure CLI access only) Azure managed identity name.").StringVar(&cf.AzureIdentity)
gcloud := app.Command("gcloud", "Access GCP API with the gcloud command.").Interspersed(false)
gcloud.Arg("command", "`gcloud` command and subcommands arguments.").StringsVar(&cf.GCPCommandArgs)
gcloud.Flag("app", "Optional name of the GCP application to use if logged into multiple.").StringVar(&cf.AppName)
gcloud.Flag("app", "Optional name of the GCP application to use if logged into multiple.").SetValue(&cf.AppSQN)
gcloud.Flag("gcp-service-account", "(For GCP CLI access only) GCP service account name.").StringVar(&cf.GCPServiceAccount)
gcloud.Alias("gcp")
gsutil := app.Command("gsutil", "Access Google Cloud Storage with the gsutil command.").Interspersed(false)
gsutil.Arg("command", "`gsutil` command and subcommands arguments.").StringsVar(&cf.GCPCommandArgs)
gsutil.Flag("app", "Optional name of the GCP application to use if logged into multiple.").StringVar(&cf.AppName)
gsutil.Flag("app", "Optional name of the GCP application to use if logged into multiple.").SetValue(&cf.AppSQN)
gsutil.Flag("gcp-service-account", "(For GCP CLI access only) GCP service account name.").StringVar(&cf.GCPServiceAccount)
// Applications.
@@ -1126,7 +1121,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
lsApps.Arg("labels", labelHelp).StringVar(&cf.Labels)
lsApps.Flag("all", "List apps from all clusters and proxies.").Short('R').BoolVar(&cf.ListAll)
appLogin := apps.Command("login", "Retrieve short-lived certificate for an app.")
appLogin.Arg("app", "App name to retrieve credentials for, as shown in `tsh apps ls`. Scoped apps use their scope-qualified name (\"/scope::name\").").Required().StringVar(&cf.AppName)
appLogin.Arg("app", "App name to retrieve credentials for, as shown in `tsh apps ls`. Scoped apps use their scope-qualified name (\"/scope::name\").").Required().SetValue(&cf.AppSQN)
appLogin.Flag("aws-role", "(For AWS CLI access only) Amazon IAM role ARN or role name.").StringVar(&cf.AWSRole)
appLogin.Flag("env", "(For AWS CLI access only) Obtain credentials as plain text in order to load into environments variables. Required when using per-session MFA.").Hidden().BoolVar(&cf.AppLoginAWSEnvOutput)
appLogin.Flag("azure-identity", "(For Azure CLI access only) Azure managed identity name.").StringVar(&cf.AzureIdentity)
@@ -1135,14 +1130,14 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
appLogin.Flag("interactive", "Prompt for AWS Roles interactively (--aws-role takes precedence).").Short('T').Default("true").Envar(awsLoginInteractive).BoolVar(&cf.Interactive)
appLogin.Flag("quiet", quietHelp).Short('q').BoolVar(&cf.Quiet)
appLogout := apps.Command("logout", "Remove app certificate.")
appLogout.Arg("app", "App to remove credentials for.").StringVar(&cf.AppName)
appLogout.Arg("app", "App to remove credentials for.").SetValue(&cf.AppSQN)
appConfig := apps.Command("config", "Print app connection information.")
appConfig.Arg("app", "App to print information for. Required when logged into multiple apps.").StringVar(&cf.AppName)
appConfig.Arg("app", "App to print information for. Required when logged into multiple apps.").SetValue(&cf.AppSQN)
appConfig.Flag("format", fmt.Sprintf("Optional print format, one of: %q to print app address, %q to print CA cert path, %q to print cert path, %q print key path, %q to print example curl command, %q or %q to print everything as JSON or YAML.",
appFormatURI, appFormatCA, appFormatCert, appFormatKey, appFormatCURL, appFormatJSON, appFormatYAML),
).Short('f').StringVar(&cf.Format)
appLogins := apps.Command("logins", "List available logins for a Cloud console application.")
appLogins.Arg("app", "App name to list logins for. Currently only AWS is supported.").Required().StringVar(&cf.AppName)
appLogins.Arg("app", "App name to list logins for. Currently only AWS is supported.").Required().SetValue(&cf.AppSQN)
appLogins.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)).Short('f').Default(teleport.Text).EnumVar(&cf.Format, defaults.DefaultFormats...)
// Recordings.
@@ -1184,30 +1179,30 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
proxyDB.Flag("disable-access-request", "Disable automatic resource Access Requests.").BoolVar(&cf.disableAccessRequest)
proxyApp := proxy.Command("app", "Start local TLS proxy for app connection when using Teleport in single-port mode.")
proxyApp.Arg("app", "The name of the application to start local proxy for.").Required().StringVar(&cf.AppName)
proxyApp.Arg("app", "The name of the application to start local proxy for.").Required().SetValue(&cf.AppSQN)
proxyApp.Flag("port", "Specifies the listening port used by the proxy app listener. Accepts an optional target port of a multi-port TCP app after a colon, e.g. \"1234:5678\".").Short('p').StringVar(&cf.LocalProxyPortMapping)
proxyApp.Flag("cluster", clusterHelp).Short('c').StringVar(&cf.SiteName)
proxyApp.Flag("https-tunnel", "Use the teleport-app-https ALPN protocol (HTTPS tunneled over mTLS) for HTTP apps.").Hidden().BoolVar(&cf.AppHTTPSTunnel)
proxyMCP := proxy.Command("mcp", "Start local proxy for MCP access.")
proxyMCP.Arg("app", "The name of the MCP application to start local proxy for.").Required().StringVar(&cf.AppName)
proxyMCP.Arg("app", "The name of the MCP application to start local proxy for.").Required().SetValue(&cf.AppSQN)
proxyMCP.Flag("port", "Specifies the listening port used by the proxy app listener.").Short('p').StringVar(&cf.LocalProxyPortMapping)
proxyMCP.Flag("cluster", clusterHelp).Short('c').StringVar(&cf.SiteName)
proxyAWS := proxy.Command("aws", "Start local proxy for AWS access.")
proxyAWS.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").StringVar(&cf.AppName)
proxyAWS.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").SetValue(&cf.AppSQN)
proxyAWS.Flag("port", "Specifies the source port used by the proxy listener.").Short('p').StringVar(&cf.LocalProxyPort)
proxyAWS.Flag("endpoint-url", "Run local proxy to serve as an AWS endpoint URL. If not specified, local proxy serves as an HTTPS proxy.").Short('e').Hidden().BoolVar(&cf.AWSEndpointURLMode)
proxyAWS.Flag("format", awsProxyFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, awsProxyFormats...)
proxyAzure := proxy.Command("azure", "Start local proxy for Azure access.")
proxyAzure.Flag("app", "Optional Name of the Azure application to use if logged into multiple.").StringVar(&cf.AppName)
proxyAzure.Flag("app", "Optional Name of the Azure application to use if logged into multiple.").SetValue(&cf.AppSQN)
proxyAzure.Flag("port", "Specifies the source port used by the proxy listener.").Short('p').StringVar(&cf.LocalProxyPort)
proxyAzure.Flag("format", envVarFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, envVarFormats...)
proxyAzure.Alias("az")
proxyGcloud := proxy.Command("gcloud", "Start local proxy for GCP access.")
proxyGcloud.Flag("app", "Optional Name of the GCP application to use if logged into multiple.").StringVar(&cf.AppName)
proxyGcloud.Flag("app", "Optional Name of the GCP application to use if logged into multiple.").SetValue(&cf.AppSQN)
proxyGcloud.Flag("port", "Specifies the source port used by the proxy listener.").Short('p').StringVar(&cf.LocalProxyPort)
proxyGcloud.Flag("format", envVarFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, envVarFormats...)
proxyGcloud.Alias("gcp")
+2 -1
View File
@@ -32,6 +32,7 @@ import (
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/clientcache"
libhwk "github.com/gravitational/teleport/lib/hardwarekey"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/vnet"
)
@@ -297,7 +298,7 @@ func (p *vnetClientApplication) reissueAppCert(ctx context.Context, tc *client.T
return tls.Certificate{}, trace.Wrap(err, "logging in to app")
}
cert, err := keyRing.AppTLSCert(routeToApp.Name)
cert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope})
if err != nil {
return tls.Certificate{}, trace.Wrap(err, "getting TLS cert from key")
}