
Ferramenta de linting para templates CloudFormation
A ferramenta cfn-nag procura padrões em modelos do CloudFormation que possam indicar infraestrutura insegura. De modo geral, ela procurará por:
Para mais informações sobre a ferramenta, consulte esta publicação no blog da Stelligent:
Supondo que Ruby >= 2.5.x esteja instalado, a instalação é apenas uma questão de:
gem install cfn-nag
No MacOS ou Linux, você pode instalar alternativamente com brew:
brew install ruby brew-gem
brew gem install cfn-nag
Para executar cfn_nag como uma ação no CodePipeline, você pode implantar via AWS Serverless Application Repository.
Para executar:
cfn_nag_scan --input-path <path to cloudformation json>
O caminho pode ser um diretório ou um modelo específico. Se for um diretório, todos os arquivos .json, .template, .yml e .yaml serão processados, incluindo recursão em subdiretórios.
O formato de saída padrão é texto livre, mas a saída em json pode ser selecionada com a flag --output-format json.
Opcionalmente, uma flag --debug despeja informações sobre os detalhes internos do carregamento de regras.
Execute com --help para uma lista completa das opções suportadas.
Para ver uma lista de todas as regras que o cfn-nag suporta atualmente, há um utilitário de linha de comando que as envia para stdout:
cfn_nag_rules
Um Dockerfile é fornecido para conveniência. Ele é publicado no DockerHub como stelligent/cfn_nag.
https://hub.docker.com/r/stelligent/cfn_nag
Você também pode compilá-lo localmente.
docker build -t stelligent/cfn_nag .
Você pode montar um diretório local contendo modelos no contêiner Docker e então chamar o cfn_nag no contêiner. Este exemplo usa os modelos de teste usados nos testes unitários do cfn_nag:
$ docker run -v `pwd`/spec/test_templates:/templates -t stelligent/cfn_nag /templates/json/efs/filesystem_with_encryption.json
{
"failure_count": 0,
"violations": [
]
}
$ docker run -v `pwd`/spec/test_templates:/templates -t stelligent/cfn_nag /templates/json/efs/filesystem_with_no_encryption.json
{
"failure_count": 1,
"violations": [
{
"id": "F27",
"type": "FAIL",
"message": "EFS FileSystem should have encryption enabled",
"logical_resource_ids": [
"filesystem"
]
}
]
}
cfn_nag_scan pode ser executado como parte de um GitHub Workflow para avaliar o código durante pipelines de integração contínua.
No seu arquivo de GitHub Workflow, crie uma etapa que use a cfn_nag Action:
- name: Simple test
uses: stelligent/cfn_nag@master
with:
input_path: tests
Mais informações sobre a GitHub Action podem ser encontradas aqui.
O cfn-nag suporta a noção de um "perfil", que é efetivamente uma lista de permissões de regras a aplicar. O perfil é um arquivo de texto que deve conter um identificador de regra por linha. Quando especificado via argumento de linha de comando --profile-path, o cfn-nag retornará SOMENTE violações dessas regras específicas.
A motivação por trás da criação de um "perfil" é que diferentes desenvolvedores podem se importar com regras diferentes. Por exemplo, um "infrastructure_developer" pode se importar com regras de IAM, enquanto um "app_developer" pode nem conseguir criar recursos IAM e, portanto, não se importar com essas regras.
Aqui está um exemplo de perfil:
F1
F2
F27
W3
W5
A lista de negação é basicamente o oposto do perfil: é uma lista de regras que NUNCA devem ser aplicadas. Quando especificada via argumento de linha de comando --deny-list-path, o cfn-nag NUNCA retornará violações dessas regras específicas especificadas no arquivo.
Caso uma regra seja especificada em ambos, a lista de negação terá prioridade sobre o perfil, e a regra não será aplicada.
O formato é o seguinte. Os únicos dois campos relevantes são RulesToSuppress e o id de cada item. O reason não será interpretado pelo cfn-nag, mas é recomendado para justificar e documentar por que a regra nunca deve ser aplicada.
RulesToSuppress:
- id: W3
reason: W3 is something we never care about at enterprise X
Caso exista uma regra que você queira suprimir, uma chave Metadata do cfn_nag pode ser adicionada ao recurso afetado para dizer ao cfn_nag para não gerar uma falha ou aviso para essa regra.
Por exemplo, se você estiver configurando um ELB público aberto a conexões de entrada da internet com recursos como os seguintes:
public_alb.yaml
# Partial template
PublicAlbSecurityGroup:
Properties:
GroupDescription: 'Security group for a public Application Load Balancer'
VpcId:
Ref: vpc
Type: AWS::EC2::SecurityGroup
PublicAlbSecurityGroupHttpIngress:
Properties:
CidrIp: 0.0.0.0/0
FromPort: 80
GroupId:
Ref: PublicAlbSecurityGroup
IpProtocol: tcp
ToPort: 80
Type: AWS::EC2::SecurityGroupIngress
O cfn_nag emitirá avisos como os seguintes:
$ cfn_nag_scan -i public_alb.yaml
------------------------------------------------------------
public_alb.yaml
------------------------------------------------------------------------------------------------------------------------
| WARN W9
|
| Resources: ["PublicAlbSecurityGroup"]
|
| Security Groups found with ingress cidr that is not /32
------------------------------------------------------------
| WARN W2
|
| Resources: ["PublicAlbSecurityGroup"]
|
| Security Groups found with cidr open to world on ingress. This should never be true on instance. Permissible on ELB
Failures count: 0
Warnings count: 2
Ao adicionar os metadados, esses avisos podem ser suprimidos:
public_alb_with_suppression.yaml
# Partial template
PublicAlbSecurityGroup:
Properties:
GroupDescription: 'Security group for a public Application Load Balancer'
VpcId:
Ref: vpc
Type: AWS::EC2::SecurityGroup
Metadata:
cfn_nag:
rules_to_suppress:
- id: W9
reason: "This is a public facing ELB and ingress from the internet should be permitted."
- id: W2
reason: "This is a public facing ELB and ingress from the internet should be permitted."
PublicAlbSecurityGroupHttpIngress:
Properties:
CidrIp: 0.0.0.0/0
FromPort: 80
GroupId:
Ref: PublicAlbSecurityGroup
IpProtocol: tcp
ToPort: 80
Type: AWS::EC2::SecurityGroupIngress
$ cfn_nag_scan -i public_alb_with_suppression.yaml
------------------------------------------------------------
public_alb_with_supression.yaml
------------------------------------------------------------
Failures count: 0
Warnings count: 0
Os Parâmetros de Modelo do CloudFormation podem apresentar um problema para a análise estática, pois os valores são especificados no momento da implantação. Em outras palavras, os valores não estão disponíveis quando a análise estática é feita - a análise estática só pode observar o "código" que está à sua frente. Portanto, uma regra de entrada de grupo de segurança de 0.0.0.0/0 não será sinalizada se o cidr for parametrizado e o 0.0.0.0/0 for informado no momento da implantação.
Para permitir a verificação dos valores dos parâmetros, um usuário pode especificar os valores dos parâmetros em um arquivo JSON passado na linha de comando para cfn_nag e cfn_nag_scan com a flag --parameter-values-path=<filename/uri>.
O formato do JSON é uma única chave, "Parameters", cujo valor é um dicionário em que cada par chave/valor mapeia para os Parâmetros:
{
"Parameters": {
"Cidr": "0.0.0.0/0"
}
}
Isso fornecerá "0.0.0.0/0" ao seguinte Parâmetro:
Parameters:
Cidr:
Type: String
CUIDADO se houver parâmetros extras no JSON, eles serão ignorados silenciosamente (para permitir que cfn_nag_scan aplique o mesmo JSON em todos os modelos).
Se o JSON estiver malformado ou não atender à especificação acima, a análise falhará com uma violação FATAL.
Antes da versão 0.5.55, chamadas a Fn::FindInMap eram efetivamente ignoradas. O modelo subjacente as deixava como estavam e, assim, elas apareciam como valores Hash para as regras. Por exemplo: { "Fn::FindInMap" => [map1, key1, key2]}
A partir da versão 0.5.55, o modelo tentará calcular o valor de uma chamada a FindInMap e apresentar esse valor às regras. Essa avaliação suporta chaves que são:
Se a lógica de avaliação não conseguir descobrir o valor de uma chave, ela usará o comportamento antigo de retornar o Hash para a expressão inteira.
Também antes da versão 0.5.55, chamadas a pseudofunções da AWS eram efetivamente ignoradas. O modelo subjacente as deixava como estavam e, assim, elas apareciam como valores Hash para as regras. Por exemplo: {"Ref"=>"AWS::Region"}. Um caso de uso comum é organizar mapeamentos por região, portanto a avaliação de pseudofunções é importante para suportar melhor a avaliação de mapas.
A partir da versão 0.5.55, o modelo apresentará as seguintes pseudofunções da AWS às regras com os valores padrão:
'AWS::URLSuffix' => 'amazonaws.com',
'AWS::Partition' => 'aws',
'AWS::NotificationARNs' => '',
'AWS::AccountId' => '111111111111',
'AWS::Region' => 'us-east-1',
'AWS::StackId' => 'arn:aws:cloudformation:us-east-1:111111111111:stack/stackname/51af3dc0-da77-11e4-872e-1234567db123',
'AWS::StackName' => 'stackname'
Além disso, o usuário final pode substituir o valor fornecido por meio do mecanismo tradicional de substituição de parâmetros. Por exemplo:
{
"Parameters": {
"AWS::Region": "eu-west-1"
}
}
Até a versão 0.4.66 do cfn_nag, o modelo subjacente não fazia nenhum processamento de Fn::If dentro de um modelo. Isso significava que, se uma propriedade tivesse um valor condicional, cabia à regra interpretar o Fn::If. Como um Fn::If poderia aparecer praticamente em qualquer lugar, isso criava uma situação de whack-a-mole para os desenvolvedores de regras. Na melhor das hipóteses, a lógica da regra poderia ignorar valores que fossem Hash, presumindo que o valor não fosse um Hash em primeiro lugar.
Para resolver esse problema, o comportamento padrão do cfn_nag agora é substituir Fn::If pelo resultado verdadeiro. Isso significa que, por padrão, as regras não inspecionarão os resultados falsos em busca de violações de segurança.
Além de substituir Fn::If no nível do valor da propriedade, o mesmo comportamento é aplicado a Fn::If no nível superior de Properties. Por exemplo:
Resource1:
Type: Foo
Properties: !If
- IsNone
- Description: Up
- Description: DOwn
Ficará igual a:
Resource1:
Type: Foo
Properties:
Description: Up
Para fornecer algum controle sobre esse comportamento, um usuário pode especificar os valores das conditions em um arquivo JSON passado na linha de comando para cfn_nag e cfn_nag_scan com a flag --condition-values-path=<filename/uri>.
O formato do JSON é um dicionário em que cada par chave/valor corresponde às Conditions:
{
"Condition1": true,
"Condition2": false
}
A base do SPCM está descrita na publicação do blog Thought Experiment Proposed Complexity Metric for IAM Policy Documents.
A partir da versão 0.6.0 do cfn_nag:
spcm_scan pode escanear um diretório de modelos do CloudFormation (como o cfn_nag_scan) e gerar um relatório com as métricas SPCM em formato JSON ou HTMLcfn_nag_scan --rule-arguments spcm_threshold:100--rule-arguments. O objeto Rule só precisa declarar um attr_accessor, por exemplo attr_accessor :spcm_threshold, e o cfn_nag cuidará dos detalhes para injetar valores do --rule-argumentsO lançamento 0.5.x inclui algumas mudanças importantes em como regras personalizadas podem ser distribuídas e carregadas. Antes deste lançamento, havia dois locais de onde as regras eram carregadas: o diretório lib/cfn-nag/custom_rules dentro da gem principal cfn_nag, e o diretório de regras personalizadas especificado na linha de comando.
Há dois casos de uso que forçaram um redesenho de como/onde as regras personalizadas são carregadas. O mecanismo de carregamento de regras foi generalizado para que repositórios de regras personalizadas possam ser usados para descobrir regras.
Um monte de "arquivos de regra" espalhados por um sistema de arquivos não é ideal do ponto de vista tradicional de desenvolvimento de software. Não há versão ou rastreabilidade nesses arquivos, então o 0.5.x introduz a noção de uma "gem de regras do cfn_nag". Um desenvolvedor pode desenvolver regras personalizadas como parte de uma gem separada, versioná-la e instalá-la... e essas regras são referenciadas pelo cfn_nag desde que os metadados da gem incluam cfn_nag_rules => true. Para uma gem chamada, por exemplo, "cfn-nag-hipaa-rules", qualquer *.rb sob lib/cfn-nag-hipaa-rules será carregado. Quaisquer regras personalizadas devem derivar de CfnNag::BaseRule em cfn-nag/base_rule (não cfn-nag/custom-rules/base). Se a regra precisar derivar de outra coisa, definir um método cfn_nag_rule? que retorne true também fará com que ela seja carregada como regra.
Quando o cfn_nag está sendo executado em uma AWS Lambda - não há realmente um sistema de arquivos (além de /tmp) no sentido tradicional. Portanto, apenas as regras principais são utilizáveis a partir da Lambda. Para suportar regras personalizadas, o cfn_nag suporta a descoberta de regras a partir de um bucket S3 em vez do sistema de arquivos.
Tudo o que você provavelmente já viu sobre como desenvolver regras personalizadas em Ruby continua válido.
Para descobrir regras de um bucket S3, crie um arquivo s3.yml com este conteúdo:
---
repo_class_name: S3BucketBasedRuleRepo
repo_arguments:
s3_bucket_name: cfn-nag-rules-my-enterprise
prefix: /rules
Para aplicar arquivos *Rule.rb no bucket cfn-nag-rules-my-enterprise com o prefixo /rules (por exemplo, /rules/MyNewRule.rb), especifique este arquivo na linha de comando do cfn_nag da seguinte forma:
cat my_cfn_template.yml | cfn_nag --rule-repository s3.yml
Se as regras estiverem em mais de um bucket, crie vários arquivos s3*.yml e especifique-os no argumento --rule-repository.
Se as credenciais AWS do ambiente tiverem permissão para acessar o bucket cfn-nag-rules-enterprise, ele encontrará todas as regras como /rules/*Rule.rb. Se um aws_profile específico deve ser usado, adicione-o como uma chave em repo_arguments, por exemplo aws_profile: my_aws_profile
Além do sistema de arquivos, instalações de gems e S3 - a nova arquitetura suporta teoricamente o desenvolvimento de outros "repositórios de regras" para carregar regras do DynamoDb, bancos de dados relacionais ou outros serviços web.
Para criar novas regras para seu próprio uso e/ou contribuição com a comunidade, consulte Custom Rule Development para obter detalhes.
Um screencast demonstrando o desenvolvimento de regras personalizadas com TDD de ponta a ponta está disponível aqui:
https://www.youtube.com/watch?v=JRZct0naFd4&t=1601s
Para executar os specs, você precisa garantir que o Docker esteja instalado e as dependências do cfn_nag instaladas via
gem install bundle
bundle install
Então, para executar todos os specs, basta rodar rake test:all.
Para executar os testes de ponta a ponta, rode rake test:e2e. O script agrupará todas as gems do Gemfile, compilará e instalará a gem cfn_nag localmente, instalará as dependências dos specs e então executará os testes marcados com 'end_to_end'. Ele também baixará modelos de exemplo fornecidos pela Amazon e executará o cfn_nag_scan contra eles, para ver se algum modelo conhecidamente bom causa exceções no cfn-nag.
Para instalar a branch git atual localmente:
bundle install
scripts/deploy_local.sh
Há um ambiente de desenvolvimento remoto completo criado e configurado com todas as ferramentas e definições pré-configuradas para facilitar o desenvolvimento e a criação de regras. Você pode habilitá-lo usando a funcionalidade de desenvolvimento remoto do VS Code.
Folder contains a dev container configuration file. Reopen folder to develop in a container, clique no botão Reopen in Container[Dev Container] cfn_nag DevelopmentMais informações sobre a configuração de desenvolvimento remoto do VS Code podem ser encontradas aqui, VS Code Remote Development.
Para relatar um bug ou solicitar um recurso, envie uma issue pelo repositório do GitHub via: https://github.com/stelligent/cfn_nag/issues/new