Imagen destacada del artículo: Seguridad y costes en S3: donde se separa un usuario de un Cloud Engineer

Seguridad y costes en S3: donde se separa un usuario de un Cloud Engineer

Amazon S3 es tan fiable que invita a confiarse. Y ahí empieza el problema.

La mayoría de incidentes relacionados con S3 no vienen de fallos del servicio. Vienen de malas decisiones humanas:

  • 🔓 Permisos demasiado abiertos
  • 🌍 Buckets públicos “sin querer”
  • ⏳ Lifecycle inexistente
  • 💸 Facturas que crecen sin que nadie sepa por qué

Este artículo va de responsabilidad real. Porque cuando gestionas S3 en producción, no solo almacenas datos: proteges información y dinero.

He visto empresas facturadas $50,000 al mes por buckets mal configurados. He investigado leaks de datos que empezaron con un simple “Block Public Access: Off” en un entorno de testing que pasó a producción.

La diferencia entre un usuario de S3 y un Cloud Engineer no está en saber qué es un bucket. Está en diseñar sistemas que no colapsan a las 3 AM ni aparecen en las noticias.

El modelo de seguridad de S3: quién manda aquí

En Amazon Web Services S3, la seguridad no vive en un solo sitio. Vive en varias capas, y entender cuál tiene prioridad es crítico.

Las capas principales:

Modelo de seguridad S3:
  1. IAM Policies:
      - Quién puede hacer qué
      - Asociadas a usuarios, roles, grupos
      - Scope: identity-based
  
  2. Bucket Policies:
      - Qué puede hacerse con este bucket
      - Condiciones: IP, VPC, encriptación
      - Scope: resource-based
  
  3. ACLs (Access Control Lists):
      - Legado, evita si puedes
      - Granularidad por objeto
      - Casos de uso: compartir con otras cuentas AWS
  
  4. Block Public Access:
      - Override de emergencia
      - Última línea de defensa
      - Debería estar SIEMPRE activado

Regla de oro:

El permiso más restrictivo siempre gana.

Muchos leaks históricos han ocurrido no porque alguien “abriera” un bucket, sino porque no entendía qué policy estaba aplicando realmente.

Caso real de exposición accidental:

Escenario:
  - Bucket con Block Public Access: OFF (testing)
  - Bucket Policy: restrictiva (solo VPC)
  - ACL: por defecto
  - Problema: desarrollador añade ACL pública a un objeto
  
Resultado:
  ✅ Block Public Access hubiera bloqueado esto
  ❌ Sin él: objeto expuesto públicamente
  
Exposición: 3 días hasta detección
Datos comprometidos: 12,000 registros PII

Este error cuesta carreras, no solo dinero.

IAM Policy vs Bucket Policy: la confusión clásica

La pregunta más frecuente en auditorías:
“¿Por qué este rol puede acceder aunque la bucket policy lo niega?”

La diferencia:

IAM Policy: define quién puede hacer qué sobre recursos AWS.

Bucket Policy: define qué puede hacerse con este bucket y bajo qué condiciones.

Ejemplo mental:

IAM Policy (en el rol):
  "Este rol puede leer objetos S3 en general"

Bucket Policy (en el bucket):
  "Solo permitir acceso desde:
   - Esta cuenta AWS
   - Esta VPC
   - Con TLS habilitado"

Ambas deben permitir la acción para que funcione.

Caso práctico: restricción por VPC

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AccessOnlyFromVPC",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-private-bucket",
        "arn:aws:s3:::my-private-bucket/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:SourceVpc": "vpc-12345678"
        }
      }
    }
  ]
}

Lo que hace:

  • ✅ Acceso permitido solo desde VPC específica
  • ❌ Bloquea acceso desde internet, consola AWS, CLI local
  • 🎯 Úsalo para: buckets con datos sensibles en arquitecturas multi-tenant

Un Cloud Engineer piensa en políticas como contratos, no como listas de permisos.

Block Public Access: tu último cinturón de seguridad

S3 incluye un mecanismo explícito para evitar exposiciones accidentales: Block Public Access.

Configuración recomendada:

