Skip to content

Commit b89c092

Browse files
committed
TUN-7134: Acquire token for cloudflared tail
cloudflared tail will now fetch the management token from by making a request to the Cloudflare API using the cert.pem (acquired from cloudflared login). Refactored some of the credentials code into it's own package as to allow for easier use between subcommands outside of `cloudflared tunnel`.
1 parent 8dc0697 commit b89c092

21 files changed

+497
-250
lines changed

certutil/certutil.go

Lines changed: 0 additions & 58 deletions
This file was deleted.

certutil/certutil_test.go

Lines changed: 0 additions & 51 deletions
This file was deleted.

cfapi/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type TunnelClient interface {
88
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
99
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
1010
GetTunnelToken(tunnelID uuid.UUID) (string, error)
11+
GetManagementToken(tunnelID uuid.UUID) (string, error)
1112
DeleteTunnel(tunnelID uuid.UUID) error
1213
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
1314
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)

cfapi/tunnel.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ type newTunnel struct {
5050
TunnelSecret []byte `json:"tunnel_secret"`
5151
}
5252

53+
type managementRequest struct {
54+
Resources []string `json:"resources"`
55+
}
56+
5357
type CleanupParams struct {
5458
queryParams url.Values
5559
}
@@ -133,6 +137,28 @@ func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error
133137
return "", r.statusCodeToError("get tunnel token", resp)
134138
}
135139

140+
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) {
141+
endpoint := r.baseEndpoints.accountLevel
142+
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID))
143+
144+
body := &managementRequest{
145+
Resources: []string{"logs"},
146+
}
147+
148+
resp, err := r.sendRequest("POST", endpoint, body)
149+
if err != nil {
150+
return "", errors.Wrap(err, "REST request failed")
151+
}
152+
defer resp.Body.Close()
153+
154+
if resp.StatusCode == http.StatusOK {
155+
err = parseResponse(resp.Body, &token)
156+
return token, err
157+
}
158+
159+
return "", r.statusCodeToError("get tunnel token", resp)
160+
}
161+
136162
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
137163
endpoint := r.baseEndpoints.accountLevel
138164
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))

cmd/cloudflared/cliutil/build_info.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ func (bi *BuildInfo) GetBuildTypeMsg() string {
4747
}
4848
return fmt.Sprintf(" with %s", bi.BuildType)
4949
}
50+
51+
func (bi *BuildInfo) UserAgent() string {
52+
return fmt.Sprintf("cloudflared/%s", bi.CloudflaredVersion)
53+
}

cmd/cloudflared/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func main() {
9090
updater.Init(Version)
9191
tracing.Init(Version)
9292
token.Init(Version)
93-
tail.Init(Version)
93+
tail.Init(bInfo)
9494
runApp(app, graceShutdownC)
9595
}
9696

cmd/cloudflared/tail/cmd.go

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tail
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"net/http"
78
"net/url"
@@ -10,28 +11,32 @@ import (
1011
"syscall"
1112
"time"
1213

14+
"github.com/google/uuid"
1315
"github.com/mattn/go-colorable"
1416
"github.com/rs/zerolog"
1517
"github.com/urfave/cli/v2"
1618
"nhooyr.io/websocket"
1719

20+
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
21+
"github.com/cloudflare/cloudflared/credentials"
1822
"github.com/cloudflare/cloudflared/logger"
1923
"github.com/cloudflare/cloudflared/management"
2024
)
2125

2226
var (
23-
version string
27+
buildInfo *cliutil.BuildInfo
2428
)
2529

26-
func Init(v string) {
27-
version = v
30+
func Init(bi *cliutil.BuildInfo) {
31+
buildInfo = bi
2832
}
2933

3034
func Command() *cli.Command {
3135
return &cli.Command{
32-
Name: "tail",
33-
Action: Run,
34-
Usage: "Stream logs from a remote cloudflared",
36+
Name: "tail",
37+
Action: Run,
38+
Usage: "Stream logs from a remote cloudflared",
39+
UsageText: "cloudflared tail [tail command options] [TUNNEL-ID]",
3540
Flags: []cli.Flag{
3641
&cli.StringFlag{
3742
Name: "connector-id",
@@ -75,6 +80,12 @@ func Command() *cli.Command {
7580
Usage: "Application logging level {debug, info, warn, error, fatal}",
7681
EnvVars: []string{"TUNNEL_LOGLEVEL"},
7782
},
83+
&cli.StringFlag{
84+
Name: credentials.OriginCertFlag,
85+
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
86+
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
87+
Value: credentials.FindDefaultOriginCertPath(),
88+
},
7889
},
7990
}
8091
}
@@ -159,6 +170,59 @@ func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
159170
}, nil
160171
}
161172

