From b973ef5facd0f54a67d2e9bf42c9fbcf1fdacd88 Mon Sep 17 00:00:00 2001 From: Jim Wang Date: Mon, 1 Feb 2021 20:30:06 -0700 Subject: [PATCH] feat(security): Fix redis start issue #2863 Now redis starts with conf file with credentials and thus insecure gap is removed - Refactor security-bootstrap-redis to absorbed into security-bootstrapper as one of command - Remove security-bootstrap-redis binary build - Redis db server starts with config file with credentials - Update snaps Closes: #2863 Signed-off-by: Jim Wang --- .gitignore | 1 - Makefile | 4 - cmd/security-bootstrap-redis/README.md | 23 --- cmd/security-bootstrap-redis/main.go | 27 --- cmd/security-bootstrapper/Dockerfile | 6 +- .../entrypoint-scripts/redis_wait_install.sh | 41 +++-- .../res-bootstrap-redis}/configuration.toml | 3 + .../security/bootstrapper/helper/helper.go | 62 +++++++ .../bootstrapper/helper/helper_test.go | 65 +++++++ .../bootstrapper/helper/redis_conf.go | 159 ++++++++++++++++++ .../bootstrapper/helper/redis_config_test.go | 57 +++++++ internal/security/bootstrapper/main.go | 23 ++- .../{ => bootstrapper}/redis/config/config.go | 17 +- .../redis/configure.go} | 26 +-- .../redis/container/config.go | 3 +- .../bootstrapper/redis/handlers/handlers.go | 132 +++++++++++++++ internal/security/redis/constants.go | 22 --- internal/security/redis/handlers.go | 156 ----------------- snap/hooks/install | 14 +- snap/snapcraft.yaml | 30 +++- 20 files changed, 587 insertions(+), 284 deletions(-) delete mode 100644 cmd/security-bootstrap-redis/README.md delete mode 100644 cmd/security-bootstrap-redis/main.go rename cmd/{security-bootstrap-redis/res => security-bootstrapper/res-bootstrap-redis}/configuration.toml (94%) create mode 100644 internal/security/bootstrapper/helper/helper.go create mode 100644 internal/security/bootstrapper/helper/helper_test.go create mode 100644 internal/security/bootstrapper/helper/redis_conf.go create mode 100644 internal/security/bootstrapper/helper/redis_config_test.go rename internal/security/{ => bootstrapper}/redis/config/config.go (89%) rename internal/security/{redis/main.go => bootstrapper/redis/configure.go} (77%) rename internal/security/{ => bootstrapper}/redis/container/config.go (94%) create mode 100644 internal/security/bootstrapper/redis/handlers/handlers.go delete mode 100644 internal/security/redis/constants.go delete mode 100644 internal/security/redis/handlers.go diff --git a/.gitignore b/.gitignore index 4bfb1742bd..99358243f7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ cmd/support-notifications/support-notifications cmd/support-scheduler/support-scheduler cmd/sys-mgmt-agent/sys-mgmt-agent cmd/sys-mgmt-executor/sys-mgmt-executor -cmd/security-bootstrap-redis/security-bootstrap-redis cmd/secrets-config/secrets-config cmd/security-bootstrapper/security-bootstrapper diff --git a/Makefile b/Makefile index 427467ec97..48d22ad7f5 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,6 @@ MICROSERVICES= \ cmd/security-proxy-setup/security-proxy-setup \ cmd/security-secretstore-setup/security-secretstore-setup \ cmd/security-file-token-provider/security-file-token-provider \ - cmd/security-bootstrap-redis/security-bootstrap-redis \ cmd/secrets-config/secrets-config \ cmd/security-bootstrapper/security-bootstrapper @@ -82,9 +81,6 @@ cmd/security-secretstore-setup/security-secretstore-setup: cmd/security-file-token-provider/security-file-token-provider: $(GO) build $(GOFLAGS) -o ./cmd/security-file-token-provider/security-file-token-provider ./cmd/security-file-token-provider -cmd/security-bootstrap-redis/security-bootstrap-redis: - $(GO) build $(GOFLAGS) -o ./cmd/security-bootstrap-redis/security-bootstrap-redis ./cmd/security-bootstrap-redis - cmd/secrets-config/secrets-config: $(GO) build $(GOFLAGS) -o ./cmd/secrets-config ./cmd/secrets-config diff --git a/cmd/security-bootstrap-redis/README.md b/cmd/security-bootstrap-redis/README.md deleted file mode 100644 index 88c4e3e781..0000000000 --- a/cmd/security-bootstrap-redis/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# EdgeX Foundry Security Service - Security Bootstrap Redis - -[![license](https://img.shields.io/badge/license-Apache%20v2.0-blue.svg)](LICENSE) - -## Summary - -Implements a service to read the Redis password from Vault and set it in Redis. The service exits when it completes. The Docker entrypoint keeps the image from exiting until it receives an interrupt. - -## Explanation - -In the Geneva release, this code was liberally borrowed from [edgexfoundry/docker-edgex-mongo](https://github.com/edgexfoundry/docker-edgex-mongo), tightly coupled with security-secretstore-setup, and used a shared file to pass the Redis password to Redis. For the Hanoi release, the goal is to conform to the [ADR for secret creation and distribution](https://docs.edgexfoundry.org/1.2/design/adr/security/0008-Secret-Creation-and-Distribution/) and support configuration overrides via go-mod-boostrap. - -The service is organized in the Docker compose file to run after security-secretstore-setup and Redis are started. This isn't guaranteed by Docker so the security-bootstrap-redis will keep retrying until the retry timer expires. The service starts by reading the Redis password from the vault, creates a connection to Redis (which when it starts does not require authentication), and attempts to set the password obtained from the vault. - -If security-bootstrap-redis cannot create an unauthenticated connection to Redis, it will attempt to create an authenticated connection using the credentials received from vault. It is an error if this authenticated connection cannot be established as it means Redis is out of sync with the vault. - -The service does not exit when started via the Docker. - -## Tight Coupling - -* res/configuration.toml and redis/config/config.go -* res-file-token-provider/configuration.toml and clients.SecurityBootstrapRedisKey ("edgex-security-bootstrap-redis") -* security-secretstore-setup and vault key layout diff --git a/cmd/security-bootstrap-redis/main.go b/cmd/security-bootstrap-redis/main.go deleted file mode 100644 index 2e0ac02496..0000000000 --- a/cmd/security-bootstrap-redis/main.go +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * Copyright 2020 Redis Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - *******************************************************************************/ - -package main - -import ( - "context" - - "github.com/edgexfoundry/edgex-go/internal/security/redis" - "github.com/gorilla/mux" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - redis.Main(ctx, cancel, mux.NewRouter(), nil) -} diff --git a/cmd/security-bootstrapper/Dockerfile b/cmd/security-bootstrapper/Dockerfile index 9a112a7ba8..ce3b925085 100644 --- a/cmd/security-bootstrapper/Dockerfile +++ b/cmd/security-bootstrapper/Dockerfile @@ -32,8 +32,7 @@ RUN go mod download COPY . . -RUN make cmd/security-bootstrapper/security-bootstrapper && \ - make cmd/security-bootstrap-redis/security-bootstrap-redis +RUN make cmd/security-bootstrapper/security-bootstrapper FROM alpine:3.12 @@ -59,8 +58,7 @@ COPY --from=builder /edgex-go/cmd/security-bootstrapper/security-bootstrapper . COPY --from=builder /edgex-go/cmd/security-bootstrapper/res/configuration.toml ./res/ # needed for bootstrapping Redis db -COPY --from=builder /edgex-go/cmd/security-bootstrap-redis/security-bootstrap-redis ${BOOTSTRAP_REDIS_DIR}/ -COPY --from=builder /edgex-go/cmd/security-bootstrap-redis/res/configuration.toml ${BOOTSTRAP_REDIS_DIR}/res/ +COPY --from=builder /edgex-go/cmd/security-bootstrapper/res-bootstrap-redis/configuration.toml ${BOOTSTRAP_REDIS_DIR}/res/ # Expose the file directory as a volume since there's long-running state VOLUME ${SECURITY_INIT_DIR} diff --git a/cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh index 724bcf1e90..ce70b835fa 100755 --- a/cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh +++ b/cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh @@ -33,28 +33,37 @@ echo "$(date) Executing waitFor on Redis with waiting on TokensReadyPort \ -uri tcp://"${STAGEGATE_SECRETSTORESETUP_HOST}":"${STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT}" \ -timeout "${STAGEGATE_WAITFOR_TIMEOUT}" -# the bootstrap-redis needs the connection from Redis db to set it up. -# Hence, here bootstrap-redis runs in background and then after bootstrap-redis starts, -# the Redis db starts in background. +# the configureRedis retrieves the redis default user's credentials from secretstore (i.e. Vault) and +# generates the redis configuration file with ACL rules in it. +# The redis database server will start with the generated configuration file so that it is +# started securely. echo "$(date) ${STAGEGATE_SECRETSTORESETUP_HOST} tokens ready, bootstrapping redis..." -/edgex-init/bootstrap-redis/security-bootstrap-redis --confdir=/edgex-init/bootstrap-redis/res & -redis_bootstrapper_pid=$! +/edgex-init/security-bootstrapper --confdir=/edgex-init/bootstrap-redis/res configureRedis -# give some time for bootstrap-redis to start up -sleep 1 - -echo "$(date) Starting edgex-redis..." -exec /usr/local/bin/docker-entrypoint.sh redis-server & - -# wait for bootstrap-redis to finish before signal the redis is ready -wait $redis_bootstrapper_pid redis_bootstrapping_status=$? -if [ $redis_bootstrapping_status -eq 0 ]; then - echo "$(date) redis is bootstrapped and ready" -else +if [ $redis_bootstrapping_status -ne 0 ]; then echo "$(date) failed to bootstrap redis" + exit 1 fi +# make sure the config file is present before redis server starts up +/edgex-init/security-bootstrapper --confdir=/edgex-init/res waitFor \ + -uri file://"${DATABASECONFIG_PATH}"/"${DATABASECONFIG_NAME}" \ + -timeout "${STAGEGATE_WAITFOR_TIMEOUT}" + +# starting redis with config file +echo "$(date) Starting edgex-redis ..." +exec /usr/local/bin/docker-entrypoint.sh redis-server "${DATABASECONFIG_PATH}"/"${DATABASECONFIG_NAME}" & + +# wait for the Redis port +echo "$(date) Executing waitFor on database redis with waiting on its own port \ + tcp://${STAGEGATE_DATABASE_HOST}:${STAGEGATE_DATABASE_PORT}" +/edgex-init/security-bootstrapper --confdir=/edgex-init/res waitFor \ + -uri tcp://"${STAGEGATE_DATABASE_HOST}":"${STAGEGATE_DATABASE_PORT}" \ + -timeout "${STAGEGATE_WAITFOR_TIMEOUT}" + +echo "$(date) redis is bootstrapped and ready" + # Signal that Redis is ready for services blocked waiting on Redis /edgex-init/security-bootstrapper --confdir=/edgex-init/res listenTcp \ --port="${STAGEGATE_DATABASE_READYPORT}" --host="${DATABASES_PRIMARY_HOST}" diff --git a/cmd/security-bootstrap-redis/res/configuration.toml b/cmd/security-bootstrapper/res-bootstrap-redis/configuration.toml similarity index 94% rename from cmd/security-bootstrap-redis/res/configuration.toml rename to cmd/security-bootstrapper/res-bootstrap-redis/configuration.toml index 432ba3f36a..52e1c8a479 100644 --- a/cmd/security-bootstrap-redis/res/configuration.toml +++ b/cmd/security-bootstrapper/res-bootstrap-redis/configuration.toml @@ -39,3 +39,6 @@ TokenFile = '/vault/config/assets/resp-init.json' Timeout = 5000 Type = 'redisdb' +[DatabaseConfig] + Path = '/user/local/etc/redis/conf' + Name = 'redis.conf' diff --git a/internal/security/bootstrapper/helper/helper.go b/internal/security/bootstrapper/helper/helper.go new file mode 100644 index 0000000000..4260cbd207 --- /dev/null +++ b/internal/security/bootstrapper/helper/helper.go @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + *******************************************************************************/ + +package helper + +import ( + "io/ioutil" + "os" + "path/filepath" + "strconv" + "time" +) + +// MarkComplete creates a doneFile file +func MarkComplete(dirPath, doneFile string) error { + doneFilePath := filepath.Join(dirPath, doneFile) + if !checkIfFileExists(doneFilePath) { + if err := writeFile(doneFilePath); err != nil { + return err + } + } + + return nil +} + +// CreateDirectoryIfNotExists makes a directory if not exists yet +func CreateDirectoryIfNotExists(dirName string) (err error) { + if _, err = os.Stat(dirName); err == nil { + // already exists, skip + return nil + } else if os.IsNotExist(err) { + // dirName not exists yet, create it + err = os.MkdirAll(dirName, os.ModePerm) + } + + return +} + +func checkIfFileExists(fileName string) bool { + fileInfo, statErr := os.Stat(fileName) + if os.IsNotExist(statErr) { + return false + } + return !fileInfo.IsDir() +} + +func writeFile(aFileName string) error { + timestamp := []byte(strconv.FormatInt(time.Now().Unix(), 10)) + return ioutil.WriteFile(aFileName, timestamp, 0400) +} diff --git a/internal/security/bootstrapper/helper/helper_test.go b/internal/security/bootstrapper/helper/helper_test.go new file mode 100644 index 0000000000..0b4e256892 --- /dev/null +++ b/internal/security/bootstrapper/helper/helper_test.go @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + *******************************************************************************/ + +package helper + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarkComplete(t *testing.T) { + testDir := "testDir" + doneFile := "testDone" + + defer (cleanupDir(testDir))() + + err := os.MkdirAll(testDir, os.ModePerm) + require.NoError(t, err) + + err = MarkComplete(testDir, doneFile) + require.NoError(t, err) + + require.FileExists(t, filepath.Join(testDir, doneFile)) +} + +func TestCreateDirectoryIfNotExists(t *testing.T) { + testDir := "testDirNew" + require.NoDirExists(t, testDir) + defer (cleanupDir(testDir))() + + err := CreateDirectoryIfNotExists(testDir) + require.NoError(t, err) + require.DirExists(t, testDir) + + testPreCreatedDir := "pre-created-dir" + defer (cleanupDir(testPreCreatedDir))() + + err = os.MkdirAll(testPreCreatedDir, os.ModePerm) + require.NoError(t, err) + err = CreateDirectoryIfNotExists(testPreCreatedDir) + require.NoError(t, err) + require.DirExists(t, testPreCreatedDir) +} + +// cleanupDir deletes all files in the directory and files in the directory +func cleanupDir(dir string) func() { + return func() { + _ = os.RemoveAll(dir) + } +} diff --git a/internal/security/bootstrapper/helper/redis_conf.go b/internal/security/bootstrapper/helper/redis_conf.go new file mode 100644 index 0000000000..413a97952e --- /dev/null +++ b/internal/security/bootstrapper/helper/redis_conf.go @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + *******************************************************************************/ + +package helper + +import ( + "fmt" + "io" + "text/template" +) + +/* Redis ACL configuration +* +* The followings are contents excerpted from redis 6.0 conf documentation: +* +# Redis ACL users are defined in the following format: +# +# user ... acl rules ... +# +# For example: +# +# user worker +@list +@connection ~jobs:* on >ffa9203c493aa99 +# +# The special username "default" is used for new connections. If this user +# has the "nopass" rule, then new connections will be immediately authenticated +# as the "default" user without the need of any password provided via the +# AUTH command. Otherwise if the "default" user is not flagged with "nopass" +# the connections will start in not authenticated state, and will require +# AUTH (or the HELLO command AUTH option) in order to be authenticated and +# start to work. +# +# The ACL rules that describe what a user can do are the following: +# +# on Enable the user: it is possible to authenticate as this user. +# off Disable the user: it's no longer possible to authenticate +# with this user, however the already authenticated connections +# will still work. +# + Allow the execution of that command +# - Disallow the execution of that command +# +@ Allow the execution of all the commands in such category +# with valid categories are like @admin, @set, @sortedset, ... +# and so forth, see the full list in the server.c file where +# the Redis command table is described and defined. +# The special category @all means all the commands, but currently +# present in the server, and that will be loaded in the future +# via modules. +# +|subcommand Allow a specific subcommand of an otherwise +# disabled command. Note that this form is not +# allowed as negative like -DEBUG|SEGFAULT, but +# only additive starting with "+". +# allcommands Alias for +@all. Note that it implies the ability to execute +# all the future commands loaded via the modules system. +# nocommands Alias for -@all. +# ~ Add a pattern of keys that can be mentioned as part of +# commands. For instance ~* allows all the keys. The pattern +# is a glob-style pattern like the one of KEYS. +# It is possible to specify multiple patterns. +# allkeys Alias for ~* +# resetkeys Flush the list of allowed keys patterns. +# > Add this password to the list of valid password for the user. +# For example >mypass will add "mypass" to the list. +# This directive clears the "nopass" flag (see later). +# < Remove this password from the list of valid passwords. +# nopass All the set passwords of the user are removed, and the user +# is flagged as requiring no password: it means that every +# password will work against this user. If this directive is +# used for the default user, every new connection will be +# immediately authenticated with the default user without +# any explicit AUTH command required. Note that the "resetpass" +# directive will clear this condition. +# resetpass Flush the list of allowed passwords. Moreover removes the +# "nopass" status. After "resetpass" the user has no associated +# passwords and there is no way to authenticate without adding +# some password (or setting it as "nopass" later). +# reset Performs the following actions: resetpass, resetkeys, off, +# -@all. The user returns to the same state it has immediately +# after its creation. +# +# ACL rules can be specified in any order: for instance you can start with +# passwords, then flags, or key patterns. However note that the additive +# and subtractive rules will CHANGE MEANING depending on the ordering. +# For instance see the following example: +# +# user alice on +@all -DEBUG ~* >somepassword +# +# This will allow "alice" to use all the commands with the exception of the +# DEBUG command, since +@all added all the commands to the set of the commands +# alice can use, and later DEBUG was removed. However if we invert the order +# of two ACL rules the result will be different: +# +# user alice on -DEBUG +@all ~* >somepassword +# +# Now DEBUG was removed when alice had yet no commands in the set of allowed +# commands, later all the commands are added, so the user will be able to +# execute everything. +# +# Basically ACL rules are processed left-to-right. +# +# For more information about ACL configuration please refer to +# the Redis web site at https://redis.io/topics/acl +# +# IMPORTANT NOTE: starting with Redis 6 "requirepass" is just a compatibility +# layer on top of the new ACL system. The option effect will be just setting +# the password for the default user. Clients will still authenticate using +# AUTH as usually, or more explicitly with AUTH default +# if they follow the new protocol: both will work. +# +# requirepass foobared +* +*/ + +const ( + // aclDefaultUserTemplate is the ACL rule for "default" user + aclDefaultUserTemplate = "user default on allkeys +@all -@dangerous >{{.RedisPwd}}" + + // requirePassTemplate is the authenticate password for "default" user + requirePassTemplate = "requirepass {{.RedisPwd}}" +) + +// GenerateConfig writes the redis config based on the pre-defined templates +func GenerateConfig(wr io.Writer, pwd *string) error { + acl, err := template.New("redis-acl").Parse(aclDefaultUserTemplate + fmt.Sprintln()) + if err != nil { + return fmt.Errorf("failed to parse ACL template %s: %v", aclDefaultUserTemplate, err) + } + + // writing the ACL rules: + if err := acl.Execute(wr, map[string]interface{}{ + "RedisPwd": pwd, + }); err != nil { + return fmt.Errorf("failed to execute ACL for config %s: %v", aclDefaultUserTemplate, err) + } + + // writing the required pwd: + requirePass, err := template.New("redis-require-pass").Parse(requirePassTemplate + fmt.Sprintln()) + if err != nil { + return fmt.Errorf("failed to parse requirePass template %s: %v", requirePassTemplate, err) + } + + if err := requirePass.Execute(wr, map[string]interface{}{ + "RedisPwd": pwd, + }); err != nil { + return fmt.Errorf("failed to execute requirePass for config %s: %v", requirePassTemplate, err) + } + + return nil +} diff --git a/internal/security/bootstrapper/helper/redis_config_test.go b/internal/security/bootstrapper/helper/redis_config_test.go new file mode 100644 index 0000000000..d99295d710 --- /dev/null +++ b/internal/security/bootstrapper/helper/redis_config_test.go @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + *******************************************************************************/ + +package helper + +import ( + "bufio" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenerateConfig(t *testing.T) { + testConfFile := "testConfFile" + confFile, err := os.OpenFile(testConfFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + require.NoError(t, err) + defer func() { + err = confFile.Close() + require.NoError(t, err) + err = os.RemoveAll(testConfFile) + require.NoError(t, err) + }() + + fw := bufio.NewWriter(confFile) + testFakePwd := "123456abcdefg!@#$%^&" + + err = GenerateConfig(fw, &testFakePwd) + require.NoError(t, err) + require.NoError(t, fw.Flush()) + + inputFile, err := os.Open(testConfFile) + require.NoError(t, err) + defer inputFile.Close() + inputScanner := bufio.NewScanner(inputFile) + inputScanner.Split(bufio.ScanLines) + lineCount := 0 + // Read until a newline for each Scan + for inputScanner.Scan() { + lineCount++ + line := inputScanner.Text() + require.Contains(t, line, testFakePwd) + } + require.Equal(t, 2, lineCount) +} diff --git a/internal/security/bootstrapper/main.go b/internal/security/bootstrapper/main.go index 3acdae818f..c50c39bc43 100644 --- a/internal/security/bootstrapper/main.go +++ b/internal/security/bootstrapper/main.go @@ -17,6 +17,8 @@ package bootstrapper import ( "context" + "flag" + "fmt" "os" "github.com/gorilla/mux" @@ -26,6 +28,7 @@ import ( "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/container" "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/handlers" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/redis" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" @@ -34,7 +37,8 @@ import ( ) const ( - securityBootstrapperServiceKey = "edgex-security-bootstrapper" + securityBootstrapperServiceKey = "edgex-security-bootstrapper" + configureDatabaseSubcommandName = "configureRedis" ) // Main function is the wrapper for the security bootstrapper main @@ -48,6 +52,23 @@ func Main(ctx context.Context, cancel context.CancelFunc, _ *mux.Router, _ chan< f.Parse(os.Args[1:]) + // find out the subcommand name before assigning the real concrete configuration + // bootstrapRedis has its own configuration settings + var confdir string + flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flagSet.StringVar(&confdir, "confdir", "", "") // handled by bootstrap; duplicated here to prevent arg parsing errors + err := flagSet.Parse(os.Args[1:]) + if err != nil { + fmt.Println(err) + os.Exit(0) + } + + // branch out to bootstrap redis if it is configureRedis + if flagSet.Arg(0) == configureDatabaseSubcommandName { + redis.Configure(ctx, cancel, f) + return + } + configuration := &config.ConfigurationStruct{} dic := di.NewContainer(di.ServiceConstructorMap{ container.ConfigurationName: func(get di.Get) interface{} { diff --git a/internal/security/redis/config/config.go b/internal/security/bootstrapper/redis/config/config.go similarity index 89% rename from internal/security/redis/config/config.go rename to internal/security/bootstrapper/redis/config/config.go index f7fca4270b..cfa6c050b6 100644 --- a/internal/security/redis/config/config.go +++ b/internal/security/bootstrapper/redis/config/config.go @@ -1,4 +1,5 @@ /******************************************************************************* + * Copyright 2021 Intel Corporation * Copyright 2020 Redis Labs * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -11,7 +12,6 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. * - * @author: Andre Srinivasan *******************************************************************************/ package config @@ -20,11 +20,12 @@ import ( ) // ConfigurationStruct has a 1:1 relationship to the configuration.toml for the service. Writable is -// the runtime extension of the static configuraiton. +// the runtime extension of the static configuration. type ConfigurationStruct struct { - Writable WritableInfo - SecretStore bootstrapConfig.SecretStoreInfo - Databases map[string]bootstrapConfig.Database + Writable WritableInfo + SecretStore bootstrapConfig.SecretStoreInfo + Databases map[string]bootstrapConfig.Database + DatabaseConfig DatabaseBootstrapConfigInfo } // WritableInfo contains configuration properties that can be updated and applied without restarting @@ -33,6 +34,12 @@ type WritableInfo struct { LogLevel string } +// DatabaseBootstrapConfigInfo contains the configuration properties for bootstrapping the database +type DatabaseBootstrapConfigInfo struct { + Path string + Name string +} + // Implement interface.Configuration // UpdateFromRaw converts configuration received from the registry to a service-specific diff --git a/internal/security/redis/main.go b/internal/security/bootstrapper/redis/configure.go similarity index 77% rename from internal/security/redis/main.go rename to internal/security/bootstrapper/redis/configure.go index 612d2fa038..018a4a291a 100644 --- a/internal/security/redis/main.go +++ b/internal/security/bootstrapper/redis/configure.go @@ -1,4 +1,5 @@ /******************************************************************************* +* Copyright 2021 Intel Corporation * Copyright 2020 Redis Labs * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -21,8 +22,10 @@ import ( "os" "github.com/edgexfoundry/edgex-go/internal" - "github.com/edgexfoundry/edgex-go/internal/security/redis/config" - "github.com/edgexfoundry/edgex-go/internal/security/redis/container" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/redis/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/redis/container" + redisHandlers "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/redis/handlers" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/flags" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/handlers" @@ -30,16 +33,14 @@ import ( "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/gorilla/mux" ) -func Main(ctx context.Context, cancel context.CancelFunc, _ *mux.Router, _ chan<- bool) { +// Configure is the main entry point for configuring the database redis before startup +func Configure(ctx context.Context, + cancel context.CancelFunc, + flags flags.Common) { startupTimer := startup.NewStartUpTimer(clients.SecurityBootstrapRedisKey) - // All common command-line flags have been moved to DefaultCommonFlags. - f := flags.New() - f.Parse(os.Args[1:]) - configuration := &config.ConfigurationStruct{} dic := di.NewContainer(di.ServiceConstructorMap{ container.ConfigurationName: func(get di.Get) interface{} { @@ -47,7 +48,7 @@ func Main(ctx context.Context, cancel context.CancelFunc, _ *mux.Router, _ chan< }, }) - handler := NewHandler() + redisBootstrapHdl := redisHandlers.NewHandler() // bootstrap.RunAndReturnWaitGroup is needed for the underlying configuration system. // Conveniently, it also creates a pipeline of functions as the list of BootstrapHandler's is @@ -55,7 +56,7 @@ func Main(ctx context.Context, cancel context.CancelFunc, _ *mux.Router, _ chan< _, _, ok := bootstrap.RunAndReturnWaitGroup( ctx, cancel, - f, + flags, clients.SecurityBootstrapRedisKey, internal.ConfigStemCore+internal.ConfigMajorVersion, configuration, @@ -64,9 +65,8 @@ func Main(ctx context.Context, cancel context.CancelFunc, _ *mux.Router, _ chan< dic, []interfaces.BootstrapHandler{ handlers.SecureProviderBootstrapHandler, - handler.getCredentials, - handler.connect, - handler.maybeSetCredentials, + redisBootstrapHdl.GetCredentials, + redisBootstrapHdl.SetupConfFile, }, ) diff --git a/internal/security/redis/container/config.go b/internal/security/bootstrapper/redis/container/config.go similarity index 94% rename from internal/security/redis/container/config.go rename to internal/security/bootstrapper/redis/container/config.go index 69bcd42c04..89e48e4fd3 100644 --- a/internal/security/redis/container/config.go +++ b/internal/security/bootstrapper/redis/container/config.go @@ -17,7 +17,8 @@ package container import ( - "github.com/edgexfoundry/edgex-go/internal/security/redis/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/redis/config" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" ) diff --git a/internal/security/bootstrapper/redis/handlers/handlers.go b/internal/security/bootstrapper/redis/handlers/handlers.go new file mode 100644 index 0000000000..96ab6d0182 --- /dev/null +++ b/internal/security/bootstrapper/redis/handlers/handlers.go @@ -0,0 +1,132 @@ +/******************************************************************************* +* Copyright 2021 Intel Corporation +* Copyright 2020 Redis Labs +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software distributed under the License +* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +* or implied. See the License for the specific language governing permissions and limitations under +* the License. +* +*******************************************************************************/ + +package handlers + +import ( + "bufio" + "context" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/helper" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/redis/container" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/secret" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" +) + +// Handler is the redis bootstrapping handler +type Handler struct { + credentials bootstrapConfig.Credentials +} + +// NewHandler instantiates a new Handler +func NewHandler() *Handler { + return &Handler{} +} + +// GetCredentials retrieves the redis database credentials from secretstore +func (handler *Handler) GetCredentials(ctx context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, + dic *di.Container) bool { + lc := bootstrapContainer.LoggingClientFrom(dic.Get) + config := container.ConfigurationFrom(dic.Get) + secretProvider := bootstrapContainer.SecretProviderFrom(dic.Get) + + var credentials = bootstrapConfig.Credentials{ + Username: "unset", + Password: "unset", + } + + for startupTimer.HasNotElapsed() { + secrets, err := secretProvider.GetSecrets(config.Databases["Primary"].Type) + if err == nil { + credentials.Username = secrets[secret.UsernameKey] + credentials.Password = secrets[secret.PasswordKey] + break + } + + lc.Warnf("Could not retrieve database credentials (startup timer has not expired): %s", err.Error()) + startupTimer.SleepForInterval() + } + + if credentials.Password == "unset" { + lc.Error("Failed to retrieve database credentials before startup timer expired") + return false + } + + handler.credentials = credentials + return true +} + +// SetupConfFile dynamically creates redis config file with the retrieved credentials +func (handler *Handler) SetupConfFile(ctx context.Context, _ *sync.WaitGroup, _ startup.Timer, + dic *di.Container) bool { + lc := bootstrapContainer.LoggingClientFrom(dic.Get) + config := container.ConfigurationFrom(dic.Get) + + dbConfigDir := strings.TrimSpace(config.DatabaseConfig.Path) + dbConfigFile := strings.TrimSpace(config.DatabaseConfig.Name) + + // required + if dbConfigDir == "" { + lc.Error("required configuration for DatabaseConfig.Path is empty") + return false + } + + if dbConfigFile == "" { + lc.Error("required configuration for DatabaseConfig.Name is empty") + return false + } + + if err := helper.CreateDirectoryIfNotExists(dbConfigDir); err != nil { + lc.Errorf("failed to create database config directory %s: %v", dbConfigDir, err) + return false + } + + dbConfigFilePath := filepath.Join(dbConfigDir, dbConfigFile) + lc.Infof("Setting up the database config file %s", dbConfigFilePath) + + // open config file with read-write and overwritten attribute (TRUNC) + confFile, err := os.OpenFile(dbConfigFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + lc.Errorf("failed to open db config file %s: %v", dbConfigFilePath, err) + return false + } + defer func() { + _ = confFile.Close() + }() + + // writing the config file + fwriter := bufio.NewWriter(confFile) + if err := helper.GenerateConfig(fwriter, &handler.credentials.Password); err != nil { + lc.Errorf("cannot write the db config file %s: %v", dbConfigFilePath, err) + return false + } + if err := fwriter.Flush(); err != nil { + lc.Errorf("failed to flush the file writer buffer %v", err) + return false + } + + lc.Info("database credentials have been set in the config file") + + return true +} diff --git a/internal/security/redis/constants.go b/internal/security/redis/constants.go deleted file mode 100644 index 01d5248b3e..0000000000 --- a/internal/security/redis/constants.go +++ /dev/null @@ -1,22 +0,0 @@ -/******************************************************************************* - * Copyright 2020 Redis Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - * - * @author: Andre Srinivasan - *******************************************************************************/ - -package redis - -const SecretStore = "EDGEX_SECURITY_SECRET_STORE" -const Confdir = "res" -const ConfigFileName = "configuration.toml" -const VaultToken = "X-Vault-Token" diff --git a/internal/security/redis/handlers.go b/internal/security/redis/handlers.go deleted file mode 100644 index c99fc8dd8c..0000000000 --- a/internal/security/redis/handlers.go +++ /dev/null @@ -1,156 +0,0 @@ -/******************************************************************************* -* Copyright 2020 Redis Labs -* -* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -* in compliance with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software distributed under the License -* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express -* or implied. See the License for the specific language governing permissions and limitations under -* the License. -* -* @author: Andre Srinivasan -*******************************************************************************/ - -package redis - -import ( - "context" - "fmt" - "sync" - - "github.com/edgexfoundry/edgex-go/internal/pkg/db" - "github.com/edgexfoundry/edgex-go/internal/pkg/db/redis" - "github.com/edgexfoundry/edgex-go/internal/security/redis/container" - bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/secret" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" - bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" - "github.com/edgexfoundry/go-mod-bootstrap/v2/di" - redigo "github.com/gomodule/redigo/redis" -) - -type Handler struct { - secured bool - credentials bootstrapConfig.Credentials - redisConn redigo.Conn -} - -func NewHandler() *Handler { - return &Handler{} -} - -func (handler *Handler) getCredentials(ctx context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { - lc := bootstrapContainer.LoggingClientFrom(dic.Get) - config := container.ConfigurationFrom(dic.Get) - secretProvider := bootstrapContainer.SecretProviderFrom(dic.Get) - - var credentials = bootstrapConfig.Credentials{ - Username: "unset", - Password: "unset", - } - - for startupTimer.HasNotElapsed() { - secrets, err := secretProvider.GetSecrets(config.Databases["Primary"].Type) - if err == nil { - credentials.Username = secrets[secret.UsernameKey] - credentials.Password = secrets[secret.PasswordKey] - break - } - - lc.Warn(fmt.Sprintf("Could not retrieve database credentials (startup timer has not expired): %s", err.Error())) - startupTimer.SleepForInterval() - } - - if credentials.Password == "unset" { - lc.Error("Failed to retrieve database credentials before startup timer expired") - return false - } - - handler.credentials = credentials - return true -} - -func (handler *Handler) connect(ctx context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { - lc := bootstrapContainer.LoggingClientFrom(dic.Get) - config := container.ConfigurationFrom(dic.Get) - - var redisConn redigo.Conn - - for startupTimer.HasNotElapsed() { - lc.Debug("Attempting unauthenticated connection") - redisClient, err := redis.NewClient(db.Configuration{ - Host: config.Databases["Primary"].Host, - Port: config.Databases["Primary"].Port, - }, lc) - if err != nil { - lc.Warn(fmt.Sprintf("Could not create database client (startup timer has not expired): %s", err.Error())) - startupTimer.SleepForInterval() - continue - } - - redisConn = redisClient.Pool.Get() - if err = testConnection(redisConn); err == nil { - break - } - lc.Debug("Unauthenticated connection failed. Attempting authenticated connection.") - - if err = authenticate(redisConn, handler.credentials); err == nil { - if err = testConnection(redisConn); err == nil { - handler.secured = true - break - } - } - - redisConn = nil - lc.Debug("Authenticated connected failed.") - - lc.Warn(fmt.Sprintf("Could not create database client (startup timer has not expired): %s", err.Error())) - startupTimer.SleepForInterval() - } - - if redisConn == nil { - lc.Error("Failed to create database client before startup timer expired") - return false - } - - lc.Info("Connected to database.") - handler.redisConn = redisConn - - return true -} - -func (handler *Handler) maybeSetCredentials(ctx context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { - lc := bootstrapContainer.LoggingClientFrom(dic.Get) - - if !handler.secured { - _, err := handler.redisConn.Do("CONFIG", "SET", "REQUIREPASS", handler.credentials.Password) - if err != nil { - lc.Error(fmt.Sprintf("Could not set Redis password: %s", err.Error())) - return false - } - - handler.secured = true - lc.Info("Database credentials have been set.") - } - - if err := testConnection(handler.redisConn); err != nil { - lc.Error(fmt.Sprintf("Connection verification failed: %s", err.Error())) - return false - } - - lc.Info("Connection verified.") - return true -} - -func testConnection(redisConn redigo.Conn) error { - _, err := redisConn.Do("INFO", "SERVER") - return err -} - -func authenticate(redisConn redigo.Conn, credentials bootstrapConfig.Credentials) error { - _, err := redisConn.Do("AUTH", credentials.Password) - return err -} diff --git a/snap/hooks/install b/snap/hooks/install index 2351eaf122..78a50b6b74 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -10,10 +10,18 @@ SNAP_CURRENT=${SNAP/%$SNAP_REVISION/current} # into $SNAP_DATA/config # note that app-service-configurable is handled separately mkdir -p "$SNAP_DATA/config" -for service in security-file-token-provider security-proxy-setup security-secretstore-setup core-command core-data core-metadata support-notifications support-scheduler sys-mgmt-agent device-virtual security-bootstrap-redis; do +for service in security-file-token-provider security-proxy-setup security-secretstore-setup core-command core-data core-metadata support-notifications support-scheduler sys-mgmt-agent device-virtual security-bootstrapper; do if [ ! -f "$SNAP_DATA/config/$service/res/configuration.toml" ]; then mkdir -p "$SNAP_DATA/config/$service/res" - cp "$SNAP/config/$service/res/configuration.toml" "$SNAP_DATA/config/$service/res/configuration.toml" + + # for security-bootstrapper, we only need the configureRedis subcommand portion and associated + # configuration.toml file + if [ "$service" == "security-bootstrapper" ]; then + cp "$SNAP/config/$service/res-bootstrap-redis/configuration.toml" \ + "$SNAP_DATA/config/$service/res/configuration.toml" + else + cp "$SNAP/config/$service/res/configuration.toml" "$SNAP_DATA/config/$service/res/configuration.toml" + fi # replace $SNAP, $SNAP_DATA, $SNAP_COMMON env vars for file-token-provider, # as it doesn't support env var overrides @@ -75,6 +83,8 @@ done # create redis data dir mkdir -p "$SNAP_DATA/redis" +# create redis conf dir +mkdir -p "$SNAP_DATA/redis/conf" # set redis as the prevdbtype in order to support configure hook switching echo "redis" > "$SNAP_DATA/prevdbtype" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 8b11c9c712..777544de93 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -80,12 +80,14 @@ apps: plugs: [network, network-bind] redis: adapter: full - after: [security-secretstore-setup] - command: bin/redis-server $DIR_OPT $SAVE_OPT1 $SAVE_OPT2 + after: [security-bootstrap-redis] + command: bin/redis-server $CONFIG_FILE $DIR_OPT $SAVE_OPT1 $SAVE_OPT2 environment: DIR_OPT: "--dir $SNAP_DATA/redis" SAVE_OPT1: "--save 900 1" SAVE_OPT2: "--save 300 10" + CONFIG_FILE: "$SNAP_DATA/redis/conf/redis.conf" + DONE_FILE: "$SNAP_DATA/redis/conf/.done" daemon: simple plugs: [network, network-bind] postgres: @@ -178,19 +180,22 @@ apps: daemon: oneshot start-timeout: 15m plugs: [network] - # This is a simple service which calls into vault to retrieve the Redis password and then makes a - # Redis call to set the password. As such, Redis is now started after security-secretstore-setup - # has run, following by this service. Once the password has been set, this service exits. In the - # Docker version, the entrypoint.sh waits until it is interrupted before it exits. + # This is a simple service which calls into vault to retrieve the Redis password and then + # generate Redis config file for Redis to start up with credentials and ACL rules. + # Redis should be start once the doneFile is created. Once the config file has been generated and + # verified authenticated connection, this service exits. In the Docker version, + # the customized redis' entrypoint.sh performs the similar actions as described above. security-bootstrap-redis: adapter: none after: - - redis - command: bin/security-bootstrap-redis -confdir $SNAP_DATA/config/security-bootstrap-redis/res + - security-secretstore-setup + command: bin/security-bootstrapper -confdir $SNAP_DATA/config/security-bootstrapper/res configureRedis environment: # TODO: determine the correct cmd-line args & env var overrides... SECRETSTORE_SERVERNAME: localhost SECRETSTORE_TOKENFILE: $SNAP_DATA/secrets/edgex-security-bootstrap-redis/secrets-token.json + DATABASECONFIG_PATH: $SNAP_DATA/redis/conf + DATABASECONFIG_NAME: redis.conf daemon: oneshot plugs: [network] core-data: @@ -592,7 +597,7 @@ parts: # copy service binaries, configuration, and license files into the snap install for service in core-command core-data core-metadata support-notifications support-scheduler sys-mgmt-agent \ security-proxy-setup security-secretstore-setup security-file-token-provider \ - security-bootstrap-redis secrets-config; do + security-bootstrapper secrets-config; do install -DT "./cmd/$service/$service" "$SNAPCRAFT_PART_INSTALL/bin/$service" @@ -601,6 +606,13 @@ parts: install -DT "./cmd/security-secretstore-setup/res-file-token-provider/configuration.toml" \ "$SNAPCRAFT_PART_INSTALL/config/security-file-token-provider/res/configuration.toml" ;; + # For security bootstrapping Redis, we only need the configuration file used for "configureRedis" + # as part of the whole "security-bootstrapper". The other parts of security-bootstrapper is only + # for Docker version running in docker-compose file cases. + "security-bootstrapper") + install -DT "./cmd/security-bootstrapper/res-bootstrap-redis/configuration.toml" \ + "$SNAPCRAFT_PART_INSTALL/config/security-bootstrapper/res-bootstrap-redis/configuration.toml" + ;; # The security-secrets-config doesn't have a default configuration.toml, but since it shares # the same config as proxy-setup, just use that one. "secrets-config")