# Terraform: activar Block Public Access
resource "aws_s3_bucket_public_access_block" "main" {
  bucket = aws_s3_bucket.main.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Debe estar:

  • ✅ Activado a nivel de cuenta AWS
  • ✅ Activado a nivel de bucket individual

¿Excepciones?

Sí, pero muy controladas:

Casos válidos para desactivar:
  - Static websites públicos (con CloudFront mejor)
  - Assets públicos versionados (logos, CSS, JS)
  - Compartir datasets públicos (investigación, open data)

Casos NO válidos:
  - "Porque no me deja probar algo"
  - "Solo temporalmente"
  - "Es un entorno de desarrollo"

Si desactivas esto “porque no te deja probar algo”, el problema no es S3, es el diseño.

Checklist antes de desactivar Block Public Access:

  • ¿Realmente necesitas acceso público o puedes usar presigned URLs?
  • ¿Has considerado CloudFront con Origin Access Identity?
  • ¿Tienes alertas configuradas para detectar cambios?
  • ¿Está documentado y aprobado el motivo?
  • ¿Hay revisión periódica programada?

Encriptación: no todo es KMS (ni todo debe serlo)

S3 cifra los datos en reposo por defecto desde 2023, pero hay matices importantes según tu caso de uso.

Opciones de encriptación:

Tipos de Server-Side Encryption:

  SSE-S3 (AES-256):
    - Encriptación gestionada por AWS
    - Sin coste adicional
    - Sin control sobre rotación de keys
    - Ideal para: mayoría de casos
  
  SSE-KMS:
    - Keys gestionadas en AWS KMS
    - Control granular de permisos
    - Auditoría completa en CloudTrail
    - Rotación automática de keys
    - Coste: $0.03 por 10,000 requests
    - Ideal para: datos sensibles, compliance
  
  SSE-C (Customer-Provided):
    - Tú gestionas las keys
    - AWS nunca almacena la key
    - Complejidad operacional alta
    - Ideal para: requisitos legales específicos

Cuándo usar KMS:

Casos de uso SSE-KMS:
  ✅ Datos financieros
  ✅ PII (Personal Identifiable Information)
  ✅ Datos médicos (HIPAA)
  ✅ Requisitos regulatorios (GDPR, PCI-DSS)
  ✅ Necesidad de auditoría detallada
  ✅ Control de acceso por clave

Cuándo NO usar KMS:

No necesitas KMS para:
  ❌ Logs de aplicación masivos
  ❌ Datos fácilmente regenerables
  ❌ Assets públicos (imágenes, CSS)
  ❌ Artifacts de CI/CD
  ❌ Backups con retención < 30 días

Pagar KMS sin necesitarlo es tan mala práctica como no cifrar cuando toca.

Coste real de KMS en S3:

# Ejemplo: bucket con 10M de objetos, 100k requests/día

# Con SSE-S3
Coste encriptación: $0

# Con SSE-KMS
- Key storage: $1/mes
- Requests: 100,000 * 30 = 3M requests/mes
- Coste: (3,000,000 / 10,000) * $0.03 = $9/mes

# Total adicional: ~$10/mes

# ¿Vale la pena? Depende del caso de uso.

Forzar encriptación vía Bucket Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnencryptedObjectUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "AES256"
        }
      }
    }
  ]
}

Esto previene subidas sin encriptación, incluso por error.

Auditoría: CloudTrail y access logs

Si no sabes quién accede a tus buckets, no estás operando S3, solo lo estás usando.

Dos tipos de logging:

1. CloudTrail (API calls):
    - Quién hizo qué y cuándo
    - Llamadas a la API de S3
    - Integración con GuardDuty
    - Coste: ~$2 por 100,000 eventos
    - Ejemplo: s3:PutObject, s3:DeleteBucket

2. S3 Server Access Logs:
    - Requests HTTP individuales
    - IP origen, user-agent, latencia
    - Útil para debugging
    - Gratis (solo pagas storage)
    - Ejemplo: GET /file.jpg 200 OK

Configuración de Access Logs:

# Terraform
resource "aws_s3_bucket_logging" "main" {
  bucket = aws_s3_bucket.main.id

  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "s3-access-logs/"
}

