GitHub ActionsAWSOIDCCI/CDSecurityIAMTerraformSAM

Despliegue en AWS desde GitHub Actions con autenticación OIDC

Sloth255
Sloth255
·4 min read·872 words

Introducción

Al desplegar recursos de AWS desde GitHub Actions, el enfoque tradicional ha sido almacenar las claves de acceso de usuarios IAM en los Secrets de GitHub. Sin embargo, este método tiene varios desafíos:

  • Riesgo de filtración de claves de acceso
  • Necesidad de trabajo de rotación periódica
  • Incremento en los secrets a gestionar

La autenticación OIDC (OpenID Connect) resuelve estos desafíos. Este artículo proporciona una explicación detallada de cómo configurar la integración OIDC entre GitHub Actions y AWS.

¿Qué es OIDC?

OIDC es un protocolo de autenticación basado en OAuth 2.0. GitHub Actions emite un token que puede demostrar "Soy un workflow de este repositorio" a AWS, y AWS lo verifica y proporciona credenciales temporales.

Beneficios de OIDC

  • No se requieren secrets: No es necesario almacenar claves de acceso en GitHub
  • Seguridad mejorada: Solo se usan credenciales temporales
  • Control de permisos granular: Los permisos se pueden restringir por repositorio o rama
  • Costos de gestión reducidos: No se necesita rotación de claves

Configuración de AWS

1. Crear un proveedor de identidad IAM

Primero, crea un proveedor de identidad IAM en la consola de AWS Management.

  1. Abre la consola IAM
  2. Haz clic en "Proveedores de identidad" → "Añadir proveedor"
  3. Introduce la siguiente información:
Tipo de proveedor: OpenID Connect
URL del proveedor: https://token.actions.githubusercontent.com
Audiencia: sts.amazonaws.com

Ejemplo con Terraform

resource "aws_iam_openid_connect_provider" "github_actions" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1"
  ]
}

Ejemplo con AWS SAM

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: GitHub Actions OIDC Provider

Resources:
  GitHubOIDCProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com
      ThumbprintList:
        - 6938fd4d98bab03faadb97b34396831e3780aea1

2. Crear un rol IAM

A continuación, crea un rol IAM que GitHub Actions asumirá.

Configurar la política de confianza

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*"
        }
      }
    }
  ]
}

Detalles de las condiciones

Puedes restringir el acceso usando el valor token.actions.githubusercontent.com:sub.

# Repositorio específico completo
repo:your-org/your-repo:*

# Solo rama específica
repo:your-org/your-repo:ref:refs/heads/main

# Solo entorno específico
repo:your-org/your-repo:environment:production

# Solo pull requests
repo:your-org/your-repo:pull_request

Ejemplo con Terraform

data "aws_iam_policy_document" "github_actions_assume_role" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github_actions.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:your-org/your-repo:*"]
    }
  }
}

resource "aws_iam_role" "github_actions" {
  name               = "github-actions-deploy-role"
  assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json
}

# Adjuntar políticas de permisos requeridas
resource "aws_iam_role_policy_attachment" "deploy_policy" {
  role       = aws_iam_role.github_actions.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess"
}

Ejemplo con AWS SAM

template.yaml
Resources:
  GitHubActionsRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: github-actions-deploy-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Federated: !GetAtt GitHubOIDCProvider.Arn
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
              StringLike:
                token.actions.githubusercontent.com:sub: repo:your-org/your-repo:*
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonECS_FullAccess

Outputs:
  GitHubActionsRoleArn:
    Description: ARN of the GitHub Actions IAM Role
    Value: !GetAtt GitHubActionsRole.Arn
    Export:
      Name: GitHubActionsRoleArn

Para desplegar con SAM:

sam build
sam deploy --guided

Después del despliegue inicial, registra el ARN del rol de salida en los Secrets de GitHub como AWS_ROLE_ARN.

3. Adjuntar políticas de permisos

Adjunta los permisos necesarios para el despliegue al rol. Por ejemplo:

  • Para despliegue en S3: AmazonS3FullAccess
  • Para despliegue en ECS: AmazonECS_FullAccess
  • Para Lambda: AWSLambda_FullAccess

Para entornos de producción, recomendamos crear políticas personalizadas basadas en el principio de mínimo privilegio.

Configuración de GitHub Actions

Crear un archivo de workflow

Crea .github/workflows/deploy.yml.

name: Deploy to AWS

on:
  push:
    branches:
      - main

# Configuración de permisos para obtener token OIDC (¡Importante!)
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy-role
          aws-region: ap-northeast-1

      - name: Deploy to S3
        run: |
          aws s3 sync ./dist s3://your-bucket-name --delete

      - name: Verify deployment
        run: |
          aws s3 ls s3://your-bucket-name

Puntos importantes

1. Configuración de permissions

permissions:
  id-token: write  # Requerido para obtener el token OIDC
  contents: read   # Requerido para checkout del repositorio

Sin esta configuración, no podrás obtener el token OIDC y obtendrás un error.

