Introducción

La infraestructura como código amplifica tanto las buenas decisiones de seguridad como las malas. Una configuración insegura puede replicarse en decenas de recursos con una sola línea de código. Por eso, la seguridad en Terraform no es opcional: es una responsabilidad fundamental de cualquier ingeniero de infraestructura.

En este módulo aprenderás los principios y técnicas que separan una implementación de Terraform amateur de una implementación profesional y segura. Cubriremos desde la gestión de credenciales hasta la auditoría de cambios, pasando por el análisis estático de seguridad y las políticas de cumplimiento normativo.


Principios de Seguridad en IaC

Antes de entrar en las técnicas específicas, es importante entender los principios que guían todas las decisiones de seguridad en infraestructura como código:

  1. Principio de mínimo privilegio: Cada componente, usuario y proceso solo debe tener los permisos mínimos necesarios para funcionar
  2. Defensa en profundidad: No confiar en una única capa de seguridad; añadir múltiples capas complementarias
  3. Seguridad por defecto: Los valores por defecto de la configuración deben ser seguros, no permisivos
  4. Inmutabilidad: Preferir reemplazar recursos en lugar de modificarlos; reduce el riesgo de configuraciones inconsistentes
  5. Auditoría completa: Todo cambio de infraestructura debe ser rastreable (quién, qué, cuándo y por qué)
  6. Nunca secretos en código: Las credenciales y secretos jamás deben estar en los archivos de configuración

Gestión Segura de Credenciales

Esta es la práctica de seguridad más crítica en proyectos de Terraform. Una filtración de credenciales puede resultar en el robo de toda la infraestructura o en facturas millonarias de la nube.

Método 1: Variables de Entorno (el más básico)

# Para AWS: Terraform busca automáticamente estas variables de entorno
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="us-east-1"

# Para pasar secretos a variables de Terraform, usar el prefijo TF_VAR_
# Terraform buscará automáticamente TF_VAR_<nombre_variable>
export TF_VAR_db_password="mi-contraseña-segura-123"
export TF_VAR_api_key="sk-prod-abc123"

# Verificar que las variables están configuradas (sin mostrar el valor)
echo "AWS_ACCESS_KEY_ID está configurada: ${AWS_ACCESS_KEY_ID:+Sí}"

Método 2: HashiCorp Vault

HashiCorp Vault es la solución más robusta para gestión de secretos en entornos enterprise:

