add charset modifier to secret generation (#521)

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: toolshed/abra#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:
2025-03-21 10:29:21 +00:00
committed by p4u1
parent 80ad6c6681
commit d0f982456e
4 changed files with 75 additions and 17 deletions

View File

@ -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)