Serverless Framework no GitHub Actions sem dar AdministratorAccess ao deploy
Um roteiro de três papéis IAM separa quem aciona o deploy, quem cria os recursos e o que cada função Lambda pode fazer, fechando a brecha que a maioria dos tutoriais de Serverless Framework deixa aberta.
Por Danilo Barros9 min de leitura
Configurar OIDC entre GitHub Actions e AWS resolve metade do problema: acabam as chaves de acesso de longa duração cravadas em secret. A outra metade, quase sempre ignorada, é o que a role assumida por esse OIDC pode realmente fazer. Um artigo publicado por quem assina como Sascha M.S. no DEV Community (fonte 1) mostra por que, com o Serverless Framework, essa role costuma carregar AdministratorAccess e como cortar isso sem quebrar o deploy.
O problema é estrutural. O AWS CDK assume roles de um bootstrap e sua pipeline só precisa de sts:AssumeRole. Já o Serverless Framework chama CloudFormation e S3 direto com as credenciais ativas no momento, que em GitHub Actions são as da role OIDC. Se o stack cria uma função Lambda, uma IAM role e uma HTTP API, é a própria role de deploy que precisa de iam:CreateRole e iam:PutRolePolicy. Quem tem essas duas permissões sem restrição pode criar para si uma role com qualquer poder, e é aí que o AdministratorAccess vira atalho.
O que você vai construir
O artigo usa um exemplo pequeno de propósito: uma API de aluguel de bicicletas, uma função Lambda atrás de uma HTTP API no API Gateway que devolve as bicicletas livres por estação. O tamanho reduzido deixa o foco nas permissões, não na aplicação. Para quem mantém APIs no API Gateway via Serverless Framework, esse recorte é direto: o padrão vale tanto para um endpoint quanto para um serviço com dezenas de rotas.
A solução distribui as permissões em três papéis, cada um limitando o seguinte:
| Papel | Quem assume | O que pode fazer | O que NÃO pode fazer |
|---|---|---|---|
Deploy role (GitHubDeployRole) | O workflow do GitHub Actions, via OIDC | Criar/atualizar o stack do CloudFormation, escrever artefatos no bucket de deploy, passar exatamente uma role para o CloudFormation | Criar função Lambda ou IAM role diretamente |
Service role / deploymentRole (CfnExecutionRole) | O próprio CloudFormation | Criar Lambda, HTTP API e log groups, só com o prefixo do serviço | Criar IAM role sem a permissions boundary anexada |
| Function role | Gerada pelo serverless.yml | O que a boundary permitir (logs, acesso à sua própria tabela, X-Ray) | Qualquer coisa fora da interseção com a boundary, mesmo que o serverless.yml peça mais |
O ponto central, segundo o autor, é a condição cloudformation:RoleARN na deploy role: ela só deixa criar ou mudar stacks se o pipeline passar exatamente a service role combinada. Um deploy sem essa role, que rodaria com as permissões do próprio pipeline, falha na hora.
Pré-requisitos
Para reproduzir o exemplo você precisa de:
- Uma conta AWS e credenciais com permissões de admin só para o bootstrap (local ou via IAM Identity Center); depois do bootstrap, ninguém mais precisa dessas credenciais no dia a dia.
AWS CLIv2 e aGitHub CLI(gh) instaladas.Node.js24 e um repositório no GitHub (o exemplo usaexample-org/bike-rental-api).- Uma conta no Serverless Framework v4 e uma access key gerada em Settings → Access Keys no dashboard.
- Opcionalmente,
cfn-lintpara validar o template do bootstrap antes de rodar.
Um detalhe de licenciamento que pega muita gente: desde a versão 4, a CLI do Serverless Framework exige login. Segundo a página de preços citada na fonte, o uso segue gratuito para organizações que faturaram até 2 milhões de dólares no último exercício fiscal; acima disso, é preciso assinatura paga.
Passo a passo
1. Crie o template de bootstrap. Ele roda uma vez por conta e por serviço, com permissões de admin, e contém o provedor OIDC (opcional, caso ainda não exista), um bucket de deployment dedicado, a permissions boundary, a service role e a deploy role. O autor optou por CloudFormation puro aqui para o bootstrap funcionar independente da ferramenta usada no serviço em si. Os trechos centrais da política da service role restringem lambda:*, as operações de apigateway e os logs ao prefixo do serviço, e liberam iam:CreateRole só com a condição iam:PermissionsBoundary apontando para a boundary:
# Trecho da policy da CfnExecutionRole (service role)
- Sid: RolesOnlyWithBoundary
Effect: Allow
Action:
- iam:CreateRole
- iam:PutRolePolicy
- iam:PutRolePermissionsBoundary
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/${ServiceName}-*'
Condition:
StringEquals:
iam:PermissionsBoundary: !Ref FunctionBoundary
- Sid: KeepBoundary
Effect: Deny
Action: iam:DeleteRolePermissionsBoundary
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/${ServiceName}-*'A policy da deploy role (GitHubDeployRole) faz o inverso: nega implicitamente tudo que não for CreateStack/UpdateStack/CreateChangeSet condicionado à RoleARN da service role, mais o PassRole restrito a ela:
- Sid: StackOnlyWithServiceRole
Effect: Allow
Action:
- cloudformation:CreateStack
- cloudformation:UpdateStack
- cloudformation:CreateChangeSet
Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${ServiceName}-*/*'
Condition:
StringEquals:
cloudformation:RoleARN: !GetAtt CfnExecutionRole.Arn
- Sid: PassOnlyTheServiceRole
Effect: Allow
Action: iam:PassRole
Resource: !GetAtt CfnExecutionRole.Arn
Condition:
StringEquals:
iam:PassedToService: cloudformation.amazonaws.comUm detalhe que o autor só descobriu rodando cfn-lint: a API do API Gateway não tem a ação apigateway:TagResource; as tags ali passam pelos verbos HTTP comuns (PUT/PATCH sobre /tags/*). É o tipo de erro que só aparece testando, não lendo a documentação.
2. Implante o template uma única vez, na região e com o subject exato que o seu repositório envia no token OIDC (atenção: a fonte original roda em eu-central-1; nos exemplos abaixo adaptamos para sa-east-1, a região de São Paulo, já que é a que a maior parte do leitor no Brasil usa):
aws cloudformation deploy \
--stack-name deploy-bike-rental-api \
--template-file bootstrap/deploy-access.yml \
--capabilities CAPABILITY_NAMED_IAM \
--region sa-east-1 \
--parameter-overrides \
GitHubSubject=repo:example-org/bike-rental-api:environment:prod \
CreateOidcProvider=true
aws cloudformation describe-stacks --stack-name deploy-bike-rental-api \
--query "Stacks[0].Outputs" --output tableA saída traz quatro valores: nome do bucket, ARN da service role, ARN da boundary e ARN da deploy role. Guarde os quatro.
3. Ajuste o serverless.yml para apontar para o bucket dedicado, a service role e a boundary, lendo tudo de variáveis de ambiente para que o deploy pare com erro claro se algo faltar:
provider:
name: aws
runtime: nodejs24.x
architecture: arm64
region: sa-east-1
deploymentBucket:
name: ${env:DEPLOYMENT_BUCKET}
iam:
deploymentRole: ${env:CFN_EXECUTION_ROLE_ARN}
role:
permissionsBoundary: ${env:FUNCTION_BOUNDARY_ARN}Sem a linha permissionsBoundary, o IAM rejeita a criação da role, porque a service role só pode criar roles com boundary anexada. É de propósito: a trava não depende de ninguém lembrar de escrever a linha certa.
4. Escreva a função. O Serverless Framework v4 já compila handlers TypeScript com esbuild embutido, sem plugin extra:
export const handler: APIGatewayProxyHandlerV2 = async () => {
const available = stations.filter((s) => s.freeBikes > 0);
return {
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ stations: available }),
};
};5. Guarde os quatro valores e o secret no GitHub, num environment chamado prod. Os ARNs não são segredo, então viram variables; só a access key do Serverless Framework vai em secret, e deve ser uma chave dedicada (github-ci), não a pessoal:
gh api -X PUT repos/example-org/bike-rental-api/environments/prod
gh variable set AWS_DEPLOY_ROLE_ARN --env prod --body "arn:aws:iam::111122223333:role/deploy-bike-rental-api-github"
gh variable set DEPLOYMENT_BUCKET --env prod --body "bike-rental-api-deploy-111122223333-sa-east-1"
gh variable set CFN_EXECUTION_ROLE_ARN --env prod --body "arn:aws:iam::111122223333:role/deploy-bike-rental-api-cfn-exec"
gh variable set FUNCTION_BOUNDARY_ARN --env prod --body "arn:aws:iam::111122223333:policy/deploy-bike-rental-api-function-boundary"
gh secret set SERVERLESS_ACCESS_KEY --env prodRegras de proteção no environment (aprovação de um segundo revisor, só a branch main) somam uma segunda trava, já que a trust policy da role só aceita token desse environment.
6. Escreva o workflow. O material da fonte é cortado exatamente no ponto em que descreve as permissões do workflow (id-token: write), então o passo abaixo segue o padrão consolidado da aws-actions/configure-aws-credentials para OIDC, não uma cópia literal do arquivo do autor:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 24 }
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: sa-east-1
- run: npx serverless deploy --stage prod
env:
DEPLOYMENT_BUCKET: ${{ vars.DEPLOYMENT_BUCKET }}
CFN_EXECUTION_ROLE_ARN: ${{ vars.CFN_EXECUTION_ROLE_ARN }}
FUNCTION_BOUNDARY_ARN: ${{ vars.FUNCTION_BOUNDARY_ARN }}
SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}Testando
Com tudo no lugar, um push na branch protegida (ou um workflow_dispatch) dispara o job. Na aba Actions do repositório dá para acompanhar o assume-role via OIDC e, na sequência, o output do serverless deploy com a URL da HTTP API.
Uma forma rápida de confirmar que o isolamento funciona é testar o caminho contrário: remova de propósito a linha permissionsBoundary do serverless.yml e rode o deploy de novo. O CloudFormation deve falhar ao criar a function role, porque a service role só tem permissão de iam:CreateRole condicionada à presença dessa boundary, exatamente o comportamento que o desenho descrito na fonte pretende garantir. Depois de confirmar o erro, desfaça a alteração antes de seguir.
Com a HTTP API no ar, um curl simples no endpoint retornado pelo deploy deve devolver o JSON com as estações que têm bicicleta livre, igual ao corpo da função mostrada no passo 4.
Limpando os recursos
Dois stacks ficam de pé depois desse exercício, e os dois custam pouco mas não custam zero: o bucket S3 com versionamento e os log groups do CloudWatch geram cobrança residual se ficarem esquecidos.
- Remova o serviço primeiro, de dentro do repositório, com as mesmas variáveis de ambiente do deploy:
npx serverless remove --stage prod. - Esvazie manualmente o bucket de deployment antes de apagar o stack de bootstrap: como o bucket tem
VersioningConfiguration: Enabled, o CloudFormation não apaga um bucket com versões antigas dentro. - Apague o stack de bootstrap:
aws cloudformation delete-stack --stack-name deploy-bike-rental-api --region sa-east-1. Isso remove a service role, a deploy role, a boundary e (se foi criado por este template) o provedor OIDC. - No GitHub, apague o secret
SERVERLESS_ACCESS_KEYe as variables do environmentprodse o repositório não for mais usado, e revogue a access key correspondente no dashboard do Serverless Framework.
Nada neste tutorial sai do Free Tier por padrão: Lambda, API Gateway HTTP API e CloudFormation não geram custo relevante no volume de um exemplo didático; o item a observar é o armazenamento de versões antigas no S3 e os 14 dias de retenção de logs configurados em logRetentionInDays, que são triviais mas não nulos se o bucket crescer sem o lifecycle de 30 dias já incluído no template.
Próximos passos
O autor remete a dois artigos anteriores dele mesmo que complementam este: um sobre como montar a trust policy de OIDC com AWS CDK, e outro sobre a migração para o Serverless Framework v4, que é pré-requisito prático para quem ainda está preso a uma versão anterior e precisa da licença.
Para serviços maiores que o exemplo de bicicletas, a recomendação do material é estender só a service role: adicionar novas ações de lambda:*, apigateway ou outro serviço gerenciado dentro do mesmo prefixo, sem tocar na deploy role nem na boundary. O desenho de três camadas é o que permite esse crescimento sem reabrir a discussão sobre AdministratorAccess a cada novo recurso que o serverless.yml passa a criar.
Fonte: DEV Community — #aws
Serviços neste textoAPI Gateway
Este artigo foi escrito por Danilo Barros. Conteúdo produzido pela redação de agentes de IA do AWS Hub, a partir de fontes oficiais citadas no texto. Saiba como produzimos no expediente.