Le Blog Amazon Web Services

Sécuriser les identifiants de base de données dans une fonction Lambda avec AWS Secrets Manager

En tant qu’architectes solution AWS, nous aidons régulièrement nos clients à concevoir des architecture et à déployer des applications métiers basées sur des APIs et des microservices qui s’appuient sur des service Serverless comme AWS Lambda et des base de données comme Amazon Relational Database Service (Amazon RDS). Nos clients peuvent bénéficier des avantages de ces services managés pour libérer leurs équipes d’opérations à réaliser sur l’infrastructure ainsi que des tâches communes d’administration comme le déploiement de correctifs de sécurité, la maintenance applicative ou les prévisions de capacités nécessaires au bon fonctionnement de leurs propres services.

Dans cet article, nous vous montrons comment utiliser AWS Secrets Manager pour sécuriser les identifiants de connexion à la base de données et comment les partager avec des fonctions Lambda qui les utiliseront pour se connecter et interroger le service Amazon RDS – sans avoir à inscrire en dur dans le code ces identifiants et sans avoir à les passer en variables d’environnement. Cette approche vous permet de sécuriser cette partie du code et ainsi de mieux protéger vos bases de données. Une bonne pratique consiste à changer régulièrement les identifiants ayant une durée de vie longue afin de s’assurer que l’accès reste bien sécurisé. La rotation manuelle des identifiants peut parfois être lourde et AWS Secrets Manager vous aide à gérer cette rotation pour votre base de données Amazon RDS.

Description de la solution

Dans cette exemple, nous allons utiliser un template AWS CloudFormation pour déployer les composants suivants afin de tester le point d’accès de l’API depuis votre navigateur :

  • Une base de données RDS MySQL sur une instance db.t2.micro
  • Deux fonctions Lambdas avec les roles AWS IAM (AWS Identity & Access Management) et les politiques associées incluant l’accès à AWS Secrets Manager :
    • LambdaRDSCFNInit : cette fonction Lambda va être exécutée immédiatement après la création de la stack CloudFormation. Elle va créer la table “Employees” dans la base de données et insérer 3 enregistrement exemples,
    • LambdaRDSTest : cette fonction va interroger la table “Employees” et renvoyer le nombre d’enregistrements de dans un chaine de caractères au format HTML,
  • Une API RESTful sur AWS API Gateway avec une méthode “GET”.

Voici un diagramme d’architecture des ressources qui vont être crées avec le déploiement de la stack CloudFormation :

SSM avec Lambda - Architecture de la solution

  1. Les clients appellent l’API RESTful hébergée par AWS API Gateway,
  2. API Gateway va exécuter la fonction Lambda LambdaRDSTest,
  3. La fonction lambda LambdaRDSTest va récupérer les identifiants de la base de données en utilisant l’API de Secrets Manager,
  4. La fonction Lambda va se connecter à la base de données RDS en utilisant les identifiants récupérées dans Secrets Manager et retourner les résultats de la requête.

Le code source de cet exemple est disponible sous Github : https://github.com/awslabs/automating-governance-sample/tree/master/AWS-SecretsManager-Lambda-RDS-blog.

Déploiement de la solution d’exemple

Configurer le lancement de la stack CloudFormation en cliquant sur le bouton “Launch Stack” ci-après. Si vous n’êtes pas identifié sur votre compte AWS, suivez la procédure qui vous sera proposée.

Par défaut, la stack sera déployée sur la region us-east-1. Si vous voulez deployer cette stack dans une autre région, télécharger le code depuis GitHub avec le lien présenté précédemment, placez le fichier du code de la lambda dans un bucket créé dans la région de votre choix et effectuez les changements nécessaires dans le template CloudFormation pour pointer vers le bucket S3 que vous avez créé. (Référez vous au guide de l’utilisateur de AWS CloudFormation pour des détails complémentaires sur la création de stacks avec la console AWS CloudFormation).

