(buddy) Pass JWT headers on websocket requests (#15667)

* transport: Rewrite headers, including JWTs, for websockets.

Applications can otherwise 401 on websocket requests, as they do not
present any authentication headers.

docs: Fix the reserved JWT header name.

Signed-off-by: Roman Tkachenko <roman@goteleport.com>

* Add test for JWT header in websocket apps

Signed-off-by: Roman Tkachenko <roman@goteleport.com>
Co-authored-by: Alex Vandiver <alex@chmrr.net>
This commit is contained in:
Roman Tkachenko
2022-08-22 20:43:50 +00:00
committed by GitHub
co-authored by Alex Vandiver
parent 91c89b6126
commit 31b4a00a86
3 changed files with 61 additions and 16 deletions
@@ -283,7 +283,7 @@ requests forwarded to a web application.
Headers injected this way override any headers with the same names that may
be sent by an application. The following headers are reserved and can't be
rewritten: `X-Teleport-Jwt`, `Cf-Access-Token`, `X-Forwarded-*`.
rewritten: `Teleport-Jwt-Assertion`, `Cf-Access-Token`, and any `X-Forwarded-*`.
Rewritten header values support the same templating variables as [role templates](../../access-controls/guides/role-templates.mdx).
In the example above, `X-Internal-Trait` header will be populated with the value
+41 -1
View File
@@ -29,6 +29,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"strings"
"testing"
@@ -365,6 +366,20 @@ func (p *pack) appAccessJWT(t *testing.T) {
// Verify JWT token.
verifyJWT(t, p, token, p.jwtAppURI)
// Connect to websocket application that dumps the upgrade request.
wsCookie := p.createAppSession(t, p.wsHeaderAppPublicAddr, p.wsHeaderAppClusterName)
body, err := p.makeWebsocketRequest(wsCookie, "/")
require.NoError(t, err)
// Parse the upgrade request the websocket application received.
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(body)))
require.NoError(t, err)
// Extract JWT token from header and verify it.
wsToken := req.Header.Get(teleport.AppJWTHeader)
require.NotEmpty(t, wsToken, "websocket upgrade request doesn't contain JWT header")
verifyJWT(t, p, wsToken, p.wsHeaderAppURI)
}
func verifyJWT(t *testing.T, pack *pack, token, appURI string) {
@@ -792,6 +807,11 @@ type pack struct {
headerAppClusterName string
headerAppURI string
wsHeaderAppName string
wsHeaderAppPublicAddr string
wsHeaderAppClusterName string
wsHeaderAppURI string
flushAppName string
flushAppPublicAddr string
flushAppClusterName string
@@ -890,6 +910,10 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
headerAppPublicAddr: "app-04.example.com",
headerAppClusterName: "example.com",
wsHeaderAppName: "ws-header",
wsHeaderAppPublicAddr: "ws-header.example.com",
wsHeaderAppClusterName: "example.com",
flushAppName: "app-05",
flushAppPublicAddr: "app-05.example.com",
flushAppClusterName: "example.com",
@@ -901,7 +925,6 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conn, err := upgrader.Upgrade(w, r, nil)
require.NoError(t, err)
handler(conn)
@@ -954,10 +977,21 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
c.Close()
})
t.Cleanup(func() { leafTCPServer.Close() })
// JWT server writes generated JWT token in the response.
jwtServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.Header.Get(teleport.AppJWTHeader))
}))
t.Cleanup(jwtServer.Close)
// Websocket header server dumps initial HTTP upgrade request in the response.
wsHeaderServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := (&websocket.Upgrader{}).Upgrade(w, r, nil)
require.NoError(t, err)
reqDump, err := httputil.DumpRequest(r, false)
require.NoError(t, err)
require.NoError(t, conn.WriteMessage(websocket.BinaryMessage, reqDump))
require.NoError(t, conn.Close())
}))
t.Cleanup(wsHeaderServer.Close)
headerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, headerName := range forwardedHeaderNames {
fmt.Fprintln(w, r.Header.Get(headerName))
@@ -999,6 +1033,7 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack {
p.leafTCPAppURI = fmt.Sprintf("tcp://%v", leafTCPServer.Addr().String())
p.jwtAppURI = jwtServer.URL
p.headerAppURI = headerServer.URL
p.wsHeaderAppURI = wsHeaderServer.URL
p.flushAppURI = flushServer.URL
p.dumperAppURI = dumperServer.URL
@@ -1544,6 +1579,11 @@ func (p *pack) startRootAppServers(t *testing.T, count int, extraApps []service.
URI: p.headerAppURI,
PublicAddr: p.headerAppPublicAddr,
},
{
Name: p.wsHeaderAppName,
URI: p.wsHeaderAppURI,
PublicAddr: p.wsHeaderAppPublicAddr,
},
{
Name: p.flushAppName,
URI: p.flushAppURI,
+19 -14
View File
@@ -115,7 +115,7 @@ func newTransport(ctx context.Context, c *transportConfig) (*transport, error) {
c: c,
uri: uri,
tr: tr,
ws: newWebsocketTransport(uri, tr.TLSClientConfig),
ws: newWebsocketTransport(uri, tr.TLSClientConfig, c),
}, nil
}
@@ -173,27 +173,28 @@ func (t *transport) rewriteRequest(r *http.Request) error {
r.URL.Host = t.uri.Host
// Add headers from rewrite configuration.
if t.c.app.GetRewrite() != nil && len(t.c.app.GetRewrite().Headers) > 0 {
t.rewriteHeaders(r)
}
// Add in JWT headers.
r.Header.Set(teleport.AppJWTHeader, t.c.jwt)
r.Header.Set(teleport.AppCFHeader, t.c.jwt)
rewriteHeaders(r, t.c)
return nil
}
// rewriteHeaders applies headers rewrites from the application configuration.
func (t *transport) rewriteHeaders(r *http.Request) {
for _, header := range t.c.app.GetRewrite().Headers {
func rewriteHeaders(r *http.Request, c *transportConfig) {
// Add in JWT headers.
r.Header.Set(teleport.AppJWTHeader, c.jwt)
r.Header.Set(teleport.AppCFHeader, c.jwt)
if c.app.GetRewrite() == nil || len(c.app.GetRewrite().Headers) == 0 {
return
}
for _, header := range c.app.GetRewrite().Headers {
if common.IsReservedHeader(header.Name) {
t.c.log.Debugf("Not rewriting Teleport header %q.", header.Name)
c.log.Debugf("Not rewriting Teleport header %q.", header.Name)
continue
}
values, err := services.ApplyValueTraits(header.Value, t.c.traits)
values, err := services.ApplyValueTraits(header.Value, c.traits)
if err != nil {
t.c.log.Debugf("Failed to apply traits to %q: %v.", header.Value, err)
c.log.Debugf("Failed to apply traits to %q: %v.", header.Value, err)
continue
}
r.Header.Del(header.Name)
@@ -331,11 +332,12 @@ func isRedirect(code int) bool {
type websocketTransport struct {
uri *url.URL
dialer forward.Dialer
c *transportConfig
}
// newWebsocketTransport returns transport that knows how to rewrite and
// dial websocket requests.
func newWebsocketTransport(uri *url.URL, tlsConfig *tls.Config) *websocketTransport {
func newWebsocketTransport(uri *url.URL, tlsConfig *tls.Config, c *transportConfig) *websocketTransport {
return &websocketTransport{
uri: uri,
dialer: func(network, address string) (net.Conn, error) {
@@ -346,6 +348,7 @@ func newWebsocketTransport(uri *url.URL, tlsConfig *tls.Config) *websocketTransp
// Request is going to "ws://".
return net.Dial(network, address)
},
c: c,
}
}
@@ -359,4 +362,6 @@ func (r *websocketTransport) Rewrite(req *http.Request) {
}
req.URL.Host = r.uri.Host
req.Host = r.uri.Host
rewriteHeaders(req, r.c)
}