Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(security): Implementation to set up Consul ACL #3215

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/security-bootstrapper/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ COPY --from=builder /edgex-go/cmd/security-bootstrapper/res/configuration.toml .
# needed for bootstrapping Redis db
COPY --from=builder /edgex-go/cmd/security-bootstrapper/res-bootstrap-redis/configuration.toml ${BOOTSTRAP_REDIS_DIR}/res/

# copy Consul ACL related configs
COPY --from=builder /edgex-go/cmd/security-bootstrapper/consul-acl/ ${SECURITY_INIT_DIR}/consul-bootstrapper/

# Expose the file directory as a volume since there's long-running state
VOLUME ${SECURITY_INIT_DIR}

Expand Down
7 changes: 7 additions & 0 deletions cmd/security-bootstrapper/consul-acl/config_consul_acl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"acl": {
"enabled": true,
"default_policy": "allow",
"enable_token_persistence": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ done
DEFAULT_CONSUL_LOCAL_CONFIG='
{
"enable_local_script_checks": true,
"disable_update_check": true
"disable_update_check": true,
"ports": {
"dns": -1
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved
}
}
'

Expand All @@ -63,15 +66,49 @@ export CONSUL_LOCAL_CONFIG

echo "$(date) CONSUL_LOCAL_CONFIG: ${CONSUL_LOCAL_CONFIG}"

echo "$(date) Starting edgex-consul..."
exec docker-entrypoint.sh agent -ui -bootstrap -server -client 0.0.0.0 &
echo "$(date) ENABLE_REGISTRY_ACL = ${ENABLE_REGISTRY_ACL}"

# wait for the consul port
echo "$(date) Executing waitFor on Consul with waiting on its own port \
tcp://${STAGEGATE_REGISTRY_HOST}:${STAGEGATE_REGISTRY_PORT}"
/edgex-init/security-bootstrapper --confdir=/edgex-init/res waitFor \
-uri tcp://"${STAGEGATE_REGISTRY_HOST}":"${STAGEGATE_REGISTRY_PORT}" \
-timeout "${STAGEGATE_WAITFOR_TIMEOUT}"
if [ "${ENABLE_REGISTRY_ACL}" == "true" ]; then
echo "$(date) Starting edgex-core-consul with ACL enabled ..."
docker-entrypoint.sh agent \
-ui \
-bootstrap \
-server \
-config-file=/edgex-init/consul-bootstrapper/config_consul_acl.json \
-client 0.0.0.0 &
# wait for the secretstore tokens ready as we need the token for bootstrapping
echo "$(date) Executing waitFor on Consul with waiting on TokensReadyPort \
tcp://${STAGEGATE_SECRETSTORESETUP_HOST}:${STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT}"
/edgex-init/security-bootstrapper --confdir=/edgex-init/res waitFor \
-uri tcp://"${STAGEGATE_SECRETSTORESETUP_HOST}":"${STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT}" \
-timeout "${STAGEGATE_WAITFOR_TIMEOUT}"

# we don't want to exit out the whole Consul process when ACL bootstrapping failed, just that
# Consul won't have ACL to be used
set +e
# call setupRegistryACL bootstrapping command, containing both ACL bootstrapping and re-configure consul access steps
/edgex-init/security-bootstrapper --confdir=/edgex-init/res setupRegistryACL
setupACL_code=$?
if [ "${setupACL_code}" -ne 0 ]; then
echo "$(date) failed to set up Consul ACL"
fi
set -e
# no need to wait for Consul's port since it is in ready state after all ACL stuff
else
echo "$(date) Starting edgex-core-consul with ACL disabled ..."
docker-entrypoint.sh agent \
-ui \
-bootstrap \
-server \
-client 0.0.0.0 &
# wait for the consul port
# this waitFor is not necessary in the other ACL case, as it is already in the ready state
echo "$(date) Executing waitFor on Consul with waiting on its own port \
tcp://${STAGEGATE_REGISTRY_HOST}:${STAGEGATE_REGISTRY_PORT}"
/edgex-init/security-bootstrapper --confdir=/edgex-init/res waitFor \
-uri tcp://"${STAGEGATE_REGISTRY_HOST}":"${STAGEGATE_REGISTRY_PORT}" \
-timeout "${STAGEGATE_WAITFOR_TIMEOUT}"
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved
fi

# Signal that Consul is ready for services blocked waiting on Consul
/edgex-init/security-bootstrapper --confdir=/edgex-init/res listenTcp \
Expand Down
18 changes: 18 additions & 0 deletions cmd/security-bootstrapper/res/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,28 @@ LogLevel = 'INFO'
Host = "edgex-core-consul"
Port = 8500
ReadyPort = 54324
[StageGate.Registry.ACL]
Protocol = "http"
# this is the filepath for the generated Consul management token from ACL bootstrap
BootstrapTokenPath = "/tmp/edgex/secrets/edgex-consul/admin/bootstrap_token.json"
# this is the filepath for the Vault token created from secretstore-setup
SecretsAdminTokenPath = "/tmp/edgex/secrets/edgex-consul/admin/token.json"
# this is the filepath for the sentinel file to indicate the registry ACL is set up successfully
SentinelFilePath = "/edgex-init/consul-bootstrapper/consul_acl_done"
[StageGate.KongDb]
Host = "kong-db"
Port = 5432
ReadyPort = 54325
[StageGate.WaitFor]
Timeout = "10s"
RetryInterval = "1s"

# this configuration is just part of the whole go-mod-bootstrap's secret store to have
# protocol, host, and port of secretstore using in the security-bootstrapper
# we are not really using the secret store provider from go-mod-bootstrap in the code
# also this is needed as snap does not have those environments from env-files
[SecretStore]
Type = 'vault'
Protocol = 'http'
Host = 'localhost'
Port = 8200
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ LogLevel = 'DEBUG'
[SecretStore]
Type = "vault"
Protocol = "http"
Host = "edgex-vault"
Host = "localhost"
Port = 8200
ServerName = ""
CaFilePath = ""
Expand Down
2 changes: 1 addition & 1 deletion cmd/security-secretstore-setup/res/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ LogLevel = 'DEBUG'
[SecretStore]
Type = "vault"
Protocol = "http"
Host = "edgex-vault"
Host = "localhost"
Port = 8200
CertPath = ""
CaFilePath = ""
Expand Down
7 changes: 5 additions & 2 deletions internal/security/bootstrapper/command/cmd_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/gethttpstatus"
"github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/listen"
"github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/ping"
"github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/setupacl"
"github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/waitfor"
"github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config"
"github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces"
Expand All @@ -44,8 +45,8 @@ func NewCommand(
var err error

if len(args) < 1 {
return nil, fmt.Errorf("subcommand required (%s, %s, %s, %s, %s, %s)", gate.CommandName, listen.CommandName,
ping.CommandName, gethttpstatus.CommandName, genpassword.CommandName, waitfor.CommandName)
return nil, fmt.Errorf("subcommand required (%s, %s, %s, %s, %s, %s, %s)", gate.CommandName, listen.CommandName,
ping.CommandName, gethttpstatus.CommandName, genpassword.CommandName, waitfor.CommandName, setupacl.CommandName)
}

commandName := args[0]
Expand All @@ -63,6 +64,8 @@ func NewCommand(
command, err = genpassword.NewCommand(ctx, wg, lc, configuration, args[1:])
case waitfor.CommandName:
command, err = waitfor.NewCommand(ctx, wg, lc, configuration, args[1:])
case setupacl.CommandName:
command, err = setupacl.NewCommand(ctx, wg, lc, configuration, args[1:])
default:
command = nil
err = fmt.Errorf("unsupported command %s", commandName)
Expand Down
11 changes: 7 additions & 4 deletions internal/security/bootstrapper/command/cmd_dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func TestNewCommand(t *testing.T) {
{"Good: genPassword command", []string{"genPassword"}, "genPassword", false},
{"Good: getHttpStatus command", []string{"getHttpStatus", "--url=http://localhost:55555"}, "getHttpStatus", false},
{"Good: waitFor command", []string{"waitFor", "--uri=http://localhost:55555"}, "waitFor", false},
{"Good: setupRegistryACL command", []string{"setupRegistryACL"}, "setupRegistryACL", false},
{"Bad: unknown command", []string{"unknown"}, "", true},
{"Bad: empty command", []string{}, "", true},
{"Bad: listenTcp command missing required --port", []string{"listenTcp"}, "", true},
Expand All @@ -62,18 +63,20 @@ func TestNewCommand(t *testing.T) {
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
test := tt //capture as local copy
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// Act
command, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs)
command, err := NewCommand(ctx, wg, lc, config, test.cmdArgs)

// Assert
if tt.expectedErr {
if test.expectedErr {
require.Error(t, err)
require.Nil(t, command)
} else {
require.NoError(t, err)
require.NotNil(t, command)
require.Equal(t, tt.expectedCmdName, command.GetCommandName())
require.Equal(t, test.expectedCmdName, command.GetCommandName())
}
})
}
Expand Down
17 changes: 9 additions & 8 deletions internal/security/bootstrapper/command/flags_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,14 @@ func HelpCallback() {
" --confdir Specify local configuration directory\n"+
"\n"+
"Commands:\n"+
" help Show available commands (this text)\n"+
" gate Do security bootstrapper gating on stages while starting services\n"+
" listenTcp Start up a TCP listener\n"+
" pingPgDb Test Postgres database readiness\n"+
" getHttpStatus Do an HTTP GET call to get the status code\n"+
" genPassword Generate a random password\n"+
" waitfor Wait for the other services with specified URI(s) to connect:\n"+
" the URI(s) can be communication protocols like tcp/tcp4/tcp6/http/https or files\n",
" gate Do security bootstrapper gating on stages while starting services\n"+
" genPassword Generate a random password\n"+
" getHttpStatus Do an HTTP GET call to get the status code\n"+
" help Show available commands (this text)\n"+
" listenTcp Start up a TCP listener\n"+
" pingPgDb Test Postgres database readiness\n"+
" setupRegistryACL Set up registry's ACL and configure the access\n"+
" waitFor Wait for the other services with specified URI(s) to connect:\n"+
" the URI(s) can be communication protocols like tcp/tcp4/tcp6/http/https or files\n",
os.Args[0])
}
112 changes: 112 additions & 0 deletions internal/security/bootstrapper/command/setupacl/aclbootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*******************************************************************************
* 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 setupacl

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"

"github.com/edgexfoundry/go-mod-secrets/v2/pkg/token/fileioperformer"
)

