Skip to content

Commit 906452a

Browse files
committed
TUN-8960: Connect to FED API GW based on the OriginCert's endpoint
## Summary Within the scope of the FEDRamp High RM, it is necessary to detect if an user should connect to a FEDRamp colo. At first, it was considered to add the --fedramp as global flag however this could be a footgun for the user or even an hindrance, thus, the proposal is to save in the token (during login) if the user authenticated using the FEDRamp Dashboard. This solution makes it easier to the user as they will only be required to pass the flag in login and nothing else. * Introduces the new field, endpoint, in OriginCert * Refactors login to remove the private key and certificate which are no longer used * Login will only store the Argo Tunnel Token * Remove namedTunnelToken as it was only used to for serialization Closes TUN-8960
1 parent d969fde commit 906452a

File tree

10 files changed

+131
-75
lines changed

10 files changed

+131
-75
lines changed

cmd/cloudflared/flags/flags.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,7 @@ const (
149149

150150
// MetricsUpdateFreq is the command line flag to define how frequently tunnel metrics are updated
151151
MetricsUpdateFreq = "metrics-update-freq"
152+
153+
// ApiURL is the command line flag used to define the base URL of the API
154+
ApiURL = "api-url"
152155
)

cmd/cloudflared/tail/cmd.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ import (
2323
"github.com/cloudflare/cloudflared/management"
2424
)
2525

26-
var (
27-
buildInfo *cliutil.BuildInfo
28-
)
26+
var buildInfo *cliutil.BuildInfo
2927