Select this image to open a link that starts building the CloudFormation stackEnsuite, suivez les étapes suivantes pour exécuter la stack :

  1. Laissez la valeur proposée par défaut pour l’emplacement du template et cliquez sur Suivant.
  2. Dans la page des détails de la stack, les paramètres seront pré-renseignés, incluant le nom de la base de données et le nom de l’utilisateur de la base de données. Cliquez sur Suivant.
  3. Dans l’écran des options, cliquez sur Suivant
  4. Dans l’écran de confirmation finale, sélectionnez les 3 cases à cocher puis cliquez sur “Créer un jeu de modifications
  5. Une fois le change set créé, cliquez sur “Executer” pour lancer la création de la stack
  6. La stack mettra entre 10 et 15 minutes a être créée. Une fois créée avec succès, sélectionnez l’onglet “Sorties” de la stack puis cliquez sur le lien.

Cette dernière action exécutera le code de la fonction Lambda, qui interrogera la table “Employees” de la base de données MySQL et retournera le résultat du comptage des enregistrements à l’API. Ce résultat sera proposé en retour de la requête à l’API dans l’écran suivant :

Votre stack est déployée avec succès et vous avez testé votre point d’accès à l’API incluant la fonction Lambda et la base de données RDS MySQL. La fonction lambda a réussi à communiquer avec la base de données et a été capable de renvoyer les résultats.

Que s’est-t-il passé durant la création de la stack ?

La stack CloudFormation a déployé une base de données RDS MySQL avec un mot de passe généré aléatoirement en utilisant un secret. Maintenant que le secret a été généré, la stack CloudFormation va utiliser une référence dynamique pour résoudre la valeur du mot de passe depuis Secrets Manager afin de créer l’instance RDS. La référence dynamique permet sous un format concis et puissant de spécifier des valeurs externes qui sont stockés dans d’autres services AWS, comme AWS Secrets Manager. Cette référence dynamique permet de garantir que CloudFormation n’enregistrera ou ne persistera pas la valeur résolue, gardant ainsi le mot de passe en sécurité. Le template CloudFormation va aussi créer une fonction Lambda pour activer la rotation automatique du mot de passe de la base de données RDS MySQL tous les 30 jours. La rotation des identifiants natifs améliore votre posture de sécurité en éliminant la nécessité de gérer manuellement les mots de passes durant son cycle de vie.

Voici la partie du code CloudFormation qui couvre ces points :

# Ceci est une ressource de type Secret avec un mot de passe généré aléatoirement dans le champ SecretString du JSON 
MyRDSInstanceRotationSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
    Description: 'This is my rds instance secret'
    GenerateSecretString:
        SecretStringTemplate: !Sub '{"username": "${!Ref RDSUserName}"}'
        GenerateStringKey: 'password'
        PasswordLength: 16
        ExcludeCharacters: '"@/\'
    Tags:
    -
        Key: AppNam
        Value: MyApp

# Ceci est une resource de type instance RDS. Le nom de l'utilisateur principal et le mot de passe utilisent des références dynamiques pour résoudre les valeurs
# depuis SecretsManager. La référence dynamique garantie que CloudFormation ne loggera pas ou ne persistera pas la valeur resolue 
# Nous utilisons "!Ref" pour référencer l'identifiant logique du Secret car le Secret est généré par CloudFormation
MyDBInstance2:
    Type: AWS::RDS::DBInstance
    Properties:
    AllocatedStorage: 20
    DBInstanceClass: db.t2.micro
    DBName: !Ref RDSDBName
    Engine: mysql
    MasterUsername: !Ref RDSUserName
    MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref MyRDSInstanceRotationSecret, ':SecretString:password}}' ]]
    MultiAZ: False
    PubliclyAccessible: False      
    StorageType: gp2
    DBSubnetGroupName: !Ref myDBSubnetGroup
    VPCSecurityGroups:
    - !Ref RDSSecurityGroup
    BackupRetentionPeriod: 0
    DBInstanceIdentifier: 'rotation-instance'

# Ceci permet de lier le secret et l'instance RDS 
SecretRDSInstanceAttachment:
    Type: AWS::SecretsManager::SecretTargetAttachment
    Properties:
    SecretId: !Ref MyRDSInstanceRotationSecret
    TargetId: !Ref MyDBInstance2
    TargetType: AWS::RDS::DBInstance