// BootStrapACLTokenInfo is the key portion of the response metadata from consulACLBootstrapAPI
type BootStrapACLTokenInfo struct {
SecretID string `json:"SecretID"`
Policies []Policy `json:"Policies"`
}

// Policy is the metadata for ACL policy
type Policy struct {
ID string `json:"ID"`
Name string `json:"Name"`
}

// generateBootStrapACLToken should only be called once per Consul agent
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved
func (c *cmd) generateBootStrapACLToken() (*BootStrapACLTokenInfo, error) {
aclBootstrapURL, err := c.getRegistryApiUrl(consulACLBootstrapAPI)
if err != nil {
return nil, err
}

req, err := http.NewRequest(http.MethodPut, aclBootstrapURL, http.NoBody)
if err != nil {
return nil, fmt.Errorf("Failed to prepare request for http URL: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("Failed to send request for http URL: %w", err)
}

defer func() {
_ = resp.Body.Close()
}()

responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Failed to read response body of bootstrap ACL: %w", err)
}

var bootstrapACLToken BootStrapACLTokenInfo
switch resp.StatusCode {
case http.StatusOK:
if err := json.NewDecoder(bytes.NewReader(responseBody)).Decode(&bootstrapACLToken); err != nil {
return nil, fmt.Errorf("failed to decode bootstrapACLToken json data: %v", err)
}
return &bootstrapACLToken, nil
default:
return nil, fmt.Errorf("failed to bootstrap Consul's ACL via URL [%s] and status code= %d: %s", aclBootstrapURL,
resp.StatusCode, string(responseBody))
}
}

