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:
- Principio de mínimo privilegio: Cada componente, usuario y proceso solo debe tener los permisos mínimos necesarios para funcionar
- Defensa en profundidad: No confiar en una única capa de seguridad; añadir múltiples capas complementarias
- Seguridad por defecto: Los valores por defecto de la configuración deben ser seguros, no permisivos
- Inmutabilidad: Preferir reemplazar recursos en lugar de modificarlos; reduce el riesgo de configuraciones inconsistentes
- Auditoría completa: Todo cambio de infraestructura debe ser rastreable (quién, qué, cuándo y por qué)
- 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 seguridadRevisió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 22Problema 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
.gitignorecompleto 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,tflintycheckov- Escritura de pruebas con
terraform testy 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.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
