VULN-118896: Linux service: Use --token-file instead of --token

When installed with a token, e.g.,

$ cloudflared service install \<token\>

cloudflared will set itself up to be run by the init system using the --token argument. This results in tokens being visible in the output of ps aux by unprivileged users. Change this such that this installation method instead puts the token in a file with mode 600 in /etc/cloudflared and uses the --token-file flag instead.
This commit is contained in:
Rhys Rustad-Elliott
2026-07-13 09:57:07 +00:00
parent ecb88678f1
commit f70adda11c
2 changed files with 96 additions and 18 deletions
+43
View File
@@ -1,6 +1,10 @@
package main package main
import ( import (
"errors"
"fmt"
"os"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@@ -8,6 +12,44 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel" "github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
) )
const (
defaultTokenFile = "token"
tokenPerms os.FileMode = 0o600
)
func writeTokenToFile(path string, token string) error {
if _, err := tunnel.ParseToken(token); err != nil {
return cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
}
if err := os.WriteFile(path, []byte(token), tokenPerms); err != nil {
return fmt.Errorf("failed to write token to %s: %v", path, err)
}
// If the token file already existed with unrestrictive perms, os.WriteFile
// above will not update them
if err := os.Chmod(path, tokenPerms); err != nil {
return fmt.Errorf("failed to restrict permissions on token file %s: %v", path, err)
}
return nil
}
func removeTokenFile(tokenPath string, log *zerolog.Logger) {
err := os.Remove(tokenPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Warn().Msgf("Could not remove service token file at %s: %v", tokenPath, err)
}
}
func buildArgsForTokenFile(tokenPath string) []string {
return []string{
"tunnel", "run", "--token-file", tokenPath,
}
}
// nolint:unused // This function is used by macos and Windows builds, the unused warning when building for Linux is spurious
func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) { func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
token := c.Args().First() token := c.Args().First()
if _, err := tunnel.ParseToken(token); err != nil { if _, err := tunnel.ParseToken(token); err != nil {
@@ -19,6 +61,7 @@ func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
}, nil }, nil
} }
// nolint:unused // This function is used by macos and Windows builds, the unused warning when building for Linux is spurious
func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) { func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) {
if c.NArg() > 0 { if c.NArg() > 0 {
// currently, we only support extra args for token // currently, we only support extra args for token
+53 -18
View File
@@ -24,8 +24,21 @@ func runApp(app *cli.App, _ chan struct{}) {
Usage: "Manages the cloudflared system service", Usage: "Manages the cloudflared system service",
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
{ {
Name: "install", Name: "install",
Usage: "Install cloudflared as a system service", Usage: "Install cloudflared as a system service",
ArgsUsage: "[TOKEN]",
Description: `
Installs cloudflared as a service using the detected init system (e.g., sysv,
systemd, openrc).
A token may optionally be provided. If a token is provided, it will be written
to disk in the service configuration directory and the cloudflared service
configured to use it via the --token-file argument.
If no token is provided, cloudflared will attempt to find a configuration file
with tunnel credentials from a predetermined list of configuration directory
paths. If found, it will use that configuration file and credentials (or error
out if no configuration file with credentials was found).`,
Action: cliutil.ConfiguredAction(installLinuxService), Action: cliutil.ConfiguredAction(installLinuxService),
Flags: []cli.Flag{ Flags: []cli.Flag{
noUpdateServiceFlag, noUpdateServiceFlag,
@@ -48,6 +61,7 @@ const (
serviceConfigFile = "config.yml" serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem" serviceCredentialFile = "cert.pem"
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
tokenPath = serviceConfigDir + "/" + defaultTokenFile
cloudflaredService = "cloudflared.service" cloudflaredService = "cloudflared.service"
cloudflaredUpdateService = "cloudflared-update.service" cloudflaredUpdateService = "cloudflared-update.service"
cloudflaredUpdateTimer = "cloudflared-update.timer" cloudflaredUpdateTimer = "cloudflared-update.timer"
@@ -246,23 +260,45 @@ func installLinuxService(c *cli.Context) error {
Path: etPath, Path: etPath,
} }
// Check if the "no update flag" is set // Both installation methods below need the config directory to be present,
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name) // either to hold the token file, or the configuration yaml
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
if c.NArg() == 0 {
extraArgsFunc = buildArgsForConfig
} else {
extraArgsFunc = buildArgsForToken
}
extraArgs, err := extraArgsFunc(c, log)
if err != nil {
return err return err
} }
var extraArgs []string
if c.NArg() == 0 {
// If passed no arguments e.g., "$ cloudflared service install",
// install the service using the detected config file (or error-out if
// no config exists).
if extraArgs, err = buildArgsForConfig(c, log); err != nil {
return err
}
} else {
// If passed one argument e.g., "$ cloudflared service install <token>"
// write the token to the config directory and install the service
// using --token-file pointing to that file. This is the quick setup
// the tunnel UI suggests.
// Ensure token file is removed if install fails
defer func() {
if err != nil {
removeTokenFile(tokenPath, log)
}
}()
if err = writeTokenToFile(tokenPath, c.Args().First()); err != nil {
return err
}
extraArgs = buildArgsForTokenFile(tokenPath)
}
templateArgs.ExtraArgs = extraArgs templateArgs.ExtraArgs = extraArgs
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
switch { switch {
case inits.IsSystemd(): case inits.IsSystemd():
log.Info().Msgf("Using Systemd") log.Info().Msgf("Using Systemd")
@@ -282,10 +318,6 @@ func installLinuxService(c *cli.Context) error {
} }
func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) { func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return nil, err
}
src, _, err := config.ReadConfigFile(c, log) src, _, err := config.ReadConfigFile(c, log)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -426,6 +458,9 @@ func uninstallLinuxService(c *cli.Context) error {
if err == nil { if err == nil {
log.Info().Msg("Linux service for cloudflared uninstalled successfully") log.Info().Msg("Linux service for cloudflared uninstalled successfully")
} }
removeTokenFile(tokenPath, log)
return err return err
} }