Automatize a rotação de segredos genéricos armazenados no KMS Secrets Manager usando o Function Compute e o CloudFlow. Este guia aborda dois cenários de rotação de banco de dados: instâncias do ApsaraDB RDS acessadas pela API do RDS e bancos de dados MySQL autogerenciados acessados por meio de uma conta privilegiada.
O KMS Secrets Manager oferece segredos dinâmicos com rotação automática integrada para ApsaraDB RDS, RAM e ECS. Se os segredos dinâmicos atenderem ao seu caso de uso, utilize-os em vez desta abordagem, pois têm custo menor e não exigem código personalizado. Consulte Visão geral dos segredos dinâmicos do ApsaraDB RDS, Visão geral dos segredos dinâmicos do RAM ou Visão geral dos segredos dinâmicos do ECS.
Faturamento
A rotação de segredos do KMS é gratuita. As cobranças referem-se ao uso do Function Compute e do CloudFlow. Consulte Faturamento do Function Compute e Faturamento do CloudFlow.
Escolha uma estratégia de rotação
Ambos os cenários deste guia utilizam a rotação de conta dupla (usuário alternado). Essa estratégia mantém duas contas de banco de dados — apenas uma ativa por vez — garantindo que sua aplicação sempre tenha credenciais válidas durante a rotação. A conta inativa recebe as novas credenciais e, em seguida, torna-se a conta ativa.
|
Estratégia |
Funcionamento |
Disponibilidade durante a rotação |
Mais indicada para |
|
Conta única |
Atualiza as credenciais de uma conta no mesmo local |
Breve interrupção durante a atualização das credenciais |
Configurações simples, usuários ad hoc |
|
Conta dupla |
Alterna entre duas contas; uma está sempre ativa |
Sem interrupção |
Bancos de dados de produção, requisitos de alta disponibilidade |
O sufixo de rotação _rt identifica a conta alternativa. Por exemplo, se a conta ativa for appuser, a conta alternativa será appuser_rt. Os nomes das contas, incluindo o sufixo, não podem exceder 16 caracteres.
Como funciona
Os dois cenários seguem o mesmo processo orquestrado pelo CloudFlow. Um agendamento baseado em tempo do CloudFlow aciona o fluxo, que chama a mesma função do Function Compute várias vezes com diferentes valores de Step:
|
Etapa |
Ação da função |
Idempotência |
|
|
Gera uma nova senha e a grava na versão pendente do segredo ( |
Se |
|
|
Cria ou atualiza a conta alternativa do banco de dados com as credenciais pendentes |
Seguro para retentativas — verifica o estado da conta antes de aplicar alterações |
|
|
Verifica se as credenciais pendentes conseguem se conectar ao banco de dados (apenas no cenário MySQL) |
Verificação de conectividade somente leitura |
|
|
Promove |
A atualização do estágio é atômica |
Cada etapa é idempotente, portanto, o fluxo pode ser retomado com segurança após falhas parciais.
Cenário 1: Rotacionar um segredo genérico do ApsaraDB RDS
Utilize esta abordagem quando seu banco de dados estiver executando no ApsaraDB RDS e você desejar rotacionar as credenciais usando a API do RDS.
Pré-requisitos
Antes de começar, verifique se você possui:
Uma conta ativa do Function Compute. Consulte Ativar o Function Compute.
Uma conta de banco de dados do ApsaraDB RDS e um segredo genérico correspondente no console do KMS.
Uma função de serviço do Function Compute com as permissões listadas na Etapa 1.
Etapa 1: Configurar permissões
Crie uma função de serviço para o Function Compute e anexe as seguintes políticas:
AliyunFCDefaultRolePolicy — política do sistema
AliyunSTSAssumeRoleAccess — política do sistema
Uma política personalizada que concede acesso ao KMS e ao RDS:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:GetSecretValue",
"kms:GetRandomPassword",
"kms:PutSecretValue",
"kms:UpdateSecretVersionStage"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"rds:GrantAccountPrivilege",
"rds:DescribeAccounts",
"rds:ResetAccountPassword",
"rds:CreateAccount"
],
"Resource": "*"
}
]
}
Consulte Conceder permissões ao Function Compute para acessar outros serviços da Alibaba Cloud para obter instruções sobre como anexar políticas.
Etapa 2: Criar o segredo genérico
Crie um segredo genérico no console do KMS usando o JSON abaixo como valor do segredo. O nome do segredo será posteriormente passado como parâmetro de agendamento para o CloudFlow.
{
"AccountName": "<rds-account-name>",
"AccountPassword": "<rds-account-password>"
}
|
Campo |
Descrição |
|
|
Nome de usuário da conta do banco de dados RDS |
|
|
Senha da conta do banco de dados RDS |
Etapa 3: Criar a função de rotação
No console do Function Compute, crie um serviço. Clique em Show Advanced Options, defina Access to VPC como Yes e configure VPC, vSwitch e Security Group. O grupo de segurança deve permitir que a função acesse tanto a API do RDS quanto o endpoint de VPC do KMS (
kms-vpc.<region-id>.aliyuncs.com).Crie uma função com Runtime Environments definido como Python 3.9. Utilize o seguinte código:
# -*- coding: utf-8 -*-
import json
import logging
import os
from aliyunsdkrds.request.v20140815.CreateAccountRequest import CreateAccountRequest
from aliyunsdkrds.request.v20140815.DescribeAccountsRequest import DescribeAccountsRequest
from aliyunsdkrds.request.v20140815.GrantAccountPrivilegeRequest import GrantAccountPrivilegeRequest
from aliyunsdkrds.request.v20140815.ResetAccountPasswordRequest import ResetAccountPasswordRequest
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkcore.auth.credentials import StsTokenCredential
from aliyunsdkcore.client import AcsClient
from aliyunsdkkms.request.v20160120.GetRandomPasswordRequest import GetRandomPasswordRequest
from aliyunsdkkms.request.v20160120.GetSecretValueRequest import GetSecretValueRequest
from aliyunsdkkms.request.v20160120.PutSecretValueRequest import PutSecretValueRequest
from aliyunsdkkms.request.v20160120.UpdateSecretVersionStageRequest import UpdateSecretVersionStageRequest
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event, context):
evt = json.loads(event)
secret_name = evt['SecretName']
region_id = evt['RegionId']
step = evt['Step']
instance_id = evt['InstanceId']
version_id = evt.get('VersionId')
if not version_id:
version_id = context.requestId
credentials = StsTokenCredential(context.credentials.accessKeyId, context.credentials.accessKeySecret,
context.credentials.securityToken)
client = AcsClient(region_id=region_id, credential=credentials)
endpoint = "kms-vpc." + region_id + ".aliyuncs.com"
client.add_endpoint(region_id, 'kms', endpoint)
resp = get_secret_value(client, secret_name)
if "Generic" != resp['SecretType']:
logger.error("Secret %s is not enabled for rotation" % secret_name)
raise ValueError("Secret %s is not enabled for rotation" % secret_name)
if step == "new":
new_phase(client, secret_name, version_id)
elif step == "set":
set_phase(client, instance_id, secret_name, version_id)
elif step == "end":
end_phase(client, secret_name, version_id)
else:
logger.error("handler: Invalid step parameter %s for secret %s" % (step, secret_name))
raise ValueError("Invalid step parameter %s for secret %s" % (step, secret_name))
return {"VersionId": version_id}
def new_phase(client, secret_name, version_id):
current_dict = get_secret_dict(client, secret_name, "ACSCurrent")
try:
get_secret_dict(client, secret_name, "ACSPending", version_id)
logger.info("new: Successfully retrieved secret for %s." % secret_name)
except ServerException as e:
if e.error_code != 'Forbidden.ResourceNotFound':
raise ValueError("Can to find secret %s " % (secret_name))
current_dict['AccountName'] = get_alt_account_name(current_dict['AccountName'])
exclude_characters = os.environ[
'EXCLUDE_CHARACTERS'] if 'EXCLUDE_CHARACTERS' in os.environ else "\\\"\',./:;<>?[]{|}~`"
passwd = get_random_password(client, exclude_characters)
current_dict['AccountPassword'] = passwd['RandomPassword']
put_secret_value(client, secret_name, version_id, json.dumps(current_dict),
json.dumps(['ACSPending']))
logger.info(
"new: Successfully put secret for secret_name %s and version %s." % (secret_name, version_id))
def set_phase(client, instance_id, secret_name, version_id):
current_dict = get_secret_dict(client, secret_name, "ACSCurrent")
pending_dict = get_secret_dict(client, secret_name, "ACSPending", version_id)
pending_resp = describe_accounts(client, instance_id, pending_dict["AccountName"])
pending_accounts = pending_resp["Accounts"]["DBInstanceAccount"]
if get_alt_account_name(current_dict['AccountName']) != pending_dict['AccountName']:
logger.error("set: Attempting to modify user %s other than current user or rotation %s" % (
pending_dict['AccountName'], current_dict['AccountName']))
raise ValueError("Attempting to modify user %s other than current user or rotation %s" % (
pending_dict['AccountName'], current_dict['AccountName']))
current_resp = describe_accounts(client, instance_id, current_dict["AccountName"])
current_accounts = current_resp["Accounts"]["DBInstanceAccount"]
if len(current_accounts) == 0:
logger.error("set: Unable to log into database using current credentials for secret %s" % secret_name)
raise ValueError("Unable to log into database using current credentials for secret %s" % secret_name)
if len(pending_accounts) == 0:
create_rds_account(client, instance_id, pending_dict["AccountName"],
pending_dict["AccountPassword"], current_accounts[0]["AccountType"])
pending_accounts = describe_accounts(client, instance_id, pending_dict["AccountName"])["Accounts"][
"DBInstanceAccount"]
else:
# reset password
reset_account_password(client, instance_id, pending_dict["AccountName"], pending_dict["AccountPassword"])
current_privileges = current_accounts[0]["DatabasePrivileges"]["DatabasePrivilege"]
pending_privileges = pending_accounts[0]["DatabasePrivileges"]["DatabasePrivilege"]
if len(current_privileges) > 0:
for current_privilege in current_privileges:
is_contains = False
for pending_privilege in pending_privileges:
if current_privilege["DBName"] == pending_privilege["DBName"] and current_privilege[
"AccountPrivilege"] == pending_privilege["AccountPrivilege"]:
is_contains = True
continue
if not is_contains:
grant_account_privilege(client, instance_id, pending_dict["AccountName"], current_privilege["DBName"],
current_privilege["AccountPrivilege"])
def end_phase(client, secret_name, version_id):
update_secret_version_stage(client, secret_name, 'ACSCurrent', move_to_version=version_id)
update_secret_version_stage(client, secret_name, 'ACSPending', remove_from_version=version_id)
logger.info(
"end: Successfully set ACSCurrent stage to version %s for secret %s." % (version_id, secret_name))
def get_secret_dict(client, secret_name, stage, version_id=None):
required_fields = ['AccountName', 'AccountPassword']
if version_id:
secret = get_secret_value(client, secret_name, version_id, stage)
else:
secret = get_secret_value(client, secret_name, stage=stage)
plaintext = secret['SecretData']
secret_dict = json.loads(plaintext)
for field in required_fields:
if field not in secret_dict:
raise KeyError("%s key is missing from secret JSON" % field)
return secret_dict
def get_alt_account_name(current_account_name):
rotation_suffix = "_rt"
if current_account_name.endswith(rotation_suffix):
return current_account_name[:(len(rotation_suffix) * -1)]
else:
new_account_name = current_account_name + rotation_suffix
if len(new_account_name) > 16:
raise ValueError(
"Unable to rotation user, account_name length with _rotation appended would exceed 16 characters")
return new_account_name
def get_secret_value(client, secret_name, version_id=None, stage=None):
request = GetSecretValueRequest()
request.set_accept_format('json')
request.set_SecretName(secret_name)
if version_id:
request.set_VersionId(version_id)
if stage:
request.set_VersionStage(stage)
response = client.do_action_with_exception(request)
return json.loads(response)
def put_secret_value(client, secret_name, version_id, secret_data, version_stages=None):
request = PutSecretValueRequest()
request.set_accept_format('json')
request.set_SecretName(secret_name)
request.set_VersionId(version_id)
if version_stages:
request.set_VersionStages(version_stages)
request.set_SecretData(secret_data)
response = client.do_action_with_exception(request)
return json.loads(response)
def get_random_password(client, exclude_characters=None):
request = GetRandomPasswordRequest()
request.set_accept_format('json')
if exclude_characters:
request.set_ExcludeCharacters(exclude_characters)
response = client.do_action_with_exception(request)
return json.loads(response)
def update_secret_version_stage(client, secret_name, version_stage, remove_from_version=None, move_to_version=None):
request = UpdateSecretVersionStageRequest()
request.set_accept_format('json')
request.set_VersionStage(version_stage)
request.set_SecretName(secret_name)
if remove_from_version:
request.set_RemoveFromVersion(remove_from_version)
if move_to_version:
request.set_MoveToVersion(move_to_version)
response = client.do_action_with_exception(request)
return json.loads(response)
def create_rds_account(client, db_instance_id, account_name, account_password, account_type):
request = CreateAccountRequest()
request.set_accept_format('json')
request.set_DBInstanceId(db_instance_id)
request.set_AccountName(account_name)
request.set_AccountPassword(account_password)
request.set_AccountType(account_type)
response = client.do_action_with_exception(request)
return json.loads(response)
def grant_account_privilege(client, db_instance_id, account_name, db_name, account_privilege):
request = GrantAccountPrivilegeRequest()
request.set_accept_format('json')
request.set_DBInstanceId(db_instance_id)
request.set_AccountName(account_name)
request.set_DBName(db_name)
request.set_AccountPrivilege(account_privilege)
response = client.do_action_with_exception(request)
return json.loads(response)
def describe_accounts(client, db_instance_id, account_name):
request = DescribeAccountsRequest()
request.set_accept_format('json')
request.set_DBInstanceId(db_instance_id)
request.set_AccountName(account_name)
response = client.do_action_with_exception(request)
return json.loads(response)
def reset_account_password(client, db_instance_id, account_name, account_password):
request = ResetAccountPasswordRequest()
request.set_accept_format('json')
request.set_DBInstanceId(db_instance_id)
request.set_AccountName(account_name)
request.set_AccountPassword(account_password)
response = client.do_action_with_exception(request)
return json.loads(response)
Etapa 4: Criar o fluxo de rotação no CloudFlow
Faça login no console do CloudFlow e selecione a mesma região das suas funções do Function Compute.
Na página Flows, clique em Create flow.
Na página Create Flow, clique em Create Flow with Code, defina o YAML de Definition conforme abaixo e clique em Next Step:
version: v1
type: flow
steps:
- type: task
name: RotateSecretNew
resourceArn: <function-arn>
inputMappings:
- target: SecretName
source: $input.payload.SecretName
- target: RegionId
source: $input.payload.RegionId
- target: InstanceId
source: $input.payload.InstanceId
- target: Step
source: new
- type: task
name: RotateSecretSet
resourceArn: <function-arn>
inputMappings:
- target: SecretName
source: $input.payload.SecretName
- target: RegionId
source: $input.payload.RegionId
- target: InstanceId
source: $input.payload.InstanceId
- target: Step
source: set
- target: VersionId
source: $local.VersionId
- type: task
name: RotateSecretEnd
resourceArn: <function-arn>
inputMappings:
- target: SecretName
source: $input.payload.SecretName
- target: RegionId
source: $input.payload.RegionId
- target: InstanceId
source: $input.payload.InstanceId
- target: Step
source: end
- target: VersionId
source: $local.VersionId
Substitua <function-arn> pelo ARN (Alibaba Cloud Resource Name) da função criada.
Configure uma função de fluxo e clique em Create Flow.
Para agendar a rotação automática, crie um agendamento baseado em tempo. Consulte Criar um agendamento baseado em tempo. Defina Payload como:
{
"SecretName": "<secret-name>",
"RegionId": "<region-id>",
"InstanceId": "<rds-instance-id>"
}
|
Campo |
Descrição |
|
|
Nome do segredo genérico no KMS |
|
|
Região onde o segredo e a função estão implantados |
|
|
ID da instância do ApsaraDB RDS |
Etapa 5: Verificar a rotação
Após configurar o fluxo, acione manualmente uma execução para confirmar se tudo funciona corretamente:
No console do CloudFlow, abra seu fluxo e clique em Start Execution usando o mesmo JSON de payload da Etapa 4.
Aguarde a conclusão da execução. Todas as três etapas (RotateSecretNew, RotateSecretSet, RotateSecretEnd) devem exibir o status Succeeded.
No console do KMS, abra o segredo e verifique seu histórico de versões. O rótulo
ACSCurrentdeve agora apontar para uma nova versão.
Etapa 6: (Opcional) Conectar aplicações ao Secrets Manager
Utilize o Secrets Manager JDBC para recuperar as credenciais rotacionadas em sua aplicação sem alterar o código. Consulte Secrets Manager JDBC.
Cenário 2: Rotacionar um segredo genérico de MySQL autogerenciado
Adote esta abordagem quando seu banco de dados for uma instância MySQL autogerenciada. Este cenário utiliza uma conta privilegiada para gerenciar a rotação de credenciais e adiciona uma etapa test para verificar a conectividade antes de concluir a rotação.
Pré-requisitos
Antes de começar, verifique se você possui:
Uma conta ativa do Function Compute. Consulte Ativar o Function Compute.
Uma conta MySQL privilegiada e um segredo genérico correspondente no console do KMS.
Um segundo segredo genérico para a conta regular que será rotacionada.
Uma função de serviço do Function Compute com as permissões listadas na Etapa 1.
Etapa 1: Configurar permissões
Crie uma função de serviço para o Function Compute e anexe as seguintes políticas:
AliyunFCDefaultRolePolicy — política do sistema
AliyunSTSAssumeRoleAccess — política do sistema
Uma política personalizada que concede acesso ao KMS:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:GetSecretValue",
"kms:GetRandomPassword",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:PutSecretValue",
"kms:UpdateSecretVersionStage"
],
"Resource": "*"
}
]
}
Etapa 2: Criar os segredos genéricos
Crie dois segredos genéricos no console do KMS.
Segredo da conta privilegiada — usado para criar e gerenciar contas regulares:
{
"Endpoint": "<mysql-host>",
"AccountName": "<privileged-account-name>",
"AccountPassword": "<privileged-account-password>",
"SSL": false
}
|
Campo |
Descrição |
Padrão |
|
|
Nome de domínio ou endereço IP do banco de dados MySQL |
— |
|
|
Nome de usuário da conta privilegiada |
— |
|
|
Senha da conta privilegiada |
— |
|
|
Indica se deve usar um certificado SSL para a conexão |
|
Segredo da conta regular — o segredo que será rotacionado. Seu nome é passado como parâmetro de agendamento para o CloudFlow:
{
"Endpoint": "<mysql-host>",
"AccountName": "<account-name>",
"AccountPassword": "<account-password>",
"MasterSecret": "<privileged-secret-name>",
"SSL": false
}
|
Campo |
Descrição |
Padrão |
|
|
Nome de domínio ou endereço IP do banco de dados MySQL |
— |
|
|
Nome de usuário da conta a ser rotacionada |
— |
|
|
Senha da conta a ser rotacionada |
— |
|
|
Nome do segredo da conta privilegiada no KMS |
— |
|
|
Indica se deve usar um certificado SSL para a conexão |
|
QuandoSSLétrue, a função espera encontrar o certificado SSL em/opt/python/certs/cert.pem. Adicione o certificado a uma camada personalizada (consulte a Etapa 3).
Etapa 3: Criar a função de rotação
Crie um serviço do Function Compute com acesso à VPC habilitado (mesma configuração do Cenário 1). Certifique-se de que a função consiga acessar a instância MySQL e o endpoint de VPC do KMS.
Crie uma função com Runtime Environments definido como Python 3.9. Utilize o seguinte código:
# -*- coding: utf-8 -*-
import json
import logging
import os
try:
import pymysql
except:
os.system('pip install pymysql -t ./')
import pymysql
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkcore.auth.credentials import StsTokenCredential
from aliyunsdkcore.client import AcsClient
from aliyunsdkkms.request.v20160120.GetRandomPasswordRequest import GetRandomPasswordRequest
from aliyunsdkkms.request.v20160120.GetSecretValueRequest import GetSecretValueRequest
from aliyunsdkkms.request.v20160120.PutSecretValueRequest import PutSecretValueRequest
from aliyunsdkkms.request.v20160120.UpdateSecretVersionStageRequest import UpdateSecretVersionStageRequest
from aliyunsdkrds.request.v20140815.DescribeDBInstancesRequest import DescribeDBInstancesRequest
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event, context):
evt = json.loads(event)
secret_name = evt['SecretName']
region_id = evt['RegionId']
step = evt['Step']
version_id = evt.get('VersionId')
if not version_id:
version_id = context.requestId
credentials = StsTokenCredential(context.credentials.accessKeyId, context.credentials.accessKeySecret,
context.credentials.securityToken)
client = AcsClient(region_id=region_id, credential=credentials)
endpoint = "kms-vpc." + region_id + ".aliyuncs.com"
client.add_endpoint(region_id, 'kms', endpoint)
resp = get_secret_value(client, secret_name)
if "Generic" != resp['SecretType']:
logger.error("Secret %s is not enabled for rotation" % secret_name)
raise ValueError("Secret %s is not enabled for rotation" % secret_name)
if step == "new":
new_phase(client, secret_name, version_id)
elif step == "set":
set_phase(client, secret_name, version_id)
elif step == "test":
test_phase(client, secret_name, version_id)
elif step == "end":
end_phase(client, secret_name, version_id)
else:
logger.error("handler: Invalid step parameter %s for secret %s" % (step, secret_name))
raise ValueError("Invalid step parameter %s for secret %s" % (step, secret_name))
return {"VersionId": version_id}
def new_phase(client, secret_name, version_id):
current_dict = get_secret_dict(client, secret_name, "ACSCurrent")
try:
get_secret_dict(client, secret_name, "ACSPending", version_id)
logger.info("new: Successfully retrieved secret for %s." % secret_name)
except ServerException as e:
if e.error_code != 'Forbidden.ResourceNotFound':
raise
current_dict['AccountName'] = get_alt_account_name(current_dict['AccountName'])
exclude_characters = os.environ['EXCLUDE_CHARACTERS'] if 'EXCLUDE_CHARACTERS' in os.environ else '/@"\'\\'
passwd = get_random_password(client, exclude_characters)
current_dict['AccountPassword'] = passwd['RandomPassword']
put_secret_value(client, secret_name, version_id, json.dumps(current_dict),
json.dumps(['ACSPending']))
logger.info(
"new: Successfully put secret for secret_name %s and version %s." % (secret_name, version_id))
def set_phase(client, secret_name, version_id):
current_dict = get_secret_dict(client, secret_name, "ACSCurrent")
pending_dict = get_secret_dict(client, secret_name, "ACSPending", version_id)
conn = get_connection(pending_dict)
if conn:
conn.close()
logger.info(
"set: ACSPending secret is already set as password in MySQL DB for secret secret_name %s." % secret_name)
return
if get_alt_account_name(current_dict['AccountName']) != pending_dict['AccountName']:
logger.error("set: Attempting to modify user %s other than current user or rotation %s" % (
pending_dict['AccountName'], current_dict['AccountName']))
raise ValueError("Attempting to modify user %s other than current user or rotation %s" % (
pending_dict['AccountName'], current_dict['AccountName']))
if current_dict['Endpoint'] != pending_dict['Endpoint']:
logger.error("set: Attempting to modify user for Endpoint %s other than current Endpoint %s" % (
pending_dict['Endpoint'], current_dict['Endpoint']))
raise ValueError("Attempting to modify user for Endpoint %s other than current Endpoint %s" % (
pending_dict['Endpoint'], current_dict['Endpoint']))
conn = get_connection(current_dict)
if not conn:
logger.error("set: Unable to access the given database using current credentials for secret %s" % secret_name)
raise ValueError("Unable to access the given database using current credentials for secret %s" % secret_name)
conn.close()
master_secret = current_dict['MasterSecret']
master_dict = get_secret_dict(client, master_secret, "ACSCurrent")
if current_dict['Endpoint'] != master_dict['Endpoint'] and not is_rds_replica_database(current_dict, master_dict):
logger.error("set: Current database Endpoint %s is not the same Endpoint as/rds replica of master %s" % (
current_dict['Endpoint'], master_dict['Endpoint']))
raise ValueError("Current database Endpoint %s is not the same Endpoint as/rds replica of master %s" % (
current_dict['Endpoint'], master_dict['Endpoint']))
conn = get_connection(master_dict)
if not conn:
logger.error(
"set: Unable to access the given database using credentials in master secret secret %s" % master_secret)
raise ValueError("Unable to access the given database using credentials in master secret secret %s" % master_secret)
try:
with conn.cursor() as cur:
cur.execute("SELECT User FROM mysql.user WHERE User = %s", pending_dict['AccountName'])
if cur.rowcount == 0:
cur.execute("CREATE USER %s IDENTIFIED BY %s",
(pending_dict['AccountName'], pending_dict['AccountPassword']))
cur.execute("SHOW GRANTS FOR %s", current_dict['AccountName'])
for row in cur.fetchall():
if 'XA_RECOVER_ADMIN' in row[0]:
continue
grant = row[0].split(' TO ')
new_grant_escaped = grant[0].replace('%', '%%') # % is a special character in Python format strings.
cur.execute(new_grant_escaped + " TO %s ", (pending_dict['AccountName'],))
cur.execute("SELECT VERSION()")
ver = cur.fetchone()[0]
escaped_encryption_statement = get_escaped_encryption_statement(ver)
cur.execute("SELECT ssl_type, ssl_cipher, x509_issuer, x509_subject FROM mysql.user WHERE User = %s",
current_dict['AccountName'])
tls_options = cur.fetchone()
ssl_type = tls_options[0]
if not ssl_type:
cur.execute(escaped_encryption_statement + " NONE", pending_dict['AccountName'])
elif ssl_type == "ANY":
cur.execute(escaped_encryption_statement + " SSL", pending_dict['AccountName'])
elif ssl_type == "X509":
cur.execute(escaped_encryption_statement + " X509", pending_dict['AccountName'])
else:
cur.execute(escaped_encryption_statement + " CIPHER %s AND ISSUER %s AND SUBJECT %s",
(pending_dict['AccountName'], tls_options[1], tls_options[2], tls_options[3]))
password_option = get_password_option(ver)
cur.execute("SET PASSWORD FOR %s = " + password_option,
(pending_dict['AccountName'], pending_dict['AccountPassword']))
conn.commit()
logger.info("set: Successfully changed password for %s in MySQL DB for secret secret_name %s." % (
pending_dict['AccountName'], secret_name))
finally:
conn.close()
def test_phase(client, secret_name, version_id):
conn = get_connection(get_secret_dict(client, secret_name, "ACSPending", version_id))
if conn:
try:
with conn.cursor() as cur:
cur.execute("SELECT NOW()")
conn.commit()
finally:
conn.close()
logger.info("test: Successfully accessed into MySQL DB with ACSPending secret in %s." % secret_name)
return
else:
logger.error(
"test: Unable to access the given database with pending secret of secret secret_name %s" % secret_name)
raise ValueError("Unable to access the given database with pending secret of secret secret_name %s" % secret_name)
def end_phase(client, secret_name, version_id):
update_secret_version_stage(client, secret_name, 'ACSCurrent', move_to_version=version_id)
update_secret_version_stage(client, secret_name, 'ACSPending', remove_from_version=version_id)
logger.info(
"end: Successfully update ACSCurrent stage to version %s for secret %s." % (version_id, secret_name))
def get_connection(secret_dict):
port = int(secret_dict['Port']) if 'Port' in secret_dict else 3306
dbname = secret_dict['DBName'] if 'DBName' in secret_dict else None
use_ssl, fall_back = get_ssl_config(secret_dict)
conn = connect_and_authenticate(secret_dict, port, dbname, use_ssl)
if conn or not fall_back:
return conn
else:
return connect_and_authenticate(secret_dict, port, dbname, False)
def get_ssl_config(secret_dict):
if 'SSL' not in secret_dict:
return True, True
if isinstance(secret_dict['SSL'], bool):
return secret_dict['SSL'], False
if isinstance(secret_dict['SSL'], str):
ssl = secret_dict['SSL'].lower()
if ssl == "true":
return True, False
elif ssl == "false":
return False, False
else:
return True, True
return True, True
def connect_and_authenticate(secret_dict, port, dbname, use_ssl):
ssl = {'ca': '/opt/python/certs/cert.pem'} if use_ssl else None
try:
conn = pymysql.connect(host=secret_dict['Endpoint'], user=secret_dict['AccountName'],
password=secret_dict['AccountPassword'],
port=port, database=dbname, connect_timeout=5, ssl=ssl)
logger.info("Successfully established %s connection as user '%s' with Endpoint: '%s'" % (
"SSL/TLS" if use_ssl else "non SSL/TLS", secret_dict['AccountName'], secret_dict['Endpoint']))
return conn
except pymysql.OperationalError as e:
if 'certificate verify failed: IP address mismatch' in e.args[1]:
logger.error(
"Hostname verification failed when estlablishing SSL/TLS Handshake with Endpoint: %s" % secret_dict[
'Endpoint'])
return None
def get_secret_dict(client, secret_name, stage, version_id=None):
required_fields = ['Endpoint', 'AccountName', 'AccountPassword']
if version_id:
secret = get_secret_value(client, secret_name, version_id, stage)
else:
secret = get_secret_value(client, secret_name, stage=stage)
plaintext = secret['SecretData']
secret_dict = json.loads(plaintext)
for field in required_fields:
if field not in secret_dict:
raise KeyError("%s key is missing from secret JSON" % field)
return secret_dict
def get_alt_account_name(current_account_name):
rotation_suffix = "_rt"
if current_account_name.endswith(rotation_suffix):
return current_account_name[:(len(rotation_suffix) * -1)]
else:
new_account_name = current_account_name + rotation_suffix
if len(new_account_name) > 16:
raise ValueError(
"Unable to rotate user, account_name length with _rotation appended would exceed 16 characters")
return new_account_name
def get_password_option(version):
if version.startswith("8"):
return "%s"
else:
return "PASSWORD(%s)"
def get_escaped_encryption_statement(version):
if version.startswith("5.6"):
return "GRANT USAGE ON *.* TO %s@'%%' REQUIRE"
else:
return "ALTER USER %s@'%%' REQUIRE"
def is_rds_replica_database(client, replica_dict, master_dict):
replica_instance_id = replica_dict['Endpoint'].split(".")[0].replace('io', '')
master_instance_id = master_dict['Endpoint'].split(".")[0].replace('io', '')
try:
describe_response = describe_db_instances(client, replica_instance_id)
except Exception as err:
logger.warning("Encountered error while verifying rds replica status: %s" % err)
return False
items = describe_response['Items']
instances = items.get("DBInstance")
if not instances:
logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id)
return False
current_instance = instances[0]
return master_instance_id == current_instance.get('DBInstanceId')
def get_secret_value(client, secret_name, version_id=None, stage=None):
request = GetSecretValueRequest()
request.set_accept_format('json')
request.set_SecretName(secret_name)
if version_id:
request.set_VersionId(version_id)
if stage:
request.set_VersionStage(stage)
response = client.do_action_with_exception(request)
return json.loads(response)
def put_secret_value(client, secret_name, version_id, secret_data, version_stages=None):
request = PutSecretValueRequest()
request.set_accept_format('json')
request.set_SecretName(secret_name)
request.set_VersionId(version_id)
if version_stages:
request.set_VersionStages(version_stages)
request.set_SecretData(secret_data)
response = client.do_action_with_exception(request)
return json.loads(response)
def get_random_password(client, exclude_characters=None):
request = GetRandomPasswordRequest()
request.set_accept_format('json')
if exclude_characters:
request.set_ExcludeCharacters(exclude_characters)
response = client.do_action_with_exception(request)
return json.loads(response)
def update_secret_version_stage(client, secret_name, version_stage, remove_from_version=None, move_to_version=None):
request = UpdateSecretVersionStageRequest()
request.set_accept_format('json')
request.set_VersionStage(version_stage)
request.set_SecretName(secret_name)
if remove_from_version:
request.set_RemoveFromVersion(remove_from_version)
if move_to_version:
request.set_MoveToVersion(move_to_version)
response = client.do_action_with_exception(request)
return json.loads(response)
def describe_db_instances(client, db_instance_id):
request = DescribeDBInstancesRequest()
request.set_accept_format('json')
request.set_DBInstanceId(db_instance_id)
response = client.do_action_with_exception(request)
return json.loads(response)
-
Crie uma camada personalizada para empacotar a dependência PyMySQL (e opcionalmente o certificado SSL):
-
Execute os seguintes comandos para criar o pacote da camada:
mkdir my-secret-rotate cd my-secret-rotate pip install --target ./python pymysql # If SSL is required, create the certs directory and add your certificate: # mkdir python/certs && cp cert.pem python/certs/ zip -r my-secret-rotate.zip python No console do Function Compute, crie uma camada personalizada usando o pacote ZIP. Consulte Criar uma camada personalizada.
-
Anexe a camada personalizada à sua função. Na página Services, clique em Functions referente ao seu serviço, localize a função, clique em Configure e adicione a camada na seção Layers. Consulte Gerenciar camadas.
Etapa 4: Criar o fluxo de rotação no CloudFlow
Faça login no console do CloudFlow e selecione a mesma região das suas funções.
Na página Flows, clique em Create flow.
Na página Create Flow, clique em Create Flow with Code, defina o YAML de Definition conforme abaixo e clique em Next Step:
version: v1
type: flow
steps:
- type: task
name: RotateSecretNew
resourceArn: <function-arn>
inputMappings:
- target: SecretName
source: $input.payload.SecretName
- target: RegionId
source: $input.payload.RegionId
- target: Step
source: new
- type: task
name: RotateSecretSet
resourceArn: <function-arn>
inputMappings:
- target: SecretName
source: $input.payload.SecretName
- target: RegionId
source: $input.payload.RegionId
- target: Step
source: set
- target: VersionId
source: $local.VersionId
- type: task
name: RotateSecretTest
resourceArn: <function-arn>
inputMappings:
- target: SecretName
source: $input.payload.SecretName
- target: RegionId
source: $input.payload.RegionId
- target: Step
source: test
- target: VersionId
source: $local.VersionId
- type: task
name: RotateSecretEnd
resourceArn: <function-arn>
inputMappings:
- target: SecretName
source: $input.payload.SecretName
- target: RegionId
source: $input.payload.RegionId
- target: Step
source: end
- target: VersionId
source: $local.VersionId
Substitua <function-arn> pelo ARN da função criada.
Configure uma função de fluxo e clique em Create Flow.
Para agendar a rotação automática, crie um agendamento baseado em tempo e defina Payload como:
{
"SecretName": "<secret-name>",
"RegionId": "<region-id>"
}
|
Campo |
Descrição |
|
|
Nome do segredo genérico da conta regular no KMS |
|
|
Região onde o segredo e a função estão implantados |
Etapa 5: Verificar a rotação
Acione manualmente uma execução para confirmar a configuração:
No console do CloudFlow, abra seu fluxo e clique em Start Execution usando o JSON de payload da Etapa 4.
Aguarde a conclusão da execução. Todas as quatro etapas (RotateSecretNew, RotateSecretSet, RotateSecretTest, RotateSecretEnd) devem exibir o status Succeeded.
No console do KMS, abra o segredo e verifique seu histórico de versões. O rótulo
ACSCurrentdeve apontar para uma nova versão.
Etapa 6: (Opcional) Conectar aplicações ao Secrets Manager
Utilize o Secrets Manager JDBC para recuperar automaticamente as credenciais rotacionadas. Consulte Secrets Manager JDBC.
Próximos passos
Secrets Manager JDBC — integre credenciais rotacionadas com aplicações baseadas em JDBC
Visão geral dos segredos dinâmicos do ApsaraDB RDS — considere usar segredos dinâmicos se seu banco de dados estiver no ApsaraDB RDS
O que é o Function Compute? — saiba mais sobre o serviço de computação serverless utilizado neste guia