diff --git a/docs/pages/enroll-resources/application-access/guides/connecting-apps.mdx b/docs/pages/enroll-resources/application-access/guides/connecting-apps.mdx index 48aea459dd71f..0fdc2f93776f1 100644 --- a/docs/pages/enroll-resources/application-access/guides/connecting-apps.mdx +++ b/docs/pages/enroll-resources/application-access/guides/connecting-apps.mdx @@ -17,7 +17,7 @@ Application Service, which uses a join token to establish trust with the Teleport Auth Service. Users visit Teleport-protected web applications through the Teleport Web UI. The Teleport Proxy Service routes browser traffic to the Teleport Application Service, which forwards HTTP requests to and from target -applications. +applications. ## Prerequisites @@ -38,7 +38,7 @@ target application, then deploy a Teleport Agent to run the service. ### Generate a token A join token is required to authorize a Teleport Application Service to -join the cluster. +join the cluster. 1. Generate a short-lived join token. Make sure to change `app-name` to the name of your application and `app-uri` to the application's domain name and port: @@ -195,6 +195,7 @@ address. To override the public address, specify the `public_addr` field: ```yaml - name: "jira" uri: "https://localhost:8001" + # The public address must be a unique DNS name and not conflict with the Teleport cluster's public addresses. public_addr: "jira.example.com" ``` diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index 247d2b76a0ca0..e4acacc53889e 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -1489,6 +1489,10 @@ func (g *GRPCServer) UpsertApplicationServer(ctx context.Context, req *authpb.Up } } + if err := services.ValidateApp(app, auth); err != nil { + return nil, trace.Wrap(err) + } + keepAlive, err := auth.UpsertApplicationServer(ctx, server) if err != nil { return nil, trace.Wrap(err) @@ -3846,6 +3850,9 @@ func (g *GRPCServer) CreateApp(ctx context.Context, app *types.AppV3) (*emptypb. if app.Origin() == "" { app.SetOrigin(types.OriginDynamic) } + if err := services.ValidateApp(app, auth); err != nil { + return nil, trace.Wrap(err) + } if err := auth.CreateApp(ctx, app); err != nil { return nil, trace.Wrap(err) } @@ -3861,6 +3868,9 @@ func (g *GRPCServer) UpdateApp(ctx context.Context, app *types.AppV3) (*emptypb. if app.Origin() == "" { app.SetOrigin(types.OriginDynamic) } + if err := services.ValidateApp(app, auth); err != nil { + return nil, trace.Wrap(err) + } if err := auth.UpdateApp(ctx, app); err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/grpcserver_test.go b/lib/auth/grpcserver_test.go index 05642d00aa3c9..fc1caeb87f5b6 100644 --- a/lib/auth/grpcserver_test.go +++ b/lib/auth/grpcserver_test.go @@ -3680,6 +3680,45 @@ func TestAppsCRUD(t *testing.T) { iterOut = append(iterOut, app) } require.Empty(t, iterOut) + + err = srv.Auth().UpsertProxy(ctx, &types.ServerV2{ + Kind: types.KindProxy, + Metadata: types.Metadata{ + Name: "proxy", + }, + Spec: types.ServerSpecV2{ + PublicAddrs: []string{"proxy.example.com:443"}, + }, + }) + require.NoError(t, err) + + t.Run("Creating an app with a public address matching a proxy address should fail", func(t *testing.T) { + misconfiguredApp, err := types.NewAppV3(types.Metadata{ + Name: "misconfigured-app", + Labels: map[string]string{types.OriginLabel: types.OriginDynamic}, + }, types.AppSpecV3{ + URI: "localhost1", + PublicAddr: "proxy.example.com", + }) + require.NoError(t, err) + + err = clt.CreateApp(ctx, misconfiguredApp) + require.ErrorIs(t, err, trace.BadParameter(`Application "misconfigured-app" public address "proxy.example.com" conflicts with the Teleport Proxy public address. Configure the application to use a unique public address that does not match the proxy's public addresses. Refer to https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/.`)) + }) + + t.Run("Updating an app with a public address matching a proxy address should fail", func(t *testing.T) { + misconfiguredApp, err := types.NewAppV3(types.Metadata{ + Name: "misconfigured-app", + Labels: map[string]string{types.OriginLabel: types.OriginDynamic}, + }, types.AppSpecV3{ + URI: "localhost1", + PublicAddr: "proxy.example.com", + }) + require.NoError(t, err) + + err = clt.UpdateApp(ctx, misconfiguredApp) + require.ErrorIs(t, err, trace.BadParameter(`Application "misconfigured-app" public address "proxy.example.com" conflicts with the Teleport Proxy public address. Configure the application to use a unique public address that does not match the proxy's public addresses. Refer to https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/.`)) + }) } // TestAppServersCRUD tests application server resource operations. @@ -3778,6 +3817,34 @@ func TestAppServersCRUD(t *testing.T) { }) require.NoError(t, err) require.Empty(t, resources.Resources) + + t.Run("App server with an app that has a public address matching a proxy address should fail", func(t *testing.T) { + err = srv.Auth().UpsertProxy(ctx, &types.ServerV2{ + Kind: types.KindProxy, + Metadata: types.Metadata{ + Name: "proxy", + }, + Spec: types.ServerSpecV2{ + PublicAddrs: []string{"proxy.example.com:443"}, + }, + }) + require.NoError(t, err) + + misconfiguredApp, err := types.NewAppV3(types.Metadata{ + Name: "misconfigured-app", + Labels: map[string]string{types.OriginLabel: types.OriginDynamic}, + }, types.AppSpecV3{ + URI: "localhost1", + PublicAddr: "proxy.example.com", + }) + require.NoError(t, err) + + appServer, err := types.NewAppServerV3FromApp(misconfiguredApp, "misconfigured-app", "hostID") + require.NoError(t, err) + + _, err = clt.UpsertApplicationServer(ctx, appServer) + require.ErrorIs(t, err, trace.BadParameter(`Application "misconfigured-app" public address "proxy.example.com" conflicts with the Teleport Proxy public address. Configure the application to use a unique public address that does not match the proxy's public addresses. Refer to https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/.`)) + }) } // TestDatabasesCRUD tests database resource operations. diff --git a/lib/service/service.go b/lib/service/service.go index 143725678eadd..518af0b988a3c 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -6402,6 +6402,10 @@ func (process *TeleportProcess) initApps() { return trace.Wrap(err) } + if err := services.ValidateApp(a, accessPoint); err != nil { + return trace.Wrap(err) + } + applications = append(applications, a) } diff --git a/lib/services/app.go b/lib/services/app.go index 096d136000b8c..dbd970edc06de 100644 --- a/lib/services/app.go +++ b/lib/services/app.go @@ -25,6 +25,7 @@ import ( "net" "net/url" "os" + "slices" "strconv" "strings" "sync" @@ -64,6 +65,43 @@ type Applications interface { DeleteAllApps(context.Context) error } +// ValidateApp validates the Application resource. +func ValidateApp(app types.Application, proxyGetter ProxyGetter) error { + // Prevent routing conflicts and session hijacking by ensuring the application's public address does not match the + // public address of any proxy. If an application shares a public address with a proxy, requests intended for the + // proxy could be misrouted to the application, compromising security. + if app.GetPublicAddr() != "" { + proxyServers, err := proxyGetter.GetProxies() + if err != nil { + return trace.Wrap(err) + } + + for _, proxyServer := range proxyServers { + proxyAddrs, err := utils.ParseAddrs(proxyServer.GetPublicAddrs()) + if err != nil { + return trace.Wrap(err) + } + + if slices.ContainsFunc( + proxyAddrs, + func(proxyAddr utils.NetAddr) bool { + return app.GetPublicAddr() == proxyAddr.Host() + }, + ) { + return trace.BadParameter( + "Application %q public address %q conflicts with the Teleport Proxy public address. "+ + "Configure the application to use a unique public address that does not match the proxy's public addresses. "+ + "Refer to https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/.", + app.GetName(), + app.GetPublicAddr(), + ) + } + } + } + + return nil +} + // MarshalApp marshals Application resource to JSON. func MarshalApp(app types.Application, opts ...MarshalOption) ([]byte, error) { cfg, err := CollectOptions(opts) diff --git a/lib/services/app_test.go b/lib/services/app_test.go index 2dadd3fdb6cf6..65b58133f1282 100644 --- a/lib/services/app_test.go +++ b/lib/services/app_test.go @@ -29,6 +29,83 @@ import ( "github.com/gravitational/teleport/lib/utils" ) +func TestValidateApp(t *testing.T) { + tests := []struct { + name string + app types.Application + proxyAddrs []string + wantErr string + }{ + { + name: "no public addr, no error", + app: func() types.Application { + app, _ := types.NewAppV3(types.Metadata{Name: "app"}, types.AppSpecV3{URI: "http://localhost:8080"}) + return app + }(), + proxyAddrs: []string{"web.example.com:443"}, + }, + { + name: "public addr does not conflict", + app: func() types.Application { + app, _ := types.NewAppV3(types.Metadata{Name: "app"}, types.AppSpecV3{URI: "http://localhost:8080", PublicAddr: "app.example.com"}) + return app + }(), + proxyAddrs: []string{"web.example.com:443"}, + }, + { + name: "public addr matches proxy host", + app: func() types.Application { + app, _ := types.NewAppV3(types.Metadata{Name: "app"}, types.AppSpecV3{URI: "http://localhost:8080", PublicAddr: "web.example.com"}) + return app + }(), + proxyAddrs: []string{"web.example.com:443"}, + wantErr: "conflicts with the Teleport Proxy public address", + }, + { + name: "multiple proxy addrs, one matches", + app: func() types.Application { + app, _ := types.NewAppV3(types.Metadata{Name: "app"}, types.AppSpecV3{URI: "http://localhost:8080", PublicAddr: "web.example.com"}) + return app + }(), + proxyAddrs: []string{"other.com:443", "web.example.com:443"}, + wantErr: "conflicts with the Teleport Proxy public address", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateApp(tt.app, &mockProxyGetter{addrs: tt.proxyAddrs}) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +// mockProxyGetter is a test implementation of ProxyGetter. +type mockProxyGetter struct { + addrs []string +} + +func (m *mockProxyGetter) GetProxies() ([]types.Server, error) { + servers := make([]types.Server, 0, len(m.addrs)) + + for _, addr := range m.addrs { + servers = append( + servers, + &types.ServerV2{ + Spec: types.ServerSpecV2{ + PublicAddrs: []string{addr}, + }, + }, + ) + } + + return servers, nil +} + // TestApplicationUnmarshal verifies an app resource can be unmarshaled. func TestApplicationUnmarshal(t *testing.T) { expected, err := types.NewAppV3(types.Metadata{ diff --git a/lib/web/app/middleware.go b/lib/web/app/middleware.go index 2d364e8bc109e..71bf21020b15b 100644 --- a/lib/web/app/middleware.go +++ b/lib/web/app/middleware.go @@ -73,13 +73,15 @@ func (h *Handler) redirectToLauncher(w http.ResponseWriter, r *http.Request, p l } if h.c.WebPublicAddr == "" { - // The error below tends to be swallowed by the Web UI, so log a warning for - // admins as well. - const msg = "Application Service requires public_addr to be set in the Teleport Proxy Service configuration. " + - "Please contact your Teleport cluster administrator or refer to " + - "https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/." - h.logger.ErrorContext(r.Context(), msg) - return trace.BadParameter("public address of the proxy is not set") + const errMsg = "Application Service requires public_addr to be set in the Teleport Proxy Service configuration. " + + "Update the Teleport Proxy Service configuration to include a public_addr. " + + "Refer to https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/." + + // Log the error to warn admins 🚩 + h.logger.ErrorContext(r.Context(), errMsg) + + // Immediately return an error since this is a critical misconfiguration 🛑 + return trace.BadParameter(errMsg) } addr, err := utils.ParseAddr(r.Host) @@ -87,11 +89,26 @@ func (h *Handler) redirectToLauncher(w http.ResponseWriter, r *http.Request, p l return trace.Wrap(err) } - var proxyPublicAddrs []string + proxyPublicAddrs := make([]string, 0, len(h.c.ProxyPublicAddrs)) + for _, proxyAddr := range h.c.ProxyPublicAddrs { - // preserving full `host:port` to support proxy hosted on non-standard HTTPS port. + if p.publicAddr == proxyAddr.Host() { + const errMsg = "Application public address conflicts with the Teleport Proxy public address. " + + "Configure the application to use a unique public address that does not match the proxy's public addresses. " + + "Refer to https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/." + + // Log the error to warn admins 🚩 + h.logger.ErrorContext(r.Context(), errMsg, "launcher_params", p) + + // Immediately return an error since this is a critical misconfiguration 🛑 + return trace.BadParameter(errMsg) + } + + // Append the full proxy address (host:port) to the list, preserving the port information. + // This is necessary to support proxies running on non-standard HTTPS ports and ensure accurate DNS matching. proxyPublicAddrs = append(proxyPublicAddrs, proxyAddr.String()) } + proxyDNSName := utils.FindMatchingProxyDNS(r.Host, proxyPublicAddrs) urlString := makeAppRedirectURL(r, proxyDNSName, addr.Host(), p) http.Redirect(w, r, urlString, http.StatusFound) @@ -122,7 +139,7 @@ func makeHandler(handler handlerFunc) http.HandlerFunc { // response writer. func writeError(w http.ResponseWriter, err error) { code := trace.ErrorToCode(err) - http.Error(w, http.StatusText(code), code) + http.Error(w, err.Error(), code) } type routerFunc func(http.ResponseWriter, *http.Request, httprouter.Params) error