Pular para conteúdo

Secrets Management

Gerenciamento seguro de secrets e credenciais.

Estratégia

AWS Secrets Manager

Para: - Database credentials - API keys de terceiros - OAuth client secrets - Certificates e private keys

Vantagens: - Rotação automática - Encryption at rest (KMS) - Audit trail (CloudTrail) - Versioning

Systems Manager Parameter Store (SSM)

Para: - Configurações não-sensíveis - Endpoints de serviços - Feature flags - Database connection strings (menos sensíveis)

Vantagens: - Grátis para standard parameters - Integração fácil - Hierarquia de parâmetros - SecureString com KMS

Secrets Manager

Criar Secret

# Via CLI
aws secretsmanager create-secret \
  --name /app/production/database \
  --description "Production database credentials" \
  --secret-string '{
    "username": "app_user",
    "password": "super-secure-password",
    "host": "prod-db.rds.amazonaws.com",
    "port": 5432,
    "database": "app_db"
  }'

Usar em Lambda (SAM)

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.main.lambda_handler
      Runtime: python3.11
      Policies:
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Ref DatabaseSecret
      Environment:
        Variables:
          DATABASE_SECRET_ARN: !Ref DatabaseSecret

  DatabaseSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub '/${AWS::StackName}/database'
      Description: Database credentials
      SecretString: !Sub |
        {
          "username": "app_user",
          "password": "${DatabasePassword}",
          "host": "${DatabaseHost}",
          "database": "app_db"
        }

Acessar no Código

import boto3
import json
from functools import lru_cache

secretsmanager = boto3.client('secretsmanager')

@lru_cache()
def get_secret(secret_name: str) -> dict:
    """Get secret from Secrets Manager (cached)."""
    try:
        response = secretsmanager.get_secret_value(SecretId=secret_name)
        return json.loads(response['SecretString'])
    except Exception as e:
        logger.error(f"Failed to get secret {secret_name}: {e}")
        raise

# Usage
db_secret = get_secret(os.environ['DATABASE_SECRET_ARN'])
DATABASE_URL = f"postgresql://{db_secret['username']}:{db_secret['password']}@{db_secret['host']}/{db_secret['database']}"

Rotação Automática

DatabaseSecretRotation:
  Type: AWS::SecretsManager::RotationSchedule
  Properties:
    SecretId: !Ref DatabaseSecret
    RotationLambdaARN: !GetAtt RotationFunction.Arn
    RotationRules:
      AutomaticallyAfterDays: 30

SSM Parameter Store

Criar Parameters

# String simples
aws ssm put-parameter \
  --name /app/production/api-endpoint \
  --value "https://api.example.com" \
  --type String

# SecureString (encrypted)
aws ssm put-parameter \
  --name /app/production/api-key \
  --value "secret-key-value" \
  --type SecureString \
  --key-id alias/aws/ssm

# Hierarquia
/app/production/database/host
/app/production/database/port
/app/staging/database/host

Usar em Lambda

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Policies:
        - SSMParameterReadPolicy:
            ParameterName: app/production/*
      Environment:
        Variables:
          API_ENDPOINT: '{{resolve:ssm:/app/production/api-endpoint}}'

Acessar no Código

import boto3

ssm = boto3.client('ssm')

def get_parameter(name: str, decrypt: bool = False) -> str:
    """Get parameter from SSM."""
    response = ssm.get_parameter(
        Name=name,
        WithDecryption=decrypt
    )
    return response['Parameter']['Value']

# Usage
api_endpoint = get_parameter('/app/production/api-endpoint')
api_key = get_parameter('/app/production/api-key', decrypt=True)

Environment Variables

Hierarquia de Precedência

  1. AWS Secrets Manager (mais sensível)
  2. SSM SecureString
  3. SSM String
  4. Environment variables (menos sensível)

Exemplo Completo

import os
from functools import lru_cache

class Settings:
    """Application settings."""

    # Non-sensitive: env vars
    ENVIRONMENT: str = os.getenv('ENVIRONMENT', 'development')
    LOG_LEVEL: str = os.getenv('LOG_LEVEL', 'INFO')

    # Moderately sensitive: SSM
    @property
    @lru_cache()
    def API_ENDPOINT(self) -> str:
        return get_parameter('/app/production/api-endpoint')

    # Highly sensitive: Secrets Manager
    @property
    @lru_cache()
    def DATABASE_URL(self) -> str:
        secret = get_secret(os.environ['DATABASE_SECRET_ARN'])
        return f"postgresql://{secret['username']}:{secret['password']}@{secret['host']}/{secret['database']}"

settings = Settings()

Regras de Segurança

✅ Fazer

  • Usar Secrets Manager para dados sensíveis
  • Rotacionar secrets regularmente
  • Criptografar secrets at rest
  • Audit trail de acesso (CloudTrail)
  • Least privilege IAM policies
  • Usar roles, não access keys

❌ Nunca

  • Hardcode secrets no código
  • Commit secrets no Git
  • Compartilhar secrets via email/Slack
  • Usar mesmo secret em staging e production
  • Logar secrets (mesmo em debug)
  • Usar secrets em URLs

Secrets no CI/CD

GitHub Actions

- name: Deploy
  env:
    DATABASE_PASSWORD: ${{ secrets.PROD_DATABASE_PASSWORD }}
  run: |
    # Password não aparece nos logs
    sam deploy --parameter-overrides DatabasePassword=$DATABASE_PASSWORD

GitHub Secrets

Configurar em: Repository → Settings → Secrets

Production: - PROD_DATABASE_PASSWORD - AWS_PROD_ROLE_ARN - SLACK_WEBHOOK

Staging: - STAGING_DATABASE_PASSWORD - AWS_STAGING_ROLE_ARN

Rotação de Secrets

Scheduled Rotation

# Lambda de rotação
def lambda_handler(event, context):
    """Rotate database password."""
    secret_arn = event['SecretId']
    token = event['ClientRequestToken']
    step = event['Step']

    if step == "createSecret":
        # Gerar nova senha
        new_password = generate_secure_password()
        secretsmanager.put_secret_value(
            SecretId=secret_arn,
            ClientRequestToken=token,
            SecretString=json.dumps({"password": new_password}),
            VersionStages=['AWSPENDING']
        )

    elif step == "setSecret":
        # Atualizar senha no banco
        pending_secret = get_secret_version(secret_arn, "AWSPENDING")
        update_database_password(pending_secret['password'])

    elif step == "testSecret":
        # Testar nova senha
        pending_secret = get_secret_version(secret_arn, "AWSPENDING")
        test_database_connection(pending_secret)

    elif step == "finishSecret":
        # Marcar como current
        secretsmanager.update_secret_version_stage(
            SecretId=secret_arn,
            VersionStage='AWSCURRENT',
            MoveToVersionId=token
        )

Security Scan

Pre-commit Hook

# .pre-commit-config.yaml
- repo: https://github.com/Yelp/detect-secrets
  rev: v1.4.0
  hooks:
    - id: detect-secrets
      args: ['--baseline', '.secrets.baseline']

CI/CD Scan

- name: Scan for secrets
  run: |
    pip install detect-secrets
    detect-secrets scan --baseline .secrets.baseline
    detect-secrets audit .secrets.baseline

Incident Response

Se secret foi comprometido:

  1. Imediatamente: Rotacionar secret
  2. Investigar: Como foi exposto?
  3. Revogar: Invalidar todas as sessões com secret antigo
  4. Monitorar: Uso não autorizado
  5. Documentar: Postmortem
  6. Prevenir: Adicionar controles

Ver Incident Response →

Referências