// Package secret provides functionality for generating and storing secrets // both in a remote swarm and locally within supported storage such as pass // stores. package secret import ( "fmt" "slices" "strconv" "strings" "sync" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/upstream/stack" loader "coopcloud.tech/abra/pkg/upstream/stack" "github.com/decentral1se/passgen" dockerClient "github.com/docker/docker/client" "github.com/sirupsen/logrus" ) // secretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config // secret definition. type secretValue struct { Version string Length int } // GeneratePasswords generates passwords. func GeneratePasswords(count, length uint) ([]string, error) { passwords, err := passgen.GeneratePasswords( count, length, passgen.AlphabetDefault, ) if err != nil { return nil, err } logrus.Debugf("generated %s", strings.Join(passwords, ", ")) return passwords, nil } // GeneratePassphrases generates human readable and rememberable passphrases. func GeneratePassphrases(count uint) ([]string, error) { passphrases, err := passgen.GeneratePassphrases( count, passgen.PassphraseWordCountDefault, rune('-'), passgen.PassphraseCasingDefault, passgen.WordListDefault, ) if err != nil { return nil, err } logrus.Debugf("generated %s", strings.Join(passphrases, ", ")) return passphrases, nil } // ReadSecretsConfig reads secret names/versions from the recipe config. The // function generalises appEnv/composeFiles because some times you have an app // and some times you don't (as the caller). We need to be able to handle the // "app new" case where we pass in the .env.sample and the "secret generate" // case where the app is created. func ReadSecretsConfig(appEnv map[string]string, composeFiles []string, recipeName string) (map[string]string, error) { secretConfigs := make(map[string]string) opts := stack.Deploy{Composefiles: composeFiles} config, err := loader.LoadComposefile(opts, appEnv) if err != nil { return secretConfigs, err } var enabledSecrets []string for _, service := range config.Services { for _, secret := range service.Secrets { enabledSecrets = append(enabledSecrets, secret.Source) } } if len(enabledSecrets) == 0 { logrus.Debugf("not generating app secrets, none enabled in recipe config") return secretConfigs, nil } for secretId, secretConfig := range config.Secrets { if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" { return secretConfigs, fmt.Errorf("missing version for secret? (%s)", secretId) } if !(slices.Contains(enabledSecrets, secretId)) { logrus.Warnf("%s not enabled in recipe config, not generating", secretId) continue } lastIdx := strings.LastIndex(secretConfig.Name, "_") secretVersion := secretConfig.Name[lastIdx+1:] secretConfigs[secretId] = secretVersion } return secretConfigs, nil } func ParseSecretValue(secret string) (secretValue, error) { values := strings.Split(secret, "#") if len(values) == 0 { return secretValue{}, fmt.Errorf("unable to parse %s", secret) } if len(values) == 1 { return secretValue{Version: values[0], Length: 0}, nil } split := strings.Split(values[1], "=") parsed := split[len(split)-1] stripped := strings.ReplaceAll(parsed, " ", "") length, err := strconv.Atoi(stripped) if err != nil { return secretValue{}, err } version := strings.ReplaceAll(values[0], " ", "") logrus.Debugf("parsed version %s and length '%v' from %s", version, length, secret) return secretValue{Version: version, Length: length}, nil } // GenerateSecrets generates secrets locally and sends them to a remote server for storage. func GenerateSecrets(cl *dockerClient.Client, secretsFromConfig map[string]string, appName, server string) (map[string]string, error) { secrets := make(map[string]string) var mutex sync.Mutex var wg sync.WaitGroup ch := make(chan error, len(secretsFromConfig)) for n, v := range secretsFromConfig { wg.Add(1) go func(secretName, secretValue string) { defer wg.Done() parsedSecretValue, err := ParseSecretValue(secretValue) if err != nil { ch <- err return } secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, parsedSecretValue.Version) logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server) if parsedSecretValue.Length > 0 { passwords, err := GeneratePasswords(1, uint(parsedSecretValue.Length)) if err != nil { ch <- err return } if err := client.StoreSecret(cl, secretRemoteName, passwords[0], server); err != nil { if strings.Contains(err.Error(), "AlreadyExists") { logrus.Warnf("%s already exists, moving on...", secretRemoteName) ch <- nil } else { ch <- err } return } mutex.Lock() defer mutex.Unlock() secrets[secretName] = passwords[0] } else { passphrases, err := GeneratePassphrases(1) if err != nil { ch <- err return } if err := client.StoreSecret(cl, secretRemoteName, passphrases[0], server); err != nil { if strings.Contains(err.Error(), "AlreadyExists") { logrus.Warnf("%s already exists, moving on...", secretRemoteName) ch <- nil } else { ch <- err } return } mutex.Lock() defer mutex.Unlock() secrets[secretName] = passphrases[0] } ch <- nil }(n, v) } wg.Wait() for range secretsFromConfig { err := <-ch if err != nil { return nil, err } } logrus.Debugf("generated and stored %s on %s", secrets, server) return secrets, nil }