# versions.tf - Configurar el provider de Vault
terraform {
  required_providers {
    vault = {
      source  = "hashicorp/vault"
      version = "~> 3.0"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Conectar con el servidor de Vault
# Las credenciales de Vault se configuran via variables de entorno:
# VAULT_ADDR="https://vault.empresa.com"
# VAULT_TOKEN="s.xxxxxxxxxxxxx"
provider "vault" {}
# data.tf - Obtener secretos de Vault

# Leer un secreto del motor de secretos KV (Key-Value)
data "vault_kv_secret_v2" "database_credentials" {
  mount = "secret"                         # Motor de secretos KV
  name  = "production/database/postgres"   # Ruta del secreto en Vault
}

# Usar las credenciales en el recurso de RDS
resource "aws_db_instance" "main" {
  engine         = "postgres"
  engine_version = "15.4"
  instance_class = "db.t3.medium"

  # Las credenciales vienen de Vault, nunca están en los archivos .tf
  db_name  = "appdb"
  username = data.vault_kv_secret_v2.database_credentials.data["username"]
  password = data.vault_kv_secret_v2.database_credentials.data["password"]

  # Marcar la contraseña como sensible para que no aparezca en los logs
  lifecycle {
    ignore_changes = [password]  # No recrear la instancia si la contraseña cambia externamente
  }
}
# Vault también puede generar credenciales de AWS dinámicas y temporales
# Esto es más seguro que usar credenciales estáticas de IAM

data "vault_aws_access_credentials" "terraform" {
  backend = "aws"          # Motor de secretos AWS de Vault
  role    = "terraform-role"  # Rol de Vault con permisos de AWS limitados
  type    = "iam_user"
}

# Las credenciales generadas son temporales y se revocan automáticamente
provider "aws" {
  access_key = data.vault_aws_access_credentials.terraform.access_key
  secret_key = data.vault_aws_access_credentials.terraform.secret_key
}

Método 3: AWS Secrets Manager y Azure Key Vault

# Obtener secretos de AWS Secrets Manager (sin Vault)

data "aws_secretsmanager_secret" "db_credentials" {
  name = "prod/rds/postgresql/credentials"  # Nombre del secreto en AWS
}

# Obtener la versión actual del secreto
data "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = data.aws_secretsmanager_secret.db_credentials.id
  # Sin especificar version_id, obtiene la versión AWSCURRENT (más reciente)
}

locals {
  # jsondecode() convierte el string JSON en un mapa de Terraform
  # El secreto almacenado tiene formato: {"username": "admin", "password": "..."}
  db_creds = jsondecode(
    data.aws_secretsmanager_secret_version.db_credentials.secret_string
  )
}

resource "aws_db_instance" "main" {
  username = local.db_creds["username"]  # Del secreto, no hardcodeado
  password = local.db_creds["password"]  # Del secreto, no hardcodeado
}
# Azure Key Vault - equivalente para infraestructura en Azure

data "azurerm_key_vault" "main" {
  name                = "empresa-keyvault-prod"
  resource_group_name = "rg-security"
}

data "azurerm_key_vault_secret" "db_password" {
  name         = "postgresql-admin-password"  # Nombre del secreto en Key Vault
  key_vault_id = data.azurerm_key_vault.main.id
}

resource "azurerm_postgresql_server" "main" {
  administrator_login          = "psqladmin"
  administrator_login_password = data.azurerm_key_vault_secret.db_password.value
}

Método 4: Variables Sensibles en Terraform

# variables.tf
# El atributo sensitive = true es una característica de seguridad importante

variable "db_password" {
  description = "Contraseña del administrador de la base de datos"
  type        = string
  sensitive   = true  # EFECTO: El valor NO aparecerá en:
                      # - Output de terraform plan
                      # - Output de terraform apply
                      # - Logs de CI/CD
                      # - terraform show
}

variable "api_secret_key" {
  description = "Clave secreta para la API externa"
  type        = string
  sensitive   = true
}
# outputs.tf
# Si un output deriva de un valor sensible, también debe marcarse como sensible

output "database_connection_string" {
  description = "Cadena de conexión a la base de datos"
  value       = "postgresql://${aws_db_instance.main.username}@${aws_db_instance.main.endpoint}/${aws_db_instance.main.db_name}"
  sensitive   = true  # Requerido si el valor contiene información sensible
}

Lo que NUNCA se Debe Hacer

# ============================================================
# EJEMPLOS DE PRACTICAS INSEGURAS (NO COPIAR)
# ============================================================

# MAL: Credenciales hardcodeadas en el provider
provider "aws" {
  access_key = "AKIAIOSFODNN7EXAMPLE"     # NUNCA hardcodear
  secret_key = "wJalrXUtnFEMI/K7MDENG"  # NUNCA hardcodear
  region     = "us-east-1"
}

# MAL: Contraseñas en texto plano en recursos
resource "aws_db_instance" "database" {
  username = "admin"
  password = "MiPassword123!"  # NUNCA hardcodear contraseñas
}

# MAL: Valores sensibles en terraform.tfvars commiteado en Git
# terraform.tfvars:
# db_password = "MiPassword123!"
# api_key = "sk-prod-abc123"

Principio de Mínimo Privilegio en IAM

El principio de mínimo privilegio establece que cada identidad (usuario, rol, servicio) solo debe tener exactamente los permisos que necesita para realizar su función, ni más ni menos.

# MAL EJEMPLO: Permisos de administrador para una función Lambda
resource "aws_iam_role_policy" "lambda_policy_bad" {
  name = "lambda-policy"
  role = aws_iam_role.lambda_execution.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = "*"          # Todos los permisos: NUNCA hacer esto en producción
        Resource = "*"          # Sobre todos los recursos: extremadamente peligroso
      }
    ]
  })
}