173+
// getManagementToken will make a call to the Cloudflare API to acquire a management token for the requested tunnel.
174+
func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
175+
userCreds, err := credentials.Read(c.String(credentials.OriginCertFlag), log)
176+
if err != nil {
177+
return "", err
178+
}
179+
180+
client, err := userCreds.Client(c.String("api-url"), buildInfo.UserAgent(), log)
181+
if err != nil {
182+
return "", err
183+
}
184+
185+
tunnelIDString := c.Args().First()
186+
if tunnelIDString == "" {
187+
return "", errors.New("no tunnel ID provided")
188+
}
189+
tunnelID, err := uuid.Parse(tunnelIDString)
190+
if err != nil {
191+
return "", errors.New("unable to parse provided tunnel id as a valid UUID")
192+
}
193+
194+
token, err := client.GetManagementToken(tunnelID)
195+
if err != nil {
196+
return "", err
197+
}
198+
199+
return token, nil
200+
}
201+
202+
// buildURL will build the management url to contain the required query parameters to authenticate the request.
203+
func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
204+
var err error
205+
managementHostname := c.String("management-hostname")
206+
token := c.String("token")
207+
if token == "" {
208+
token, err = getManagementToken(c, log)
209+
if err != nil {
210+
return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err)
211+
}
212+
}
213+
query := url.Values{}
214+
query.Add("access_token", token)
215+
connector := c.String("connector-id")
216+
if connector != "" {
217+
connectorID, err := uuid.Parse(connector)
218+
if err != nil {
219+
return url.URL{}, fmt.Errorf("unabled to parse 'connector-id' flag into a valid UUID: %w", err)
220+
}
221+
query.Add("connector_id", connectorID.String())
222+
}
223+
return url.URL{Scheme: "wss", Host: managementHostname, Path: "/logs", RawQuery: query.Encode()}, nil
224+
}
225+
162226
// Run implements a foreground runner
163227
func Run(c *cli.Context) error {
164228
log := createLogger(c)
@@ -173,12 +237,14 @@ func Run(c *cli.Context) error {
173237
return nil
174238
}
175239

176-
managementHostname := c.String("management-hostname")
177-
token := c.String("token")
178-
u := url.URL{Scheme: "wss", Host: managementHostname, Path: "/logs", RawQuery: "access_token=" + token}
240+
u, err := buildURL(c, log)
241+
if err != nil {
242+
log.Err(err).Msg("unable to construct management request URL")
243+
return nil
244+
}
179245

180246
header := make(http.Header)
181-
header.Add("User-Agent", "cloudflared/"+version)
247+
header.Add("User-Agent", buildInfo.UserAgent())
182248
trace := c.String("trace")
183249
if trace != "" {
184250
header["cf-trace-id"] = []string{trace}
@@ -206,6 +272,11 @@ func Run(c *cli.Context) error {
206272
log.Error().Err(err).Msg("unable to request logs from management tunnel")
207273
return nil
208274
}
275+
log.Debug().
276+
Str("tunnel-id", c.Args().First()).
277+
Str("connector-id", c.String("connector-id")).
278+
Interface("filters", filters).
279+
Msg("connected")
209280

210281
readerDone := make(chan struct{})
211282

cmd/cloudflared/tunnel/cmd.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
2929
"github.com/cloudflare/cloudflared/config"
3030
"github.com/cloudflare/cloudflared/connection"
31+
"github.com/cloudflare/cloudflared/credentials"
3132
"github.com/cloudflare/cloudflared/features"
3233
"github.com/cloudflare/cloudflared/ingress"
3334
"github.com/cloudflare/cloudflared/logger"
@@ -751,10 +752,10 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
751752
Hidden: shouldHide,
752753
},
753754
altsrc.NewStringFlag(&cli.StringFlag{
754-
Name: "origincert",
755+
Name: credentials.OriginCertFlag,
755756
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
756757
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
757-
Value: findDefaultOriginCertPath(),
758+
Value: credentials.FindDefaultOriginCertPath(),
758759
Hidden: shouldHide,
759760
}),
760761
altsrc.NewDurationFlag(&cli.DurationFlag{

0 commit comments

Comments
 (0)