From fd143bca65ac0d1bc15f90e7e7129bfbe164c660 Mon Sep 17 00:00:00 2001 From: Bryon Nevis Date: Wed, 8 Feb 2023 08:55:58 -0800 Subject: [PATCH] feat: Vault tokens are now identity-based instead of anonymous (#4327) This is an internal refactoring of security-secretstore-setup, security-file-token-provider, and security-spiffe-token-provider to generate Vault tokens that are based on a Vault identity instead of being anonymous tokens with an attached ACL. The consequence of this refactoring is that the token issuing components of EdgeX run with fewer required privileges than before, and it is possible to ask Vault for a verifiable JWT-based identity assertion of an EdgeX service. Signed-off-by: Bryon Nevis --- .../res/token-config.json | 96 ++++++++--- .../configuration.toml | 2 + .../res/configuration.toml | 11 ++ .../defaults.go => common/tokenpolicy.go} | 43 ++--- .../tokenpolicy_test.go} | 27 ++-- internal/security/common/usermanager.go | 137 ++++++++++++++++ internal/security/common/usermanager_test.go | 7 + .../security/fileprovider/config/config.go | 6 +- internal/security/fileprovider/provider.go | 45 +++--- .../security/fileprovider/provider_test.go | 153 +++++++----------- internal/security/fileprovider/tokenconfig.go | 11 +- .../security/fileprovider/tokenconfig_test.go | 1 - internal/security/secretstore/constants.go | 41 +++-- internal/security/secretstore/init.go | 39 ++++- internal/security/secretstore/init_test.go | 4 +- .../spiffetokenprovider/config/config.go | 18 ++- internal/security/spiffetokenprovider/init.go | 4 +- .../security/spiffetokenprovider/maketoken.go | 83 +++------- 18 files changed, 458 insertions(+), 270 deletions(-) rename internal/security/{fileprovider/defaults.go => common/tokenpolicy.go} (50%) rename internal/security/{fileprovider/defaults_test.go => common/tokenpolicy_test.go} (67%) create mode 100644 internal/security/common/usermanager.go create mode 100644 internal/security/common/usermanager_test.go diff --git a/cmd/security-file-token-provider/res/token-config.json b/cmd/security-file-token-provider/res/token-config.json index 3dd311cb78..731d640ca2 100644 --- a/cmd/security-file-token-provider/res/token-config.json +++ b/cmd/security-file-token-provider/res/token-config.json @@ -32,22 +32,61 @@ "support-scheduler": { "edgex_use_defaults": true }, - "security-secretstore-setup": { + "security-proxy-auth": { + "edgex_use_defaults": true + }, + "security-proxy-setup": { "edgex_use_defaults": true, "custom_policy": { "path": { - "secret/edgex/security-proxy-setup/kong-tls": { + "identity/entity/name": { + "capabilities": [ + "list" + ] + }, + "identity/entity/name/*": { "capabilities": [ - "list", "read", "create", "update", "delete" ] }, - "secret/edgex/redis/*": { + "identity/entity-alias": { + "capabilities": [ + "create", + "update" + ] + }, + "identity/oidc/role": { + "capabilities": [ + "list" + ] + }, + "identity/oidc/role/*": { + "capabilities": [ + "create", + "update" + ] + }, + "auth/userpass/users/*": { + "capabilities": [ + "create", + "update" + ] + }, + "sys/auth": { + "capabilities": [ + "read" + ] + }, + "sys/policies/acl": { + "capabilities": [ + "list" + ] + }, + "sys/policies/acl/edgex-user-*": { "capabilities": [ - "list", "read", "create", "update", @@ -57,35 +96,54 @@ } } }, - "security-proxy-setup": { - "edgex_use_defaults": true - }, - "security-file-token-provider": { - "edgex_use_defaults": true - }, "security-spiffe-token-provider": { "edgex_use_defaults": true, "custom_policy": { "path": { - "auth/token/create": { + "identity/entity/name": { + "capabilities": [ + "list" + ] + }, + "identity/entity/name/*": { "capabilities": [ + "read", "create", "update", - "sudo" + "delete" ] }, - "auth/token/create-orphan": { + "identity/entity-alias": { "capabilities": [ "create", - "update", - "sudo" + "update" + ] + }, + "identity/oidc/role": { + "capabilities": [ + "list" ] }, - "auth/token/create/*": { + "identity/oidc/role/*": { "capabilities": [ "create", - "update", - "sudo" + "update" + ] + }, + "auth/userpass/users/*": { + "capabilities": [ + "create", + "update" + ] + }, + "sys/auth": { + "capabilities": [ + "read" + ] + }, + "sys/policies/acl": { + "capabilities": [ + "list" ] }, "sys/policies/acl/edgex-service-*": { diff --git a/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml b/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml index 0882321f0d..ef3a83cb59 100644 --- a/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml +++ b/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml @@ -14,3 +14,5 @@ ConfigFile = "res-file-token-provider/token-config.json" OutputDir = "/tmp/edgex/secrets" OutputFilename = "secrets-token.json" DefaultTokenTTL = "1h" +DefaultJWTTTL = "15m" +UserPassMountPoint = "userpass" diff --git a/cmd/security-spiffe-token-provider/res/configuration.toml b/cmd/security-spiffe-token-provider/res/configuration.toml index 52a0dd5138..ba29e40399 100644 --- a/cmd/security-spiffe-token-provider/res/configuration.toml +++ b/cmd/security-spiffe-token-provider/res/configuration.toml @@ -38,6 +38,17 @@ Port = 6379 Timeout = 5000 Type = "redisdb" + +[TokenConfig] +#PrivilegedTokenPath = "UNUSED" +#ConfigFile = "UNUSED" +#OutputDir = "UNUSED" +#OutputFilename = "UNUSED" +DefaultTokenTTL = "1h" +DefaultJWTTTL = "15m" +UserPassMountPoint = "userpass" + + [SPIFFE] EndpointSocket = "/tmp/edgex/secrets/spiffe/public/api.sock" TrustDomain = "edgexfoundry.org" diff --git a/internal/security/fileprovider/defaults.go b/internal/security/common/tokenpolicy.go similarity index 50% rename from internal/security/fileprovider/defaults.go rename to internal/security/common/tokenpolicy.go index 0f278cf1d5..ec03c26acd 100644 --- a/internal/security/fileprovider/defaults.go +++ b/internal/security/common/tokenpolicy.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 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 @@ -11,23 +11,30 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // -package fileprovider +package common -func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { +func MakeDefaultTokenPolicy(serviceName string) map[string]interface{} { // protected path for secret/ - protectedPath := "secret/edgex/" + serviceName + "/*" - capabilities := []string{"create", "update", "delete", "list", "read"} - acl := map[string]interface{}{"capabilities": capabilities} + secretsPath := "secret/edgex/" + serviceName + "/*" + secretsAcl := map[string]interface{}{"capabilities": []string{"create", "update", "delete", "list", "read"}} // path for consul tokens registryCredsPath := "consul/creds/" + serviceName - registryCredsCapabilities := []string{"read"} - registryCredsACL := map[string]interface{}{"capabilities": registryCredsCapabilities} + registryCredsACL := map[string]interface{}{"capabilities": []string{"read"}} + // allow request identity JWT + jwtRequestPath := "identity/oidc/token/" + serviceName + jwtRequestACL := map[string]interface{}{"capabilities": []string{"read"}} + // allow introspect JWT + jwtIntrospectPath := "identity/oidc/introspect" + jwtIntrospectACL := map[string]interface{}{"capabilities": []string{"create", "update"}} + // access spec pathObject := map[string]interface{}{ - protectedPath: acl, + secretsPath: secretsAcl, registryCredsPath: registryCredsACL, + jwtRequestPath: jwtRequestACL, + jwtIntrospectPath: jwtIntrospectACL, } retval := map[string]interface{}{"path": pathObject} return retval @@ -40,18 +47,14 @@ func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { }, "consul/creds/service-name": { "capabilities": [ "read" ] + }, + "identity/oidc/token/service-name": { + "capabilities": [ "read" ] + }, + "identity/oidc/introspect": { + "capabilities": [ "create", "update" ] } } } */ } - -func makeDefaultTokenParameters(serviceName string, defaultTTL string, defaultPeriod string) map[string]interface{} { - return map[string]interface{}{ - "display_name": serviceName, - "no_parent": true, - "ttl": defaultTTL, - "period": defaultPeriod, - "policies": []string{"edgex-service-" + serviceName}, - } -} diff --git a/internal/security/fileprovider/defaults_test.go b/internal/security/common/tokenpolicy_test.go similarity index 67% rename from internal/security/fileprovider/defaults_test.go rename to internal/security/common/tokenpolicy_test.go index baee6d041e..e26f70db96 100644 --- a/internal/security/fileprovider/defaults_test.go +++ b/internal/security/common/tokenpolicy_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 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 @@ -11,9 +11,9 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // -package fileprovider +package common import ( "encoding/json" @@ -24,7 +24,7 @@ import ( func TestDefaultTokenPolicy(t *testing.T) { // Act - policies := makeDefaultTokenPolicy("service-name") + policies := MakeDefaultTokenPolicy("service-name") // Assert bytes, err := json.Marshal(policies) @@ -39,21 +39,14 @@ func TestDefaultTokenPolicy(t *testing.T) { "consul/creds/service-name": map[string]interface{}{ "capabilities": []string{"read"}, }, + "identity/oidc/token/service-name": map[string]interface{}{ + "capabilities": []string{"read"}, + }, + "identity/oidc/introspect": map[string]interface{}{ + "capabilities": []string{"create", "update"}, + }, }, } require.Equal(t, expected, policies) } - -func TestDefaultTokenParameters(t *testing.T) { - // Act - parameters := makeDefaultTokenParameters("service-name", "1h", "1h") - - // Assert - bytes, err := json.Marshal(parameters) - require.NoError(t, err) - - expected := `{"display_name":"service-name","no_parent":true,"period":"1h","policies":["edgex-service-service-name"],"ttl":"1h"}` - actual := string(bytes) - require.Equal(t, expected, actual) -} diff --git a/internal/security/common/usermanager.go b/internal/security/common/usermanager.go new file mode 100644 index 0000000000..47f44c1195 --- /dev/null +++ b/internal/security/common/usermanager.go @@ -0,0 +1,137 @@ +// +// Copyright (c) 2023 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package common + +import ( + "encoding/json" + "fmt" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger" + "github.com/edgexfoundry/go-mod-secrets/v3/secrets" +) + +type UserManager struct { + logger logger.LoggingClient + secretStoreClient secrets.SecretStoreClient + userPassMountPoint string // userPassMountPoint is the name of the userpass mount point, almost always "userpass" + jwtKeyName string // jwtKeyName is the key identifier of the JWT signing key (e.g. edgex-identity) + privilegedToken string // privilegedToken is a Vault token that has permissions to do a lot of stuff like below + tokenTTL string // This is the TTL of the Vault token (which is renewable) + jwtTTL string // JWT's created using the Vault token have an independent validity period +} + +func NewUserManager( + logger logger.LoggingClient, + secretStoreClient secrets.SecretStoreClient, + userPassMountPoint string, + jwtKeyName string, + privilegedToken string, + tokenTTL string, + jwtTTL string, +) *UserManager { + return &UserManager{ + logger, + secretStoreClient, + userPassMountPoint, + jwtKeyName, + privilegedToken, + tokenTTL, + jwtTTL, + } +} + +// CreatePasswordUserWithPolicy creates a vault identity with an attached policy +// using userpass authentication engine. +// username should be the name of the user or service to be created +// password should be a random password to be assigned +// policyPrefix is prefixed to username and should be "edgex-user-" or policyPrefix +// policy is a map that will be serialized as a policy attached to the identity +func (m *UserManager) CreatePasswordUserWithPolicy(username string, password string, policyPrefix string, policy map[string]interface{}) error { + + m.logger.Infof("creating policy, identity, and userpass binding for %s", username) + + // Derive the name of the attached policy and create it + + policyName := policyPrefix + username + + policyBytes, err := json.Marshal(policy) + if err != nil { + m.logger.Errorf("failed encode policy for %s: %s", username, err.Error()) + return err + } + + if err := m.secretStoreClient.InstallPolicy(m.privilegedToken, policyName, string(policyBytes)); err != nil { + m.logger.Errorf("failed to install policy %s: %s", policyName, err.Error()) + return err + } + + // Create or update underlying vault identity + identityMetadata := map[string]string{ + // we will also put a name claim in any generated JWT's + "name": username, + } + identityPolicies := []string{policyName} + identityId, err := m.secretStoreClient.CreateOrUpdateIdentity(m.privilegedToken, username, identityMetadata, identityPolicies) + if err != nil { + return err + } + if identityId == "" { + // Updating an entity doesn't return its ID (grr!), in that case, need to look it up + identityId, err = m.secretStoreClient.LookupIdentity(m.privilegedToken, username) + if err != nil { + return err + } + } + + // When logging in "default" policy is added automatically to the token and identity_policy is inherited. + err = m.secretStoreClient.CreateOrUpdateUser(m.privilegedToken, m.userPassMountPoint, username, password, m.tokenTTL, []string{}) + if err != nil { + return err + } + + authHandle, err := m.secretStoreClient.LookupAuthHandle(m.privilegedToken, m.userPassMountPoint) + if err != nil { + return err + } + + err = m.secretStoreClient.BindUserToIdentity(m.privilegedToken, identityId, authHandle, username) + if err != nil { + return err + } + + // See https://developer.hashicorm.com/vault/docs/secrets/identity/identity-token#token-contents-and-templates + // for including custom claims. We will include a claim identifying the calling EdgeX service + // We will use the OIDC standard "name" claim. + customClaims := fmt.Sprintf(`{"name": "%s"}`, username) + err = m.secretStoreClient.CreateOrUpdateIdentityRole(m.privilegedToken, username, m.jwtKeyName, customClaims, m.jwtTTL) + if err != nil { + return err + } + + return nil +} + +func (m *UserManager) DeletePasswordUser(username string) error { + + identityId, err := m.secretStoreClient.LookupIdentity(m.privilegedToken, username) + if err != nil { + return err + } + if identityId != "" { + err = m.secretStoreClient.DeleteIdentity(m.privilegedToken, username) + if err != nil { + return err + } + } + + err = m.secretStoreClient.DeleteUser(m.privilegedToken, m.userPassMountPoint, username) + if err != nil { + return err + } + + return nil +} diff --git a/internal/security/common/usermanager_test.go b/internal/security/common/usermanager_test.go new file mode 100644 index 0000000000..2dd4861909 --- /dev/null +++ b/internal/security/common/usermanager_test.go @@ -0,0 +1,7 @@ +// +// Copyright (c) 2023 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package common diff --git a/internal/security/fileprovider/config/config.go b/internal/security/fileprovider/config/config.go index 6e4728ef91..18126d824d 100644 --- a/internal/security/fileprovider/config/config.go +++ b/internal/security/fileprovider/config/config.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. - * Copyright 2019 Intel Corporation. + * Copyright 2019-2023 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 @@ -38,6 +38,10 @@ type TokenFileProviderInfo struct { OutputFilename string // Default duration of issued tokens DefaultTokenTTL string + // Default duration of issued OIDC JWT tokens + DefaultJWTTTL string + // UserPassMountPoint is where the userpass auth engine is mounted (usually "userauth") + UserPassMountPoint string } // UpdateFromRaw converts configuration received from the registry to a service-specific configuration struct diff --git a/internal/security/fileprovider/provider.go b/internal/security/fileprovider/provider.go index 8f382e9600..ddcffbcc91 100644 --- a/internal/security/fileprovider/provider.go +++ b/internal/security/fileprovider/provider.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 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 @@ -11,18 +11,22 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // package fileprovider import ( + "context" "encoding/json" "os" "path/filepath" "strconv" + "github.com/edgexfoundry/edgex-go/internal/security/common" + securityCommon "github.com/edgexfoundry/edgex-go/internal/security/common" "github.com/edgexfoundry/edgex-go/internal/security/fileprovider/config" + "github.com/edgexfoundry/edgex-go/internal/security/secretstore" secretstoreConfig "github.com/edgexfoundry/edgex-go/internal/security/secretstore/config" "github.com/edgexfoundry/go-mod-secrets/v3/secrets" @@ -95,20 +99,23 @@ func (p *fileTokenProvider) Run() error { // The tokenConfEnv only uses default settings. tokenConf = tokenConfEnv.mergeWith(tokenConf) + credentialGenerator := secretstore.NewDefaultCredentialGenerator() + + userManager := common.NewUserManager(p.logger, p.secretStoreClient, p.tokenConfig.UserPassMountPoint, "edgex-identity", + privilegedToken, p.tokenConfig.DefaultTokenTTL, p.tokenConfig.DefaultJWTTTL) + for serviceName, serviceConfig := range tokenConf { p.logger.Infof("generating policy/token defaults for service %s", serviceName) servicePolicy := make(map[string]interface{}) - createTokenParameters := make(map[string]interface{}) if serviceConfig.UseDefaults { p.logger.Infof("using policy/token defaults for service %s", serviceName) - servicePolicy = makeDefaultTokenPolicy(serviceName) + servicePolicy = securityCommon.MakeDefaultTokenPolicy(serviceName) defaultPolicyPaths := servicePolicy["path"].(map[string]interface{}) for pathKey, policy := range defaultPolicyPaths { servicePolicy["path"].(map[string]interface{})[pathKey] = policy } - createTokenParameters = makeDefaultTokenParameters(serviceName, p.tokenConfig.DefaultTokenTTL, p.tokenConfig.DefaultTokenTTL) } if serviceConfig.CustomPolicy != nil { @@ -124,37 +131,29 @@ func (p *fileTokenProvider) Run() error { } } - if serviceConfig.CustomTokenParameters != nil { - // Custom token parameters override the defaults - createTokenParameters = mergeMaps(createTokenParameters, serviceConfig.CustomTokenParameters) - } + // Generate a random password - // Set a meta property that consuming serices can use to automatically scope secret queries - createTokenParameters["meta"] = map[string]interface{}{ - "edgex-service-name": serviceName, + randomPassword, err := credentialGenerator.Generate(context.TODO()) + if err != nil { + return err } - // Always create a policy with this name - policyName := "edgex-service-" + serviceName + // Create a user with the random password - policyBytes, err := json.Marshal(servicePolicy) + err = userManager.CreatePasswordUserWithPolicy(serviceName, randomPassword, "edgex-service-", servicePolicy) if err != nil { - p.logger.Errorf("failed encode service policy for %s: %s", serviceName, err.Error()) return err } - if err := p.secretStoreClient.InstallPolicy(privilegedToken, policyName, string(policyBytes)); err != nil { - p.logger.Errorf("failed to install policy %s: %s", policyName, err.Error()) - return err - } + // Immediately log in the user to get a vault token var createTokenResponse interface{} - - if createTokenResponse, err = p.secretStoreClient.CreateToken(privilegedToken, createTokenParameters); err != nil { - p.logger.Errorf("failed to create vault token for service %s: %s", serviceName, err.Error()) + if createTokenResponse, err = p.secretStoreClient.InternalServiceLogin(privilegedToken, p.tokenConfig.UserPassMountPoint, serviceName, randomPassword); err != nil { return err } + // Serialize the vault token to disk + outputTokenDir := filepath.Join(p.tokenConfig.OutputDir, serviceName) outputTokenFilename := filepath.Join(outputTokenDir, p.tokenConfig.OutputFilename) if err := p.fileOpener.MkdirAll(outputTokenDir, os.FileMode(0700)); err != nil { diff --git a/internal/security/fileprovider/provider_test.go b/internal/security/fileprovider/provider_test.go index 7293e994eb..209883b8f5 100644 --- a/internal/security/fileprovider/provider_test.go +++ b/internal/security/fileprovider/provider_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 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 @@ -10,7 +10,8 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 + package fileprovider import ( @@ -75,17 +76,24 @@ func TestMultipleTokensWithNoDefaults(t *testing.T) { expectedService1Policy := "{}" expectedService2Policy := "{}" - expectedService1Parameters := makeMetaServiceName("service1") - expectedService2Parameters := makeMetaServiceName("service2") + mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service1", expectedService1Policy). - Return(nil) - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service2", expectedService2Policy). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService2Parameters). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service1", expectedService1Policy).Return(nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service2", expectedService2Policy).Return(nil) + + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "service1", map[string]string{"name": "service1"}, []string{"edgex-service-service1"}).Return("service1id", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "service1", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "service1id", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "service1").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "service1", "edgex-identity", "{\"name\": \"service1\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "service1", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) + + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "service2", map[string]string{"name": "service2"}, []string{"edgex-service-service2"}).Return("service2id", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "service2", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "service2id", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "service2").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "service2", "edgex-identity", "{\"name\": \"service2\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "service2", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ @@ -124,14 +132,6 @@ func createTokenResponse() map[string]interface{} { return t } -func makeMetaServiceName(serviceName string) map[string]interface{} { - createTokenParameters := make(map[string]interface{}) - meta := make(map[string]interface{}) - meta["edgex-service-name"] = serviceName - createTokenParameters["meta"] = meta - return createTokenParameters -} - func expectedTokenFile() []byte { tokenResponse := createTokenResponse() b := new(bytes.Buffer) @@ -157,12 +157,15 @@ func TestNoDefaultsCustomPolicy(t *testing.T) { mockAuthTokenLoader.On("Load", privilegedTokenPath).Return("fake-priv-token", nil) expectedService1Policy := `{"path":{"secret/non/standard/location/*":{"capabilities":["list","read"]}}}` - expectedService1Parameters := makeMetaServiceName("myservice") + //expectedService1Parameters := makeMetaServiceName("myservice") mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", expectedService1Policy). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", expectedService1Policy).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "myservice", map[string]string{"name": "myservice"}, []string{"edgex-service-myservice"}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "myservice", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "myservice").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "myservice", "edgex-identity", "{\"name\": \"myservice\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "myservice", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ @@ -186,54 +189,6 @@ func TestNoDefaultsCustomPolicy(t *testing.T) { assert.Equal(t, expectedTokenFile(), service1Buffer.Bytes()) } -// TestNoDefaultsCustomTokenParameters -func TestNoDefaultsCustomTokenParameters(t *testing.T) { - // Arrange - mockLogger := logger.MockLogger{} - - mockFileIoPerformer := &fileMock.FileIoPerformer{} - expectedService1Dir := filepath.Join(outputDir, "myservice") - expectedService1File := filepath.Join(expectedService1Dir, outputFilename) - service1Buffer := new(bytes.Buffer) - mockFileIoPerformer.On("MkdirAll", expectedService1Dir, os.FileMode(0700)).Return(nil) - mockFileIoPerformer.On("OpenFileReader", configFile, os.O_RDONLY, os.FileMode(0400)).Return(strings.NewReader(`{"myservice":{"custom_token_parameters":{"key1":"value1"}}}`), nil) - mockFileIoPerformer.On("OpenFileWriter", expectedService1File, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0600)).Return(&writeCloserBuffer{service1Buffer}, nil) - - mockAuthTokenLoader := &loaderMock.AuthTokenLoader{} - mockAuthTokenLoader.On("Load", privilegedTokenPath).Return("fake-priv-token", nil) - - expectedService1Policy := "{}" - expectedService1Parameters := make(map[string]interface{}) - expectedService1Parameters["key1"] = "value1" - expectedService1Parameters["meta"] = makeMetaServiceName("myservice")["meta"] - mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", expectedService1Policy). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) - - p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) - p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ - PrivilegedTokenPath: privilegedTokenPath, - ConfigFile: configFile, - OutputDir: outputDir, - OutputFilename: outputFilename, - }) - - // Act - err := p.Run() - - // Assert - // - {OutputDir}/myservice/{OutputFilename} w/proper contents - // - Correct token parameters for myservice - // - All other expectations met - assert.NoError(t, err) - mockFileIoPerformer.AssertExpectations(t) - mockAuthTokenLoader.AssertExpectations(t) - mockSecretStoreClient.AssertExpectations(t) - assert.Equal(t, expectedTokenFile(), service1Buffer.Bytes()) -} - // TestTokenUsingDefaults func TestTokenUsingDefaults(t *testing.T) { // Good cases: @@ -284,12 +239,14 @@ func TestTokenFilePermissions(t *testing.T) { mockAuthTokenLoader := &loaderMock.AuthTokenLoader{} mockAuthTokenLoader.On("Load", privilegedTokenPath).Return("fake-priv-token", nil) - expectedService1Parameters := makeMetaServiceName("myservice") mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", "{}"). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", "{}").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "myservice", map[string]string{"name": "myservice"}, []string{"edgex-service-myservice"}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "myservice", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "myservice").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "myservice", "edgex-identity", "{\"name\": \"myservice\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "myservice", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ @@ -430,6 +387,12 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi policy := map[string]interface{}{ "path": map[string]interface{}{ + "identity/oidc/introspect": map[string]interface{}{ + "capabilities": []string{"create", "update"}, + }, + "identity/oidc/token/" + serviceName: map[string]interface{}{ + "capabilities": []string{"read"}, + }, "secret/edgex/" + serviceName + "/*": map[string]interface{}{ "capabilities": []string{"create", "update", "delete", "list", "read"}, }, @@ -440,19 +403,26 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi } expectedService1Policy, err := json.Marshal(&policy) require.NoError(t, err) - expectedService1Parameters := makeDefaultTokenParameters(serviceName, "1h", "1h") - expectedService1Parameters["meta"] = makeMetaServiceName(serviceName)["meta"] mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+serviceName, string(expectedService1Policy)). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters, mock.Anything). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+serviceName, string(expectedService1Policy)).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", serviceName, map[string]string{"name": serviceName}, []string{"edgex-service-" + serviceName}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", serviceName, mock.AnythingOfType("string"), "1h", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", serviceName).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", serviceName, "edgex-identity", "{\"name\": \""+serviceName+"\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", serviceName, mock.AnythingOfType("string")).Return(createTokenResponse(), nil) // setup expected things for additional services from env if any for service := range expectedTokenConfigs { policy := map[string]interface{}{ "path": map[string]interface{}{ + "identity/oidc/introspect": map[string]interface{}{ + "capabilities": []string{"create", "update"}, + }, + "identity/oidc/token/" + service: map[string]interface{}{ + "capabilities": []string{"read"}, + }, "secret/edgex/" + service + "/*": map[string]interface{}{ "capabilities": []string{"create", "update", "delete", "list", "read"}, }, @@ -464,14 +434,13 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi expectedServicePolicy, err := json.Marshal(&policy) require.NoError(t, err) - expectedServiceParameters := makeDefaultTokenParameters(service, "1h", "1h") - - expectedServiceParameters["meta"] = makeMetaServiceName(service)["meta"] - - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+service, string(expectedServicePolicy)). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedServiceParameters, mock.Anything). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+service, string(expectedServicePolicy)).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", service, map[string]string{"name": service}, []string{"edgex-service-" + service}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", service, mock.AnythingOfType("string"), "1h", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", service).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", service, "edgex-identity", "{\"name\": \""+service+"\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", service, mock.AnythingOfType("string")).Return(createTokenResponse(), nil) } p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) diff --git a/internal/security/fileprovider/tokenconfig.go b/internal/security/fileprovider/tokenconfig.go index 0cd21a93b7..3c65a11454 100644 --- a/internal/security/fileprovider/tokenconfig.go +++ b/internal/security/fileprovider/tokenconfig.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 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 @@ -11,7 +11,7 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // package fileprovider @@ -67,10 +67,9 @@ type FilePermissions struct { } type ServiceKey struct { - UseDefaults bool `json:"edgex_use_defaults"` - CustomPolicy map[string]interface{} `json:"custom_policy"` // JSON serialization of HCL - CustomTokenParameters map[string]interface{} `json:"custom_token_parameters"` - FilePermissions *FilePermissions `json:"file_permissions,omitempty"` + UseDefaults bool `json:"edgex_use_defaults"` + CustomPolicy map[string]interface{} `json:"custom_policy"` // JSON serialization of HCL + FilePermissions *FilePermissions `json:"file_permissions,omitempty"` } func LoadTokenConfig(fileOpener fileioperformer.FileIoPerformer, path string, tokenConf *TokenConfFile) error { diff --git a/internal/security/fileprovider/tokenconfig_test.go b/internal/security/fileprovider/tokenconfig_test.go index 0e802bfc36..d9007b1039 100644 --- a/internal/security/fileprovider/tokenconfig_test.go +++ b/internal/security/fileprovider/tokenconfig_test.go @@ -55,7 +55,6 @@ func TestLoadTokenConfig(t *testing.T) { var path = aService.CustomPolicy["path"].(map[string]interface{}) assert.Contains(t, path, "secret/non/standard/location/*") // Don't need to go further down the type assertion rabbit hole to prove that this is working - assert.Contains(t, aService.CustomTokenParameters, "custom_option") } func TestLoadTokenConfigError1(t *testing.T) { diff --git a/internal/security/secretstore/constants.go b/internal/security/secretstore/constants.go index c53c96db4d..b14bf74bd4 100644 --- a/internal/security/secretstore/constants.go +++ b/internal/security/secretstore/constants.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2021 Intel Corporation + * Copyright 2021-2023 Intel Corporation * Copyright 2019 Dell Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -39,26 +39,45 @@ const ( // per-service tokens and policies // nolint:gosec TokenCreatorPolicy = ` -path "auth/token/create" { - capabilities = ["create", "update", "sudo"] +path "identity/entity/name" { + capabilities = ["list"] +} + +path "identity/entity/name/*" { + capabilities = ["create", "update", "read"] } -path "auth/token/create-orphan" { - capabilities = ["create", "update", "sudo"] +path "identity/entity-alias" { + capabilities = ["create", "update"] } -path "auth/token/create/*" { - capabilities = ["create", "update", "sudo"] +path "identity/oidc/role" { + capabilities = ["list"] } -path "sys/policies/acl/edgex-service-*" -{ +path "identity/oidc/role/*" { + capabilities = ["create", "update"] +} + +path "auth/userpass/users/*" { + capabilities = ["create", "update"] + } + +path "sys/auth" { + capabilities = ["read"] +} + +path "sys/policies/acl/edgex-service-*" { capabilities = ["create", "read", "update", "delete" ] } -path "sys/policies/acl" -{ +path "sys/policies/acl" { capabilities = ["list"] } ` + + // UPAuthMountPoint is where the username/password auth engine is mounted + UPAuthMountPoint = "userpass" + // UserPassAuthEngine is the auth engine name + UserPassAuthEngine = "userpass" ) diff --git a/internal/security/secretstore/init.go b/internal/security/secretstore/init.go index 0406474e18..b3cb95ade0 100644 --- a/internal/security/secretstore/init.go +++ b/internal/security/secretstore/init.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2022 Intel Corporation + * Copyright 2022-2023 Intel Corporation * Copyright 2019 Dell Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -147,7 +147,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s switch sCode { case http.StatusOK: // Load the init response from disk since we need it to regenerate root token later - if err := loadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { + if err := LoadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { lc.Errorf("unable to load init response: %s", err.Error()) return true } @@ -196,7 +196,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s case http.StatusServiceUnavailable: lc.Infof("vault is sealed (status code: %d). Starting unseal phase", sCode) - if err := loadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { + if err := LoadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { lc.Errorf("unable to load init response: %s", err.Error()) return true } @@ -320,6 +320,33 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s } } + // Enable userpass auth engine + upAuthEnabled, err := client.CheckAuthMethodEnabled(rootToken, UPAuthMountPoint, UserPassAuthEngine) + if err != nil { + lc.Errorf("failed to check if %s auth method enabled: %s", UserPassAuthEngine, err.Error()) + return false + } else if !upAuthEnabled { + // Enable userpass engine at /v1/auth/{eng.path} path (/v1 prefix supplied by Vault) + lc.Infof("Enabling userpass authentication for the first time...") + if err := client.EnablePasswordAuth(rootToken, UPAuthMountPoint); err != nil { + lc.Errorf("failed to enable userpass secrets engine: %s", err.Error()) + return false + } + lc.Infof("Userpass authentication engine enabled at path %s", UPAuthMountPoint) + } + + // Create a key for issuing JWTs + keyExists, err := client.CheckIdentityKeyExists(rootToken, "edgex-identity") + if err != nil { + lc.Errorf("failed to check for JWT issuing key: %s", err.Error()) + return false + } else if !keyExists { + if err := client.CreateNamedIdentityKey(rootToken, "edgex-identity", "ES384"); err != nil { + lc.Errorf("failed to create JWT issuing key: %s", err.Error()) + return false + } + } + //Step 4: Launch token handler tokenProvider := NewTokenProvider(ctx, lc, NewDefaultExecRunner()) if secretStoreConfig.TokenProvider != "" { @@ -368,7 +395,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s redisCredentials, err := getCredential("security-bootstrapper-redis", secretStore, redisSecretName) if err != nil { if err != errNotFound { - lc.Error("failed to determine if Redis credentials already exist or not: %w", err) + lc.Errorf("failed to determine if Redis credentials already exist or not: %s", err.Error()) return false } @@ -429,7 +456,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s msgBusCredentials, err = getCredential(internal.BootstrapMessageBusServiceKey, secretStore, messagebusSecretName) if err != nil { if err != errNotFound { - lc.Errorf("failed to determine if %s credentials already exist or not: %w", configuration.SecureMessageBus.Type, err) + lc.Errorf("failed to determine if %s credentials already exist or not: %s", configuration.SecureMessageBus.Type, err.Error()) return false } @@ -726,7 +753,7 @@ func storeCredential(lc logger.LoggingClient, credBootstrapStem string, cred Cre return err } -func loadInitResponse( +func LoadInitResponse( lc logger.LoggingClient, fileOpener fileioperformer.FileIoPerformer, secretConfig config.SecretStoreInfo, diff --git a/internal/security/secretstore/init_test.go b/internal/security/secretstore/init_test.go index b17d32714d..ca4a21de6d 100644 --- a/internal/security/secretstore/init_test.go +++ b/internal/security/secretstore/init_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Intel Corporation +// Copyright (c) 2021-2023 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // @@ -49,7 +49,7 @@ func TestLoadInitResponse(t *testing.T) { initResponse := types.InitResponse{} // Act - err := loadInitResponse(mockLogger, fileOpener, secretConfig, &initResponse) + err := LoadInitResponse(mockLogger, fileOpener, secretConfig, &initResponse) // Assert assert.NoError(t, err) diff --git a/internal/security/spiffetokenprovider/config/config.go b/internal/security/spiffetokenprovider/config/config.go index 69bbca657e..449a11d97f 100644 --- a/internal/security/spiffetokenprovider/config/config.go +++ b/internal/security/spiffetokenprovider/config/config.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2022 Intel Corporation + * Copyright 2022-2023 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 @@ -16,6 +16,7 @@ package config import ( + fileProviderConfig "github.com/edgexfoundry/edgex-go/internal/security/fileprovider/config" bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v3/config" ) @@ -28,13 +29,14 @@ type SpiffeInfo struct { } type ConfigurationStruct struct { - Writable WritableInfo - MessageBus bootstrapConfig.MessageBusInfo - Clients map[string]bootstrapConfig.ClientInfo - Database bootstrapConfig.Database - Registry bootstrapConfig.RegistryInfo - Service bootstrapConfig.ServiceInfo - Spiffe SpiffeInfo + Writable WritableInfo + MessageBus bootstrapConfig.MessageBusInfo + Clients map[string]bootstrapConfig.ClientInfo + Database bootstrapConfig.Database + Registry bootstrapConfig.RegistryInfo + Service bootstrapConfig.ServiceInfo + TokenConfig fileProviderConfig.TokenFileProviderInfo + Spiffe SpiffeInfo } type WritableInfo struct { diff --git a/internal/security/spiffetokenprovider/init.go b/internal/security/spiffetokenprovider/init.go index 7ba6224474..d734c90d66 100644 --- a/internal/security/spiffetokenprovider/init.go +++ b/internal/security/spiffetokenprovider/init.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2022 Intel Corporation + * Copyright 2022-2023 Intel Corporation * Copyright 2019 Dell Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -265,7 +265,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s w.WriteHeader(http.StatusInternalServerError) return } - vaultTokenResponse, err := makeToken(serviceName, privilegedToken, secretStoreClient, lc) + vaultTokenResponse, err := makeToken(serviceName, privilegedToken, configuration.TokenConfig, secretStoreClient, lc) if err != nil { lc.Errorf("failed create secret store token: %v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/internal/security/spiffetokenprovider/maketoken.go b/internal/security/spiffetokenprovider/maketoken.go index 37e9fca094..c716bc979f 100644 --- a/internal/security/spiffetokenprovider/maketoken.go +++ b/internal/security/spiffetokenprovider/maketoken.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Intel Corporation +// Copyright (c) 2022-2023 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 @@ -17,94 +17,53 @@ package spiffetokenprovider import ( - "encoding/json" - "fmt" + "context" + "github.com/edgexfoundry/edgex-go/internal/security/common" + securityCommon "github.com/edgexfoundry/edgex-go/internal/security/common" + fileProviderConfig "github.com/edgexfoundry/edgex-go/internal/security/fileprovider/config" + "github.com/edgexfoundry/edgex-go/internal/security/secretstore" "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger" "github.com/edgexfoundry/go-mod-secrets/v3/secrets" ) -const ( - DefaultTokenTTL = "1h" -) - -func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { - // protected path for secret/ - protectedPath := "secret/edgex/" + serviceName + "/*" - capabilities := []string{"create", "update", "delete", "list", "read"} - acl := map[string]interface{}{"capabilities": capabilities} - // path for consul tokens - registryCredsPath := "consul/creds/" + serviceName - registryCredsCapabilities := []string{"read"} - registryCredsACL := map[string]interface{}{"capabilities": registryCredsCapabilities} - pathObject := map[string]interface{}{ - protectedPath: acl, - registryCredsPath: registryCredsACL, - } - retval := map[string]interface{}{"path": pathObject} - return retval - - /* - { - "path": { - "secret/edgex/service-name/*": { - "capabilities": [ "create", "update", "delete", "list", "read" ] - }, - "consul/creds/service-name": { - "capabilities": [ "read" ] - } - } - } - */ -} - -func makeDefaultTokenParameters(serviceName string, defaultTTL string, defaultPeriod string) map[string]interface{} { - return map[string]interface{}{ - "display_name": serviceName, - "no_parent": true, - "ttl": defaultTTL, - "period": defaultPeriod, - "policies": []string{"edgex-service-" + serviceName}, - } -} - func makeToken(serviceName string, privilegedToken string, + tokenConfig fileProviderConfig.TokenFileProviderInfo, secretStoreClient secrets.SecretStoreClient, lc logger.LoggingClient) (interface{}, error) { lc.Infof("generating policy/token defaults for service %s", serviceName) lc.Infof("using policy/token defaults for service %s", serviceName) - servicePolicy := makeDefaultTokenPolicy(serviceName) + servicePolicy := securityCommon.MakeDefaultTokenPolicy(serviceName) defaultPolicyPaths := servicePolicy["path"].(map[string]interface{}) for pathKey, policy := range defaultPolicyPaths { servicePolicy["path"].(map[string]interface{})[pathKey] = policy } - createTokenParameters := makeDefaultTokenParameters(serviceName, DefaultTokenTTL, DefaultTokenTTL) - // Set a meta property that consuming serices can use to automatically scope secret queries - createTokenParameters["meta"] = map[string]interface{}{ - "edgex-service-name": serviceName, - } + credentialGenerator := secretstore.NewDefaultCredentialGenerator() - // Always create a policy with this name - policyName := "edgex-service-" + serviceName + userManager := common.NewUserManager(lc, secretStoreClient, tokenConfig.UserPassMountPoint, "edgex-identity", + privilegedToken, tokenConfig.DefaultTokenTTL, tokenConfig.DefaultJWTTTL) - policyBytes, err := json.Marshal(servicePolicy) + // Generate a random password + + randomPassword, err := credentialGenerator.Generate(context.TODO()) if err != nil { - lc.Error(fmt.Sprintf("failed encode service policy for %s: %s", serviceName, err.Error())) return nil, err } - if err := secretStoreClient.InstallPolicy(privilegedToken, policyName, string(policyBytes)); err != nil { - lc.Error(fmt.Sprintf("failed to install policy %s: %s", policyName, err.Error())) + // Create a user with the random password + + err = userManager.CreatePasswordUserWithPolicy(serviceName, randomPassword, "edgex-service-", servicePolicy) + if err != nil { return nil, err } - var createTokenResponse interface{} + // Immediately log in the user to get a vault token - if createTokenResponse, err = secretStoreClient.CreateToken(privilegedToken, createTokenParameters); err != nil { - lc.Error(fmt.Sprintf("failed to create vault token for service %s: %s", serviceName, err.Error())) + var createTokenResponse interface{} + if createTokenResponse, err = secretStoreClient.InternalServiceLogin(privilegedToken, tokenConfig.UserPassMountPoint, serviceName, randomPassword); err != nil { return nil, err }