# BUEN EJEMPLO: Permisos específicos y mínimos para la misma función Lambda
resource "aws_iam_role_policy" "lambda_policy_good" {
  name = "lambda-s3-reader-policy"
  role = aws_iam_role.lambda_execution.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",      # Solo lectura de objetos
          "s3:ListBucket"      # Listar el contenido del bucket
          # Sin s3:DeleteObject, s3:PutObject, s3:* ni nada más
        ]
        Resource = [
          aws_s3_bucket.data.arn,          # Solo este bucket específico
          "${aws_s3_bucket.data.arn}/*"    # Y sus objetos
          # Sin "*" que abarcaría todos los buckets
        ]
      },
      {
        # Permisos para escribir logs (obligatorio para debugging)
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:log-group:/aws/lambda/${aws_lambda_function.main.function_name}:*"
        # Solo el grupo de logs de esta función Lambda específica
      }
    ]
  })
}
# Boundary de permisos: Limitar el máximo de permisos que un rol puede tener
# Incluso si alguien añade más permisos al rol, el boundary lo limita

resource "aws_iam_policy" "developer_boundary" {
  name        = "developer-permissions-boundary"
  description = "Define el máximo de permisos que pueden tener los desarrolladores"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:*",
          "s3:*",
          "rds:*",
          "lambda:*",
          "cloudwatch:*"
        ]
        Resource = "*"
        # Los desarrolladores pueden tener hasta estos permisos, nunca más
      },
      {
        # Negar explícitamente acciones de alto riesgo
        Effect = "Deny"
        Action = [
          "iam:CreateUser",
          "iam:DeleteUser",
          "iam:AttachRolePolicy",  # Escalar privilegios
          "organizations:*",       # Gestión de la organización
          "account:*"              # Configuración de la cuenta raíz
        ]
        Resource = "*"
      }
    ]
  })
}

# Aplicar el boundary a un rol
resource "aws_iam_role" "developer_role" {
  name                 = "developer-role"
  assume_role_policy   = data.aws_iam_policy_document.assume_role.json
  permissions_boundary = aws_iam_policy.developer_boundary.arn  # Aplicar boundary
}

Cifrado del Estado de Terraform

El archivo de estado puede contener información extremadamente sensible: contraseñas de bases de datos, claves de API, certificados TLS. Protegerlo es obligatorio.

# versions.tf
# Configuración de backend S3 con cifrado y control de acceso

terraform {
  backend "s3" {
    bucket         = "empresa-terraform-state-prod"  # Bucket dedicado para el estado
    key            = "environments/prod/terraform.tfstate"
    region         = "us-east-1"

    # Cifrado del estado en reposo
    encrypt        = true           # Habilitar cifrado SSE
    kms_key_id     = "arn:aws:kms:us-east-1:123456789:key/abc-123"  # Clave KMS propia

    # DynamoDB para bloqueo del estado (evitar corrupción por ejecuciones paralelas)
    dynamodb_table = "empresa-terraform-state-lock"

    # Perfil de AWS con permisos solo para el estado (no para toda la infraestructura)
    profile        = "terraform-state-manager"
  }
}
# El bucket S3 del estado debe tener configuración de seguridad estricta
# Este código crea el bucket del estado de forma segura:

resource "aws_s3_bucket" "terraform_state" {
  bucket = "empresa-terraform-state-prod"

  # Prevenir borrado accidental del bucket
  lifecycle {
    prevent_destroy = true  # terraform destroy fallará si intenta eliminar este bucket
  }

  tags = {
    Name        = "Terraform State Storage"
    Sensitivity = "critical"
    ManagedBy   = "terraform"
  }
}

# Habilitar versionado: permite recuperar estados anteriores si algo sale mal
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Cifrado del servidor con clave KMS propia (más control que AES256 por defecto)
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.terraform_state.arn
    }
    bucket_key_enabled = true  # Reduce el costo de las llamadas a KMS
  }
}

# Bloquear todo acceso público al bucket del estado
resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true   # Bloquear ACLs públicas
  block_public_policy     = true   # Bloquear políticas de bucket públicas
  ignore_public_acls      = true   # Ignorar ACLs públicas existentes
  restrict_public_buckets = true   # Restringir acceso público al bucket
}

# Política de bucket: solo permitir acceso desde la cuenta de AWS específica
resource "aws_s3_bucket_policy" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyInsecureTransport"
        Effect = "Deny"
        Principal = "*"
        Action    = "s3:*"
        Resource = [
          aws_s3_bucket.terraform_state.arn,
          "${aws_s3_bucket.terraform_state.arn}/*"
        ]
        Condition = {
          Bool = {
            "aws:SecureTransport" = "false"  # Denegar conexiones HTTP (no HTTPS)
          }
        }
      }
    ]
  })
}

