Gérer les fichiers de configuration sur AWS avec AWS Secrets Manager

Gérer les fichiers de configuration sur AWS avec AWS Secrets Manager

1 mars 2023

Introduction

image

Il y a un aspect que nous avons tendance à négliger mais qui s’avère souvent gênant : c’est le stockage de tous les secrets utilisés par le serveur dans un fichier nommé config.yaml.

Je me suis posé la question : “Est-ce vraiment correct de conserver ce fichier en tant que source sur Github ?”

Github offre une fonctionnalité de gestion des secrets pour les dépôts, mais on ressent une certaine exposition peu sécurisée.

Première pensée : les secrets GitHub

  • J’ai déjà pensé à utiliser la fonctionnalité de secrets de Github pour masquer toutes les variables nécessaires à la build.
  • Cependant, cette méthode est peut-être avantageuse pour masquer certaines variables, mais il est difficile de comprendre quand ces variables sont injectées, et comme cela fonctionne uniquement dans l’environnement limité de Github, il semblait nécessaire de trouver quelque chose de plus général.

Objectif

Au cours de mes réflexions pour gérer les secrets, j’ai défini trois critères :

  1. La barrière d’entrée doit être basse et facile à utiliser afin que d’autres membres de l’équipe puissent les voir et les utiliser immédiatement.
  2. La méthode doit être suffisamment générale pour être utilisable par différents services.
  3. Éviter le sur-engineering et lire/consommer le config avec un minimum de modifications.

ChatGPT

  • Je manquais de connaissances pertinentes. J’ai d’abord cherché quelles alternatives existaient. Auparavant, j’aurais demandé de l’aide à Google, mais j’ai utilisé ChatGPT, un bon outil d’exploration.

    image

  • Les systèmes de gestion de clés fournis par Vault et d’autres services cloud ont attiré mon attention. Comme Acloset utilise de nombreux services fournis par AWS, j’ai prêté attention à AWS Secrets Manager qui s’intègre bien avec AWS.

    image

  • J’ai rapidement pris connaissance des principales fonctionnalités.

AWS Secrets Manager

  • Plutôt que de perdre du temps à réfléchir, j’ai immédiatement testé.

    image

Étapes

  • En plus de stocker des clés et des valeurs simples, il est possible de configurer les secrets en saisissant des informations d’adresse et de port pour AWS RDS et d’autres bases de données.

    image

  • Puisque l’objectif est de crypter l’actuel config.yaml utilisé dans le service, j’ai exploré d’autres options de secrets de sécurité.

    image

  • Bien qu’il soit possible de générer des secrets via les paires clé-valeur AWS, j’ai cliqué sur texte brut car je voulais saisir le config.yaml entier.

    image

  • Même si la valeur par défaut est {} et que cela peut être interprété comme ne prenant en charge que le format JSON, c’est plutôt tout texte qui est possible, donc j’ai transféré l’intégralité du config.yaml de notre serveur.

    image
  • La prochaine étape est la configuration du secret. Comme le nom du secret devient une clé pour accéder au secret via le client AWS, il est sage de choisir un bon nom. J’ai choisi someapp/config/dev.

    image
  • C’est la dernière étape. Bien que je n’ai pas configuré de rotation pour l’instant, il semble y avoir une fonctionnalité utile qui pourrait être exploitée lorsque la rotation ou l’utilisation de clés nécessitant une rotation est nécessaire. Cliquez sur enregistrer sans configurer.

    image

Rotation des secrets

  • Bien qu’il existe des secrets permanents (informations sur les bases de données, mots de passe, e-mails, etc.), il existe aussi des secrets dynamiques. Ex) information de jeton d’API pour des services externes
  • AWS Secrets Manager fournit un moyen simple et pratique de gérer les clés dynamiques par le biais de fonctions AWS Lambda, permettant de les rafraîchir/enregistrer périodiquement.

