TUN-5915: New cloudflared command to allow to retrieve the token credentials for a Tunnel
This commit is contained in:
parent
4836216a9b
commit
98736a03e1
|
@ -1,3 +1,9 @@
|
||||||
|
## 2022.3.4
|
||||||
|
### New Features
|
||||||
|
- It is now possible to retrieve the credentials that allow to run a Tunnel in case you forgot/lost them. This is
|
||||||
|
achievable with: `cloudflared tunnel token --cred-file /path/to/file.json TUNNEL`. This new feature only works for
|
||||||
|
Tunnels created with cloudflared version 2022.3.0 or more recent.
|
||||||
|
|
||||||
## 2022.3.3
|
## 2022.3.3
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
- `cloudflared service install` now starts the underlying agent service on Windows operating system (similarly to the
|
- `cloudflared service install` now starts the underlying agent service on Windows operating system (similarly to the
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
type TunnelClient interface {
|
type TunnelClient interface {
|
||||||
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
|
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
|
||||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||||
|
GetTunnelToken(tunnelID uuid.UUID) (string, error)
|
||||||
DeleteTunnel(tunnelID uuid.UUID) error
|
DeleteTunnel(tunnelID uuid.UUID) error
|
||||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||||
|
|
|
@ -116,6 +116,23 @@ func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||||
return nil, r.statusCodeToError("get tunnel", resp)
|
return nil, r.statusCodeToError("get tunnel", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error) {
|
||||||
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/token", tunnelID))
|
||||||
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
err = parseResponse(resp.Body, &token)
|
||||||
|
return token, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", r.statusCodeToError("get tunnel token", resp)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||||
|
|
|
@ -109,6 +109,7 @@ func Commands() []*cli.Command {
|
||||||
buildIngressSubcommand(),
|
buildIngressSubcommand(),
|
||||||
buildDeleteCommand(),
|
buildDeleteCommand(),
|
||||||
buildCleanupCommand(),
|
buildCleanupCommand(),
|
||||||
|
buildTokenCommand(),
|
||||||
// for compatibility, allow following as tunnel subcommands
|
// for compatibility, allow following as tunnel subcommands
|
||||||
proxydns.Command(true),
|
proxydns.Command(true),
|
||||||
cliutil.RemovedCommand("db-connect"),
|
cliutil.RemovedCommand("db-connect"),
|
||||||
|
|
|
@ -341,6 +341,21 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *subcommandContext) getTunnelTokenCredentials(tunnelID uuid.UUID) (*connection.TunnelToken, error) {
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := client.GetTunnelToken(tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
sc.log.Err(err).Msgf("Could not get the Token for the given Tunnel %v", tunnelID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
|
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -216,6 +216,10 @@ func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, er
|
||||||
return &tunnel.tunnel, nil
|
return &tunnel.tunnel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, error) {
|
||||||
|
return "token", nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||||
tunnel, ok := d.mockTunnels[tunnelID]
|
tunnel, ok := d.mockTunnels[tunnelID]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -714,6 +714,59 @@ func cleanupCommand(c *cli.Context) error {
|
||||||
return sc.cleanupConnections(tunnelIDs)
|
return sc.cleanupConnections(tunnelIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildTokenCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "token",
|
||||||
|
Action: cliutil.ConfiguredAction(tokenCommand),
|
||||||
|
Usage: "Fetch the credentials token for an existing tunnel (by name or UUID) that allows to run it",
|
||||||
|
UsageText: "cloudflared tunnel [tunnel command options] token [subcommand options] TUNNEL",
|
||||||
|
Description: "cloudflared tunnel token will fetch the credentials token for a given tunnel (by its name or UUID), which is then used to run the tunnel. This command fails if the tunnel does not exist or has been deleted. Use the flag `cloudflared tunnel token --cred-file /my/path/file.json TUNNEL` to output the token to the credentials JSON file. Note: this command only works for Tunnels created since cloudflared version 2022.3.0",
|
||||||
|
Flags: []cli.Flag{credentialsFileFlagCLIOnly},
|
||||||
|
CustomHelpTemplate: commandHelpTemplate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenCommand(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error setting up logger")
|
||||||
|
}
|
||||||
|
|
||||||
|
warningChecker := updater.StartWarningCheck(c)
|
||||||
|
defer warningChecker.LogWarningIfAny(sc.log)
|
||||||
|
|
||||||
|
if c.NArg() != 1 {
|
||||||
|
return cliutil.UsageError(`"cloudflared tunnel token" requires exactly 1 argument, the name or UUID of tunnel to fetch the credentials token for.`)
|
||||||
|
}
|
||||||
|
tunnelID, err := sc.findID(c.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error parsing tunnel ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := sc.getTunnelTokenCredentials(tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if path := c.String(CredFileFlag); path != "" {
|
||||||
|
credentials := token.Credentials()
|
||||||
|
err := writeTunnelCredentials(path, &credentials)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error writing token credentials to JSON file in path %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedToken, err := token.Encode()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s", encodedToken)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildRouteCommand() *cli.Command {
|
func buildRouteCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "route",
|
Name: "route",
|
||||||
|
|
|
@ -72,14 +72,19 @@ class NamedTunnelConfig(NamedTunnelBaseConfig):
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def get_tunnel_id(self):
|
||||||
|
return self.full_config["tunnel"]
|
||||||
|
|
||||||
def get_token(self):
|
def get_token(self):
|
||||||
with open(self.credentials_file) as json_file:
|
creds = self.get_credentials_json()
|
||||||
creds = json.load(json_file)
|
|
||||||
token_dict = {"a": creds["AccountTag"], "t": creds["TunnelID"], "s": creds["TunnelSecret"]}
|
token_dict = {"a": creds["AccountTag"], "t": creds["TunnelID"], "s": creds["TunnelSecret"]}
|
||||||
token_json_str = json.dumps(token_dict)
|
token_json_str = json.dumps(token_dict)
|
||||||
|
|
||||||
return base64.b64encode(token_json_str.encode('utf-8'))
|
return base64.b64encode(token_json_str.encode('utf-8'))
|
||||||
|
|
||||||
|
def get_credentials_json(self):
|
||||||
|
with open(self.credentials_file) as json_file:
|
||||||
|
return json.load(json_file)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ClassicTunnelBaseConfig(BaseConfig):
|
class ClassicTunnelBaseConfig(BaseConfig):
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from setup import get_config_from_file, persist_origin_cert
|
||||||
|
from util import start_cloudflared
|
||||||
|
|
||||||
|
|
||||||
|
class TestToken:
|
||||||
|
def test_get_token(self, tmp_path, component_tests_config):
|
||||||
|
config = component_tests_config()
|
||||||
|
tunnel_id = config.get_tunnel_id()
|
||||||
|
|
||||||
|
token_args = ["--origincert", cert_path(), "token", tunnel_id]
|
||||||
|
output = start_cloudflared(tmp_path, config, token_args)
|
||||||
|
|
||||||
|
assert parse_token(config.get_token()) == parse_token(output.stdout)
|
||||||
|
|
||||||
|
def test_get_credentials_file(self, tmp_path, component_tests_config):
|
||||||
|
config = component_tests_config()
|
||||||
|
tunnel_id = config.get_tunnel_id()
|
||||||
|
|
||||||
|
cred_file = tmp_path / "test_get_credentials_file.json"
|
||||||
|
token_args = ["--origincert", cert_path(), "token", "--cred-file", cred_file, tunnel_id]
|
||||||
|
start_cloudflared(tmp_path, config, token_args)
|
||||||
|
|
||||||
|
with open(cred_file) as json_file:
|
||||||
|
assert config.get_credentials_json() == json.load(json_file)
|
||||||
|
|
||||||
|
|
||||||
|
def cert_path():
|
||||||
|
return get_config_from_file()["origincert"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_token(token):
|
||||||
|
return json.loads(base64.b64decode(token))
|
|
@ -2,6 +2,7 @@ package connection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||||
"github.com/cloudflare/cloudflared/websocket"
|
"github.com/cloudflare/cloudflared/websocket"
|
||||||
|
@ -65,6 +67,15 @@ func (t TunnelToken) Credentials() Credentials {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t TunnelToken) Encode() (string, error) {
|
||||||
|
val, err := json.Marshal(t)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "could not JSON encode token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
type ClassicTunnelProperties struct {
|
type ClassicTunnelProperties struct {
|
||||||
Hostname string
|
Hostname string
|
||||||
OriginCert []byte
|
OriginCert []byte
|
||||||
|
|
Loading…
Reference in New Issue