Análisis Estático de Seguridad con checkov y terrascan

# Checkov: Análisis de seguridad con más de 1000 políticas predefinidas

# Instalación
pip install checkov

# Escaneo básico del directorio actual
checkov -d .

# Escanear con framework específico
checkov -d . --framework terraform

# Ver solo los checks fallidos (omitir los pasados)
checkov -d . --compact

# Generar reporte en formato SARIF (compatible con GitHub Security)
checkov -d . --output sarif > results.sarif

# Ejemplo de como suprimir un check específico con justificación:
# En el código de Terraform, puedes suprimir checks de checkov
# con comentarios especiales. SIEMPRE documenta la razón.

resource "aws_s3_bucket" "public_website" {
  bucket = "empresa-website-publico"

  # checkov:skip=CKV_AWS_20:Este bucket está diseñado para hosting de sitio web público.
  # La política de bucket controla exactamente qué se puede acceder (solo objetos HTML/CSS/JS)
  # Aprobado por el equipo de seguridad en issue SEC-456 el 2024-01-15
}

Comparación de Configuraciones Inseguras vs Seguras

Aspecto Configuración Insegura Configuración Segura
Credenciales Hardcodeadas en archivos .tf Variables de entorno o Vault
Estado de Terraform Local en el disco Backend S3 con cifrado KMS
Permisos IAM Action: "*", Resource: "*" Permisos mínimos y específicos
Bucket S3 Acceso público habilitado Block public access + política restrictiva
RDS Sin cifrado, públicamente accesible Cifrado habilitado, en subnet privada
Security Groups Puerto 22 abierto a 0.0.0.0/0 Puerto 22 solo desde IPs corporativas
Variables sensibles sensitive = false por defecto sensitive = true explícito
Versionado de providers Sin restricciones de versión Versiones ancladas (~> 5.0)
Logs de auditoría Sin CloudTrail configurado CloudTrail en todos los servicios
Encriptación en tránsito HTTP permitido Solo HTTPS/TLS

Políticas de Sentinel (Terraform Enterprise)

Sentinel es el framework de políticas de HashiCorp para Terraform Cloud/Enterprise. Permite definir reglas que se verifican ANTES de que terraform apply se ejecute.

# policies/require-tags.sentinel
# Política Sentinel: Todos los recursos deben tener etiquetas obligatorias

import "tfplan/v2" as tfplan

# Definir las etiquetas que son obligatorias en TODOS los recursos
required_tags = ["Environment", "Owner", "CostCenter", "ManagedBy"]

# Obtener todos los recursos del plan (los que se van a crear o modificar)
all_resources = filter tfplan.resource_changes as _, resource_change {
  resource_change.change.actions contains "create" or
  resource_change.change.actions contains "update"
}

# Función que verifica si un recurso tiene todas las etiquetas requeridas
has_required_tags = func(resource) {
  tags = resource.change.after.tags

  # Verificar cada etiqueta requerida
  all required_tags as tag {
    # La etiqueta debe existir y no estar vacía
    tags[tag] is defined and length(tags[tag]) > 0
  }
}

# Evaluar todos los recursos
all_resources_have_tags = all all_resources as _, resource {
  has_required_tags(resource)
}

# Resultado de la política: true = pasa, false = bloquea el apply
main = rule {
  all_resources_have_tags
}
# policies/restrict-instance-types.sentinel
# Política: Solo permitir ciertos tipos de instancia para controlar costos

import "tfplan/v2" as tfplan

# Lista de tipos de instancia permitidos (optimizados en costo/rendimiento)
allowed_instance_types = [
  "t3.micro", "t3.small", "t3.medium", "t3.large",
  "t3.xlarge", "t3.2xlarge",
  "m5.large", "m5.xlarge", "m5.2xlarge"
]

# Obtener todas las instancias EC2 en el plan
ec2_instances = filter tfplan.resource_changes as _, resource_change {
  resource_change.type == "aws_instance" and
  (resource_change.change.actions contains "create" or
   resource_change.change.actions contains "update")
}

