diff --git a/build.assets/charts/Dockerfile-tbot-distroless b/build.assets/charts/Dockerfile-tbot-distroless index 9e1e4d8897c07..790699bdb2544 100644 --- a/build.assets/charts/Dockerfile-tbot-distroless +++ b/build.assets/charts/Dockerfile-tbot-distroless @@ -19,3 +19,4 @@ RUN --mount=type=bind,target=/ctx dpkg-deb -R /ctx/$TELEPORT_DEB_FILE_NAME /opt/ FROM $BASE_IMAGE COPY --from=teleport /opt/staging/usr/local/bin/tbot /usr/local/bin/tbot ENTRYPOINT ["/usr/local/bin/tbot"] +CMD ["start"] diff --git a/build.assets/charts/Dockerfile-tbot-distroless-fips b/build.assets/charts/Dockerfile-tbot-distroless-fips index 7592a8993ec69..1788a4910dc9b 100644 --- a/build.assets/charts/Dockerfile-tbot-distroless-fips +++ b/build.assets/charts/Dockerfile-tbot-distroless-fips @@ -19,3 +19,4 @@ RUN --mount=type=bind,target=/ctx dpkg-deb -R /ctx/$TELEPORT_DEB_FILE_NAME /opt/ FROM $BASE_IMAGE COPY --from=teleport /opt/staging/usr/local/bin/tbot /usr/local/bin/tbot ENTRYPOINT ["/usr/local/bin/tbot", "--fips"] +CMD ["start"] diff --git a/docs/pages/reference/cli/tbot.mdx b/docs/pages/reference/cli/tbot.mdx index c07073df05392..65f0a5e4635b3 100644 --- a/docs/pages/reference/cli/tbot.mdx +++ b/docs/pages/reference/cli/tbot.mdx @@ -297,22 +297,23 @@ another dedicated mode instead. ### Flags -| Flag | Description | -|----------------------|------------------------------------------------------------------------------------------------| -| `-d/--debug` | Enable verbose logging to stderr. | -| `-c/--config` | Path to a Machine ID configuration file. | -| `--[no-]fips` | Whether to run tbot in FIPS compliance mode. This requires the FIPS `tbot` binary. | -| `-a/--auth-server` | Address of the Teleport Auth Service. Prefer using --proxy-server where possible | -| `--proxy-server` | Address of the Teleport Proxy Server. | -| `--token` | A bot join token, if attempting to onboard a new bot; used on first connect. Can also be an absolute path to a file containing the token. | -| `--ca-pin` | CA pin to validate the Teleport Auth Service; used on first connect. | +| Flag | Description | +|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `-d/--debug` | Enable verbose logging to stderr. Can also be configured using the `TBOT_DEBUG` environment variable. | +| `-c/--config` | Path to a tbot configuration file. Mutually exclusive with `--config-string`. Can also be configured with the `TBOT_CONFIG_PATH` environment variable. | +| `--config-string` | Allows the tbot configuration to be provided as a base 64 string directly via this flag or the `TBOT_CONFIG` environment variable. Mutually exclusive with `--config` | +| `--[no-]fips` | Whether to run tbot in FIPS compliance mode. This requires the FIPS `tbot` binary. | +| `-a/--auth-server` | Address of the Teleport Auth Service. Prefer using --proxy-server where possible | +| `--proxy-server` | Address of the Teleport Proxy Server. | +| `--token` | A bot join token, if attempting to onboard a new bot; used on first connect. Can also be an absolute path to a file containing the token. | +| `--ca-pin` | CA pin to validate the Teleport Auth Service; used on first connect. | | `--data-dir` | Directory to store internal bot data. In production environments access to this directory should be limited only to an isolated linux user as an owner with `0600` permissions. | -| `--destination-dir` | Directory to write short-lived machine certificates. | -| `--certificate-ttl` | TTL of short-lived machine certificates. | -| `--renewal-interval` | Interval at which short-lived certificates are renewed; must be less than the certificate TTL. | -| `--join-method` | Method to use to join the cluster. Can be `token`, `azure`, `circleci`, `gcp`, `github`, `gitlab` or `iam`. | -| `--oneshot` | If set, quit after the first renewal. | -| `--log-format` | Controls the format of output logs. Can be `json` or `text`. Defaults to `text`. | +| `--destination-dir` | Directory to write short-lived machine certificates. | +| `--certificate-ttl` | TTL of short-lived machine certificates. | +| `--renewal-interval` | Interval at which short-lived certificates are renewed; must be less than the certificate TTL. | +| `--join-method` | Method to use to join the cluster. Can be `token`, `azure`, `circleci`, `gcp`, `github`, `gitlab` or `iam`. | +| `--oneshot` | If set, quit after the first renewal. | +| `--log-format` | Controls the format of output logs. Can be `json` or `text`. Defaults to `text`. | ### Examples diff --git a/lib/tbot/cli/cli.go b/lib/tbot/cli/cli.go index ff820fc0d0f89..ff82aae569929 100644 --- a/lib/tbot/cli/cli.go +++ b/lib/tbot/cli/cli.go @@ -40,6 +40,14 @@ const ( // ProxyServerEnvVar is the environment variable that overrides the // configured proxy server address. ProxyServerEnvVar = "TELEPORT_PROXY" + // TBotDebugEnvVar is the environment variable that enables debug logging. + TBotDebugEnvVar = "TBOT_DEBUG" + // TBotConfigPathEnvVar is the environment variable that overrides the + // configured config file path. + TBotConfigPathEnvVar = "TBOT_CONFIG_PATH" + // TBotConfigEnvVar is the environment variable that provides tbot + // configuration with base64 encoded string. + TBotConfigEnvVar = "TBOT_CONFIG" ) var log = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentTBot) @@ -154,11 +162,18 @@ func LoadConfigWithMutators(globals *GlobalArgs, mutators ...ConfigMutator) (*co var cfg *config.BotConfig var err error - if globals.staticConfigYAML != "" { + if globals.ConfigString != "" && globals.ConfigPath != "" { + return nil, trace.BadParameter("cannot specify both config and config-string") + } else if globals.staticConfigYAML != "" { cfg, err = config.ReadConfig(strings.NewReader(globals.staticConfigYAML), false) if err != nil { return nil, trace.Wrap(err) } + } else if globals.ConfigString != "" { + cfg, err = config.ReadConfigFromBase64String(globals.ConfigString, false) + if err != nil { + return nil, trace.Wrap(err, "loading bot config from base64 encoded string") + } } else if globals.ConfigPath != "" { cfg, err = config.ReadConfigFromFile(globals.ConfigPath, false) diff --git a/lib/tbot/cli/globals.go b/lib/tbot/cli/globals.go index dcd762920daa0..8796e6e0ba46c 100644 --- a/lib/tbot/cli/globals.go +++ b/lib/tbot/cli/globals.go @@ -42,6 +42,9 @@ type GlobalArgs struct { // ConfigPath is a path to a YAML configuration file to load, if any. ConfigPath string + // ConfigString is a base64 encoded string of a YAML configuration file to load, if any. + ConfigString string + // Debug enables debug-level logging, when set Debug bool @@ -67,8 +70,9 @@ type GlobalArgs struct { func NewGlobalArgs(app *kingpin.Application) *GlobalArgs { g := &GlobalArgs{} - app.Flag("debug", "Verbose logging to stdout.").Short('d').BoolVar(&g.Debug) - app.Flag("config", "Path to a configuration file.").Short('c').StringVar(&g.ConfigPath) + app.Flag("debug", "Verbose logging to stdout.").Short('d').Envar(TBotDebugEnvVar).BoolVar(&g.Debug) + app.Flag("config", "Path to a configuration file.").Short('c').Envar(TBotConfigPathEnvVar).StringVar(&g.ConfigPath) + app.Flag("config-string", "Base64 encoded configuration string.").Hidden().Envar(TBotConfigEnvVar).StringVar(&g.ConfigString) app.Flag("fips", "Runs tbot in FIPS compliance mode. This requires the FIPS binary is in use.").BoolVar(&g.FIPS) app.Flag("trace", "Capture and export distributed traces.").Hidden().BoolVar(&g.Trace) app.Flag("trace-exporter", "An OTLP exporter URL to send spans to.").Hidden().StringVar(&g.TraceExporter) diff --git a/lib/tbot/config/config.go b/lib/tbot/config/config.go index 7b50ae9519074..7c56766ce7ccd 100644 --- a/lib/tbot/config/config.go +++ b/lib/tbot/config/config.go @@ -19,7 +19,9 @@ package config import ( + "bytes" "context" + "encoding/base64" "errors" "fmt" "io" @@ -592,6 +594,16 @@ func ReadConfigFromFile(filePath string, manualMigration bool) (*BotConfig, erro return ReadConfig(f, manualMigration) } +// ReadConfigFromBase64String reads and parses a YAML config from a base64 encoded string. +func ReadConfigFromBase64String(b64Str string, manualMigration bool) (*BotConfig, error) { + data, err := base64.StdEncoding.DecodeString(b64Str) + if err != nil { + return nil, trace.Wrap(err, "failed to decode base64 encoded config") + } + r := bytes.NewReader(data) + return ReadConfig(r, manualMigration) +} + type Version string var ( diff --git a/lib/tbot/config/config_test.go b/lib/tbot/config/config_test.go index 1cdb24c6266d7..e7a98b7f886f7 100644 --- a/lib/tbot/config/config_test.go +++ b/lib/tbot/config/config_test.go @@ -517,3 +517,57 @@ func TestCredentialLifetimeValidate(t *testing.T) { }) } } + +// TestBotConfig_Base64 ensures that config can be read from bas64 encoded YAML +func TestBotConfig_Base64(t *testing.T) { + tests := []struct { + name string + configBase64 string + expected BotConfig + }{ + { + name: "minimal config, proxy server", + configBase64: "dmVyc2lvbjogdjIKcHJveHlfc2VydmVyOiAiZXhhbXBsZS50ZWxlcG9ydC5zaDo0NDMiCm9uYm9hcmRpbmc6CiAgdG9rZW46ICJteS10b2tlbiIKICBqb2luX21ldGhvZDogInRva2VuIgpzZXJ2aWNlczoKLSB0eXBlOiBhcHBsaWNhdGlvbi10dW5uZWwKICBhcHBfbmFtZTogdGVzdGFwcAogIGxpc3RlbjogdGNwOi8vMTI3LjAuMC4xOjgwODA=", + expected: BotConfig{ + Version: V2, + ProxyServer: "example.teleport.sh:443", + Onboarding: OnboardingConfig{ + JoinMethod: "token", + TokenValue: "my-token", + }, + Services: []ServiceConfig{ + &ApplicationTunnelService{ + Listen: "tcp://127.0.0.1:8080", + AppName: "testapp", + }, + }, + }, + }, + { + name: "minimal config, auth server", + configBase64: "dmVyc2lvbjogdjIKYXV0aF9zZXJ2ZXI6ICJleGFtcGxlLnRlbGVwb3J0LnNoOjQ0MyIKb25ib2FyZGluZzoKICB0b2tlbjogIm15LXRva2VuIgogIGpvaW5fbWV0aG9kOiAidG9rZW4iCnNlcnZpY2VzOgotIHR5cGU6IGFwcGxpY2F0aW9uLXR1bm5lbAogIGFwcF9uYW1lOiB0ZXN0YXBwCiAgbGlzdGVuOiB0Y3A6Ly8xMjcuMC4wLjE6ODA4MA==", + expected: BotConfig{ + Version: V2, + AuthServer: "example.teleport.sh:443", + Onboarding: OnboardingConfig{ + JoinMethod: "token", + TokenValue: "my-token", + }, + Services: []ServiceConfig{ + &ApplicationTunnelService{ + Listen: "tcp://127.0.0.1:8080", + AppName: "testapp", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := ReadConfigFromBase64String(tt.configBase64, false) + require.NoError(t, err) + require.Equal(t, tt.expected, *cfg) + }) + } +}