add charset modifier to secret generation (#521)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
since we need special chars in passwords for a recipe we are working on, i have added the option to specify a charset in the same way as the length can be setted. i did not change anything in the behaviour, so if length is not specified, the charset gets ignored whether it is there or not. you can specify the following: `charset=default` - Results in passgen.AlphabetDefault being used `charset=special` - Results in passgen.AlphabetSpecial being used `charset=safespecial` - Results in `!@#%^&*_-+=` being used (so it is AlphabetSpecial without the dollar sign) `charset=default,special` or `charset=special,default` - Results in passgen.AlphabetDefault + passgen.AlphabetSpecial being used `charset=default,safespecial` or `charset=safespecial,default` - Results in passgen.AlphabetDefault + `!@#%^&*_-+=` being used ((so it is AlphabetSpecial without the dollar sign) PR for the docs: toolshed/docs.coopcloud.tech#271 Co-authored-by: p4u1 <p4u1@noreply.git.coopcloud.tech> Reviewed-on: #521 Reviewed-by: p4u1 <p4u1@noreply.git.coopcloud.tech> Co-authored-by: Apfelwurm <Alexander@volzit.de> Co-committed-by: Apfelwurm <Alexander@volzit.de>
This commit is contained in:
parent
80ad6c6681
commit
d0f982456e
@ -33,6 +33,10 @@ type Secret struct {
|
|||||||
// variable. For Example:
|
// variable. For Example:
|
||||||
// SECRET_FOO=v1 # length=12
|
// SECRET_FOO=v1 # length=12
|
||||||
Length int
|
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:
|
// RemoteName is the name of the secret on the server. For example:
|
||||||
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
|
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
|
||||||
// With the following:
|
// With the following:
|
||||||
@ -43,38 +47,38 @@ type Secret struct {
|
|||||||
RemoteName string
|
RemoteName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePasswords generates passwords.
|
// GeneratePassword generates passwords.
|
||||||
func GeneratePasswords(count, length uint) ([]string, error) {
|
func GeneratePassword(length uint, charset string) (string, error) {
|
||||||
passwords, err := passgen.GeneratePasswords(
|
passwords, err := passgen.GeneratePasswords(
|
||||||
count,
|
1,
|
||||||
length,
|
length,
|
||||||
passgen.AlphabetDefault,
|
charset,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("generated %s", strings.Join(passwords, ", "))
|
log.Debugf("generated %s", strings.Join(passwords, ", "))
|
||||||
|
|
||||||
return passwords, nil
|
return passwords[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePassphrases generates human readable and rememberable passphrases.
|
// GeneratePassphrase generates human readable and rememberable passphrases.
|
||||||
func GeneratePassphrases(count uint) ([]string, error) {
|
func GeneratePassphrase() (string, error) {
|
||||||
passphrases, err := passgen.GeneratePassphrases(
|
passphrases, err := passgen.GeneratePassphrases(
|
||||||
count,
|
1,
|
||||||
passgen.PassphraseWordCountDefault,
|
passgen.PassphraseWordCountDefault,
|
||||||
rune('-'),
|
rune('-'),
|
||||||
passgen.PassphraseCasingDefault,
|
passgen.PassphraseCasingDefault,
|
||||||
passgen.WordListDefault,
|
passgen.WordListDefault,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("generated %s", strings.Join(passphrases, ", "))
|
log.Debugf("generated %s", strings.Join(passphrases, ", "))
|
||||||
|
|
||||||
return passphrases, nil
|
return passphrases[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadSecretsConfig reads secret names/versions from the recipe config. The
|
// 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.Length = length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value.Charset = resolveCharset(modifierValues["charset"])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
secretValues[secretId] = value
|
secretValues[secretId] = value
|
||||||
@ -158,6 +164,22 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
|||||||
return secretValues, nil
|
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.
|
// 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) {
|
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
|
||||||
secretsGenerated := map[string]string{}
|
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)
|
log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
|
||||||
|
|
||||||
if secret.Length > 0 {
|
if secret.Length > 0 {
|
||||||
passwords, err := GeneratePasswords(1, uint(secret.Length))
|
password, err := GeneratePassword(uint(secret.Length), secret.Charset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- err
|
ch <- err
|
||||||
return
|
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") {
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
log.Warnf("%s already exists", secret.RemoteName)
|
log.Warnf("%s already exists", secret.RemoteName)
|
||||||
ch <- nil
|
ch <- nil
|
||||||
@ -191,15 +213,15 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
|||||||
|
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
secretsGenerated[secretName] = passwords[0]
|
secretsGenerated[secretName] = password
|
||||||
} else {
|
} else {
|
||||||
passphrases, err := GeneratePassphrases(1)
|
passphrase, err := GeneratePassphrase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- err
|
ch <- err
|
||||||
return
|
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") {
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
log.Warnf("%s already exists", secret.RemoteName)
|
log.Warnf("%s already exists", secret.RemoteName)
|
||||||
ch <- nil
|
ch <- nil
|
||||||
@ -211,7 +233,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
|||||||
|
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
secretsGenerated[secretName] = passphrases[0]
|
secretsGenerated[secretName] = passphrase
|
||||||
}
|
}
|
||||||
ch <- nil
|
ch <- nil
|
||||||
}(n, v)
|
}(n, v)
|
||||||
|
@ -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, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
|
||||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
|
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
|
||||||
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
|
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
|
||||||
|
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset)
|
||||||
|
|
||||||
// Has a length modifier
|
// Has a length modifier
|
||||||
assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
|
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, "v1", secretsFromConfig["test_pass_two"].Version)
|
||||||
assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
|
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
|
// 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, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
|
||||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
|
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
|
||||||
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
|
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) {
|
func TestReadSecretsConfigWithLongDomain(t *testing.T) {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
SECRET_TEST_PASS_ONE_VERSION=v2
|
SECRET_TEST_PASS_ONE_VERSION=v2
|
||||||
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
|
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
|
||||||
SECRET_TEST_PASS_THREE_VERSION=v2
|
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
|
||||||
|
@ -8,6 +8,9 @@ services:
|
|||||||
- test_pass_one
|
- test_pass_one
|
||||||
- test_pass_two
|
- test_pass_two
|
||||||
- test_pass_three
|
- test_pass_three
|
||||||
|
- test_pass_four
|
||||||
|
- test_pass_five
|
||||||
|
- test_pass_six
|
||||||
|
|
||||||
secrets:
|
secrets:
|
||||||
test_pass_one:
|
test_pass_one:
|
||||||
@ -19,3 +22,12 @@ secrets:
|
|||||||
test_pass_three:
|
test_pass_three:
|
||||||
external: true
|
external: true
|
||||||
name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match
|
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}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user