# Verificar que cada instancia usa un tipo permitido
all_instances_use_allowed_types = all ec2_instances as _, instance {
  instance.change.after.instance_type in allowed_instance_types
}

main = rule {
  all_instances_use_allowed_types
}

Auditoría de Cambios de Infraestructura

Una pista de auditoría completa es esencial para el cumplimiento normativo y para investigar incidentes:

# Habilitar CloudTrail para registrar todos los cambios de API en AWS

resource "aws_cloudtrail" "infrastructure_audit" {
  name           = "infrastructure-changes-trail"
  s3_bucket_name = aws_s3_bucket.audit_logs.id

  # Registrar eventos de escritura en todos los servicios (crear, modificar, eliminar)
  include_global_service_events = true  # IAM, STS, etc.
  is_multi_region_trail         = true  # Todas las regiones

  # Habilitar validación de integridad del log
  # Detecta si alguien ha modificado o eliminado los logs
  enable_log_file_validation = true

  # Registrar también las llamadas de lectura (opcional, genera más volumen)
  event_selector {
    read_write_type           = "All"       # Leer y escribir
    include_management_events = true        # Eventos de gestión (API calls)

    data_resource {
      type   = "AWS::S3::Object"            # Registrar acceso a objetos S3
      values = ["arn:aws:s3:::"]            # Todos los buckets
    }
  }

  tags = {
    Name        = "Infrastructure Audit Trail"
    Sensitivity = "high"
    ManagedBy   = "terraform"
  }
}

# Configurar alertas para cambios críticos de infraestructura
resource "aws_cloudwatch_metric_alarm" "root_account_usage" {
  alarm_name          = "root-account-usage-alert"
  alarm_description   = "Alerta cuando se usa la cuenta raíz de AWS"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  threshold           = 1
  evaluation_periods  = 1
  metric_name         = "RootAccountUsage"
  namespace           = "CloudTrailMetrics"
  period              = 300  # 5 minutos

  alarm_actions = [aws_sns_topic.security_alerts.arn]  # Notificar al equipo de seguridad
}

Gestión de Versiones del Provider (Pin de Versiones)

Anclar las versiones de los providers es una práctica de seguridad, no solo de estabilidad:

# versions.tf - Anclaje de versiones correcto

terraform {
  required_version = ">= 1.6.0, < 2.0.0"  # Rango específico para Terraform

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      # ~> 5.31.0: permite actualizaciones de parche (5.31.x) pero no de menor (5.32.x)
      # Esto garantiza que recibimos correcciones de seguridad pero no cambios breaking
      version = "~> 5.31.0"
    }

    random = {
      source  = "hashicorp/random"
      version = "~> 3.6.0"
    }

    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0.0"
    }
  }
}

# POR QUE ES UNA PRACTICA DE SEGURIDAD:
# Si no anclas las versiones, una actualización automática de un provider
# podría introducir:
# - Cambios breaking que alteren tu infraestructura inesperadamente
# - Versiones comprometidas si el repositorio del provider fuera vulnerado
# - Comportamientos distintos que rompan las pruebas de seguridad

Revisión de Módulos de Terceros

Antes de usar un módulo del Terraform Registry o de GitHub, debes revisarlo:

# Lista de verificación antes de usar un módulo de terceros:

# 1. Verificar la fuente del módulo
# - ¿Es un módulo "verified" en el Registry (con checkmark azul)?
# - ¿Es mantenido por la empresa del proveedor (ej: AWS, Google)?
# - ¿Tiene muchos stars y uso activo?

# 2. Revisar el código fuente
# Clonar el módulo y revisarlo antes de usarlo en producción
git clone https://github.com/terraform-aws-modules/terraform-aws-vpc
cd terraform-aws-vpc
# Revisar: ¿Qué permisos solicita? ¿Hace llamadas a APIs externas?
# ¿Tiene configuraciones de seguridad adecuadas por defecto?

# 3. Anclar siempre la versión del módulo
# MAL: Sin version anclada - un cambio en el módulo puede romper tu infraestructura
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  # Sin version: usa siempre la última, potencialmente inestable o comprometida
}

