AWS Builder Center
El peligro silencioso de las llaves estáticas en CI/CD 🔐 ⚠️

El peligro silencioso de las llaves estáticas en CI/CD 🔐 ⚠️

Elimina para siempre los AWS_ACCESS_KEY_ID de tus pipelines. Aprende a diseñar una arquitectura CI/CD multi-cuenta con GitHub Actions, AWS CDK y OIDC bajo el principio de mínimo privilegio y guardrails de seguridad listos para producción.

Cloud & AI Solutions Architect • GenAI • Founder Iztack • AWS SBG Leader at ONMEX
El vector de ataque más común en entornos cloud sigue siendo el mismo: credenciales de larga duración (AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY) almacenadas como secretos en repositorios o pipelines de CI/CD.
Cuando manejas topologías multi-cuenta (ej. Tooling, Development, Production), el problema escala rápidamente:
  • Rotar secretos manualmente entre múltiples cuentas se vuelve insostenible.
  • Si una clave se compromete, el atacante obtiene acceso persistente fuera del pipeline.
  • Rastrear en AWS CloudTrail la identidad real detrás de una acción se vuelve ambiguo.
La solución estándar de la industria es federación de identidades mediante OIDC (OpenID Connect) combinada con asunción de roles delegados en cada cuenta destino.

Arquitectura de la Solución

El flujo elimina credenciales persistentes. GitHub actúa como Identity Provider (IdP) verificado que intercambia un token criptográfico efímero por credenciales temporales emitidas por AWS STS.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+------------------------+
| GitHub Actions Runner |
| (OIDC Token JWT) |
+-----------+------------+
|
| 1. sts:AssumeRoleWithWebIdentity
v
+------------------------+
| Cta. Tooling/Security |
| IAM Role: GitHubOIDC | <--- Valida Issuer, Audience y Repo/Rama
+-----------+------------+
|
| 2. sts:AssumeRole (Cross-Account)
v
+------------------------+
| Cta. Workload (Prod) |
| CDK Deployment Role | <--- Despliega IaC en destino
+------------------------+
 

Trade-offs de diseño:

  1. Punto único de federación (Hub & Spoke) vs. Federación por cuenta:
    • Adoptado: Configurar el IdP en una cuenta central (Tooling/Core) y asumir roles cruzados hacia las cuentas destino. Esto centraliza la auditoría en CloudTrail y simplifica la rotación de certificados.
  2. Restricción estricta del sub claim:
    • Limitar el acceso no solo al repositorio (repo:org/repo:*), sino a la rama y ambiente exacto (repo:org/repo:environment:production o ref:refs/heads/main).

Implementación con AWS CDK (TypeScript/Python)

1. Definición del IAM OIDC Provider y Rol de Despliegue en CDK (TypeScript)

Este stack se despliega en la cuenta donde se orquesta el despliegue:
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export interface GitHubOidcStackProps extends cdk.StackProps {
githubOrg: string;
githubRepo: string;
}

export class GitHubOidcStack extends cdk.Stack {
public readonly deployRoleArn: string;

constructor(scope: Construct, id: string, props: GitHubOidcStackProps) {
super(scope, id, props);

// 1. Registrar GitHub como OIDC Provider (si no existe ya en la cuenta)
const githubProvider = new iam.OpenIdConnectProvider(this, 'GitHubProvider', {
url: 'https://token.actions.githubusercontent.com',
clientIds: ['sts.amazonaws.com'],
});

// 2. Rol con Trust Policy estricta condicionada al repositorio
const githubDeployRole = new iam.Role(this, 'GitHubActionsDeployRole', {
roleName: 'GitHubActions-CDK-Deployer',
assumedBy: new iam.FederatedPrincipal(
githubProvider.openIdConnectProviderArn,
{
StringEquals: {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
},
StringLike: {
// Restricción crítica: Solo la rama main de este repo específico
'token.actions.githubusercontent.com:sub': `repo:${props.githubOrg}/${props.githubRepo}:ref:refs/heads/main`,
},
},
'sts:AssumeRoleWithWebIdentity'
),
description: 'Rol efímero asumido por GitHub Actions para sintetizar y desplegar CDK',
maxSessionDuration: cdk.Duration.hours(1),
});

// 3. Mínimo privilegio para CDK: delegar asunción de los roles CDK de bootstrap
githubDeployRole.addToPolicy(
new iam.PolicyStatement({
sid: 'AllowCDKBootstrapRoleAssumption',
effect: iam.Effect.ALLOW,
actions: ['sts:AssumeRole'],
resources: [
`arn:aws:iam::${this.account}:role/cdk-*-lookup-role-${this.account}-*`,
`arn:aws:iam::${this.account}:role/cdk-*-deploy-role-${this.account}-*`,
`arn:aws:iam::${this.account}:role/cdk-*-file-publishing-role-${this.account}-*`,
`arn:aws:iam::${this.account}:role/cdk-*-image-publishing-role-${this.account}-*`,
],
})
);

this.deployRoleArn = githubDeployRole.roleArn;

new cdk.CfnOutput(this, 'GitHubDeployRoleArnOutput', {
value: githubDeployRole.roleArn,
exportName: 'GitHubDeployRoleArn',
});
}
}
 

Workflow de GitHub Actions (.github/workflows/deploy.yml)

Observa cómo no se configuran secretos de AWS. Solamente se requiere el permiso de id-token: write a nivel de job:
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
name: AWS CDK Multi-Account Deploy

on:
push:
branches:
- main

permissions:
id-token: write # Mandatario para solicitar el token JWT OIDC
contents: read

jobs:
deploy:
name: Synth and Deploy to Target Account
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4

- name: Setup Node.js & CDK
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install Dependencies
run: npm ci

# Intercambio de token seguro por credenciales temporales STS
- name: Configure AWS Credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActions-CDK-Deployer
aws-region: us-east-1
role-session-name: GitHubActions-${{ github.run_id }}
role-duration-seconds: 1800 # 30 min (tiempo de vida efímero)

# Verificación de identidad asumida en CloudTrail
- name: Verify Identity
run: aws sts get-caller-identity

# Despliegue determinista
- name: CDK Diff & Deploy
run: |
npx cdk diff --all
npx cdk deploy --all --require-approval never
 

Consideraciones de Producción: Seguridad y FinOps

  1. Sesiones cortas (role-duration-seconds): Mantén la duración de la sesión en el menor tiempo posible (ej. 900 a 1800 segundos). Un token efímero expirado no tiene valor residual si el runner es comprometido.
  2. FinOps & Cacheo de dependencias:
    • Configura caché para node_modules y assets de Docker en GitHub Actions para reducir los tiempos de cómputo del runner.
    • Centraliza los assets de CDK en Amazon ECR / S3 con Lifecycle Policies estrictas (eliminar imágenes y templates viejos a los 14-30 días) para evitar costes residuales por almacenamiento.
  3. Auditoría con CloudTrail: El atributo role-session-name configurado con ${{ github.run_id }} permite mapear cualquier mutación de infraestructura directamente al log específico de GitHub Actions en caso de un incidente.

Conclusión y Debate

Adoptar OIDC en AWS no solo elimina la deuda técnica y los riesgos de fugas de claves, sino que unifica la trazabilidad de operaciones en plataformas modernas.
¿Cómo gestionas actualmente la asunción cross-account en tus despliegues? ¿Utilizas un esquema Hub-and-Spoke centralizado o declaras el OIDC Provider en cada cuenta de carga de trabajo? Te leo en los comentarios.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article