Skip to content
Open
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
7 changes: 6 additions & 1 deletion carrier/carrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/url"
"os"
"strings"
"time"

"github.com/pkg/errors"
"github.com/rs/zerolog"
Expand All @@ -33,6 +34,10 @@ type StartOptions struct {
TLSClientConfig *tls.Config
AutoCloseInterstitial bool
IsFedramp bool
// Timeout is the HTTP timeout used for Access login/token requests, such as
// fetching app metadata from the edge and verifying a cached token against
// the origin. If zero, callers fall back to token.DefaultAccessTimeout.
Timeout time.Duration
}

// Connection wraps up all the needed functions to forward over the tunnel
Expand Down Expand Up @@ -140,7 +145,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque
return nil, err
}

token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, options.IsFedramp, log)
token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, options.IsFedramp, options.Timeout, log)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion carrier/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebso
return nil, err
}

appInfo, err := token.GetAppInfo(originReq.URL)
appInfo, err := token.GetAppInfo(originReq.URL, token.ResolveAccessTimeout(options.Timeout))
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions cmd/cloudflared/access/carrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func ssh(c *cli.Context) error {
Headers: headers,
Host: url.Host,
IsFedramp: c.Bool(fedrampFlag),
Timeout: c.Duration(accessTimeoutFlag),
}

