Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
37902d9
fix: apps should not be able to set public_addr to the web proxy address
cthach Aug 28, 2025
129f33a
feat: add API validation
cthach Aug 29, 2025
a54ce1a
test: add coverage for UpsertApplicationServer
cthach Sep 2, 2025
ce538a4
refactor: polish
cthach Sep 2, 2025
9b071c1
refactor: make consistent
cthach Sep 2, 2025
23fcc9b
refactor: use ValidateApp func everywhere. Revert changes to Check* m…
cthach Sep 2, 2025
3055ab3
refactor: dedupe
cthach Sep 2, 2025
e91ef38
refactor: improve error messages for application address conflicts an…
cthach Sep 2, 2025
ad2421d
ux: bubble up friendly error to UI
cthach Sep 3, 2025
350e3a5
refactor: revert unnecessary change
cthach Sep 3, 2025
9590c4d
fix: app public address in redirect
cthach Sep 3, 2025
1f29fed
Merge branch 'master' into cthach/restrict-public-addr
cthach Sep 3, 2025
0042a49
fix: streamline proxy address validation in ValidateApp function
cthach Sep 4, 2025
be130f5
refactor: remove contact cluster admin in favor of self-service. Add …
cthach Sep 4, 2025
3cddb81
Apply suggestions from code review
cthach Sep 4, 2025
293a182
fix: skip proxy servers with unset public addresses in ValidateApp fu…
cthach Sep 4, 2025
36361ed
Merge branch 'master' into cthach/restrict-public-addr
cthach Sep 4, 2025
20b176c
refactor: simplify error messages for application public address conf…
cthach Sep 4, 2025
3ffc606
fix: logging in the wrong spot
cthach Sep 4, 2025
b5e75b5
fix: handle when a server has multiple public addrs
cthach Sep 4, 2025
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 @@ -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"
```

Expand Down
10 changes: 10 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
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 @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions lib/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
38 changes: 38 additions & 0 deletions lib/services/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"net"
"net/url"
"os"
"slices"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -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)
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
37 changes: 27 additions & 10 deletions lib/web/app/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,42 @@ 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)
Comment thread
rosstimothy marked this conversation as resolved.
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)
if err != nil {
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)
Expand Down Expand Up @@ -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
Expand Down
Loading