O blog da AWS

Como fornecer credenciais de banco de dados com segurança para funções do Lambda (AWS Lambda Functions) usando o AWS Secrets Manager

Por Ramesh Adabala, Anand Komandooru, e Noorul Hasan

 

Nesta postagem do blog, mostraremos como usar o AWS Secrets Manager para proteger suas credenciais de banco de dados e enviá-las para funções Lambda que as usarão para conectar e consultar o serviço de banco de dados Amazon RDS, sem codificar os segredos no código ou passá-los por variáveis de ambiente. Essa abordagem ajudará você a proteger segredos e a proteger seus bancos de dados. As credenciais de longa duração precisam ser gerenciadas e alternadas regularmente para manter seguro o acesso a sistemas críticos. Por isso, é uma boa prática de segurança redefinir suas senhas periodicamente. Alterar manualmente as senhas seria complicado, mas o AWS Secrets Manager ajuda a gerenciar e alternar as senhas do banco de dados do RDS.

Visão Geral de Solução

Neste exemplo de Código: Iremos utilizar um template do AWS CloudFormation para implementar  os seguintes componentes para que seja possível testar a API a partir do seu navegador:

  • Uma instância do RDS MySQL em uma instância do tipot3.micro
  • Duas funções lambda com funções e políticas de IAM necessárias, incluindo acesso ao AWS Secrets Manager:
    • LambdaRDSCFNInit: Essa função Lambda será executada imediatamente após a criação da pilha CloudFormation. Ela criará uma tabela “Employees” no banco de dados, onde inserirá três registros de amostra.
    • LambdaRDSTest: Essa função consultará a tabela Employees e retornará a contagem de registros em um formato de string HTML.
  • RESTful API com método “GET” no AWS API Gateway

Aqui está a configuração de alto nível dos serviços da AWS que serão criados a partir da implantação da pilha CloudFormation:


Figura 1: Diagrama de arquitetura
  1. Os clientes chamam a API RESTful hospedada no AWS API Gateway
  2. O API Gateway executa a função Lambda
  3. A função Lambda recupera os segredos do banco de dados usando a API Secrets Manager
  4. A função Lambda se conecta ao banco de dados do RDS usando segredos do banco de dados do Secrets Manager e retorna os resultados da consulta

Você pode acessar o código-fonte de exemplo usada nesta postagem aqui: https://github.com/aws-samples/aws-secrets-manager-secure-database-credentials

Implementando a solução de exemplo

Faça login em sua conta da AWS, siga as instruções para fazer login.

Por padrão, a pilha será implantada na região us-east-1. Se você quiser implantar essa pilha em qualquer outra região, baixe o código do link do código-fonte acima, crie um arquivo zip usando o arquivo readme, coloque o arquivo zip do código Lambda em um bucket S3 específico da região e faça as alterações necessárias no modelo do CloudFormation para apontar para o bucket S3 correto. (Consulte o Guia do usuário do AWS CloudFormation para obter detalhes adicionais sobre como criar pilhas usando o console do AWS CloudFormation.)

Em seguida, siga estas etapas para executar a pilha:

  1. Após o login, acesse o serviço CloudFormation no console da AWS. Selecione a opção Pilhas e, em seguida, selecione a opção Criar pilha.

    Figura 2. Pesquise por CloudFormation na barra de pesquisa

    Figura 3. Crie uma pilha no CloudFormation
  2. Selecione O modelo está pronto e selecione Fazer upload de um arquivo de modelo. Em seguida, clique no botão Escolher arquivo para escolher o arquivo de modelo (SecretsManager_IAC.yml) da sua área de trabalho. Clique em Próximo.

    Figura 4. Faça o upload do arquivo de modelo do CloudFormation
  3. Na página Especificar detalhes da pilha, você verá os parâmetros pré-preenchidos. Esses parâmetros incluem o nome do banco de dados e o nome do usuário do banco de dados. Insira um nome no campo Nome da pilha. Selecione Próximo nesta tela.

    Figura 5. Parâmetros na página “Especificar detalhes da pilha”
  4. Na página Configurar opções da pilha, selecione o botão Próximo.
  5. Na tela Revisar, marque todas as três caixas de seleção e, em seguida, selecione o botão Criar conjunto de alterações:

    Figura 6. Marque as caixas de seleção e “Criar conjunto de alterações”
  6. Depois que a criação do conjunto de alterações for concluída, escolha o botão Executar Conjunto de Alterações para iniciar a pilha.
  7. A criação da pilha levará entre 10 e 15 minutos. Depois que a pilha for criada com sucesso, selecione a guia Saídas da pilha e selecione o link.

    Figura 7. Selecione o link na guia “Saídas”

    Essa ação acionará o código na função Lambda, que consultará a tabela “Employee” no banco de dados MySQL e retornará a contagem de resultados para a API. Você verá a tela a seguir como saída do endpoint da API RESTful:


    Figure 8. Saída da API RESTful