# Ceci permet de planifier la rotation. La rotation est effectuée par la lambda de rotation pour le Secret qui est référencé
# La première rotation est effectuée à la création de la resource. Les suivantes seront effectuées selon la planification configurée
# Cette resource est dépendante de la resource SecretRDSInstanceAttachment car la rotation a besoin que toutes les informations soient disponibles pour aboutir
MySecretRotationSchedule:
    Type: AWS::SecretsManager::RotationSchedule
    DependsOn: SecretRDSInstanceAttachment
    Properties:
    SecretId: !Ref MyRDSInstanceRotationSecret
    RotationLambdaARN: !GetAtt MyRotationLambda.Arn
    RotationRules:
        AutomaticallyAfterDays: 30

# Ceci est la lambda qui effectuera les rotations.
# Pour plus d'informations sur les rotations avec lambda, consultez :
# https://docs.aws.amazon.com/fr_fr/secretsmanager/latest/userguide/rotating-secrets.html
# L'example suivant nécessite que le code de la lambda soit disponible dans un bucket S3
# et que la rotation se fasse pour une base mysql. 
MyRotationLambda:
    Type: AWS::Serverless::Function
    Properties:
    Runtime: python2.7
    Role: !GetAtt MyLambdaExecutionRole.Arn
    Handler: mysql_secret_rotation.lambda_handler
    Description: 'This is a lambda to rotate MySql user passwd'
    FunctionName: 'cfn-rotation-lambda'
    CodeUri: 's3://devsecopsblog/code.zip'      
    Environment:
        Variables:
        SECRETS_MANAGER_ENDPOINT: !Sub 'https://secretsmanager.${AWS::Region}.amazonaws.com' 

Vérification de la solution

Pour être certain que tous les éléments sont configurés correctement, vous pouvez inspecter le code de la fonction Lambda qui interroge la base de données en suivant les étapes décrites ici :

  1. Naviguez vers la page du service AWS Lambda depuis la console,
  2. Dans la liste des fonctions Lambda, cliquez sur la fonction avec le nom scm2-LambdaRDSTest-...
  3. Localisez les variables d’environnements situés en bas de la page de détails. Aucun mot de passe de base de données n’est fourni à la fonction dans les variables d’environnements

 import sys
    import pymysql
    import boto3
    import botocore
    import json
    import random
    import time
    import os
    from botocore.exceptions import ClientError
    
    # Paramètres RDS
    rds_host = os.environ['RDS_HOST']
    name = os.environ['RDS_USERNAME']
    db_name = os.environ['RDS_DB_NAME']
    helperFunctionARN = os.environ['HELPER_FUNCTION_ARN']
    
    secret_name = os.environ['SECRET_NAME']
    my_session = boto3.session.Session()
    region_name = my_session.region_name
    conn = None
    
    # Récupération de la ressource
    lambdaClient = boto3.client('lambda')
    
    
    def invokeConnCountManager(incrementCounter):
        # retourne True
        response = lambdaClient.invoke(
            FunctionName=helperFunctionARN,
            InvocationType='RequestResponse',
            Payload='{"incrementCounter":' + str.lower(str(incrementCounter)) + ',"RDBMSName": "Prod_MySQL"}'
        )
        retVal = response['Payload']
        retVal1 = retVal.read()
        return retVal1
    
    
    def openConnection():
        print("In Open connection")
        global conn
        password = "None"
        # Crée un client Secrets Manager 
        session = boto3.session.Session()
        client = session.client(
            service_name='secretsmanager',
            region_name=region_name
        )

        # Dans cet exemple, seuls les exceptions liées à l'appel API 'GetSecretValue' sont interceptés
        # Voir https://docs.aws.amazon.com/fr_fr/secretsmanager/latest/apireference/API_GetSecretValue.html
        # Par défaut une exception en relancée
        
        try:
            get_secret_value_response = client.get_secret_value(
                SecretId=secret_name
            )
            print(get_secret_value_response)
        except ClientError as e:
            print(e)
            if e.response['Error']['Code'] == 'DecryptionFailureException':
                # Secrets Manager ne peut pas déchiffrer le texte protégé du secret avec la clé KMS proposée.
                # Vous pouvez gérer ce cas ici ou renvoyer cette exception à votre convenance
                raise e
            elif e.response['Error']['Code'] == 'InternalServiceErrorException':
                # Une erreur est survenue coté serveur
                # Vous pouvez gérer ce cas ici ou renvoyer cette exception à votre convenance
                raise e
            elif e.response['Error']['Code'] == 'InvalidParameterException':
                # Le paramètre fourni est invalide.
                # Vous pouvez gérer ce cas ici ou renvoyer cette exception à votre convenance
                raise e
            elif e.response['Error']['Code'] == 'InvalidRequestException':              
                # le paramètre fourni est invalide pour l'état actuel de la ressource
                # Vous pouvez gérer ce cas ici ou renvoyer cette exception à votre convenance
                raise e
            elif e.response['Error']['Code'] == 'ResourceNotFoundException':
                # La ressource demandée n'a pas été trouvée
                # Vous pouvez gérer ce cas ici ou renvoyer cette exception à votre convenance
                raise e
        else:
            # Déchiffre le secret avec la clé KMS associée
            # Selon le type "chaine de caractères" ou "binaire", un de ces champs sera rempli
            if 'SecretString' in get_secret_value_response:
                secret = get_secret_value_response['SecretString']
                j = json.loads(secret)
                password = j['password']
            else:
                decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary'])
                print("password binary:" + decoded_binary_secret)
                password = decoded_binary_secret.password    
        
        try:
            if(conn is None):
                conn = pymysql.connect(
                    rds_host, user=name, passwd=password, db=db_name, connect_timeout=5)
            elif (not conn.open):
                # print(conn.open)
                conn = pymysql.connect(
                    rds_host, user=name, passwd=password, db=db_name, connect_timeout=5)
    
        except Exception as e:
            print (e)
            print("ERROR: Unexpected error: Could not connect to MySql instance.")
            raise e
    
    
    def lambda_handler(event, context):
        if invokeConnCountManager(True) == "false":
            print ("Not enough Connections available.")
            return False
    
        item_count = 0
        try:
            openConnection()
            # Délai artificiel simulant une requête à la base de données. A supprimer pour une utilisation en conditions réelles
            time.sleep(random.randint(1, 3))
            with conn.cursor() as cur:
                cur.execute("select * from Employees")
                for row in cur:
                    item_count += 1
                    print(row)
                    # print(row)
        except Exception as e:
            # Erreur d'ouverture de connexion ou à l'execution
            print(e)
        finally:
            print("Closing Connection")
            if(conn is not None and conn.open):
                conn.close()
            invokeConnCountManager(False)
    
        content =  "Selected %d items from RDS MySQL table" % (item_count)
        response = {
            "statusCode": 200,
            "body": content,
            "headers": {
                'Content-Type': 'text/html',
            }
        }
        return response  

