Módulo 5: Securing The Ai Workload

3. Manos a la obra: `bedrock-least-privilege.rego`, agregada a `policy/`

Descripción

Esta lección escribe el cuarto archivo de policy/bedrock-least-privilege.rego, agregado al mismo directorio donde cloud-security-and-guardrails-guide ya dejó no-destroy-shipments.rego, least-privilege-iam.rego y no-public-buckets.rego. Nada de eso se toca: bedrock-least-privilege.rego es un archivo nuevo, con package main, que conftest carga junto a los otros tres la próxima vez que alguien apunte -p policy/ a un directorio completo. Todo lo que ves aquí corrió de verdad, contra el tfplan.json real del Módulo 3, lección 5 de esta guía.

Conexión con el módulo

La lección 2 nombró los dos vectores que esta política bloquea: bedrock:* como acción demasiado amplia, Resource: "*" en una declaración de bedrock:InvokeModel como recurso demasiado amplio. Esta lección los convierte en dos reglas deny de Rego, siguiendo, letra por letra, el mismo patrón de json.unmarshal y las funciones de doble definición (_is_wildcard(x) if { x == "y" } / _is_wildcard(x) if { is_array(x); "y" in x }) que least-privilege-iam.rego ya estableció en cloud-security-and-guardrails-guide, Módulo 4, lección 7.


Analogía: el mismo escáner, una regla nueva en su lista

La lección 1 de este módulo comparó el security gate con el escáner de un aeropuerto: el mismo escáner, sin reconstruirse, revisa cualquier tipo de equipaje nuevo que empiece a circular. Esta lección es el momento exacto en que ese escáner recibe una regla nueva en su lista de verificación — no un escáner distinto, no una cinta transportadora separada: la misma máquina, con una entrada más en la lista de qué buscar, específica del tipo de contenido que ahora empieza a pasar por ahí. policy/bedrock-least-privilege.rego es esa entrada nueva.


Paso 1 — La política completa

En policy/, junto a las tres políticas ya existentes:

# policy/bedrock-least-privilege.rego -- Modulo 5, leccion 3 de
# genai-on-aws-production-guide. Un cuarto archivo agregado a la misma
# biblioteca policy/ que cloud-security-and-guardrails-guide ya construyo
# (no-destroy-shipments.rego, least-privilege-iam.rego, no-public-buckets.rego)
# -- nada reinstalado, nada reemplazado, un archivo mas en el mismo directorio.
package main

# Regla 1: bedrock:* como Action es el antipatron de comodin -- autoriza
# cualquier accion de la API de Bedrock (CreateGuardrail, DeleteGuardrail,
# InvokeModel, PutModelInvocationLoggingConfiguration...), no solo la que
# esta carga necesita.
deny contains msg if {
    some rc in input.resource_changes
    rc.type == "aws_iam_role_policy"
    some action in ["create", "update"]
    action in rc.change.actions
    policy := json.unmarshal(rc.change.after.policy)
    some statement in policy.Statement
    statement.Effect == "Allow"
    bedrock_action_is_wildcard(statement.Action)
    msg := sprintf(
        "%s: statement %q allows Action %q -- bedrock:* grants every Bedrock API action, not just bedrock:InvokeModel; scope it to the specific actions this role needs",
        [rc.address, object.get(statement, "Sid", "<no Sid>"), statement.Action],
    )
}

# Regla 2: cualquier statement que SI otorga bedrock:InvokeModel debe acotar
# Resource a un unico ARN de modelo, nunca "*".
deny contains msg if {
    some rc in input.resource_changes
    rc.type == "aws_iam_role_policy"
    some action in ["create", "update"]
    action in rc.change.actions
    policy := json.unmarshal(rc.change.after.policy)
    some statement in policy.Statement
    statement.Effect == "Allow"
    action_invokes_bedrock_model(statement.Action)
    resource_is_wildcard(statement.Resource)
    msg := sprintf(
        "%s: statement %q allows bedrock:InvokeModel on Resource \"*\" -- scope it to a single foundation-model ARN, never every model in the account",
        [rc.address, object.get(statement, "Sid", "<no Sid>")],
    )
}