Neste momento, você implantou e testou com sucesso o endpoint da API com uma função Lambda e recursos de RDS. A função Lambda é capaz de consultar com êxito o banco de dados MySQL RDS e é capaz de retornar os resultados por meio do endpoint da API.

O que está acontecendo por trás?

A pilha do CloudFormation implantou um banco de dados MySQL RDS com uma senha gerada aleatoriamente usando um secret resource. Agora que o recurso secreto com senha gerada aleatoriamente foi criado, a pilha do CloudFormation usará a referência dinâmica para recuperar o valor da senha do Secrets Manager a fim de criar o recurso de instância do RDS. As referências dinâmicas fornecem uma maneira compacta e poderosa de especificar valores externos que são armazenados e gerenciados em outros serviços da AWS, como o Secrets Manager. A referência dinâmica garante que o CloudFormation não registre ou persista o valor resolvido, mantendo a senha do banco de dados segura. O modelo do CloudFormation também cria uma função Lambda para fazer a rotação automática da senha para o banco MySQL RDS a cada 30 dias. A rotação nativa de credenciais pode melhorar a postura de segurança, pois elimina a necessidade de lidar manualmente com as senhas do banco de dados durante o ciclo de vida do processo.

Abaixo está um código de referência do CloudFormation que abrange esses detalhes (certifique-se de usar sua versão do modelo do CloudFormation conforme descrito no arquivo readme do código de exemplo):


#This is a Secret resource with a randomly generated password in its SecretString 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

#This is a RDS instance resource. Its master username and password use dynamic references to resolve values from
#SecretsManager. The dynamic reference guarantees that CloudFormation will not log or persist the resolved value
#We use a ref to the Secret resource logical id in order to construct the dynamic reference, since the Secret name is being
#generated by 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'

#This is a SecretTargetAttachment resource which updates the referenced Secret resource with properties about
#the referenced RDS instance
SecretRDSInstanceAttachment:
    Type: AWS::SecretsManager::SecretTargetAttachment
    Properties:
    SecretId: !Ref MyRDSInstanceRotationSecret
    TargetId: !Ref MyDBInstance2
    TargetType: AWS::RDS::DBInstance
#This is a RotationSchedule resource. It configures rotation of password for the referenced secret using a rotation lambda
#The first rotation happens at resource creation time, with subsequent rotations scheduled according to the rotation rules
#We explicitly depend on the SecretTargetAttachment resource being created to ensure that the secret contains all the
#information necessary for rotation to succeed
MySecretRotationSchedule:
    Type: AWS::SecretsManager::RotationSchedule
    DependsOn: SecretRDSInstanceAttachment
    Properties:
    SecretId: !Ref MyRDSInstanceRotationSecret
    RotationLambdaARN: !GetAtt MyRotationLambda.Arn
    RotationRules:
        AutomaticallyAfterDays: 30

#This is a lambda Function resource. We will use this lambda to rotate secrets
#For details about rotation lambdas, see https://docs.thinkwithwp.com/secretsmanager/latest/userguide/rotating-secrets.html     https://docs.thinkwithwp.com/secretsmanager/latest/userguide/rotating-secrets.html
#The below example assumes that the lambda code has been uploaded to a S3 bucket, and that it will rotate a mysql database password
MyRotationLambda:
    Type: AWS::Serverless::Function
    Properties:
    Runtime: python3.9
    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' 

Verificando a Solução