# BIEN: Con version anclada al tag exacto
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.1"  # Versión revisada y aprobada por el equipo

  # Argumentos del módulo...
  name             = "prod-vpc"
  cidr             = "10.0.0.0/16"
  azs              = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets   = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = false  # Un NAT por AZ para alta disponibilidad
}

Tabla: Checklist de Seguridad para Revisión de Código Terraform

Usa esta checklist en cada revisión de pull request que involucre cambios de infraestructura:

# Área Verificación Crítico
1 Credenciales No hay credenciales hardcodeadas en archivos .tf o .tfvars SI
2 Estado El backend usa cifrado y está en almacenamiento remoto seguro SI
3 Variables sensibles Las variables con datos sensibles tienen sensitive = true SI
4 IAM Los roles y políticas siguen el principio de mínimo privilegio SI
5 Redes Los Security Groups no tienen reglas 0.0.0.0/0 en puertos sensibles SI
6 Cifrado Los datos en reposo (EBS, S3, RDS) están cifrados SI
7 Acceso público Los buckets S3 tienen block_public_access habilitado SI
8 Versiones Los providers y módulos de terceros tienen versiones ancladas SI
9 Checkov El código pasa el análisis de checkov sin failures críticos SI
10 Etiquetas Todos los recursos tienen las etiquetas requeridas por la empresa NO
11 Logs Los recursos críticos tienen logging/auditoría habilitada NO
12 Backups Las bases de datos y storages críticos tienen backup configurado NO
13 Alertas Hay alarmas configuradas para comportamiento anormal NO
14 Documentación Los cambios importantes tienen justificación en el commit/PR NO
15 Módulos Los módulos de terceros han sido revisados y tienen versión fija NO

Ejercicio: Auditar una Configuración Terraform Insegura

Configuración a auditar

Analiza el siguiente código e identifica todos los problemas de seguridad:

# CODIGO A AUDITAR - Identifica los problemas

provider "aws" {
  access_key = "AKIAIOSFODNN7EXAMPLE"
  secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  region     = "us-east-1"
}

resource "aws_s3_bucket" "datos_empresa" {
  bucket = "datos-empresa-2024"
  acl    = "public-read"
}

resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_db_instance" "base_datos" {
  engine            = "mysql"
  instance_class    = "db.t3.micro"
  username          = "admin"
  password          = "Password123"
  publicly_accessible = true
  storage_encrypted = false
}

resource "aws_iam_role_policy" "acceso_total" {
  name = "acceso-total"
  role = aws_iam_role.app.id

  policy = jsonencode({
    Statement = [{
      Effect   = "Allow"
      Action   = "*"
      Resource = "*"
    }]
  })
}

Solución: Problemas Identificados y Correcciones

Problema 1 - CRÍTICO: Credenciales hardcodeadas en el provider

# ANTES (inseguro):
provider "aws" {
  access_key = "AKIAIOSFODNN7EXAMPLE"   # CRITICO: Credenciales en texto plano
  secret_key = "wJalrXUtnFEMI/K7MDENG"
}

# DESPUÉS (seguro):
# Eliminar completamente los atributos access_key y secret_key.
# Terraform usará automáticamente las variables de entorno AWS_ACCESS_KEY_ID
# y AWS_SECRET_ACCESS_KEY, o el perfil de AWS CLI configurado.
provider "aws" {
  region = "us-east-1"
  # Las credenciales se configuran via:
  # export AWS_ACCESS_KEY_ID="..."
  # export AWS_SECRET_ACCESS_KEY="..."
  # O usando un rol IAM si ejecutamos en EC2/ECS/Lambda
}

Problema 2 - CRÍTICO: Bucket S3 con acceso público de lectura

# ANTES (inseguro):
resource "aws_s3_bucket" "datos_empresa" {
  bucket = "datos-empresa-2024"
  acl    = "public-read"  # CRITICO: Datos de empresa accesibles por cualquiera en internet
}

# DESPUÉS (seguro):
resource "aws_s3_bucket" "datos_empresa" {
  bucket = "datos-empresa-2024"
  # Eliminar el ACL público
}