func (c *cmd) saveBootstrapACLToken(tokenInfoToBeSaved *BootStrapACLTokenInfo) error {
// Write the token to the specified file
tokenFileAbsPath, err := filepath.Abs(c.configuration.StageGate.Registry.ACL.BootstrapTokenPath)
if err != nil {
return fmt.Errorf("failed to convert tokenFile to absolute path %s: %s",
c.configuration.StageGate.Registry.ACL.BootstrapTokenPath, err.Error())
}

// create the directory of tokenfile if not exists yet
dirOfToken := filepath.Dir(tokenFileAbsPath)
fileIoPerformer := fileioperformer.NewDefaultFileIoPerformer()
if err := fileIoPerformer.MkdirAll(dirOfToken, 0700); err != nil {
return fmt.Errorf("failed to create tokenpath base dir: %s", err.Error())
}

fileWriter, err := fileIoPerformer.OpenFileWriter(tokenFileAbsPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("failed to open file writer %s: %s", tokenFileAbsPath, err.Error())
}

if err := json.NewEncoder(fileWriter).Encode(tokenInfoToBeSaved); err != nil {
_ = fileWriter.Close()
return fmt.Errorf("failed to write bootstrap token: %s", err.Error())
}

if err := fileWriter.Close(); err != nil {
return fmt.Errorf("failed to close token file: %s", err.Error())
}

c.loggingClient.Infof("bootstrap token is written to %s", tokenFileAbsPath)

return nil
}
Loading