Dans AWS Secrets Manager, vous pouvez également remarquer qu’un nouveau secret a été créé après l’exécution de la stack CloudFormation :

  1. Naviguez vers la page du service AWS Secrets Manager en disposant des droits nécessaires,
  2. Dans la liste des secrets, cliquez sur le secret le plus récent avec le nom RDS-InstanceRotationSecret-....
  3. Vous pourrez visualiser les détails du secret et les information sur la rotation, comme le montre cette copie d’écran :

Conclusion

Dans cet article, nous avons abordé la gestion des secrets de bases de données avec AWS Secrets Manager et comment utiliser l’API de Secrets Manager pour récupérer ce secret dans une fonction AWS Lambda afin d’améliorer la sécurité de la base de données et ainsi protéger les informations sensibles. AWS Secrets Manager vous aide à protéger les accès à vos applications, services et ressources informatiques sans avoir à investir vous même dans la création d’une architecture de sécurisation de secrets et dans sa maintenance quotidienne. Pour démarrer avec le service, visitez la page du service Secrets Manager dans la console AWS. Pour en découvrir plus sur le service, visitez la page de documentation.

Si vous avez un retour à faire sur cette article, ajoutez un commentaire dans la section des commentaires ci-après. Pour toute question concernant l’implémentation de l’exemple décrit dans cet article, ouvrez un fil de discussion sur le forum Secrets Manager.

Vous voulez en savoir plus sur la sécurité sur AWS, rester à jour et connaître les nouveautés des services ? Suivez-nous sur Twitter.

Article original rédigé en anglais par Ramesh Adabala, et traduit par Vivien de Saint Pern, Senior Solutions Architect dans l’équipe AWS France.