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
import (
"errors"
"fmt"
"os"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
@@ -8,6 +12,44 @@ import (
"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) {
token := c.Args().First()
if _, err := tunnel.ParseToken(token); err != nil {
@@ -19,6 +61,7 @@ func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
}, 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) {
if c.NArg() > 0 {
// currently, we only support extra args for token
+51 -16
View File
@@ -26,6 +26,19 @@ func runApp(app *cli.App, _ chan struct{}) {
{
Name: "install",
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),
Flags: []cli.Flag{
noUpdateServiceFlag,
@@ -48,6 +61,7 @@ const (
serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem"
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
tokenPath = serviceConfigDir + "/" + defaultTokenFile
cloudflaredService = "cloudflared.service"
cloudflaredUpdateService = "cloudflared-update.service"
cloudflaredUpdateTimer = "cloudflared-update.timer"
@@ -246,23 +260,45 @@ func installLinuxService(c *cli.Context) error {
Path: etPath,
}
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
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 {
// Both installation methods below need the config directory to be present,
// either to hold the token file, or the configuration yaml
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
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
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
switch {
case inits.IsSystemd():
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) {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return nil, err
}
src, _, err := config.ReadConfigFile(c, log)
if err != nil {
return nil, err
@@ -426,6 +458,9 @@ func uninstallLinuxService(c *cli.Context) error {
if err == nil {
log.Info().Msg("Linux service for cloudflared uninstalled successfully")
}
removeTokenFile(tokenPath, log)
return err
}