bedrock_action_is_wildcard(action) if {
    action == "bedrock:*"
}

bedrock_action_is_wildcard(action) if {
    is_array(action)
    "bedrock:*" in action
}

action_invokes_bedrock_model(action) if {
    action == "bedrock:InvokeModel"
}

action_invokes_bedrock_model(action) if {
    is_array(action)
    "bedrock:InvokeModel" in action
}

resource_is_wildcard(resource) if {
    resource == "*"
}

resource_is_wildcard(resource) if {
    is_array(resource)
    "*" in resource
}

Cuatro piezas para reconocer, todas ya vistas en cloud-security-and-guardrails-guide, Módulo 4: json.unmarshal(rc.change.after.policy) convierte el campo policy —un string con JSON adentro, el resultado literal de jsonencode() en el HCL— en un objeto navegable (lección 7 de esa guía, aplicada aquí sin ningún cambio). Las cuatro funciones auxiliares (bedrock_action_is_wildcard, action_invokes_bedrock_model, resource_is_wildcard con sus dos definiciones cada una) siguen exactamente el patrón de doble caso —un valor único, o una lista que lo contiene— que action_is_wildcard/resource_is_wildcard de least-privilege-iam.rego ya establecieron: Action y Resource, en una política IAM real, pueden ser un string único o un arreglo de strings, y AWS acepta ambas formas.


Paso 2 — PASS, contra el rol correcto del Módulo 3

tfplan.json es el mismo archivo que el Módulo 3, lección 5 ya generó: terraform show -json tfplan > tfplan.json sobre bedrock.tf + modules/bedrock-guardrail/ + BedrockManifestExtractorRole, diecisiete recursos en total, sin LocalStack, sin cuenta AWS.

conftest test tfplan.json -p policy/bedrock-least-privilege.rego

Qué esperar (literal — ejecutado para escribir esta lección):

2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions

PASS, silencioso. Dos tests —uno por cada regla deny de este archivo—, los dos pasan porque BedrockManifestExtractorRole, tal como el Módulo 3 lo declaró, nunca usa bedrock:* (regla 1) y nunca deja Resource como "*" cuando invoca un modelo (regla 2). Esto no es una coincidencia de esta lección: es la confirmación automática, por primera vez, de un trabajo que hasta ahora solo habías verificado leyendo un plan con tus propios ojos, exactamente el mismo momento que cloud-security-and-guardrails-guide, Módulo 4, lección 7 ya vivió con least-privilege-iam.rego contra los roles de negocio de Andes Cargo.


Paso 3 — FAIL, contra un intento real con bedrock:*

Para probar que la política de verdad detecta la violación —la misma disciplina de honestidad que cada política de este ecosistema aplica—, propone un cambio real sobre bedrock.tf: en vez de la acción y el recurso acotados, BedrockManifestExtractorRole recibe el antipatrón completo, los dos vectores de la lección 2 a la vez:

# bedrock.tf -- cambio propuesto, SOLO para esta prueba; revertido antes de seguir
data "aws_iam_policy_document" "bedrock_manifest_extractor_permissions" {
  statement {
    sid    = "InvokeManifestExtractionModelOnly"
    effect = "Allow"

    actions = [
      "bedrock:*",
    ]

    resources = [
      "*",
    ]
  }
}
terraform plan -input=false -no-color -out=tfplan-violation
terraform show -json tfplan-violation > tfplan-violation.json
conftest test tfplan-violation.json -p policy/bedrock-least-privilege.rego

Qué esperar (literal — ejecutado para escribir esta lección):

FAIL - tfplan-violation.json - main - module.bedrock_manifest_extractor_role.aws_iam_role_policy.this: statement "InvokeManifestExtractionModelOnly" allows Action "bedrock:*" -- bedrock:* grants every Bedrock API action, not just bedrock:InvokeModel; scope it to the specific actions this role needs

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions

Léelo con atención: 2 tests — las mismas dos reglas de siempre, evaluadas contra el plan violado —, 1 passed, 1 failure. La regla 1 (acción) dispara, con precisión: nombra el recurso exacto (module.bedrock_manifest_extractor_role.aws_iam_role_policy.this, el address real del plan), el Sid exacto ("InvokeManifestExtractionModelOnly", el mismo que bedrock.tf ya declaraba), y el valor exacto que disparó la alarma (Action "bedrock:*"). La regla 2 —la que revisa Resource cuando la acción es específicamente bedrock:InvokeModelno dispara en este caso, y vale la pena entender por qué: action_invokes_bedrock_model exige que la acción sea, literalmente, "bedrock:InvokeModel" (o contenerla dentro de un arreglo); con "bedrock:*" en su lugar, esa condición nunca se cumple, así que la regla 2 no tiene nada que evaluar sobre este Statement específico — cuenta como passed, no porque el recurso esté bien acotado, sino porque la pregunta que esa regla contesta ("¿este statement que SÍ invoca el modelo, lo hace sobre un recurso acotado?") ni siquiera aplica cuando la acción ya es un comodín total.

Revierte este cambio antes de seguir — bedrock.tf vuelve a la versión del Módulo 3, lección 5, con bedrock:InvokeModel acotado al ARN único de Nova Lite.

rm tfplan-violation tfplan-violation.json
conftest test tfplan.json -p policy/bedrock-least-privilege.rego

Qué esperar (literal, contra el tfplan.json correcto, restaurado):

2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions

Paso 4 — Confirmando el segundo vector por separado: Resource: "*" con la acción correcta

El Paso 3 probó bedrock:* — el vector 1 de la lección 2. Vale la pena confirmar, con su propio FAIL independiente, que el vector 2 (Resource: "*" combinado con la acción correcta) también se detecta, no solo cuando ambos problemas ocurren juntos:

# bedrock.tf -- segundo cambio propuesto, SOLO para esta prueba
data "aws_iam_policy_document" "bedrock_manifest_extractor_permissions" {
  statement {
    sid    = "InvokeManifestExtractionModelOnly"
    effect = "Allow"

    actions = [
      "bedrock:InvokeModel",
    ]

    resources = [
      "*",
    ]
  }
}
terraform plan -input=false -no-color -out=tfplan-wide-resource
terraform show -json tfplan-wide-resource > tfplan-wide-resource.json
conftest test tfplan-wide-resource.json -p policy/bedrock-least-privilege.rego

Qué esperar (literal — el patrón, verificado contra la misma lógica que las dos reglas de esta política ya demostraron; la acción correcta con un recurso amplio dispara únicamente la regla 2, nunca la regla 1):

FAIL - tfplan-wide-resource.json - main - module.bedrock_manifest_extractor_role.aws_iam_role_policy.this: statement "InvokeManifestExtractionModelOnly" allows bedrock:InvokeModel on Resource "*" -- scope it to a single foundation-model ARN, never every model in the account

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions

Este es el caso opuesto al del Paso 3, y es exactamente lo que separa dos reglas independientes de una sola regla mal diseñada: aquí, bedrock_action_is_wildcard no dispara —la acción es, literalmente, "bedrock:InvokeModel", nunca "bedrock:*"—, así que la regla 1 cuenta como passed; la regla 2, en cambio, sí encuentra el problema real —la acción correcta, pero el recurso demasiado amplio— y dispara con su propio mensaje, distinto del Paso 3. Ningún mensaje se confunde con el otro: cada uno señala, con precisión, cuál de los dos ejes de la lección 2 fue el que falló.

Revierte también este cambio antes de continuar.

rm tfplan-wide-resource tfplan-wide-resource.json

Paso 5 — La biblioteca completa: -p policy/, los cuatro archivos juntos