if connectTo := c.String(sshConnectTo); connectTo != "" {
Expand Down
37 changes: 24 additions & 13 deletions cmd/cloudflared/access/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"os/exec"
"strings"
"text/template"
"time"

"github.com/getsentry/sentry-go"
"github.com/pkg/errors"
Expand Down Expand Up @@ -52,6 +51,8 @@ Host {{.Hostname}}
{{end}}
`
fedrampFlag = "fedramp"

accessTimeoutFlag = "access-timeout"
)

const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
Expand Down Expand Up @@ -80,10 +81,18 @@ func Commands() []*cli.Command {
Aliases: []string{"forward"},
Category: "Access",
Usage: "access <subcommand>",
Flags: []cli.Flag{&cli.BoolFlag{
Name: fedrampFlag,
Usage: "use when performing operations in fedramp account",
}},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: fedrampFlag,
Usage: "use when performing operations in fedramp account",
},
&cli.DurationFlag{
Name: accessTimeoutFlag,
Usage: "HTTP timeout for requests made while logging in to or verifying tokens for an Access application: fetching application metadata from the Cloudflare edge, and checking a cached token against the origin. Increase this if you see timeout errors on slow or high-latency connections.",
EnvVars: []string{"TUNNEL_ACCESS_TIMEOUT"},
Value: token.DefaultAccessTimeout,
},
},
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
Expand Down Expand Up @@ -257,7 +266,7 @@ func login(c *cli.Context) error {
return err
}

appInfo, err := token.GetAppInfo(appURL)
appInfo, err := token.GetAppInfo(appURL, c.Duration(accessTimeoutFlag))
if err != nil {
return err
}
Expand Down Expand Up @@ -314,7 +323,7 @@ func curl(c *cli.Context) error {
return err
}

appInfo, err := token.GetAppInfo(appURL)
appInfo, err := token.GetAppInfo(appURL, c.Duration(accessTimeoutFlag))
if err != nil {
return err
}
Expand All @@ -331,7 +340,7 @@ func curl(c *cli.Context) error {
log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.")
return run("curl", cmdArgs...)
}
tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log)
tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), c.Duration(accessTimeoutFlag), log)
if err != nil {
log.Err(err).Msg("Failed to refresh token")
return err
Expand Down Expand Up @@ -391,7 +400,7 @@ func generateToken(c *cli.Context) error {
return err
}

appInfo, err := token.GetAppInfo(appURL)
appInfo, err := token.GetAppInfo(appURL, c.Duration(accessTimeoutFlag))
if err != nil {
return err
}
Expand Down Expand Up @@ -447,11 +456,11 @@ func sshGen(c *cli.Context) error {
fetchTokenURL := &url.URL{}
*fetchTokenURL = *originURL

appInfo, err := token.GetAppInfo(fetchTokenURL)
appInfo, err := token.GetAppInfo(fetchTokenURL, c.Duration(accessTimeoutFlag))
if err != nil {
return err
}
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log)
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), c.Duration(accessTimeoutFlag), log)
if err != nil {
return err
}
Expand Down Expand Up @@ -551,7 +560,7 @@ func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context,
if c.IsSet(sshTokenSecretFlag) {
headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
}
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial), IsFedramp: c.Bool(fedrampFlag)}
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial), IsFedramp: c.Bool(fedrampFlag), Timeout: c.Duration(accessTimeoutFlag)}

if valid, err := isTokenValid(options, log); err != nil {
return err
Expand Down Expand Up @@ -584,12 +593,14 @@ func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, err
query.Set("cloudflared_token_check", "true")
req.URL.RawQuery = query.Encode()

timeout := token.ResolveAccessTimeout(options.Timeout)

// Do not follow redirects
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: time.Second * 5,
Timeout: timeout,
}
resp, err := client.Do(req)
if err != nil {
Expand Down
56 changes: 56 additions & 0 deletions cmd/cloudflared/access/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package access

import (
"flag"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"

"github.com/cloudflare/cloudflared/token"
)

// accessTimeoutDurationFlag pulls the real --access-timeout flag definition
// off of the "access" command, so precedence is tested against production
// wiring rather than a re-declared copy of it.
func accessTimeoutDurationFlag(t *testing.T) *cli.DurationFlag {
t.Helper()
cmds := Commands()
require.Len(t, cmds, 1)
for _, f := range cmds[0].Flags {
if df, ok := f.(*cli.DurationFlag); ok && df.Name == accessTimeoutFlag {
return df
}
}
t.Fatal("access-timeout flag not registered on the access command")
return nil
}

func parseAccessTimeout(t *testing.T, args []string) time.Duration {
t.Helper()
set := flag.NewFlagSet("access", flag.ContinueOnError)
require.NoError(t, accessTimeoutDurationFlag(t).Apply(set))
require.NoError(t, set.Parse(args))
ctx := cli.NewContext(cli.NewApp(), set, nil)
return ctx.Duration(accessTimeoutFlag)
}

func TestAccessTimeoutFlag_DefaultPreservesCurrentBehavior(t *testing.T) {
got := parseAccessTimeout(t, nil)
assert.Equal(t, token.DefaultAccessTimeout, got)
assert.Equal(t, 7*time.Second, got)
}

func TestAccessTimeoutFlag_EnvVarOverridesDefault(t *testing.T) {
t.Setenv("TUNNEL_ACCESS_TIMEOUT", "30s")
got := parseAccessTimeout(t, nil)
assert.Equal(t, 30*time.Second, got)
}

func TestAccessTimeoutFlag_FlagOverridesEnvVar(t *testing.T) {
t.Setenv("TUNNEL_ACCESS_TIMEOUT", "30s")
got := parseAccessTimeout(t, []string{"--access-timeout", "45s"})
assert.Equal(t, 45*time.Second, got)
}
40 changes: 28 additions & 12 deletions token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ const (
metadataAllowedClockSkew = 5 * time.Minute
)

// DefaultAccessTimeout is the default HTTP timeout used for Access login/token
// requests (fetching app metadata from the edge, and verifying a cached token
// against the origin) when no timeout is explicitly configured.
const DefaultAccessTimeout = 7 * time.Second

// ResolveAccessTimeout returns timeout if it is positive, and DefaultAccessTimeout
// otherwise. It is used to clamp user-configured Access timeouts (e.g. from
// --access-timeout) since a zero or negative time.Duration disables the
// timeout entirely on an http.Client.
func ResolveAccessTimeout(timeout time.Duration) time.Duration {
if timeout <= 0 {
return DefaultAccessTimeout
}
return timeout
}

var userAgent = "DEV"

type AppInfo struct {
Expand Down Expand Up @@ -269,18 +285,18 @@ func Init(version string) {

// FetchTokenWithRedirect will either load a stored token or generate a new one
// it appends the full url as the redirect URL to the access cli request if opening the browser
func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) {
return getToken(appURL, appInfo, false, autoClose, isFedramp, log)
func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, timeout time.Duration, log *zerolog.Logger) (string, error) {
return getToken(appURL, appInfo, false, autoClose, isFedramp, timeout, log)
}

// FetchToken will either load a stored token or generate a new one
// it appends the host of the appURL as the redirect URL to the access cli request if opening the browser
func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) {
return getToken(appURL, appInfo, true, autoClose, isFedramp, log)
func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, timeout time.Duration, log *zerolog.Logger) (string, error) {
return getToken(appURL, appInfo, true, autoClose, isFedramp, timeout, log)
}

// getToken will either load a stored token or generate a new one
func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) {
func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, isFedramp bool, timeout time.Duration, log *zerolog.Logger) (string, error) {
if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil {
return token, nil
}
Expand Down Expand Up @@ -315,7 +331,7 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose boo
orgToken, err = GetOrgTokenIfExists(appInfo.AuthDomain)
}
if err == nil {
if appToken, err := exchangeOrgToken(appURL, orgToken); err != nil {
if appToken, err := exchangeOrgToken(appURL, orgToken, timeout); err != nil {
log.Debug().Msgf("failed to exchange org token for app token: %s", err)
} else {
// generate app path
Expand Down Expand Up @@ -363,9 +379,9 @@ func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath strin
// a signed metadata JWT from the Cloudflare edge. The JWT signature is verified
// against the account's public keys (fetched from the auth domain's JWKS
// endpoint) to prevent an attacker-controlled server from spoofing app identity.
func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
func GetAppInfo(reqURL *url.URL, timeout time.Duration) (*AppInfo, error) {
// Fetch the metadata JWT from the edge (no redirects followed).
rawJWT, err := fetchMetadataJWT(reqURL.String())
rawJWT, err := fetchMetadataJWT(reqURL.String(), timeout)
Comment on lines +382 to +384

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 75213fc: fetchMetadataJWT now clamps timeout <= 0 to token.DefaultAccessTimeout at the top of the function, before constructing the http.Client. Since GetAppInfo passes timeout straight through to fetchMetadataJWT, this fixes all 4 call sites in cmd.go in one place. Added a regression test (TestFetchMetadataJWT_ZeroTimeoutFallsBackToDefault) that verifies a zero timeout falls back to the 7s default instead of disabling the timeout.

if err != nil {
return nil, err
}
Expand Down Expand Up @@ -419,12 +435,12 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
// fetchMetadataJWT sends a HEAD request to reqURL with the metadata request
// header and returns the raw JWT string from the response. No redirects are
// followed.
func fetchMetadataJWT(reqURL string) (string, error) {
func fetchMetadataJWT(reqURL string, timeout time.Duration) (string, error) {
client := &http.Client{
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: time.Second * 7,
Timeout: ResolveAccessTimeout(timeout),
}

req, err := http.NewRequest("HEAD", reqURL, nil)
Expand Down Expand Up @@ -491,12 +507,12 @@ func handleRedirects(req *http.Request, via []*http.Request, orgToken string) er

// exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
// flow to automatically generate and return an app token without the login page.
func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
func exchangeOrgToken(appURL *url.URL, orgToken string, timeout time.Duration) (string, error) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return handleRedirects(req, via, orgToken)
},
Timeout: time.Second * 7,
Timeout: ResolveAccessTimeout(timeout),
}

appTokenRequest, err := http.NewRequest("HEAD", appURL.String(), nil)
Expand Down
Loading