From 12d323354f222d8c4a0b5b852f15a475d943cd2b Mon Sep 17 00:00:00 2001 From: Sebastiaan Tammer Date: Wed, 10 Jun 2020 12:14:47 +0200 Subject: [PATCH] Allows flags containing sensitive stuff to be passed as files (#545) * Allows options containing sensitive stuff (passwords, tokens) to be passed as a file instead * Fixed linter error, added tests, removed notification-url (due to being an array) --- cmd/root.go | 1 + docs/notifications.md | 9 ++++---- go.mod | 1 + internal/flags/flags.go | 45 ++++++++++++++++++++++++++++++++++++ internal/flags/flags_test.go | 40 ++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5e5c4e0..bef694c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,6 +81,7 @@ func PreRun(cmd *cobra.Command, args []string) { scheduleSpec = "@every " + strconv.Itoa(interval) + "s" } + flags.GetSecretsFromFiles(cmd) cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd) if timeout < 0 { diff --git a/docs/notifications.md b/docs/notifications.md index 5dc6d45..afa23bd 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -33,7 +33,7 @@ To receive notifications by email, the following command-line options, or their - `--notification-email-server-tls-skip-verify` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY`): Do not verify the TLS certificate of the mail server. This should be used only for testing. - `--notification-email-server-port` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT`): The port used to connect to the SMTP server to send e-mails through. Defaults to `25`. - `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with. -- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. +- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used. - `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds. - `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. @@ -111,7 +111,7 @@ If watchtower is monitoring the same Docker daemon under which the watchtower co To receive notifications in Slack, add `slack` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable. -Additionally, you should set the Slack webhook URL using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable. +Additionally, you should set the Slack webhook URL using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable. This option can also reference a file, in which case the contents of the file are used. By default, watchtower will send messages under the name `watchtower`, you can customize this string through the `--notification-slack-identifier` option or the `WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER` environment variable. @@ -140,7 +140,7 @@ docker run -d \ To receive notifications in MSTeams channel, add `msteams` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable. -Additionally, you should set the MSTeams webhook URL using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable. +Additionally, you should set the MSTeams webhook URL using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable. This option can also reference a file, in which case the contents of the file are used. MSTeams notifier could send keys/values filled by `log.WithField` or `log.WithFields` as MSTeams message facts. To enable this feature add `--notification-msteams-data` flag or set `WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true` environment variable. @@ -160,7 +160,6 @@ docker run -d \ To push a notification to your Gotify instance, register a Gotify app and specify the Gotify URL and app token: - ```bash docker run -d \ --name watchtower \ @@ -171,6 +170,8 @@ docker run -d \ containrrr/watchtower ``` +`-e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN` or `--notification-gotify-token` can also reference a file, in which case the contents of the file are used. + If you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`. ### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) diff --git a/go.mod b/go.mod index 3424c44..300eb31 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/sirupsen/logrus v1.4.1 github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.3.0 github.com/theupdateframework/notary v0.6.1 // indirect diff --git a/internal/flags/flags.go b/internal/flags/flags.go index ee1d8e3..8d8d2ab 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -1,11 +1,14 @@ package flags import ( + "io/ioutil" "os" + "strings" "time" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -366,3 +369,45 @@ func setEnvOptBool(env string, opt bool) error { } return nil } + +// GetSecretsFromFiles checks if passwords/tokens/webhooks have been passed as a file instead of plaintext. +// If so, the value of the flag will be replaced with the contents of the file. +func GetSecretsFromFiles(rootCmd *cobra.Command) { + flags := rootCmd.PersistentFlags() + + secrets := []string{ + "notification-email-server-password", + "notification-slack-hook-url", + "notification-msteams-hook", + "notification-gotify-token", + } + for _, secret := range secrets { + getSecretFromFile(flags, secret) + } +} + +// getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file. +func getSecretFromFile(flags *pflag.FlagSet, secret string) { + value, err := flags.GetString(secret) + if err != nil { + log.Error(err) + } + if value != "" && isFile(value) { + file, err := ioutil.ReadFile(value) + if err != nil { + log.Fatal(err) + } + err = flags.Set(secret, strings.TrimSpace(string(file))) + if err != nil { + log.Error(err) + } + } +} + +func isFile(s string) bool { + _, err := os.Stat(s) + if os.IsNotExist(err) { + return false + } + return true +} diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 42b5ce5..b659a96 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,6 +1,7 @@ package flags import ( + "io/ioutil" "os" "testing" @@ -39,3 +40,42 @@ func TestEnvConfig_Custom(t *testing.T) { // Re-enable this test when we've moved to github actions. // assert.Equal(t, "1.99", os.Getenv("DOCKER_API_VERSION")) } + +func TestGetSecretsFromFilesWithString(t *testing.T) { + value := "supersecretstring" + + err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value) + require.NoError(t, err) + + testGetSecretsFromFiles(t, "notification-email-server-password", value) +} + +func TestGetSecretsFromFilesWithFile(t *testing.T) { + value := "megasecretstring" + + // Create the temporary file which will contain a secret. + file, err := ioutil.TempFile(os.TempDir(), "watchtower-") + require.NoError(t, err) + defer os.Remove(file.Name()) // Make sure to remove the temporary file later. + + // Write the secret to the temporary file. + secret := []byte(value) + _, err = file.Write(secret) + require.NoError(t, err) + + err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name()) + require.NoError(t, err) + + testGetSecretsFromFiles(t, "notification-email-server-password", value) +} + +func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { + cmd := new(cobra.Command) + SetDefaults() + RegisterNotificationFlags(cmd) + GetSecretsFromFiles(cmd) + value, err := cmd.PersistentFlags().GetString(flagName) + require.NoError(t, err) + + assert.Equal(t, expected, value) +}