3028
func Init(bi *cliutil.BuildInfo) {
3129
buildInfo = bi
@@ -56,7 +54,7 @@ func managementTokenCommand(c *cli.Context) error {
5654
if err != nil {
5755
return err
5856
}
59-
var tokenResponse = struct {
57+
tokenResponse := struct {
6058
Token string `json:"token"`
6159
}{Token: token}
6260

@@ -231,7 +229,7 @@ func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
231229
return "", err
232230
}
233231

234-
client, err := userCreds.Client(c.String("api-url"), buildInfo.UserAgent(), log)
232+
client, err := userCreds.Client(c.String(cfdflags.ApiURL), buildInfo.UserAgent(), log)
235233
if err != nil {
236234
return "", err
237235
}

cmd/cloudflared/tunnel/cmd.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ var (
131131
"hostname",
132132
"id",
133133
cfdflags.LBPool,
134-
"api-url",
134+
cfdflags.ApiURL,
135135
cfdflags.MetricsUpdateFreq,
136136
cfdflags.Tag,
137137
"heartbeat-interval",
@@ -716,7 +716,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
716716
Hidden: true,
717717
}),
718718
altsrc.NewStringFlag(&cli.StringFlag{
719-
Name: "api-url",
719+
Name: cfdflags.ApiURL,
720720
Usage: "Base URL for Cloudflare API v4",
721721
EnvVars: []string{"TUNNEL_API_URL"},
722722
Value: "https://api.cloudflare.com/client/v4",

cmd/cloudflared/tunnel/login.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func login(c *cli.Context) error {
6767

6868
path, ok, err := checkForExistingCert()
6969
if ok {
70-
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
70+
log.Error().Err(err).Msgf("You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
7171
return nil
7272
} else if err != nil {
7373
return err
@@ -78,7 +78,8 @@ func login(c *cli.Context) error {
7878
callbackStoreURL = c.String(callbackURLParamName)
7979
)
8080

81-
if c.Bool(fedRAMPParamName) {
81+
isFEDRamp := c.Bool(fedRAMPParamName)
82+
if isFEDRamp {
8283
baseloginURL = fedBaseLoginURL
8384
callbackStoreURL = fedCallbackStoreURL
8485
}
@@ -99,15 +100,31 @@ func login(c *cli.Context) error {
99100
log,
100101
)
101102
if err != nil {
102-
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
103+
log.Error().Err(err).Msgf("Failed to write the certificate.\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", path)
104+
return err
105+
}
106+
107+
cert, err := credentials.DecodeOriginCert(resourceData)
108+
if err != nil {
109+
log.Error().Err(err).Msg("failed to decode origin certificate")
110+
return err
111+
}
112+
113+
if isFEDRamp {
114+
cert.Endpoint = credentials.FedEndpoint
115+
}
116+
117+
resourceData, err = cert.EncodeOriginCert()
118+
if err != nil {
119+
log.Error().Err(err).Msg("failed to encode origin certificate")
103120
return err
104121
}
105122

106123
if err := os.WriteFile(path, resourceData, 0600); err != nil {
107124
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
108125
}
109126

110-
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
127+
log.Info().Msgf("You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
111128
return nil
112129
}
113130

cmd/cloudflared/tunnel/subcommand_context.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"github.com/cloudflare/cloudflared/logger"
2121
)
2222

23+
const fedRampBaseApiURL = "https://api.fed.cloudflare.com/client/v4"
24+
2325
type invalidJSONCredentialError struct {
2426
err error
2527
path string
@@ -65,7 +67,16 @@ func (sc *subcommandContext) client() (cfapi.Client, error) {
6567
if err != nil {
6668
return nil, err
6769
}
68-
sc.tunnelstoreClient, err = cred.Client(sc.c.String("api-url"), buildInfo.UserAgent(), sc.log)
70+
71+
var apiURL string
72+
if cred.IsFEDEndpoint() {
73+
sc.log.Info().Str("api-url", fedRampBaseApiURL).Msg("using fedramp base api")
74+
apiURL = fedRampBaseApiURL
75+
} else {
76+
apiURL = sc.c.String(cfdflags.ApiURL)
77+
}
78+
79+
sc.tunnelstoreClient, err = cred.Client(apiURL, buildInfo.UserAgent(), sc.log)
6980
if err != nil {
7081
return nil, err
7182
}

credentials/credentials.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
const (
1111
logFieldOriginCertPath = "originCertPath"
12+
FedEndpoint = "fed"
1213
)
1314

1415
type User struct {
@@ -32,6 +33,10 @@ func (c User) CertPath() string {
3233
return c.certPath
3334
}
3435

36+
func (c User) IsFEDEndpoint() bool {
37+
return c.cert.Endpoint == FedEndpoint
38+
}
39+
3540
// Client uses the user credentials to create a Cloudflare API client
3641
func (c *User) Client(apiURL string, userAgent string, log *zerolog.Logger) (cfapi.Client, error) {
3742
if apiURL == "" {
@@ -45,7 +50,6 @@ func (c *User) Client(apiURL string, userAgent string, log *zerolog.Logger) (cfa
4550
userAgent,
4651
log,
4752
)
48-
4953
if err != nil {
5054
return nil, err
5155
}

credentials/origin_cert.go

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package credentials
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"encoding/pem"
67
"fmt"
78
"os"
89
"path/filepath"
10+
"strings"
911

1012
"github.com/mitchellh/go-homedir"
1113
"github.com/rs/zerolog"
@@ -17,16 +19,28 @@ const (
1719
DefaultCredentialFile = "cert.pem"
1820
)
1921

20-
type namedTunnelToken struct {
22+
type OriginCert struct {
2123
ZoneID string `json:"zoneID"`
2224
AccountID string `json:"accountID"`
2325
APIToken string `json:"apiToken"`
26+
Endpoint string `json:"endpoint,omitempty"`
2427
}
2528

26-
type OriginCert struct {
27-
ZoneID string
28-
APIToken string
29-
AccountID string
29+
func (oc *OriginCert) UnmarshalJSON(data []byte) error {
30+
var aux struct {
31+
ZoneID string `json:"zoneID"`
32+
AccountID string `json:"accountID"`
33+
APIToken string `json:"apiToken"`
34+
Endpoint string `json:"endpoint,omitempty"`
35+
}
36+
if err := json.Unmarshal(data, &aux); err != nil {
37+
return fmt.Errorf("error parsing OriginCert: %v", err)
38+
}
39+
oc.ZoneID = aux.ZoneID
40+
oc.AccountID = aux.AccountID
41+
oc.APIToken = aux.APIToken
42+
oc.Endpoint = strings.ToLower(aux.Endpoint)
43+
return nil
3044
}
3145

3246
// FindDefaultOriginCertPath returns the first path that contains a cert.pem file. If none of the
@@ -41,40 +55,56 @@ func FindDefaultOriginCertPath() string {
4155
return ""
4256
}
4357

58+
func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
59+
return decodeOriginCert(blocks)
60+
}
61+
62+
func (cert *OriginCert) EncodeOriginCert() ([]byte, error) {
63+
if cert == nil {
64+
return nil, fmt.Errorf("originCert cannot be nil")
65+
}
66+
buffer, err := json.Marshal(cert)
67+
if err != nil {
68+
return nil, fmt.Errorf("originCert marshal failed: %v", err)
69+
}
70+
block := pem.Block{
71+
Type: "ARGO TUNNEL TOKEN",
72+
Headers: map[string]string{},
73+
Bytes: buffer,
74+
}
75+
var out bytes.Buffer
76+
err = pem.Encode(&out, &block)
77+
if err != nil {
78+
return nil, fmt.Errorf("pem encoding failed: %v", err)
79+
}
80+
return out.Bytes(), nil
81+
}
82+
4483
func decodeOriginCert(blocks []byte) (*OriginCert, error) {
4584
if len(blocks) == 0 {
46-
return nil, fmt.Errorf("Cannot decode empty certificate")
85+
return nil, fmt.Errorf("cannot decode empty certificate")
4786
}
4887
originCert := OriginCert{}
4988
block, rest := pem.Decode(blocks)
50-
for {
51-
if block == nil {
52-
break
53-
}
89+
for block != nil {
5490
switch block.Type {
5591
case "PRIVATE KEY", "CERTIFICATE":
5692
// this is for legacy purposes.
57-
break
5893
case "ARGO TUNNEL TOKEN":
5994
if originCert.ZoneID != "" || originCert.APIToken != "" {
60-
return nil, fmt.Errorf("Found multiple tokens in the certificate")
95+
return nil, fmt.Errorf("found multiple tokens in the certificate")
6196
}
6297
// The token is a string,
6398
// Try the newer JSON format
64-
ntt := namedTunnelToken{}
65-
if err := json.Unmarshal(block.Bytes, &ntt); err == nil {
66-
originCert.ZoneID = ntt.ZoneID
67-
originCert.APIToken = ntt.APIToken
68-
originCert.AccountID = ntt.AccountID
69-
}
99+
_ = json.Unmarshal(block.Bytes, &originCert)
70100
default:
71-
return nil, fmt.Errorf("Unknown block %s in the certificate", block.Type)
101+
return nil, fmt.Errorf("unknown block %s in the certificate", block.Type)
72102
}
73103
block, rest = pem.Decode(rest)
74104
}
75105

76106
if originCert.ZoneID == "" || originCert.APIToken == "" {
77-
return nil, fmt.Errorf("Missing token in the certificate")
107+
return nil, fmt.Errorf("missing token in the certificate")
78108
}
79109

80110
return &originCert, nil

credentials/origin_cert_test.go

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,25 @@ const (
1616
originCertFile = "cert.pem"
1717
)
1818

19-
var (
20-
nopLog = zerolog.Nop().With().Logger()
21-
)
19+
var nopLog = zerolog.Nop().With().Logger()
2220

2321
func TestLoadOriginCert(t *testing.T) {
2422
cert, err := decodeOriginCert([]byte{})
25-
assert.Equal(t, fmt.Errorf("Cannot decode empty certificate"), err)
23+
assert.Equal(t, fmt.Errorf("cannot decode empty certificate"), err)
2624
assert.Nil(t, cert)
2725

2826
blocks, err := os.ReadFile("test-cert-unknown-block.pem")
29-
assert.NoError(t, err)
27+
require.NoError(t, err)
3028
cert, err = decodeOriginCert(blocks)
31-
assert.Equal(t, fmt.Errorf("Unknown block RSA PRIVATE KEY in the certificate"), err)
29+
assert.Equal(t, fmt.Errorf("unknown block RSA PRIVATE KEY in the certificate"), err)
3230
assert.Nil(t, cert)
3331
}
3432

3533
func TestJSONArgoTunnelTokenEmpty(t *testing.T) {
3634
blocks, err := os.ReadFile("test-cert-no-token.pem")
37-
assert.NoError(t, err)
35+
require.NoError(t, err)
3836
cert, err := decodeOriginCert(blocks)
39-
assert.Equal(t, fmt.Errorf("Missing token in the certificate"), err)
37+
assert.Equal(t, fmt.Errorf("missing token in the certificate"), err)
4038
assert.Nil(t, cert)
4139
}
4240

@@ -52,51 +50,21 @@ func TestJSONArgoTunnelToken(t *testing.T) {
5250

5351
func CloudflareTunnelTokenTest(t *testing.T, path string) {
5452
blocks, err := os.ReadFile(path)
55-
assert.NoError(t, err)
53+
require.NoError(t, err)
5654
cert, err := decodeOriginCert(blocks)
57-
assert.NoError(t, err)
55+
require.NoError(t, err)
5856
assert.NotNil(t, cert)
5957
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
6058
key := "test-service-key"
6159
assert.Equal(t, key, cert.APIToken)
6260
}
6361

64-
type mockFile struct {
65-
path string
66-
data []byte
67-
err error
68-
}
69-
70-
type mockFileSystem struct {
71-
files map[string]mockFile
72-
}
73-
74-
func newMockFileSystem(files ...mockFile) *mockFileSystem {
75-
fs := mockFileSystem{map[string]mockFile{}}
76-
for _, f := range files {
77-
fs.files[f.path] = f
78-
}
79-
return &fs
80-
}
81-
82-
func (fs *mockFileSystem) ReadFile(path string) ([]byte, error) {
83-
if f, ok := fs.files[path]; ok {
84-
return f.data, f.err
85-
}
86-
return nil, os.ErrNotExist
87-
}
88-
89-
func (fs *mockFileSystem) ValidFilePath(path string) bool {
90-
_, exists := fs.files[path]
91-
return exists
92-
}
93-
9462
func TestFindOriginCert_Valid(t *testing.T) {
9563
file, err := os.ReadFile("test-cloudflare-tunnel-cert-json.pem")
9664
require.NoError(t, err)
9765
dir := t.TempDir()
9866
certPath := path.Join(dir, originCertFile)
99-
os.WriteFile(certPath, file, fs.ModePerm)
67+
_ = os.WriteFile(certPath, file, fs.ModePerm)
10068
path, err := FindOriginCert(certPath, &nopLog)
10169
require.NoError(t, err)
10270
require.Equal(t, certPath, path)
@@ -108,3 +76,28 @@ func TestFindOriginCert_Missing(t *testing.T) {
10876
_, err := FindOriginCert(certPath, &nopLog)
10977
require.Error(t, err)
11078
}
79+
80+
func TestEncodeDecodeOriginCert(t *testing.T) {
81+
cert := OriginCert{
82+
ZoneID: "zone",
83+
AccountID: "account",
84+
APIToken: "token",
85+
Endpoint: "FED",
86+
}
87+
blocks, err := cert.EncodeOriginCert()
88+
require.NoError(t, err)
89+
decodedCert, err := DecodeOriginCert(blocks)
90+
require.NoError(t, err)
91+
assert.NotNil(t, cert)
92+
assert.Equal(t, "zone", decodedCert.ZoneID)
93+
assert.Equal(t, "account", decodedCert.AccountID)
94+
assert.Equal(t, "token", decodedCert.APIToken)
95+
assert.Equal(t, FedEndpoint, decodedCert.Endpoint)
96+
}
97+
98+
func TestEncodeDecodeNilOriginCert(t *testing.T) {
99+
var cert *OriginCert
100+
blocks, err := cert.EncodeOriginCert()
101+
assert.Equal(t, fmt.Errorf("originCert cannot be nil"), err)
102+
require.Nil(t, blocks)
103+
}

0 commit comments

Comments
 (0)