Updates for getting private key from AWS secrets manager (#32789)
for #31321 # Details Small updates from [community PR](https://github.com/fleetdm/fleet/pull/31134): * Updated config vars to match [docs](https://github.com/fleetdm/fleet/blob/docs-v4.75.0/docs/Configuration/fleet-server-configuration.md#server_private_key_region) * Added support for specifying region in config (already documented) * Removed parsing of ARN for region * Made retry backoff intervals a bit longer # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. (already added in the community PR [here](https://github.com/fleetdm/fleet/blob/sgress454/updates-for-private-key-in-aws-sm/changes/private-key-secrets-manager#L0-L1) ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Added support for specifying the AWS region for server private key retrieval from AWS Secrets Manager via server.private_key_region. - Chores - Renamed configuration keys: - server.private_key_secret_arn → server.private_key_arn - server.private_key_secret_sts_assume_role_arn → server.private_key_sts_assume_role_arn - server.private_key_secret_sts_external_id → server.private_key_sts_external_id - Update your configuration to use the new keys. - Adjusted retry backoff for Secrets Manager retrieval to improve resilience. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -91,6 +91,7 @@ jobs:
|
||||
if [[ "${{ matrix.suite }}" == "main" ]]; then
|
||||
echo "CI_TEST_PKG=main" >> $GITHUB_ENV
|
||||
echo "NEED_DOCKER=1" >> $GITHUB_ENV
|
||||
echo "DOCKER_COMMAND=${{ env.DOCKER_COMMAND }} localstack" >> $GITHUB_ENV
|
||||
elif [[ "${{ matrix.suite }}" == "fast" ]]; then
|
||||
# DO NOT add any dependencies in this test suite.
|
||||
echo "CI_TEST_PKG=${{ matrix.suite }}" >> $GITHUB_ENV
|
||||
@@ -258,6 +259,8 @@ jobs:
|
||||
MINIO_STORAGE_TEST=1 \
|
||||
SAML_IDP_TEST=1 \
|
||||
MAIL_TEST=1 \
|
||||
AWS_ENDPOINT_URL="http://localhost:4566" \
|
||||
AWS_REGION=us-east-1 \
|
||||
NETWORK_TEST_GITHUB_TOKEN=${{ secrets.FLEET_RELEASE_GITHUB_PAT }} \
|
||||
CI_TEST_PKG="${{ env.CI_TEST_PKG }}" \
|
||||
make test-go 2>&1 | tee /tmp/gotest.log
|
||||
|
||||
@@ -203,6 +203,7 @@ the way that the Fleet server works.
|
||||
privateKey, err := configpkg.RetrieveSecretsManagerSecret(
|
||||
context.Background(),
|
||||
config.Server.PrivateKeySecretArn,
|
||||
config.Server.PrivateKeySecretRegion,
|
||||
config.Server.PrivateKeySecretSTSAssumeRoleArn,
|
||||
config.Server.PrivateKeySecretSTSExternalID,
|
||||
)
|
||||
|
||||
+36
-21
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/testutils"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -161,8 +162,11 @@ func TestExpandEnv(t *testing.T) {
|
||||
{map[string]string{"foo": "", "bar": "", "zoo": ""}, `$foo${bar}$zoo`, ``, nil},
|
||||
{map[string]string{}, `$foo`, ``, checkMultiErrors(t, "environment variable \"foo\" not set")},
|
||||
{map[string]string{"foo": "1"}, `$foo$bar`, ``, checkMultiErrors(t, "environment variable \"bar\" not set")},
|
||||
{map[string]string{"bar": "1"}, `$foo $bar $zoo`, ``,
|
||||
checkMultiErrors(t, "environment variable \"foo\" not set", "environment variable \"zoo\" not set")},
|
||||
{
|
||||
map[string]string{"bar": "1"},
|
||||
`$foo $bar $zoo`, ``,
|
||||
checkMultiErrors(t, "environment variable \"foo\" not set", "environment variable \"zoo\" not set"),
|
||||
},
|
||||
{map[string]string{"foo": "4", "bar": "2"}, `$foo$bar`, `42`, nil},
|
||||
{map[string]string{"foo": "42", "bar": ""}, `$foo$bar`, `42`, nil},
|
||||
{map[string]string{}, `$$`, ``, checkMultiErrors(t, "environment variable \"$\" not set")},
|
||||
@@ -180,9 +184,14 @@ func TestExpandEnv(t *testing.T) {
|
||||
{map[string]string{"foo": "", "$": "2"}, `${$}${foo}var`, `2var`, nil},
|
||||
{map[string]string{}, `${foo}var`, ``, checkMultiErrors(t, "environment variable \"foo\" not set")},
|
||||
{map[string]string{}, `foo PREVENT_ESCAPING_bar $ FLEET_VAR_`, `foo PREVENT_ESCAPING_bar $ FLEET_VAR_`, nil}, // nothing to replace
|
||||
{map[string]string{"foo": "BAR"}, `\$FLEET_VAR_$foo \${FLEET_VAR_$foo} \${FLEET_VAR_${foo}2}`,
|
||||
`$FLEET_VAR_BAR ${FLEET_VAR_BAR} ${FLEET_VAR_BAR2}`, nil}, // nested variables
|
||||
{
|
||||
map[string]string{"foo": "BAR"},
|
||||
`\$FLEET_VAR_$foo \${FLEET_VAR_$foo} \${FLEET_VAR_${foo}2}`,
|
||||
`$FLEET_VAR_BAR ${FLEET_VAR_BAR} ${FLEET_VAR_BAR2}`, nil,
|
||||
}, // nested variables
|
||||
} {
|
||||
// save the current env before clearing it.
|
||||
testutils.SaveEnv(t)
|
||||
os.Clearenv()
|
||||
for k, v := range tc.environment {
|
||||
_ = os.Setenv(k, v)
|
||||
@@ -206,9 +215,15 @@ func TestLookupEnvSecrets(t *testing.T) {
|
||||
}{
|
||||
{map[string]string{"foo": "1"}, `$foo`, map[string]string{}, nil},
|
||||
{map[string]string{"FLEET_SECRET_foo": "1"}, `$FLEET_SECRET_foo`, map[string]string{"FLEET_SECRET_foo": "1"}, nil},
|
||||
{map[string]string{"foo": "1"}, `$FLEET_SECRET_foo`, map[string]string{},
|
||||
checkMultiErrors(t, "environment variable \"FLEET_SECRET_foo\" not set")},
|
||||
{
|
||||
map[string]string{"foo": "1"},
|
||||
`$FLEET_SECRET_foo`,
|
||||
map[string]string{},
|
||||
checkMultiErrors(t, "environment variable \"FLEET_SECRET_foo\" not set"),
|
||||
},
|
||||
} {
|
||||
// save the current env before clearing it.
|
||||
testutils.SaveEnv(t)
|
||||
os.Clearenv()
|
||||
for k, v := range tc.environment {
|
||||
_ = os.Setenv(k, v)
|
||||
@@ -262,26 +277,26 @@ func TestGetExclusionZones(t *testing.T) {
|
||||
{
|
||||
[]string{"testdata", "policies", "policies.yml"},
|
||||
map[[2]int]string{
|
||||
[2]int{46, 106}: " description: This policy should always fail.\n resolution:",
|
||||
[2]int{93, 155}: " resolution: There is no resolution for this policy.\n query:",
|
||||
[2]int{268, 328}: " description: This policy should always pass.\n resolution:",
|
||||
[2]int{315, 678}: " resolution: |\n Automated method:\n Ask your system administrator to deploy the following script which will ensure proper Security Auditing Retention:\n cp /etc/security/audit_control ./tmp.txt; origExpire=$(cat ./tmp.txt | grep expire-after); sed \"s/${origExpire}/expire-after:60d OR 5G/\" ./tmp.txt > /etc/security/audit_control; rm ./tmp.txt;\n query:",
|
||||
{46, 106}: " description: This policy should always fail.\n resolution:",
|
||||
{93, 155}: " resolution: There is no resolution for this policy.\n query:",
|
||||
{268, 328}: " description: This policy should always pass.\n resolution:",
|
||||
{315, 678}: " resolution: |\n Automated method:\n Ask your system administrator to deploy the following script which will ensure proper Security Auditing Retention:\n cp /etc/security/audit_control ./tmp.txt; origExpire=$(cat ./tmp.txt | grep expire-after); sed \"s/${origExpire}/expire-after:60d OR 5G/\" ./tmp.txt > /etc/security/audit_control; rm ./tmp.txt;\n query:",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"testdata", "global_config_no_paths.yml"},
|
||||
map[[2]int]string{
|
||||
[2]int{866, 949}: " description: Collect osquery performance stats directly from osquery\n query:", //
|
||||
[2]int{1754, 1818}: " description: This policy should always fail.\n resolution:", //
|
||||
[2]int{1803, 1869}: " resolution: There is no resolution for this policy.\n query:", //
|
||||
[2]int{1986, 2050}: " description: This policy should always pass.\n resolution:", //
|
||||
[2]int{2035, 2101}: " resolution: There is no resolution for this policy.\n query:", //
|
||||
[2]int{2394, 2458}: " description: This policy should always fail.\n resolution:", //
|
||||
[2]int{2443, 2509}: " resolution: There is no resolution for this policy.\n query:", //
|
||||
[2]int{2613, 2677}: " description: This policy should always fail.\n resolution:", //
|
||||
[2]int{2662, 3035}: " resolution: |\n Automated method:\n Ask your system administrator to deploy the following script which will ensure proper Security Auditing Retention:\n cp /etc/security/audit_control ./tmp.txt; origExpire=$(cat ./tmp.txt | grep expire-after); sed \"s/${origExpire}/expire-after:60d OR 5G/\" ./tmp.txt > /etc/security/audit_control; rm ./tmp.txt;\n query:",
|
||||
[2]int{6102, 6149}: " description: A cool global label\n query:", //
|
||||
[2]int{6246, 6292}: " description: A fly global label\n hosts:", //
|
||||
{866, 949}: " description: Collect osquery performance stats directly from osquery\n query:", //
|
||||
{1754, 1818}: " description: This policy should always fail.\n resolution:", //
|
||||
{1803, 1869}: " resolution: There is no resolution for this policy.\n query:", //
|
||||
{1986, 2050}: " description: This policy should always pass.\n resolution:", //
|
||||
{2035, 2101}: " resolution: There is no resolution for this policy.\n query:", //
|
||||
{2394, 2458}: " description: This policy should always fail.\n resolution:", //
|
||||
{2443, 2509}: " resolution: There is no resolution for this policy.\n query:", //
|
||||
{2613, 2677}: " description: This policy should always fail.\n resolution:", //
|
||||
{2662, 3035}: " resolution: |\n Automated method:\n Ask your system administrator to deploy the following script which will ensure proper Security Auditing Retention:\n cp /etc/security/audit_control ./tmp.txt; origExpire=$(cat ./tmp.txt | grep expire-after); sed \"s/${origExpire}/expire-after:60d OR 5G/\" ./tmp.txt > /etc/security/audit_control; rm ./tmp.txt;\n query:",
|
||||
{6102, 6149}: " description: A cool global label\n query:", //
|
||||
{6246, 6292}: " description: A fly global label\n hosts:", //
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// SaveEnv snapshots the current environment and restores it when the test
|
||||
// ends.
|
||||
//
|
||||
// Do _not_ use this in parallel tests, as it clears the entire environment.
|
||||
func SaveEnv(t *testing.T) {
|
||||
saved := os.Environ()
|
||||
t.Cleanup(func() {
|
||||
os.Clearenv()
|
||||
for _, kv := range saved {
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
key := parts[0]
|
||||
val := ""
|
||||
if len(parts) == 2 {
|
||||
val = parts[1]
|
||||
}
|
||||
err := os.Setenv(key, val)
|
||||
if err != nil {
|
||||
t.Logf("error restoring env var %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+12
-9
@@ -107,9 +107,10 @@ type ServerConfig struct {
|
||||
FrequentCleanupsEnabled bool `yaml:"frequent_cleanups_enabled"`
|
||||
ForceH2C bool `yaml:"force_h2c"`
|
||||
PrivateKey string `yaml:"private_key"`
|
||||
PrivateKeySecretArn string `yaml:"private_key_secret_arn"`
|
||||
PrivateKeySecretSTSAssumeRoleArn string `yaml:"private_key_secret_sts_assume_role_arn"`
|
||||
PrivateKeySecretSTSExternalID string `yaml:"private_key_secret_sts_external_id"`
|
||||
PrivateKeySecretArn string `yaml:"private_key_arn"`
|
||||
PrivateKeySecretRegion string `yaml:"private_key_region"`
|
||||
PrivateKeySecretSTSAssumeRoleArn string `yaml:"private_key_sts_assume_role_arn"`
|
||||
PrivateKeySecretSTSExternalID string `yaml:"private_key_sts_external_id"`
|
||||
VPPVerifyTimeout time.Duration `yaml:"vpp_verify_timeout"`
|
||||
VPPVerifyRequestDelay time.Duration `yaml:"vpp_verify_request_delay"`
|
||||
}
|
||||
@@ -1124,9 +1125,10 @@ func (man Manager) addConfigs() {
|
||||
man.addConfigBool("server.frequent_cleanups_enabled", false, "Enable frequent cleanups of expired data (15 minute interval)")
|
||||
man.addConfigBool("server.force_h2c", false, "Force the fleet server to use HTTP2 cleartext aka h2c (ignored if using TLS)")
|
||||
man.addConfigString("server.private_key", "", "Used for encrypting sensitive data, such as MDM certificates.")
|
||||
man.addConfigString("server.private_key_secret_arn", "", "ARN of AWS Secrets Manager secret containing server private key")
|
||||
man.addConfigString("server.private_key_secret_sts_assume_role_arn", "", "ARN of role to assume for accessing private key secret")
|
||||
man.addConfigString("server.private_key_secret_sts_external_id", "", "External ID for STS role assumption when accessing private key secret")
|
||||
man.addConfigString("server.private_key_region", "", "AWS region of the Secrets Manager secret containing server private key")
|
||||
man.addConfigString("server.private_key_arn", "", "ARN of AWS Secrets Manager secret containing server private key")
|
||||
man.addConfigString("server.private_key_sts_assume_role_arn", "", "ARN of role to assume for accessing private key secret")
|
||||
man.addConfigString("server.private_key_sts_external_id", "", "External ID for STS role assumption when accessing private key secret")
|
||||
man.addConfigDuration("server.vpp_verify_timeout", 10*time.Minute, "Maximum amout of time to wait for VPP app install verification")
|
||||
man.addConfigDuration("server.vpp_verify_request_delay", 5*time.Second, "Delay in between requests to verify VPP app installs")
|
||||
|
||||
@@ -1566,9 +1568,10 @@ func (man Manager) LoadConfig() FleetConfig {
|
||||
FrequentCleanupsEnabled: man.getConfigBool("server.frequent_cleanups_enabled"),
|
||||
ForceH2C: man.getConfigBool("server.force_h2c"),
|
||||
PrivateKey: man.getConfigString("server.private_key"),
|
||||
PrivateKeySecretArn: man.getConfigString("server.private_key_secret_arn"),
|
||||
PrivateKeySecretSTSAssumeRoleArn: man.getConfigString("server.private_key_secret_sts_assume_role_arn"),
|
||||
PrivateKeySecretSTSExternalID: man.getConfigString("server.private_key_secret_sts_external_id"),
|
||||
PrivateKeySecretArn: man.getConfigString("server.private_key_arn"),
|
||||
PrivateKeySecretRegion: man.getConfigString("server.private_key_region"),
|
||||
PrivateKeySecretSTSAssumeRoleArn: man.getConfigString("server.private_key_sts_assume_role_arn"),
|
||||
PrivateKeySecretSTSExternalID: man.getConfigString("server.private_key_sts_external_id"),
|
||||
VPPVerifyTimeout: man.getConfigDuration("server.vpp_verify_timeout"),
|
||||
VPPVerifyRequestDelay: man.getConfigDuration("server.vpp_verify_request_delay"),
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/testutils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -30,6 +31,9 @@ func TestConfigRoundtrip(t *testing.T) {
|
||||
|
||||
// viper tries to load config from the environment too, clear it in case
|
||||
// any config values are set in the environment.
|
||||
|
||||
// save the current env before clearing it.
|
||||
testutils.SaveEnv(t)
|
||||
os.Clearenv()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
@@ -302,7 +306,9 @@ osquery:
|
||||
// test-case values, but that didn't seem to work, not sure how it can
|
||||
// be done in our particular setup.
|
||||
|
||||
// set the environment variables
|
||||
// save the current env before clearing it.
|
||||
testutils.SaveEnv(t)
|
||||
|
||||
os.Clearenv()
|
||||
for _, env := range c.envVars {
|
||||
kv := strings.SplitN(env, "=", 2)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
aws_config "github.com/aws/aws-sdk-go-v2/config"
|
||||
@@ -20,22 +19,6 @@ type SecretsManagerClient interface {
|
||||
optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error)
|
||||
}
|
||||
|
||||
// parseRegionFromSecretARN extracts the AWS region from a Secrets Manager ARN
|
||||
func parseRegionFromSecretARN(arn string) (string, error) {
|
||||
// ARN format: arn:aws:secretsmanager:region:account:secret:name
|
||||
parts := strings.Split(arn, ":")
|
||||
if len(parts) < 6 || parts[0] != "arn" || parts[1] != "aws" || parts[2] != "secretsmanager" {
|
||||
return "", fmt.Errorf("invalid Secrets Manager ARN format: %s", arn)
|
||||
}
|
||||
|
||||
region := parts[3]
|
||||
if region == "" {
|
||||
return "", fmt.Errorf("region not found in ARN: %s", arn)
|
||||
}
|
||||
|
||||
return region, nil
|
||||
}
|
||||
|
||||
// retrieveSecretWithRetry retrieves the secret from AWS with retry logic
|
||||
func retrieveSecretWithRetry(ctx context.Context, client SecretsManagerClient, secretArn string) (string, error) {
|
||||
const maxRetries = 3
|
||||
@@ -43,8 +26,8 @@ func retrieveSecretWithRetry(ctx context.Context, client SecretsManagerClient, s
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
// Exponential backoff with jitter: base 100ms with ±50% randomization
|
||||
baseBackoff := time.Duration(100*(1<<uint(attempt-1))) * time.Millisecond // #nosec G115 - attempt is bounded by maxRetries
|
||||
// Exponential backoff with jitter: base 500ms with ±50% randomization
|
||||
baseBackoff := time.Duration(500*(1<<uint(attempt-1))) * time.Millisecond // #nosec G115 - attempt is bounded by maxRetries
|
||||
jitter := time.Duration(rand.Float64()*float64(baseBackoff)) - baseBackoff/2 // #nosec G404 - not security sensitive
|
||||
backoff := baseBackoff + jitter
|
||||
select {
|
||||
@@ -98,18 +81,13 @@ func retrieveSecretWithRetry(ctx context.Context, client SecretsManagerClient, s
|
||||
|
||||
// RetrieveSecretsManagerSecret retrieves a secret from AWS Secrets Manager
|
||||
// with support for STS assume role authentication
|
||||
func RetrieveSecretsManagerSecret(ctx context.Context, secretArn, assumeRoleArn, externalID string) (string, error) {
|
||||
return RetrieveSecretsManagerSecretWithOptions(ctx, secretArn, assumeRoleArn, externalID)
|
||||
func RetrieveSecretsManagerSecret(ctx context.Context, secretArn, region, assumeRoleArn, externalID string) (string, error) {
|
||||
return RetrieveSecretsManagerSecretWithOptions(ctx, secretArn, region, assumeRoleArn, externalID)
|
||||
}
|
||||
|
||||
// RetrieveSecretsManagerSecretWithOptions retrieves a secret from AWS Secrets Manager
|
||||
// with custom AWS config options (useful for testing with LocalStack)
|
||||
func RetrieveSecretsManagerSecretWithOptions(ctx context.Context, secretArn, assumeRoleArn, externalID string, opts ...func(*aws_config.LoadOptions) error) (string, error) {
|
||||
region, err := parseRegionFromSecretARN(secretArn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid secret ARN: %w", err)
|
||||
}
|
||||
|
||||
func RetrieveSecretsManagerSecretWithOptions(ctx context.Context, secretArn, region, assumeRoleArn, externalID string, opts ...func(*aws_config.LoadOptions) error) (string, error) {
|
||||
configOpts := []func(*aws_config.LoadOptions) error{aws_config.WithRegion(region)}
|
||||
configOpts = append(configOpts, opts...)
|
||||
cfg, err := aws_config.LoadDefaultConfig(ctx, configOpts...)
|
||||
|
||||
@@ -3,7 +3,6 @@ package config
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -31,59 +30,6 @@ func (m *mockSecretsManagerClient) GetSecretValue(ctx context.Context, params *s
|
||||
return args.Get(0).(*secretsmanager.GetSecretValueOutput), args.Error(1)
|
||||
}
|
||||
|
||||
func TestParseRegionFromARN(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
arn string
|
||||
expectedReg string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid ARN",
|
||||
arn: "arn:aws:secretsmanager:us-west-2:123456789012:secret:fleet-private-key-abc123",
|
||||
expectedReg: "us-west-2",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid ARN different region",
|
||||
arn: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:my-secret-def456",
|
||||
expectedReg: "eu-central-1",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid ARN format",
|
||||
arn: "invalid-arn-format",
|
||||
expectedReg: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "wrong service",
|
||||
arn: "arn:aws:s3:us-west-2:123456789012:bucket/my-bucket",
|
||||
expectedReg: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty region",
|
||||
arn: "arn:aws:secretsmanager::123456789012:secret:my-secret",
|
||||
expectedReg: "",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
region, err := parseRegionFromSecretARN(tc.arn)
|
||||
if tc.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, region)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedReg, region)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveSecretWithRetry_Success(t *testing.T) {
|
||||
mockClient := &mockSecretsManagerClient{}
|
||||
expectedKey := "test-32-byte-key-for-aes-encryption"
|
||||
@@ -213,31 +159,25 @@ func TestRetrieveSecretWithRetry_ContextCancellation(t *testing.T) {
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestRetrieveSecretsManagerSecret_LocalStack(t *testing.T) {
|
||||
if os.Getenv("LOCALSTACK_URL") == "" {
|
||||
t.Skip("LOCALSTACK_URL not set, skipping LocalStack integration test")
|
||||
func TestRetrieveSecretsManagerSecret_LocalStackDefaultRegion(t *testing.T) {
|
||||
awsEndpointURL := os.Getenv("AWS_ENDPOINT_URL")
|
||||
if awsEndpointURL == "" {
|
||||
t.Skip("AWS_ENDPOINT_URL not set, skipping LocalStack integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
localStackURL := os.Getenv("LOCALSTACK_URL")
|
||||
if localStackURL == "" {
|
||||
localStackURL = "http://localhost:4566"
|
||||
}
|
||||
|
||||
localStackOpts := []func(*aws_config.LoadOptions) error{
|
||||
aws_config.WithBaseEndpoint(localStackURL),
|
||||
aws_config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
|
||||
}
|
||||
|
||||
// Configure LocalStack client
|
||||
cfg, err := aws_config.LoadDefaultConfig(ctx, append(localStackOpts, aws_config.WithRegion("us-east-1"))...)
|
||||
cfg, err := aws_config.LoadDefaultConfig(ctx, localStackOpts...)
|
||||
require.NoError(t, err)
|
||||
client := secretsmanager.NewFromConfig(cfg)
|
||||
|
||||
secretName := "fleet-test-private-key-localstack"
|
||||
privateKey := "test-key-exactly-32-bytes-long!"
|
||||
secretArn := fmt.Sprintf("arn:aws:secretsmanager:us-east-1:000000000000:secret:%s", secretName)
|
||||
|
||||
// Clean up any existing secret
|
||||
_, _ = client.DeleteSecret(ctx, &secretsmanager.DeleteSecretInput{
|
||||
@@ -245,12 +185,13 @@ func TestRetrieveSecretsManagerSecret_LocalStack(t *testing.T) {
|
||||
ForceDeleteWithoutRecovery: aws.Bool(true),
|
||||
})
|
||||
|
||||
_, err = client.CreateSecret(ctx, &secretsmanager.CreateSecretInput{
|
||||
output, err := client.CreateSecret(ctx, &secretsmanager.CreateSecretInput{
|
||||
Name: &secretName,
|
||||
SecretString: &privateKey,
|
||||
Description: aws.String("password"),
|
||||
})
|
||||
require.NoError(t, err, "Failed to create secret in LocalStack")
|
||||
secretArn := *output.ARN
|
||||
|
||||
// Clean up after test
|
||||
defer func() {
|
||||
@@ -259,13 +200,65 @@ func TestRetrieveSecretsManagerSecret_LocalStack(t *testing.T) {
|
||||
ForceDeleteWithoutRecovery: aws.Bool(true),
|
||||
})
|
||||
}()
|
||||
retrievedKey, err := RetrieveSecretsManagerSecretWithOptions(ctx, secretArn, "", "", localStackOpts...)
|
||||
retrievedKey, err := RetrieveSecretsManagerSecretWithOptions(ctx, secretArn, "", "", "", localStackOpts...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, privateKey, retrievedKey)
|
||||
|
||||
// Test with invalid ARN
|
||||
invalidArn := "arn:aws:secretsmanager:us-east-1:000000000000:secret:nonexistent-secret"
|
||||
_, err = RetrieveSecretsManagerSecretWithOptions(ctx, invalidArn, "", "", localStackOpts...)
|
||||
_, err = RetrieveSecretsManagerSecretWithOptions(ctx, invalidArn, "", "", "", localStackOpts...)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "secret not found")
|
||||
}
|
||||
|
||||
func TestRetrieveSecretsManagerSecret_LocalStackDifferentRegion(t *testing.T) {
|
||||
awsEndpointURL := os.Getenv("AWS_ENDPOINT_URL")
|
||||
if awsEndpointURL == "" {
|
||||
t.Skip("AWS_ENDPOINT_URL not set, skipping LocalStack integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
localStackOpts := []func(*aws_config.LoadOptions) error{
|
||||
aws_config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
|
||||
}
|
||||
|
||||
// Configure LocalStack client
|
||||
cfg, err := aws_config.LoadDefaultConfig(ctx, append(localStackOpts, aws_config.WithRegion("us-east-2"))...)
|
||||
require.NoError(t, err)
|
||||
client := secretsmanager.NewFromConfig(cfg)
|
||||
|
||||
secretName := "fleet-test-private-key-localstack"
|
||||
privateKey := "test-key-exactly-32-bytes-long!"
|
||||
|
||||
// Clean up any existing secret
|
||||
_, _ = client.DeleteSecret(ctx, &secretsmanager.DeleteSecretInput{
|
||||
SecretId: &secretName,
|
||||
ForceDeleteWithoutRecovery: aws.Bool(true),
|
||||
})
|
||||
|
||||
output, err := client.CreateSecret(ctx, &secretsmanager.CreateSecretInput{
|
||||
Name: &secretName,
|
||||
SecretString: &privateKey,
|
||||
Description: aws.String("password"),
|
||||
})
|
||||
require.NoError(t, err, "Failed to create secret in LocalStack")
|
||||
secretArn := *output.ARN
|
||||
|
||||
// Clean up after test
|
||||
defer func() {
|
||||
_, _ = client.DeleteSecret(ctx, &secretsmanager.DeleteSecretInput{
|
||||
SecretId: &secretName,
|
||||
ForceDeleteWithoutRecovery: aws.Bool(true),
|
||||
})
|
||||
}()
|
||||
retrievedKey, err := RetrieveSecretsManagerSecretWithOptions(ctx, secretArn, "us-east-2", "", "", localStackOpts...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, privateKey, retrievedKey)
|
||||
|
||||
// Test with invalid ARN
|
||||
invalidArn := "arn:aws:secretsmanager:us-east-1:000000000000:secret:nonexistent-secret"
|
||||
_, err = RetrieveSecretsManagerSecretWithOptions(ctx, invalidArn, "us-east-2", "", "", localStackOpts...)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "secret not found")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user