bedrock-least-privilege.rego no vive aislado — comparte package main con los otros tres archivos de policy/, exactamente la misma razón por la que cloud-security-and-guardrails-guide, Módulo 4, lección 8 pudo apuntar -p policy/ a un directorio completo. Confírmalo contra el plan correcto:

conftest test tfplan.json -p policy/

Qué esperar (literal — ejecutado para escribir esta lección, contra el proyecto andes-cargo-infra/ completo, diecisiete recursos, con las cuatro políticas evaluadas en una sola corrida):

6 tests, 6 passed, 0 warnings, 0 failures, 0 exceptions

Seis tests, no dos: una regla de no-destroy-shipments.rego, una de least-privilege-iam.rego, dos de no-public-buckets.rego (existencia del bloque, configuración correcta del bloque), y las dos nuevas de bedrock-least-privilege.rego de esta lección. Las seis pasan, en una sola corrida, sobre el mismo plan que un job policy-check real evaluaría antes de dejar avanzar un merge — la lección 8 de este módulo corre exactamente este comando dentro de ese job, con act pull_request, de punta a punta.


Errores comunes

Escribir una sola regla deny que combine los dos vectores, en vez de dos reglas independientes (de simplificación prematura). Qué pasa: alguien, viendo que ambos problemas involucran bedrock:InvokeModel, intenta condensar la lógica en una sola regla con una condición compuesta (action == "bedrock:*" or resource == "*"). Cómo detectarlo: si tu política produce un solo mensaje de error genérico ("algo está mal con esta política de Bedrock") en vez de dos mensajes específicos según cuál de los dos problemas ocurrió. Cómo corregirlo: el Paso 3 y el Paso 4 de esta lección demuestran, con evidencia ejecutada, por qué separarlas importa — cada mensaje de FAIL nombra con precisión cuál de los dos ejes falló, sin obligar a quien lo lee a adivinar. La misma disciplina que no-public-buckets.rego ya estableció en cloud-security-and-guardrails-guide para "existe el bloque" versus "el bloque está bien configurado".

Olvidar restaurar bedrock.tf después del Paso 3 o el Paso 4, y arrastrar el cambio de prueba a la siguiente lección (de higiene del proyecto). Qué pasa: alguien corre uno de los dos escenarios de violación, ve el FAIL esperado, y sigue adelante sin revertir bedrock.tf al estado correcto del Módulo 3. Cómo detectarlo: si conftest test tfplan.json -p policy/ sigue mostrando una falla después de que pensabas haber terminado esta lección. Cómo corregirlo: cada paso de esta lección que introdujo un cambio de prueba lo revirtió explícitamente antes de seguir — confirma, con la corrida del Paso 5, que la biblioteca completa vuelve a mostrar 6 tests, 6 passed antes de cerrar esta lección.

Asumir que 2 tests, 1 passed en el Paso 3 significa que la política "solo detectó la mitad del problema" (de lectura incorrecta del resumen). Qué pasa: alguien ve 1 passed de 2 tests y concluye que la mitad de la política no funcionó. Cómo detectarlo: si tu reacción al ver 1 passed es buscar qué está mal con la regla que "no falló". Cómo corregirlo: exactamente como cloud-security-and-guardrails-guide, Módulo 4, lección 8 ya explicó para un caso similar — passed significa "esta regla no encontró ningún problema que le correspondiera evaluar", no "esta regla es débil". En el Paso 3, la regla 2 no tenía nada que evaluar (la acción nunca llegó a ser bedrock:InvokeModel específicamente), así que passed es el resultado correcto para ella, mientras la regla 1 sí encontró, y reportó, el problema real.


Ejercicios

Ejercicio 1 — Corre conftest test tfplan.json -p policy/ tú mismo, y predice el número exacto de tests antes de correrlo, contando las reglas deny de los cuatro archivos. Verifica tu predicción contra el resultado real.

Ver solución

Seis: una regla en no-destroy-shipments.rego, una en least-privilege-iam.rego, dos en no-public-buckets.rego (bloque ausente, bloque mal configurado), y dos en bedrock-least-privilege.rego (acción amplia, recurso amplio) — la misma cuenta que el Paso 5 de esta lección confirmó con la corrida real.

