From 6d84c2813a3cb981a12e7bc1a37cae18d6ec180c Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Mon, 26 May 2025 11:32:41 -0700 Subject: [PATCH] cmd/containerboot: load auth key from file if TS_AUTHKEY_FILE is set This changes adds a new environment variable `TS_AUTHKEY_FILE`, that can be used instead of `TS_AUTHKEY` to specify the auth key to use for login. TS_AUTHKEY takes precedence over TS_AUTHKEY_FILE, if both are set. This is useful for cases where the auth key is stored in a docker secret or similar, and makes it possible to avoid storing the auth key in a docker compose file. Fixes Signed-off-by: Josh McKinney --- cmd/containerboot/main.go | 1 + cmd/containerboot/main_test.go | 23 +++++++++++++++++++++++ cmd/containerboot/settings.go | 16 ++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go index 954330897..4ea8c27f3 100644 --- a/cmd/containerboot/main.go +++ b/cmd/containerboot/main.go @@ -12,6 +12,7 @@ // variables. All configuration is optional. // // - TS_AUTHKEY: the authkey to use for login. +// - TS_AUTHKEY_FILE: the path to a file containing the authkey to use for login. // - TS_HOSTNAME: the hostname to request for the node. // - TS_ROUTES: subnet routes to advertise. Explicitly setting it to an empty // value will cause containerboot to stop acting as a subnet router for any diff --git a/cmd/containerboot/main_test.go b/cmd/containerboot/main_test.go index a0ccce3dd..54cbef8a8 100644 --- a/cmd/containerboot/main_test.go +++ b/cmd/containerboot/main_test.go @@ -262,6 +262,29 @@ func TestContainerBoot(t *testing.T) { }, } }, + "authkey_file": func(env *testEnv) testCase { + authFile := filepath.Join(env.d, "authkey.txt") + if err := os.WriteFile(authFile, []byte("tskey-key"), 0600); err != nil { + t.Fatalf("Failed to write authkey file: %v", err) + } + return testCase{ + // Userspace mode, ephemeral storage, authkey provided via file. + Env: map[string]string{ + "TS_AUTHKEY_FILE": authFile, + }, + Phases: []phase{ + { + WantCmds: []string{ + "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking", + "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key", + }, + }, + { + Notify: runningNotify, + }, + }, + } + }, "authkey_disk_state": func(env *testEnv) testCase { return testCase{ Env: map[string]string{ diff --git a/cmd/containerboot/settings.go b/cmd/containerboot/settings.go index 0ac9c828e..58569d277 100644 --- a/cmd/containerboot/settings.go +++ b/cmd/containerboot/settings.go @@ -85,7 +85,7 @@ type settings struct { func configFromEnv() (*settings, error) { cfg := &settings{ - AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""), + AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY", "TS_AUTHKEY_FILE", "TS_AUTH_KEY_FILE"}, ""), Hostname: defaultEnv("TS_HOSTNAME", ""), Routes: defaultEnvStringPointer("TS_ROUTES"), ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""), @@ -365,9 +365,21 @@ func defaultEnvBoolPointer(name string) *bool { return &ret } +// defaultEnvs returns the value of the first envvar in names that is set, +// or defVal if none are set. If the envvar ends with "_FILE", it reads the +// value from the file specified by the envvar. func defaultEnvs(names []string, defVal string) string { for _, name := range names { - if v, ok := os.LookupEnv(name); ok { + if strings.HasSuffix(name, "_FILE") { + if filepath, ok := os.LookupEnv(name); ok { + data, err := os.ReadFile(filepath) + if err != nil { + log.Printf("error reading env var %s from file %s: %v", name, filepath, err) + continue + } + return strings.TrimSpace(string(data)) + } + } else if v, ok := os.LookupEnv(name); ok { return v } }