diff --git a/pkg/secret/secret.go b/pkg/secret/secret.go index 298eb1ff..2d03772f 100644 --- a/pkg/secret/secret.go +++ b/pkg/secret/secret.go @@ -33,6 +33,10 @@ type Secret struct { // variable. For Example: // SECRET_FOO=v1 # length=12 Length int + // Charset comes from the charset modifier at the secret version environment + // variable. For Example: + // SECRET_FOO=v1 # charset=default,special + Charset string // RemoteName is the name of the secret on the server. For example: // name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION} // With the following: @@ -43,38 +47,38 @@ type Secret struct { RemoteName string } -// GeneratePasswords generates passwords. -func GeneratePasswords(count, length uint) ([]string, error) { +// GeneratePassword generates passwords. +func GeneratePassword(length uint, charset string) (string, error) { passwords, err := passgen.GeneratePasswords( - count, + 1, length, - passgen.AlphabetDefault, + charset, ) if err != nil { - return nil, err + return "", err } log.Debugf("generated %s", strings.Join(passwords, ", ")) - return passwords, nil + return passwords[0], nil } -// GeneratePassphrases generates human readable and rememberable passphrases. -func GeneratePassphrases(count uint) ([]string, error) { +// GeneratePassphrase generates human readable and rememberable passphrases. +func GeneratePassphrase() (string, error) { passphrases, err := passgen.GeneratePassphrases( - count, + 1, passgen.PassphraseWordCountDefault, rune('-'), passgen.PassphraseCasingDefault, passgen.WordListDefault, ) if err != nil { - return nil, err + return "", err } log.Debugf("generated %s", strings.Join(passphrases, ", ")) - return passphrases, nil + return passphrases[0], nil } // ReadSecretsConfig reads secret names/versions from the recipe config. The @@ -150,6 +154,8 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin } value.Length = length } + + value.Charset = resolveCharset(modifierValues["charset"]) break } secretValues[secretId] = value @@ -158,6 +164,22 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin return secretValues, nil } +// resolveCharset sets the passgen Alphabet required for a secret +func resolveCharset(input string) string { + switch strings.ToLower(input) { + case "special": + return passgen.AlphabetSpecial + case "safespecial": + return "!@#%^&*_-+=" + case "default,special", "special,default": + return passgen.AlphabetDefault + passgen.AlphabetSpecial + case "default,safespecial", "safespecial,default": + return passgen.AlphabetDefault + "!@#%^&*_-+=" + default: + return passgen.AlphabetDefault // Fallback to default + } +} + // GenerateSecrets generates secrets locally and sends them to a remote server for storage. func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) { secretsGenerated := map[string]string{} @@ -173,13 +195,13 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server) if secret.Length > 0 { - passwords, err := GeneratePasswords(1, uint(secret.Length)) + password, err := GeneratePassword(uint(secret.Length), secret.Charset) if err != nil { ch <- err return } - if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil { + if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil { if strings.Contains(err.Error(), "AlreadyExists") { log.Warnf("%s already exists", secret.RemoteName) ch <- nil @@ -191,15 +213,15 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server mutex.Lock() defer mutex.Unlock() - secretsGenerated[secretName] = passwords[0] + secretsGenerated[secretName] = password } else { - passphrases, err := GeneratePassphrases(1) + passphrase, err := GeneratePassphrase() if err != nil { ch <- err return } - if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil { + if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil { if strings.Contains(err.Error(), "AlreadyExists") { log.Warnf("%s already exists", secret.RemoteName) ch <- nil @@ -211,7 +233,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server mutex.Lock() defer mutex.Unlock() - secretsGenerated[secretName] = passphrases[0] + secretsGenerated[secretName] = passphrase } ch <- nil }(n, v) diff --git a/pkg/secret/secret_test.go b/pkg/secret/secret_test.go index 63d495d7..67d344ae 100644 --- a/pkg/secret/secret_test.go +++ b/pkg/secret/secret_test.go @@ -17,16 +17,37 @@ func TestReadSecretsConfig(t *testing.T) { assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName) assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version) assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length) + assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset) // Has a length modifier assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName) assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version) assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length) + assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_two"].Charset) // Secret name does not include the secret id assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName) assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version) assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length) + assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_three"].Charset) + + // Has a length modifier and a charset=default,safespecial modifier + assert.Equal(t, "test_example_com_test_pass_four_v1", secretsFromConfig["test_pass_four"].RemoteName) + assert.Equal(t, "v1", secretsFromConfig["test_pass_four"].Version) + assert.Equal(t, 12, secretsFromConfig["test_pass_four"].Length) + assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#%^&*_-+=", secretsFromConfig["test_pass_four"].Charset) + + // Has a length modifier and a charset=default,special modifier + assert.Equal(t, "test_example_com_test_pass_five_v1", secretsFromConfig["test_pass_five"].RemoteName) + assert.Equal(t, "v1", secretsFromConfig["test_pass_five"].Version) + assert.Equal(t, 12, secretsFromConfig["test_pass_five"].Length) + assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_five"].Charset) + + // Has only a charset=default,special modifier, which gets setted but ignored in the generation + assert.Equal(t, "test_example_com_test_pass_six_v1", secretsFromConfig["test_pass_six"].RemoteName) + assert.Equal(t, "v1", secretsFromConfig["test_pass_six"].Version) + assert.Equal(t, 0, secretsFromConfig["test_pass_six"].Length) + assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_six"].Charset) } func TestReadSecretsConfigWithLongDomain(t *testing.T) { diff --git a/pkg/secret/testdir/.env.sample b/pkg/secret/testdir/.env.sample index bbcc2904..104e4bb7 100644 --- a/pkg/secret/testdir/.env.sample +++ b/pkg/secret/testdir/.env.sample @@ -1,3 +1,6 @@ SECRET_TEST_PASS_ONE_VERSION=v2 SECRET_TEST_PASS_TWO_VERSION=v1 # length=10 SECRET_TEST_PASS_THREE_VERSION=v2 +SECRET_TEST_PASS_FOUR_VERSION=v1 # length=12 charset=default,safespecial +SECRET_TEST_PASS_FIVE_VERSION=v1 # length=12 charset=default,special +SECRET_TEST_PASS_SIX_VERSION=v1 # charset=default,special diff --git a/pkg/secret/testdir/compose.yaml b/pkg/secret/testdir/compose.yaml index 86cc2575..2f3ccf26 100644 --- a/pkg/secret/testdir/compose.yaml +++ b/pkg/secret/testdir/compose.yaml @@ -8,6 +8,9 @@ services: - test_pass_one - test_pass_two - test_pass_three + - test_pass_four + - test_pass_five + - test_pass_six secrets: test_pass_one: @@ -19,3 +22,12 @@ secrets: test_pass_three: external: true name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match + test_pass_four: + external: true + name: ${STACK_NAME}_test_pass_four_${SECRET_TEST_PASS_FOUR_VERSION} + test_pass_five: + external: true + name: ${STACK_NAME}_test_pass_five_${SECRET_TEST_PASS_FIVE_VERSION} + test_pass_six: + external: true + name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}