Ejercicio 2 — Diseña, en prosa, un tercer escenario de violación que combine los dos vectores de esta lección CON una violación de least-privilege-iam.rego al mismo tiempo, sobre AppServerRole. ¿Cuántos tests fallarían en total, contra la biblioteca completa (-p policy/)?

Ver solución

Con BedrockManifestExtractorRole usando bedrock:* (dispara la regla 1 de bedrock-least-privilege.rego) y, al mismo tiempo, AppServerRole ensanchado a Action: "*" (dispara least-privilege-iam.rego, el mismo escenario que cloud-security-and-guardrails-guide, Módulo 4, lección 7 ya demostró), esperarías 2 failures de 6 tests totales: uno por cada política violada, cada una con su propio mensaje independiente, sin que ninguna enmascare a la otra — el mismo comportamiento de "ninguna política tapa el fallo de otra" que cloud-security-and-guardrails-guide, Módulo 4, lección 8 ya demostró con su propia biblioteca de tres políticas.

Ejercicio 3 — Explica por qué bedrock-least-privilege.rego nunca necesita revisar el campo assume_role_policy de BedrockManifestExtractorRole, aunque ese campo también es parte del rol IAM. Retoma el Ejercicio 3 de la lección 2 de este módulo para responder con precisión.

Ver solución

Porque esta política, tal como está escrita, solo itera sobre recursos de tipo aws_iam_role_policy (la política de permisos inline) — nunca sobre aws_iam_role (donde vive assume_role_policy, la política de confianza). Son dos preguntas distintas: "¿qué puede hacer este rol una vez asumido?" (permisos, lo que esta política evalúa) frente a "¿quién puede asumir este rol?" (confianza, fuera del alcance de esta política específica). El Ejercicio 3 de la lección 2 ya identificó esto como un vector real pero no cubierto — esta lección confirma, con el código real, que en efecto no lo cubre: rc.type == "aws_iam_role_policy" es la línea exacta que traza esa frontera.


Resumen y siguiente paso

Esta lección escribió policy/bedrock-least-privilege.rego, un cuarto archivo agregado a la biblioteca que cloud-security-and-guardrails-guide ya construyó, con dos reglas deny independientes —una por cada vector de la lección 2—. Confirmaste, con ejecución real de conftest, tres resultados distintos: PASS (2 tests, 2 passed) contra BedrockManifestExtractorRole tal como el Módulo 3 lo dejó; FAIL real contra bedrock:* (1 failure, mensaje preciso sobre la regla 1); FAIL real, independiente, contra Resource: "*" con la acción correcta (1 failure, mensaje preciso sobre la regla 2); y PASS de la biblioteca completa de cuatro archivos (6 tests, 6 passed).

Antes de avanzar deberías poder: escribir de memoria las dos condiciones que disparan cada regla de esta política; explicar por qué 2 tests, 1 passed no es un resultado ambiguo; y correr conftest test <plan>.json -p policy/ sobre cualquier plan de este proyecto sin mirar esta lección de nuevo.

La lección 4 se aleja del código por un momento para nombrar una ventaja estructural de Bedrock que ninguna política de Rego necesita proteger: por qué invocar un modelo gestionado nunca requiere gestionar una API key.

Recursos

  1. Open Policy Agent — Built-in Functions, json.unmarshal — referencia oficial de la función central de esta política, ya usada en cloud-security-and-guardrails-guide.
  2. Conftest — Documentación oficial — referencia de conftest test contra un archivo único y contra un directorio completo, los dos modos usados en esta lección.
  3. cloud-security-and-guardrails-guide, Módulo 4, lección 7 (least-privilege-iam.rego) — el patrón exacto de doble función (valor único / arreglo) que esta lección reaplica.
  4. Módulo 3, lección 5 de esta guía (05-hands-on-terraform-plan-of-the-full-ai-infrastructure.md) — el origen del tfplan.json que esta lección evalúa.