Buenas prácticas de auditoría:

Checklist auditoría S3:
  ✅ CloudTrail activo en todas las regiones
  ✅ Access logs en buckets críticos
  ✅ Retention de logs según compliance (7-90 días)
  ✅ Lifecycle para mover logs a Glacier (coste)
  ✅ Alertas automáticas para:
      - Cambios en bucket policies
      - Desactivación de Block Public Access
      - Borrados masivos
      - Accesos fuera de horario laboral
  ✅ Revisión mensual, no solo en incidentes

Caso real: detección de exfiltración:

Escenario detectado vía CloudTrail:
  - Usuario: backend-service-role
  - Acción: s3:GetObject
  - Volumen: 500 GB en 2 horas (usual: 5 GB/día)
  - IP origen: fuera de VPC
  
Investigación:
  ✅ Credenciales comprometidas detectadas
  ✅ Acceso revocado en 15 minutos
  ✅ Datos no sensibles (logs públicos)
  
Sin CloudTrail: detección semanas después vía factura

Un SRE no espera al problema para mirar logs. Los mira antes.

Costes en S3: el enemigo silencioso

S3 parece barato… hasta que deja de serlo.

Factores clave de coste:

Desglose real de factura S3:

1. Storage ($):
   - Cantidad de datos * clase de storage
   - Varía 100x entre Standard y Deep Archive

2. Requests ($):
   - PUT/POST/LIST: más caros
   - GET: baratos
   - DELETE: gratis

3. Data Transfer ($$):
   - Ingress: GRATIS
   - Egress: caro ($0.09/GB)
   - Entre regiones: medio caro

4. Versiones antiguas (💰):
   - Olvidadas = dinero invisible
   - Sin lifecycle = coste infinito

5. Multipart uploads incompletos:
   - Uploads fallidos sin cleanup
   - Cobran como objetos completos

Errores típicos que explotan la factura:

Anti-patterns costosos:

❌ Versionado sin lifecycle:
   - Cada modificación = nuevo objeto
   - 100 versiones de 1 GB = 100 GB cobrados
   
❌ Logs eternos:
   - Logs de 5 años en Standard
   - Nunca consultados después de 30 días
   
❌ Glacier sin política de borrado:
   - Backups acumulándose indefinidamente
   - "Por si acaso"
   
❌ Listados masivos innecesarios:
   - LIST operations caras a escala
   - Apps que listan buckets completos cada 5 min
   
❌ Transfers sin CloudFront:
   - $0.09/GB vs $0.085/GB + caching

Caso real: optimización de $18,000 → $2,400/mes:

Bucket: logs de aplicación (3 años)
Tamaño: 800 TB
Clase: S3 Standard

Problema identificado:
  - Versionado activo sin lifecycle
  - ~50 versiones promedio por objeto
  - Logs nunca accedidos después de 90 días
  - Todo en Standard

Solución aplicada:
  1. Lifecycle policy por antigüedad
  2. Límite de versiones (últimas 5)
  3. Transition a storage classes económicos
  4. Borrado automático > 2 años

Nueva estructura:
  - < 30 días: Standard ($0.023/GB)
  - 30-90 días: Standard-IA ($0.0125/GB)
  - 90-365 días: Glacier Instant ($0.004/GB)
  - > 1 año: Deep Archive ($0.00099/GB)
  - > 2 años: DELETE

Resultado:
  Antes: $18,400/mes
  Después: $2,400/mes
  Ahorro anual: $192,000

S3 no avisa cuando estás diseñando algo caro. La factura sí.

Storage Classes: elegir bien o pagar de más

No todos los datos necesitan estar en Standard. De hecho, la mayoría no deberían.

Comparativa completa:

Storage Classes (us-east-1):

S3 Standard:
  Coste: $0.023/GB/mes
  Disponibilidad: 99.99%
  Latencia: ms
  Ideal: datos activos

S3 Intelligent-Tiering:
  Coste: $0.0025-$0.023/GB/mes (automático)
  Monitoreo: $0.0025 por 1,000 objetos
  Ideal: patrón de acceso impredecible