Serveur

  • Maintenant que les configurations sont toutes gérées par SecretManager, les AWS Access Key ID et AWS Secret Access Key sont gérés par le serveur. En réfléchissant à la manière de les injecter au moment de la compilation, j’ai opté pour les secrets GitHub.

Scénario d’injection

  • Le scénario d’injection de secret est défini comme suit.

    image

Étapes

Actions

Modifiez les arguments dans GitHub Actions lors de la build.

      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
	  {% raw %}
          tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            ARG_ENVIRONMENT=${{ env.ENVIRONMENT }}
            ARG_AWS_REGION=${{ env.AWS_REGION }}
            ARG_AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}
            ARG_AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}
	  {% endraw %}

Supprimez la partie qui injecte l’ancien chemin de CONFIG_FILE et assurez-vous de recevoir l’information nécessaire pour accéder à AWS (Région, AWS Access Key ID, AWS Secret Access Key, information de l’environnement (environnement de build: ex: dev)).

Dockerfile

Dans le Dockerfile, recevez ces arguments pour permettre au serveur d’y accéder via des flags.

ARG ARG_ENVIRONMENT
ARG ARG_AWS_ACCESS_KEY_ID
ARG ARG_AWS_SECRET_ACCESS_KEY
ARG ARG_AWS_REGION

ENV ENVIRONMENT=$ARG_ENVIRONMENT
ENV AWS_ACCESS_KEY_ID=$ARG_AWS_ACCESS_KEY_ID
ENV AWS_SECRET_ACCESS_KEY=$ARG_AWS_SECRET_ACCESS_KEY
ENV AWS_REGION=$ARG_AWS_REGION

CMD ["sh", "-c", "/app/someapp \
    --aws-access-key-id $AWS_ACCESS_KEY_ID \
    --aws-secret-access-key $AWS_SECRET_ACCESS_KEY \
    --aws-region $AWS_REGION \
    --environment $ENVIRONMENT \

Serveur

Au lieu d’injecter le chemin de configuration, faites en sorte qu’il reçoive AWS Access Key ID et Secret Access Key, et ajustez le flag pour recevoir chaque environnement.

func ParseFlags() *Flags {
	flags := &Flags{}
	flag.StringVar(&flags.AWSAccessKeyID, "aws-access-key-id", "", "aws access key id")
	flag.StringVar(&flags.AWSSecretAccessKey, "aws-secret-access-key", "", "aws secret access key")
	flag.StringVar(&flags.AWSRegion, "aws-region", "", "aws region")
	flag.StringVar(&flags.Environment, "environment", "", "environment")
	flag.Parse()
	return flags
}

Ajustez l’ordre d’injection des dépendances pour qu’AWS soit créé en premier.

	app := fx.New(
		fx.Provide(
			cli.ParseFlags,

			aws.NewAwsSession, // this!
			aws.NewAwsSecretsManager, // this!

			config.New,
			echoRouter.New,
      // ... (interruption)

Dans config.New(), recevez secretManager et flags pour obtenir secretString, puis Unmarshal en yaml avant de retourner le Config.

func New(flags *cli.Flags, secretsManager *secretsmanager.SecretsManager) *Config {
	var (
		config    Config // structure config à recevoir
		configKey = fmt.Sprintf(SecretManagerConfigKey, flags.Environment) // "someapp/config/dev"
	)

	result, err := secretsManager.GetSecretValue(&secretsmanager.GetSecretValueInput{
		SecretId:     aws.String(configKey),
		VersionStage: aws.String(versionStage),
	})
	if err != nil {
		log.New().Fatalf("err cannot get config from secretsManager key: %v", configKey)
	}

	if err = yaml.Unmarshal([]byte(*result.SecretString), &config); err != nil {
		log.New().Fatalf("err unmarshal yaml from secretString err: %v", err)
	}

	return &config
}

Résultat

Voici la vue normale de la connexion terminée et du serveur fonctionnant correctement.

image