Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -193,6 +193,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"
```

Expand Down
10 changes: 10 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,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)
Expand Down Expand Up @@ -3652,6 +3656,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)
}
Expand All @@ -3667,6 +3674,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)
}
Expand Down
67 changes: 67 additions & 0 deletions lib/auth/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3229,6 +3229,45 @@ func TestAppsCRUD(t *testing.T) {
require.NoError(t, err)
require.Empty(t, out)
require.Empty(t, next)

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.
Expand Down Expand Up @@ -3327,6 +3366,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.
Expand Down
4 changes: 4 additions & 0 deletions lib/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5880,6 +5880,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)
}

Expand Down
38 changes: 38 additions & 0 deletions lib/services/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"net/url"
"os"
"slices"
"strings"
"sync"

Expand Down Expand Up @@ -59,6 +60,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)
Expand Down
77 changes: 77 additions & 0 deletions lib/services/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
36 changes: 28 additions & 8 deletions lib/web/app/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package app

import (
"net/http"
"slices"

"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
Expand Down Expand Up @@ -73,20 +74,39 @@ 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.
h.log.Error("" +
"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/.")
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.log.Error(errMsg)

// Immediately return an error since this is a critical misconfiguration 🛑
return trace.BadParameter(errMsg)
}

addr, err := utils.ParseAddr(r.Host)
if err != nil {
return trace.Wrap(err)
}

if slices.ContainsFunc(
h.c.ProxyPublicAddrs,
func(proxyAddr utils.NetAddr) bool {
return 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.log.WithField("launcher_params", p).Error(errMsg)

// Immediately return an error since this is a critical misconfiguration 🛑
return trace.BadParameter(errMsg)
}

urlString := makeAppRedirectURL(r, h.c.WebPublicAddr, addr.Host(), p)
http.Redirect(w, r, urlString, http.StatusFound)
return nil
Expand Down Expand Up @@ -116,7 +136,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
Expand Down
Loading