resource "aws_s3_bucket_public_access_block" "datos_empresa" {
  bucket = aws_s3_bucket.datos_empresa.id

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

resource "aws_s3_bucket_server_side_encryption_configuration" "datos_empresa" {
  bucket = aws_s3_bucket.datos_empresa.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

Problema 3 - ALTO: Puerto SSH abierto a toda internet

# ANTES (inseguro):
ingress {
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]  # ALTO: SSH expuesto a toda internet = vector de ataque
}

# DESPUÉS (seguro):
ingress {
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = [var.corporate_vpn_cidr]  # Solo desde la VPN corporativa
  description = "SSH access from corporate VPN only"
}
# Mejor alternativa: usar AWS Systems Manager Session Manager
# y eliminar completamente el puerto 22

Problema 4 - CRÍTICO: Contraseña de base de datos hardcodeada y BD pública

# ANTES (inseguro):
resource "aws_db_instance" "base_datos" {
  password          = "Password123"  # CRITICO: Contraseña en texto plano en el código
  publicly_accessible = true         # CRITICO: Base de datos accesible desde internet
  storage_encrypted = false          # ALTO: Datos sin cifrar en reposo
}

# DESPUÉS (seguro):
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/rds/mysql/password"
}

resource "aws_db_instance" "base_datos" {
  engine            = "mysql"
  instance_class    = "db.t3.micro"
  username          = "admin"
  password          = jsondecode(data.aws_secretsmanager_secret_version.db_password.secret_string)["password"]
  publicly_accessible = false        # Base de datos solo en red privada
  storage_encrypted   = true         # Cifrado en reposo habilitado
  db_subnet_group_name = aws_db_subnet_group.main.name  # En subnets privadas

  backup_retention_period = 7        # 7 días de backups automáticos
  deletion_protection     = true     # Prevenir borrado accidental
}

Problema 5 - CRÍTICO: Política IAM con permisos totales

# ANTES (inseguro):
policy = jsonencode({
  Statement = [{
    Effect   = "Allow"
    Action   = "*"     # CRITICO: Permisos de administrador total
    Resource = "*"     # Sobre todos los recursos de la cuenta
  }]
})

# DESPUÉS (seguro - ejemplo para una app web que necesita S3 y DynamoDB):
policy = jsonencode({
  Version = "2012-10-17"
  Statement = [
    {
      Effect = "Allow"
      Action = [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ]
      Resource = "${aws_s3_bucket.app_data.arn}/*"
    },
    {
      Effect = "Allow"
      Action = [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:Query"
      ]
      Resource = aws_dynamodb_table.app_data.arn
    }
  ]
})

Resumen del Módulo 7: Mejores Prácticas Profesionales

A lo largo de este módulo has adquirido las habilidades que distinguen a un profesional de Terraform de un principiante:

Lo que has aprendido en este módulo 7:

07-01 - Organización del Código:

  • Estructuras de archivos estándar (main.tf, variables.tf, outputs.tf, locals.tf)
  • Separación por entorno vs separación por capa
  • Uso del bloque locals {} para el principio DRY
  • Convenciones de nomenclatura profesionales

07-02 - Control de Versiones:

  • Qué incluir y excluir del repositorio Git
  • .gitignore completo para Terraform
  • Estrategias de branching para IaC
  • Flujo de trabajo profesional con PR y revisiones

07-03 - Testing:

  • Los cuatro niveles de la pirámide de testing
  • terraform fmt, validate, tflint y checkov
  • Escritura de pruebas con terraform test y archivos .tftest.hcl
  • Integración de pruebas en pipelines CI/CD

07-04 - Seguridad (este módulo):

  • Gestión segura de credenciales con Vault y AWS Secrets Manager
  • Principio de mínimo privilegio en IAM
  • Cifrado del estado de Terraform en S3 con KMS
  • Variables sensibles con sensitive = true
  • Análisis de seguridad con checkov y terrascan
  • Políticas de Sentinel para gobernanza
  • Auditoría con CloudTrail
  • Checklist de seguridad para revisiones de código

Próximos pasos

Con los conocimientos de este módulo 7, estás preparado para el módulo 8: Integración de Terraform con CI/CD, donde aprenderás a automatizar completamente el ciclo de vida de la infraestructura usando Jenkins, GitHub Actions y Terraform Cloud, aplicando todas las prácticas de organización, control de versiones, testing y seguridad que has aprendido hasta ahora.

© Copyright 2026. Todos los derechos reservados