2021-09-04 21:29:05 +00:00
|
|
|
// Package secret provides functionality for generating and storing secrets
|
|
|
|
// both in a remote swarm and locally within supported storage such as pass
|
|
|
|
// stores.
|
2021-07-30 20:53:51 +00:00
|
|
|
package secret
|
|
|
|
|
|
|
|
import (
|
2023-10-04 13:08:59 +00:00
|
|
|
"context"
|
2021-07-31 10:47:09 +00:00
|
|
|
"fmt"
|
2023-09-25 08:31:59 +00:00
|
|
|
"slices"
|
2021-07-31 10:47:09 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2022-03-12 15:59:45 +00:00
|
|
|
"sync"
|
2021-07-31 10:47:09 +00:00
|
|
|
|
2021-09-05 19:37:03 +00:00
|
|
|
"coopcloud.tech/abra/pkg/client"
|
2023-10-04 13:08:59 +00:00
|
|
|
"coopcloud.tech/abra/pkg/config"
|
2023-09-25 08:31:59 +00:00
|
|
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
|
|
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
2022-11-14 14:18:54 +00:00
|
|
|
"github.com/decentral1se/passgen"
|
2023-10-04 13:08:59 +00:00
|
|
|
"github.com/docker/docker/api/types"
|
2023-01-31 15:09:09 +00:00
|
|
|
dockerClient "github.com/docker/docker/client"
|
2021-09-10 22:54:02 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2021-07-30 20:53:51 +00:00
|
|
|
)
|
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
// SecretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config
|
2021-09-04 21:35:56 +00:00
|
|
|
// secret definition.
|
2023-11-29 17:35:01 +00:00
|
|
|
type SecretValue struct {
|
2021-07-31 10:47:09 +00:00
|
|
|
Version string
|
|
|
|
Length int
|
|
|
|
}
|
|
|
|
|
2021-09-04 21:39:38 +00:00
|
|
|
// GeneratePasswords generates passwords.
|
2021-07-30 20:53:51 +00:00
|
|
|
func GeneratePasswords(count, length uint) ([]string, error) {
|
|
|
|
passwords, err := passgen.GeneratePasswords(
|
|
|
|
count,
|
|
|
|
length,
|
|
|
|
passgen.AlphabetDefault,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-12-25 01:03:09 +00:00
|
|
|
logrus.Debugf("generated %s", strings.Join(passwords, ", "))
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2021-07-30 20:53:51 +00:00
|
|
|
return passwords, nil
|
|
|
|
}
|
|
|
|
|
2021-09-04 21:39:38 +00:00
|
|
|
// GeneratePassphrases generates human readable and rememberable passphrases.
|
2021-07-30 20:53:51 +00:00
|
|
|
func GeneratePassphrases(count uint) ([]string, error) {
|
|
|
|
passphrases, err := passgen.GeneratePassphrases(
|
|
|
|
count,
|
|
|
|
passgen.PassphraseWordCountDefault,
|
|
|
|
rune('-'),
|
|
|
|
passgen.PassphraseCasingDefault,
|
|
|
|
passgen.WordListDefault,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-12-25 01:03:09 +00:00
|
|
|
logrus.Debugf("generated %s", strings.Join(passphrases, ", "))
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2021-07-30 20:53:51 +00:00
|
|
|
return passphrases, nil
|
|
|
|
}
|
2021-07-31 10:47:09 +00:00
|
|
|
|
2023-09-25 08:31:59 +00:00
|
|
|
// 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.
|
2023-11-29 17:35:01 +00:00
|
|
|
func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName string) (map[string]SecretValue, error) {
|
|
|
|
appEnv, appModifiers, err := config.ReadEnvWithModifiers(appEnvPath)
|
2023-10-09 12:37:20 +00:00
|
|
|
if err != nil {
|
2023-11-29 17:35:01 +00:00
|
|
|
return nil, err
|
2023-10-09 12:37:20 +00:00
|
|
|
}
|
|
|
|
|
2023-09-25 08:31:59 +00:00
|
|
|
opts := stack.Deploy{Composefiles: composeFiles}
|
|
|
|
config, err := loader.LoadComposefile(opts, appEnv)
|
|
|
|
if err != nil {
|
2023-11-29 17:35:01 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2023-12-04 08:29:56 +00:00
|
|
|
// Read the compose files without injecting environment variables.
|
2023-11-29 17:35:01 +00:00
|
|
|
configWithoutEnv, err := loader.LoadComposefile(opts, map[string]string{}, loader.SkipInterpolation)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2023-09-25 08:31:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var enabledSecrets []string
|
|
|
|
for _, service := range config.Services {
|
|
|
|
for _, secret := range service.Secrets {
|
|
|
|
enabledSecrets = append(enabledSecrets, secret.Source)
|
2021-07-31 10:47:09 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2023-09-25 08:31:59 +00:00
|
|
|
if len(enabledSecrets) == 0 {
|
|
|
|
logrus.Debugf("not generating app secrets, none enabled in recipe config")
|
2023-11-29 17:35:01 +00:00
|
|
|
return nil, nil
|
2023-09-25 08:31:59 +00:00
|
|
|
}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
secretValues := map[string]SecretValue{}
|
2023-09-25 13:51:15 +00:00
|
|
|
for secretId, secretConfig := range config.Secrets {
|
|
|
|
if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" {
|
2023-11-29 17:35:01 +00:00
|
|
|
return nil, fmt.Errorf("missing version for secret? (%s)", secretId)
|
2023-09-25 08:31:59 +00:00
|
|
|
}
|
|
|
|
|
2023-09-25 13:51:15 +00:00
|
|
|
if !(slices.Contains(enabledSecrets, secretId)) {
|
2023-10-04 13:09:14 +00:00
|
|
|
logrus.Warnf("%s not enabled in recipe config, skipping", secretId)
|
2023-09-25 08:31:59 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-09-25 13:51:15 +00:00
|
|
|
lastIdx := strings.LastIndex(secretConfig.Name, "_")
|
|
|
|
secretVersion := secretConfig.Name[lastIdx+1:]
|
2023-11-29 17:35:01 +00:00
|
|
|
value := SecretValue{Version: secretVersion}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
// Check if the length modifier is set for this secret.
|
2023-12-04 08:29:56 +00:00
|
|
|
for envName, modifierValues := range appModifiers {
|
2023-11-29 17:35:01 +00:00
|
|
|
// configWithoutEnv contains the raw name as defined in the compose.yaml
|
2023-12-04 08:29:56 +00:00
|
|
|
// The name will look something like this:
|
|
|
|
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
|
|
|
|
// To check if the current modifier is for the current secret we check
|
|
|
|
// if the raw name contains the env name (e.g. SECRET_TEST_PASS_TWO_VERSION).
|
|
|
|
if !strings.Contains(configWithoutEnv.Secrets[secretId].Name, envName) {
|
2023-11-29 17:35:01 +00:00
|
|
|
continue
|
|
|
|
}
|
2023-12-04 08:29:56 +00:00
|
|
|
lengthRaw, ok := modifierValues["length"]
|
2023-11-29 17:35:01 +00:00
|
|
|
if ok {
|
|
|
|
length, err := strconv.Atoi(lengthRaw)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
value.Length = length
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
secretValues[secretId] = value
|
2021-09-10 22:54:02 +00:00
|
|
|
}
|
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
return secretValues, nil
|
2021-07-31 10:47:09 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 21:39:38 +00:00
|
|
|
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
|
2023-11-29 17:35:01 +00:00
|
|
|
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, appName, server string) (map[string]string, error) {
|
|
|
|
secretsGenerated := map[string]string{}
|
2022-03-12 15:59:45 +00:00
|
|
|
var mutex sync.Mutex
|
|
|
|
var wg sync.WaitGroup
|
2023-11-29 17:35:01 +00:00
|
|
|
ch := make(chan error, len(secrets))
|
|
|
|
for n, v := range secrets {
|
2022-03-12 15:59:45 +00:00
|
|
|
wg.Add(1)
|
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
go func(secretName string, secret SecretValue) {
|
2022-03-12 15:59:45 +00:00
|
|
|
defer wg.Done()
|
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secret.Version)
|
2021-12-25 01:03:09 +00:00
|
|
|
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
|
2022-03-12 15:59:45 +00:00
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
if secret.Length > 0 {
|
|
|
|
passwords, err := GeneratePasswords(1, uint(secret.Length))
|
2021-07-31 13:50:04 +00:00
|
|
|
if err != nil {
|
|
|
|
ch <- err
|
|
|
|
return
|
|
|
|
}
|
2022-03-12 15:59:45 +00:00
|
|
|
|
2023-01-31 15:09:09 +00:00
|
|
|
if err := client.StoreSecret(cl, secretRemoteName, passwords[0], server); err != nil {
|
2021-12-13 11:29:26 +00:00
|
|
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
|
|
|
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
|
|
|
ch <- nil
|
|
|
|
} else {
|
|
|
|
ch <- err
|
|
|
|
}
|
2021-07-31 13:50:04 +00:00
|
|
|
return
|
|
|
|
}
|
2022-03-12 15:59:45 +00:00
|
|
|
|
|
|
|
mutex.Lock()
|
|
|
|
defer mutex.Unlock()
|
2023-11-29 17:35:01 +00:00
|
|
|
secretsGenerated[secretName] = passwords[0]
|
2021-07-31 13:50:04 +00:00
|
|
|
} else {
|
|
|
|
passphrases, err := GeneratePassphrases(1)
|
|
|
|
if err != nil {
|
|
|
|
ch <- err
|
|
|
|
return
|
|
|
|
}
|
2022-03-12 15:59:45 +00:00
|
|
|
|
2023-01-31 15:09:09 +00:00
|
|
|
if err := client.StoreSecret(cl, secretRemoteName, passphrases[0], server); err != nil {
|
2021-12-13 11:29:26 +00:00
|
|
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
|
|
|
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
|
|
|
ch <- nil
|
|
|
|
} else {
|
|
|
|
ch <- err
|
|
|
|
}
|
|
|
|
return
|
2021-07-31 13:50:04 +00:00
|
|
|
}
|
2022-03-12 15:59:45 +00:00
|
|
|
|
|
|
|
mutex.Lock()
|
|
|
|
defer mutex.Unlock()
|
2023-11-29 17:35:01 +00:00
|
|
|
secretsGenerated[secretName] = passphrases[0]
|
2021-07-31 10:47:09 +00:00
|
|
|
}
|
2021-07-31 13:50:04 +00:00
|
|
|
ch <- nil
|
2023-09-25 08:31:59 +00:00
|
|
|
}(n, v)
|
2021-07-31 13:50:04 +00:00
|
|
|
}
|
|
|
|
|
2022-03-12 15:59:45 +00:00
|
|
|
wg.Wait()
|
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
for range secrets {
|
2021-07-31 13:50:04 +00:00
|
|
|
err := <-ch
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2021-07-31 10:47:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
logrus.Debugf("generated and stored %v on %s", secrets, server)
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
return secretsGenerated, nil
|
2021-07-31 10:47:09 +00:00
|
|
|
}
|
2023-10-04 13:08:59 +00:00
|
|
|
|
|
|
|
type secretStatus struct {
|
|
|
|
LocalName string
|
|
|
|
RemoteName string
|
|
|
|
Version string
|
|
|
|
CreatedOnRemote bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type secretStatuses []secretStatus
|
|
|
|
|
|
|
|
// PollSecretsStatus checks status of secrets by comparing the local recipe
|
|
|
|
// config and deploymend server state.
|
|
|
|
func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses, error) {
|
|
|
|
var secStats secretStatuses
|
|
|
|
|
|
|
|
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
|
|
|
|
if err != nil {
|
|
|
|
return secStats, err
|
|
|
|
}
|
|
|
|
|
2023-10-09 12:37:20 +00:00
|
|
|
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
|
2023-10-04 13:08:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return secStats, err
|
|
|
|
}
|
|
|
|
|
|
|
|
filters, err := app.Filters(false, false)
|
|
|
|
if err != nil {
|
|
|
|
return secStats, err
|
|
|
|
}
|
|
|
|
|
|
|
|
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
|
|
|
|
if err != nil {
|
|
|
|
return secStats, err
|
|
|
|
}
|
|
|
|
|
|
|
|
remoteSecretNames := make(map[string]bool)
|
|
|
|
for _, cont := range secretList {
|
|
|
|
remoteSecretNames[cont.Spec.Annotations.Name] = true
|
|
|
|
}
|
|
|
|
|
2023-11-29 17:35:01 +00:00
|
|
|
for secretName, val := range secretsConfig {
|
2023-10-04 13:08:59 +00:00
|
|
|
createdRemote := false
|
|
|
|
|
|
|
|
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
|
|
|
|
if _, ok := remoteSecretNames[secretRemoteName]; ok {
|
|
|
|
createdRemote = true
|
|
|
|
}
|
|
|
|
|
|
|
|
secStats = append(secStats, secretStatus{
|
|
|
|
LocalName: secretName,
|
|
|
|
RemoteName: secretRemoteName,
|
|
|
|
Version: val.Version,
|
|
|
|
CreatedOnRemote: createdRemote,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return secStats, nil
|
|
|
|
}
|