S3 Standard-IA:
  Coste storage: $0.0125/GB/mes
  Coste retrieval: $0.01/GB
  Min storage: 30 días
  Ideal: backups mensuales

S3 One Zone-IA:
  Coste: $0.01/GB/mes
  Disponibilidad: 99.5% (single AZ)
  Ideal: datos regenerables

S3 Glacier Instant:
  Coste: $0.004/GB/mes
  Retrieval: $0.03/GB
  Latencia: ms
  Min storage: 90 días
  Ideal: archivos trimestrales

S3 Glacier Flexible:
  Coste: $0.0036/GB/mes
  Retrieval: 1min-12h ($0.01-$0.03/GB)
  Min storage: 90 días
  Ideal: archivos anuales

S3 Glacier Deep Archive:
  Coste: $0.00099/GB/mes
  Retrieval: 12-48h
  Min storage: 180 días
  Ideal: compliance 7-10 años

Lifecycle Policy completa:

{
  "Rules": [
    {
      "Id": "OptimizeLogsCost",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "logs/"
      },
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER_IR"
        },
        {
          "Days": 365,
          "StorageClass": "DEEP_ARCHIVE"
        }
      ],
      "Expiration": {
        "Days": 2555
      },
      "NoncurrentVersionTransitions": [
        {
          "NoncurrentDays": 7,
          "StorageClass": "GLACIER_IR"
        }
      ],
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 90
      }
    },
    {
      "Id": "CleanupIncompleteMultipartUploads",
      "Status": "Enabled",
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      }
    }
  ]
}

Regla simple: la clase correcta depende de frecuencia de acceso, no de intuición.

Mover datos automáticamente con lifecycle es una de las decisiones más rentables que puedes tomar en AWS.

Versionado: protección vs coste descontrolado

S3 Versioning es un arma de doble filo:

Ventajas:
  ✅ Protección contra borrados accidentales
  ✅ Recuperación de versiones anteriores
  ✅ Cumplimiento de auditorías

Peligros:
  ❌ Cada modificación = nuevo objeto cobrado
  ❌ Versiones antiguas invisibles en consola
  ❌ Delete markers ocupan espacio
  ❌ Coste exponencial sin lifecycle

Configuración inteligente:

# Terraform: versionado con límite
resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

# Lifecycle: límite de versiones
resource "aws_s3_bucket_lifecycle_configuration" "main" {
  bucket = aws_s3_bucket.main.id

  rule {
    id     = "expire-old-versions"
    status = "Enabled"

    noncurrent_version_expiration {
      noncurrent_days = 90
      newer_noncurrent_versions = 5  # Mantener solo 5 versiones
    }
  }
}

Versionado SÍ, pero con lifecycle. Siempre.

El patrón que delata inexperiencia

Se repite una y otra vez en auditorías:

Bucket creado rápidamente:
  ❌ Sin policies claras
  ❌ Sin lifecycle
  ❌ Sin logs
  ❌ Con permisos amplios "temporalmente"
  ❌ Sin tags de ownership
  ❌ Sin alertas
  ❌ Sin documentación

Ese "temporalmente" acaba durando años.

Bucket bien diseñado desde el inicio:

Checklist producción:
  ✅ Nombre descriptivo y versionado
  ✅ Tags: Environment, Owner, Project, CostCenter
  ✅ Block Public Access: ON
  ✅ Versioning: Enabled (con lifecycle)
  ✅ Encryption: SSE-S3 o SSE-KMS según caso
  ✅ Bucket Policy: restrictiva
  ✅ Lifecycle Policy: definida
  ✅ Access Logging: enabled
  ✅ CloudTrail: monitoreado
  ✅ CloudWatch Alarms: configuradas
  ✅ Backup: replication a otra región (críticos)
  ✅ Documentación: README con propósito y owner

Alertas: CloudWatch + EventBridge

No basta con configurar bien. Hay que monitorear activamente.

Alertas críticas recomendadas:

# CloudWatch Alarms para S3
resource "aws_cloudwatch_metric_alarm" "bucket_size" {
  alarm_name          = "s3-bucket-size-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "BucketSizeBytes"
  namespace           = "AWS/S3"
  period              = 86400
  statistic           = "Average"
  threshold           = 1000000000000  # 1 TB
  alarm_description   = "Bucket size exceeds 1TB"
  
  dimensions = {
    BucketName = aws_s3_bucket.main.id
    StorageType = "StandardStorage"
  }
}

EventBridge para cambios de seguridad:

{
  "source": ["aws.s3"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventName": [
      "PutBucketPublicAccessBlock",
      "DeleteBucketPublicAccessBlock",
      "PutBucketPolicy",
      "DeleteBucketPolicy"
    ]
  }
}

Configurar esto lleva 30 minutos. No hacerlo puede costarte la carrera.

Checklist mental antes de crear un bucket en producción

Un Cloud Engineer se pregunta:

Preguntas clave:

Antes de crear el bucket:

1. Acceso:
   - ¿Quién accede? (roles, servicios)
   - ¿Desde dónde? (VPC, IPs, cuentas)
   - ¿Qué operaciones necesita? (read, write, delete)

2. Ciclo de vida:
   - ¿Cuánto tiempo vive el dato?
   - ¿Se accede después de X días?
   - ¿Cuándo se puede borrar?

3. Durabilidad:
   - ¿Qué pasa si se borra?
   - ¿Necesito versionado?
   - ¿Necesito replicación a otra región?

4. Costes:
   - ¿Cuánto costará en 6 meses?
   - ¿Qué storage class es óptima?
   - ¿Tengo lifecycle configurado?

5. Seguridad:
   - ¿Necesita ser público?
   - ¿Qué nivel de encriptación?
   - ¿Tengo auditoría configurada?

6. Operaciones:
   - ¿Quién es el owner?
   - ¿Hay alertas configuradas?
   - ¿Está documentado?

Si no puedes responder a esto, no deberías crear el bucket todavía.

Terraform: infraestructura como código para S3

La única forma sana de gestionar S3 a escala es con IaC:

# Bucket completo production-ready
module "secure_bucket" {
  source = "./modules/s3-secure-bucket"

  bucket_name = "my-app-data-prod"
  
  # Seguridad
  enable_versioning          = true
  enable_encryption          = true
  encryption_type            = "SSE-KMS"
  kms_key_id                = aws_kms_key.s3.arn
  block_public_access        = true
  enable_access_logging      = true
  log_bucket                 = aws_s3_bucket.logs.id
  
  # Lifecycle
  lifecycle_rules = [
    {
      name   = "optimize-costs"
      prefix = "data/"
      transitions = [
        { days = 30,  storage_class = "STANDARD_IA" },
        { days = 90,  storage_class = "GLACIER_IR" },
        { days = 365, storage_class = "DEEP_ARCHIVE" }
      ]
      expiration_days = 2555  # 7 años
    }
  ]
  
  # Policies
  bucket_policy = data.aws_iam_policy_document.bucket.json
  
  # Replicación
  enable_replication     = true
  replication_region     = "us-west-2"
  
  # Tags
  tags = {
    Environment = "production"
    Project     = "core-app"
    Owner       = "platform-team"
    CostCenter  = "engineering"
    Compliance  = "GDPR"
  }
}

IaC no es opcional a escala. Es supervivencia.

Cierre: S3 como responsabilidad, no como servicio

S3 es uno de los servicios más sólidos de AWS. Precisamente por eso expone sin piedad los errores de diseño.

La diferencia real:

Usuario de S3:
  - Sube archivos
  - Usa la consola
  - "Funciona"
  
Cloud Engineer con S3:
  - Diseña seguridad en capas
  - Optimiza costes automáticamente
  - Audita accesos proactivamente
  - Previene incidentes antes de que ocurran
  - Protege datos y presupuesto

Cuando empiezas a cuidar:

  • Seguridad (Block Public Access, policies, encriptación)
  • Auditoría (CloudTrail, access logs, alertas)
  • Costes (storage classes, lifecycle, cleanup)

Dejas de ser “el que sube archivos” y pasas a ser el que protege datos y presupuesto.

Y ahí es donde, sin darte cuenta, ya estás actuando como Cloud Engineer o SRE, aunque tu título siga diciendo backend developer.