2. Versión de la acción configure-aws-credentials

Usa v4 o posterior. Las versiones anteriores pueden no soportar OIDC.

uses: aws-actions/configure-aws-credentials@v4

Ejemplo práctico: Despliegue de una aplicación SAM

Aquí hay un ejemplo de despliegue de una aplicación serverless usando AWS SAM.

name: Deploy SAM Application

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: ap-northeast-1
  SAM_STACK_NAME: my-sam-app

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Setup SAM CLI
        uses: aws-actions/setup-sam@v2
        with:
          use-installer: true

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: SAM Build
        run: sam build --use-container

      - name: SAM Deploy
        run: |
          sam deploy \
            --stack-name ${{ env.SAM_STACK_NAME }} \
            --capabilities CAPABILITY_IAM \
            --resolve-s3 \
            --no-fail-on-empty-changeset \
            --no-confirm-changeset

      - name: Get Stack Outputs
        run: |
          aws cloudformation describe-stacks \
            --stack-name ${{ env.SAM_STACK_NAME }} \
            --query 'Stacks[0].Outputs' \
            --output table

Ejemplo práctico: Despliegue en ECS

Como ejemplo más práctico, aquí hay un workflow de despliegue en ECS.

name: Deploy to ECS

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: my-app
  ECS_SERVICE: my-service
  ECS_CLUSTER: my-cluster
  CONTAINER_NAME: my-container

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push image to ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Download task definition
        run: |
          aws ecs describe-task-definition \
            --task-definition my-task \
            --query taskDefinition > task-definition.json

      - name: Update task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

Solución de problemas

Error: "Not authorized to perform sts:AssumeRoleWithWebIdentity"

Causa: Las condiciones de la política de confianza no coinciden

Solución:

  1. Verifica el valor token.actions.githubusercontent.com:sub en la política de confianza del rol IAM
  2. Comprueba que el nombre del repositorio y el nombre de la rama sean correctos
  3. Comprueba el valor real del claim sub en los logs de GitHub Actions
- name: Debug OIDC token
  run: |
    curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | \
      jq -R 'split(".") | .[1] | @base64d | fromjson'

Error: "Error: Credentials could not be loaded"

Causa: Falta la configuración de permissions

Solución: Añade lo siguiente a tu archivo de workflow

permissions:
  id-token: write
  contents: read

La autenticación tiene éxito pero ocurren errores de permisos

Causa: Las políticas de permisos requeridas no están adjuntas al rol IAM

Solución: Adjunta las políticas apropiadas al rol IAM

aws iam attach-role-policy \
  --role-name github-actions-deploy-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess

Mejores prácticas de seguridad

1. Principio de mínimo privilegio

Otorgar solo los permisos mínimos necesarios.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::your-specific-bucket",
        "arn:aws:s3:::your-specific-bucket/*"
      ]
    }
  ]
}

2. Restringir por rama o tag

Restringir los despliegues de producción a ramas específicas solamente.

{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
    }
  }
}

3. Separar roles por entorno

Usar diferentes roles para desarrollo, staging y producción.

- name: Configure AWS credentials (Production)
  if: github.ref == 'refs/heads/main'
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN_PROD }}
    aws-region: ap-northeast-1

- name: Configure AWS credentials (Development)
  if: github.ref == 'refs/heads/develop'
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN_DEV }}
    aws-region: ap-northeast-1

4. Auditoría con CloudTrail

Registrar todas las llamadas a la API y revisarlas regularmente.

# Bucket S3 para CloudTrail
resource "aws_s3_bucket" "cloudtrail" {
  bucket = "my-cloudtrail-logs-bucket"
}

resource "aws_s3_bucket_policy" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AWSCloudTrailAclCheck"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:GetBucketAcl"
        Resource = aws_s3_bucket.cloudtrail.arn
      },
      {
        Sid    = "AWSCloudTrailWrite"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:PutObject"
        Resource = "${aws_s3_bucket.cloudtrail.arn}/*"
        Condition = {
          StringEquals = {
            "s3:x-amz-acl" = "bucket-owner-full-control"
          }
        }
      }
    ]
  })
}

# CloudTrail
resource "aws_cloudtrail" "github_actions_audit" {
  name                          = "github-actions-audit"
  s3_bucket_name               = aws_s3_bucket.cloudtrail.id
  include_global_service_events = true
  is_multi_region_trail        = true
  enable_logging               = true

  depends_on = [aws_s3_bucket_policy.cloudtrail]
}

Conclusión

La integración OIDC entre GitHub Actions y AWS proporciona los siguientes beneficios:

  • Despliegue seguro sin claves de acceso
  • Solo se usan credenciales temporales
  • Control de permisos granular por repositorio o rama
  • Costos de gestión reducidos

Aunque la configuración inicial es algo compleja, una vez configurada, conduce a mejoras a largo plazo en seguridad y operabilidad. Recomiendo encarecidamente considerar su adopción.

Referencias