From 18388ef01f8b0f8d6c3ac9dceeb70e6c1eedc39d Mon Sep 17 00:00:00 2001 From: Marco Dinis Date: Tue, 18 Nov 2025 17:54:43 +0000 Subject: [PATCH 1/2] Fix AWS Console (w/ integration) access when using IP Pinning When IP Pinning is enabled, the certificate's pinned IP and the IP presented by the client in the connection must be the same. For AWS Console access which uses an integration, we don't use an Application Service to proxy connections, the Proxy Service is used instead. In this situation, it uses a `net.Pipe` which sets both Local/Remote addrs to `pipe`. When validating whether that's the same IP as the one present in the certificate's pinned IP, it fails because that's the address of an intermediate connection. This PR fixes this by wrapping the connection with an overrided `RemoteAddr` method which returns the true client's IP extracted from the context. --- lib/web/app/transport.go | 37 ++++++++++++++++++++++++++++++++++- lib/web/app/transport_test.go | 10 +++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/web/app/transport.go b/lib/web/app/transport.go index d186aa805e438..4a7ecec984c91 100644 --- a/lib/web/app/transport.go +++ b/lib/web/app/transport.go @@ -361,7 +361,21 @@ func (t *transport) DialContext(ctx context.Context, _, _ string) (conn net.Conn appIntegration := appServer.GetApp().GetIntegration() if appIntegration != "" { src, dst := net.Pipe() - go t.c.integrationAppHandler.HandleConnection(src) + + // Creating the connection using `net.Pipe()` results in both ends having the same local/remote address: "pipe". + // This causes IP Pinning checks to fail when validating that the connection remote address matches the certificate pinned IP. + // + // To fix this, we need to wrap the `src` connection to return the client source address as the remote address. + // The client source address is extracted from the context, the same way as in `dialAppServer`. + srcWithClientSrcAddr, err := wrapConnWithClientSrcAddrFromContext(ctx, src) + if err != nil { + // Log a warning and use the original remote address if we fail to extract the client source address from context. + // This will result in an error if the flow has pinned IP restrictions. + t.c.log.WarnContext(ctx, "Failed to extract client source address from context when proxying access to Application with integration credentials. IP Pinning checks will fail.", "error", err) + srcWithClientSrcAddr = src + } + + go t.c.integrationAppHandler.HandleConnection(srcWithClientSrcAddr) return dst, nil } @@ -387,6 +401,27 @@ func (t *transport) DialContext(ctx context.Context, _, _ string) (conn net.Conn return nil, trace.ConnectionProblem(nil, "no application servers remaining to connect") } +type connWithCustomRemoteAddr struct { + net.Conn + remoteAddr net.Addr +} + +func (w *connWithCustomRemoteAddr) RemoteAddr() net.Addr { + return w.remoteAddr +} + +func wrapConnWithClientSrcAddrFromContext(ctx context.Context, conn net.Conn) (net.Conn, error) { + clientSrcAddr, err := authz.ClientSrcAddrFromContext(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + return &connWithCustomRemoteAddr{ + Conn: conn, + remoteAddr: clientSrcAddr, + }, nil +} + // DialWebsocket dials a websocket connection over the transport's reverse // tunnel. func (t *transport) DialWebsocket(network, address string) (net.Conn, error) { diff --git a/lib/web/app/transport_test.go b/lib/web/app/transport_test.go index 78900d6c7bbce..291dcae5598ef 100644 --- a/lib/web/app/transport_test.go +++ b/lib/web/app/transport_test.go @@ -42,6 +42,7 @@ import ( "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/jwt" @@ -493,13 +494,20 @@ func Test_transport_with_integration(t *testing.T) { }) require.NoError(t, err) - conn, err := tr.DialContext(context.Background(), "", "") + ctxWithClientSrcAddr := authz.ContextWithClientSrcAddr(t.Context(), &utils.NetAddr{ + AddrNetwork: "tcp", + Addr: net.JoinHostPort("127.0.0.1", "55555"), + }) + + conn, err := tr.DialContext(ctxWithClientSrcAddr, "", "") require.NoError(t, err) require.Eventually(t, func() bool { return integrationAppHandler.getConnection() != nil }, 100*time.Millisecond, 10*time.Millisecond) + require.Equal(t, "127.0.0.1:55555", integrationAppHandler.getConnection().RemoteAddr().String()) + message := "hello world" messageSize := len(message) From 249427739ce7d03e56675da574e73ae83c2b85d1 Mon Sep 17 00:00:00 2001 From: Marco Dinis Date: Fri, 21 Nov 2025 11:24:38 +0000 Subject: [PATCH 2/2] fix log --- lib/web/app/transport.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/web/app/transport.go b/lib/web/app/transport.go index 4a7ecec984c91..44fbe80f1a442 100644 --- a/lib/web/app/transport.go +++ b/lib/web/app/transport.go @@ -371,7 +371,8 @@ func (t *transport) DialContext(ctx context.Context, _, _ string) (conn net.Conn if err != nil { // Log a warning and use the original remote address if we fail to extract the client source address from context. // This will result in an error if the flow has pinned IP restrictions. - t.c.log.WarnContext(ctx, "Failed to extract client source address from context when proxying access to Application with integration credentials. IP Pinning checks will fail.", "error", err) + t.c.log.WithFields(logrus.Fields{"app_server": appServer.GetName()}). + Warnf("Failed to extract client source address from context when proxying access to Application with integration credentials. IP Pinning checks will fail. error: %v", err) srcWithClientSrcAddr = src }