Para ter certeza de que tudo está configurado corretamente, você pode examinar o código Lambda que está consultando a tabela do banco de dados seguindo as etapas abaixo:

  1. Vá para a página de serviços doAWS Lambda
  2. Na lista de funções lambda, clique na função com o nome scm2-LambdaRDSTest-…
  3. Você pode ver as variáveis de ambiente na parte inferior da tela de detalhes da configuração da Lambda. Observe que não deve haver nenhuma senha de banco de dados fornecida como parte dessas variáveis de ambiente:

    Figura 9. Variáveis de Ambiente
    
        import sys
        import pymysql
        import boto3
        import botocore
        import json
        import random
        import time
        import os
        from botocore.exceptions import ClientError
        
        # rds settings
        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
        
        # Get the service resource.
        lambdaClient = boto3.client('lambda')
        
        
        def invokeConnCountManager(incrementCounter):
            # return 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"
            # Create a Secrets Manager client
            session = boto3.session.Session()
            client = session.client(
                service_name='secretsmanager',
                region_name=region_name
            )
            
            # In this sample we only handle the specific exceptions for the 'GetSecretValue' API.
            # See https://docs.thinkwithwp.com/secretsmanager/latest/apireference/API_GetSecretValue.html
            # We rethrow the exception by default.
            
            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 can't decrypt the protected secret text using the provided KMS key.
                    # Deal with the exception here, and/or rethrow at your discretion.
                    raise e
                elif e.response['Error']['Code'] == 'InternalServiceErrorException':
                    # An error occurred on the server side.
                    # Deal with the exception here, and/or rethrow at your discretion.
                    raise e
                elif e.response['Error']['Code'] == 'InvalidParameterException':
                    # You provided an invalid value for a parameter.
                    # Deal with the exception here, and/or rethrow at your discretion.
                    raise e
                elif e.response['Error']['Code'] == 'InvalidRequestException':
                    # You provided a parameter value that is not valid for the current state of the resource.
                    # Deal with the exception here, and/or rethrow at your discretion.
                    raise e
                elif e.response['Error']['Code'] == 'ResourceNotFoundException':
                    # We can't find the resource that you asked for.
                    # Deal with the exception here, and/or rethrow at your discretion.
                    raise e
            else:
                # Decrypts secret using the associated KMS CMK.
                # Depending on whether the secret is a string or binary, one of these fields will be populated.
                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()
                # Introducing artificial random delay to mimic actual DB query time. Remove this code for actual use.
                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:
                # Error while opening connection or processing
                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        
        

No console do AWS Secrets Manager, você também pode ver o novo segredo que foi criado a partir da execução do CloudFormation seguindo as etapas abaixo:

  1. Vá até a página de serviços do AWS Secret Manager com as permissõesIAM apropriadas
  2. Na lista de segredos, clique no último segredo com o nome MyRDSInstanceRotationSecret-…
  3. Você verá os detalhes secretos e as informações de rotação na tela, conforme mostrado na captura de tela a seguir:

    Figura 10. Detalhes do Segredo


    Figura 11. Detalhes de rotação do segredo

Conclusão

Neste post, mostramos como gerenciar segredos de banco de dados usando o AWS Secrets Manager e como aproveitar a API do Secrets Manager para recuperar os segredos em um ambiente de execução Lambda para melhorar a segurança do banco de dados e proteger dados confidenciais. O Secrets Manager ajuda você a proteger o acesso aos seus aplicativos, serviços e recursos de TI sem o investimento inicial e os custos de manutenção contínuos de operar sua própria infraestrutura de gerenciamento de segredos.  Para começar, visite o console do Secrets Manager. Para saber mais, visite a Documentação do Secrets Manager .

Se você tiver algum feedback sobre esta blog post, comente na seção abaixo. Se você tiver dúvidas sobre como implementar o exemplo usado nesta postagem, abra um tópico Secrets Manager Forum.

Quer mais conteúdo de instruções, notícias e anúncios de recursos sobre segurança da AWS? Siga-nos em Twitter.

Este artigo foi traduzido do Blog da AWS em Inglês.

 


Sobre os autores

Ramesh adabala é Arquiteto de Soluções na equipe de Enterprise Solution Architecture da AWS.

 

 

 

 

Noorul Hasan é consultor de migrações de banco de dados do time ProServe na Amazon Web Services. Sua equipe ajuda os clientes da AWS a migrar e modernizar suas cargas de trabalho para a nuvem da AWS. 

 

 

 

Anand Komandooru é Arquiteto Sênior de nuvem na Amazon Web Services. Sua equipe ajuda os clientes da AWS a concretizar sua visão com a prontidão para a escala na nuvem.

 

 

 

 

Revisores

José Augusto Ferronato atua como Arquiteto de Soluções no time de ISV no Brasil, ajudando empresas de software a utilizarem o melhor da nuvem AWS

 

 

 

 

Carolina Carneiro atua como Arquiteta de Soluções na AWS. Apoia clientes a adotarem a nuvem da AWS e incorporarem às tecnologias da melhor maneira aos seus negócios. Além disso, apoia os clientes com demandas de Machine Learning e Segurança. Fez parte do time de Training and Certification LATAM, entregando treinamentos técnicos aos clientes da AWS. Hoje faz parte do time de Solutions Architect e busca continuar aprendendo para ajudar os clientes em suas jornadas