Provisionando acesso aos recursos na AWS via CLI
Como controlamos níveis de permissão de forma automática e damos autonomia para nosso time de engenharia
Na maioria das empresas, um dos problemas mais comuns é como gerenciar acessos e permissões para os recursos na infraestrutura. Normalmente, quando uma startup nasce, aquela estrutura horizontal e o curto tempo para entregar o produto no mercado fazem com que nossas políticas de acessos fiquem em segundo ou terceiro plano, pois além dos times serem pequenos, é necessário agilidade. Quando a empresa vai ficando maior, o negócio fica mais complexo e precisamos contratar mais pessoas. Isso é ótimo, estamos fazendo bem as coisas :)
Com a chegada de novos devs, vão se formando as equipes pra atacar as diferentes áreas do negócio. O time de SRE deve provisionar todas as ferramentas necessárias pra os devs conseguirem entregar o produto no tempo planejado e de forma segura e resiliente.
O caso de QuintoAndar não é muito diferente, nosso negocio não para de crescer até hoje. Com uma infraestrutura na nuvem, a gente faz uso de diferentes recursos para suportar nossas operações. No caso da AWS, nossos times de engenharia precisam diariamente integrar as API com SQS, S3, SNS, falando dos casos mais comuns. Outros times, por exemplo Data, vão precisar de ferramentas diferentes. Como a gente consegue habilitar acesso para esses recursos?
Nosso #devops-channel
A gente tem um canal para fazer o nexo de comunicação entre time de Engenharia e SRE. Nesse canal, a gente tira dúvidas, avalia novas ideias e dá um help com todos os temas relacionados a infraestrutura e gestão de nossos recursos na nuvem.
Caso um dev precisasse acessar um bucket no S3, era só pedir no #devops-channel. Um SRE iria ler sua mensagem e habilitar o acesso. Mas como era esse processo por trás dos panos?
- SRE pega informações básicas pra te liberar acesso: Nome do bucket, ambiente que vc precisa (dev, staging, prod).
- Vai no console da AWS e cria uma permissão liberando esses acessos, depois adiciona esse perfil para teu user.
- O dev faz login na AWS e valida que tem a permissão correta.
O processo funciona, mais como falamos aqui no time de SRE:
Se eu preciso fazer uma coisa mais de 2 vezes, dá para automatizar.
Com o passar do tempo, a gente começou a notar alguns pontos críticos.
Os primeiros sintomas
Quando a empresa ainda é pequena, ter um processo manual até pode fazer sentido, mais se precisamos gerenciar acessos de >200 engenheiros, a coisa muda. Alguns pontos que a gente começou olhar primeiro, foram:
- Processo manual para criar permissões.
- Quantidade de permissões criadas na AWS.
- Permissões repetidas para varios users.
- Dificuldade para ter um mapeamento certo de permissão -> pessoa
- Sem controle de limite de permissão (Vai acumulando com o tempo).
- Permissões que não expiram.
Nesse ponto a gente viu que não dava para seguir com esse processo manual. Porém um ponto que saiu depois foi o tempo de resposta entre o time de SRE e time de Engenharia. O dev dependia completamente de uma pessoa em SRE pra ter acesso a um recurso, até não receber resposta e liberarem o acesso, simplesmente o processo de desenvolvimento ficava travado. Imagina ter que esperar até alguém ter o tempo de te responder e habilitar teu acesso…
Primeiro ponto: Conhecer nossa situação
É extremamente importante ser autocrítico e assumir que os processos que faziam sentido antes, hoje podem causar muitos erros. A gente precisava ter o mapeamento do estado atual, baseado nisso, começar pensar numa solução.
Depois de identificar as falhas no processo, chega o momento de procurar soluções. Fazer o brainstorming para decidir qual caminho seguir e o passo mas comum, mas aqui temos que dar um passo atrás: A gente esta num ponto que não da para sair quebrando a casa para consertar a janela. Precisamos conhecer a estrutura que a gente tem hoje, a nível de recursos e de pessoas, é muito importante mapear o estado atual da empresa. Uma falha nesse momento pode causar erros na implementação.
Resumindo, a gente tinha uma estrutura algo assim:
- A gestão de users é feita via Keycloak, então basicamente nenhum Cloud provider tem conhecimento de qual usuário acessa um recurso.
- A gente tem uma ferramenta na linha de comandos (CLI) que a gente chama de nossa querida QLI.
- A gente tem múltiplas contas na AWS.
- Temos 3 pessoas que vão criar a solução: Um cara muito bom que manja muito gRPC e protoc, um cara que sabe criar aplicações escaláveis e resilientes e um cara que conhece muito bem como nossa infra esta estruturada.
Maos na massa.
Criando a relação de Confiança com a Cloud
Quando você conhece uma pessoa, não vai diretamente a falar com ela para ir pro bar no primeiro momento. Antes tem que quebrar o gelo, bater um papo e conhecer se essa pessoa gosta de ir pro bar ou prefere ficar de boas. Resumindo, você precisa conhecer antes de efetuar alguma ação. Com uma máquina, software ou Cloud, o coisa não muda muito.
Primeiro, dado que queremos gerenciar acessos na AWS, precisamos entender como a cloud libera ou restringe acesso a determinados recursos. Depois de dar uma boa estudada e assistir alguns reInvent, a gente conseguiu entender como as IAM Policies funcionan. A estrutura de uma policy é assim:
{
"Version":"2012-10-17",
"Statement":[{
"Effect":"effect",
"Action":"action",
"Resource":"arn",
"Condition":{
"condition":{
"key":"value"
}
}
}
]
}
O Statement e a unidade base de cada policy, que representa a estrutura da permissão para um recurso. Primeiro ponto de atenção: A policy suporta múltiplos Statements.
Estudando um pouco mais de como é o comportamento de uma policy com 2 ou mais Statements, podemos concluir que:
- Todo recurso é proibido por default
- Se na mesma policy você libera e proíbe acesso para o mesmo recurso em diferentes Statements, resultado final é Deny.
- Pode dar permissões para muitos recursos na mesma policy.
Aprofundando um pouco mais, chegamos no conceito de resource policies, identity policies, session policies. Tem mais tipos de policies, porém vamos focar só nesses três no momento. Se você quiser dar uma aprofundada nos conceitos que a gente acabou de falar até aqui, recomendamos dar uma olhada aos seguintes conteúdos:
Beleza, agora que a gente tem uma base sobre as policies, nosso seguinte passo e pensar como podemos usar essa estrutura ao nosso favor. Mais antes disso, tem outro recurso da AWS que a gente achou muito interessante: AWS Security Token Service, conhecido como STS.
Mais o que ele faz? Resumindo, ele permite gerar credenciais temporárias para acessar na nossa conta da AWS com um set de permissões que podemos definir no momento de asumir um role.
Para aprofundar, recomendamos dar uma lida na documentação do serviço:
Nesse ponto, a gente validou que:
- Podemos gerar credenciais temporárias para acessar na AWS
- Podemos definir um set de permissões no momento de gerar as credenciais
A coisa começa ficar interessante.
“Produtizando” a solução
Agora que conhecemos as limitações a nível de recursos e contexto atual, precisamos definir como vamos resolver o problema. A linha de raciocínio vai da seguinte forma:
- Dado que queremos gerenciar acesso aos nossos recursos de forma automática, vamos precisar um aplicativo para fazer o processo.
- Se vamos criar um aplicativo para gerenciar esses acessos, vamos precisar um banco de dados para salvar os resultados do processo.
- Se os resultados do processo vão ser salvos no banco, a gente pode definir algumas métricas para medir a eficiencia do processo.
E nesse ultimo ponto chega a pergunta: Qual processo?
Precisamos então, aprofundar mais um nível. Sabemos que conseguimos gerar credenciais temporárias para acessar nossos recursos na AWS usando um set de permissões. Aquele set de permissões tem o formato de IAM policy e a estrutura é simples. Mais como vamos gerar esse conjunto de Statements?
Falta definir um ponto muito importante: O cliente. Aquele que vai fazer a requisição do acesso. Aquele cara que tem maior interesse na solução.
Mais como ele vai conseguir falar para o aplicativo “Opa, preciso acesso para baixar arquivos do bucket Acme no s3”? A gente já tem um cliente que e usado por todo time de engenharia no QuintoAndar: QLI
.
Dado que é uma linha de comandos, ela consegue pegar o input do usuário e enviar os parâmetros para o serviço com o detalhe dos recursos que precisa. Uma notícia boa, para acessar na QLI o usuário deve ter feito o login via nosso Authentication provider (OpenID). Então, a gente também consegue saber que usuário esta fazendo o request.
Nesse ponto, conhecemos os dois lados do nosso projeto, o que vai definir e fechar o escopo da solução:
- Input, definido pelo user via linha de comandos.
- Output, que vai ser as credenciais temporárias para acessar na AWS.
Agora a gente precisa definir o processo para transformar o Input ao formato de IAM Policies.
Vamos dar um passo atrás e ler novamente os problemas que a gente enxergou quando detectou os primeiros sintomas. A lista de palavras ressaltadas foram:
manual|quantidade|repetidas|mapeamento|controle|acumulando|não expiram
A primeira palavra, manual, pode ser a primeira etapa do checklist em ser marcada, porque agora a gente esta pensando numa solução automatizada. Então, o resultado fica assim:
m̶a̶n̶u̶a̶l̶|quantidade|repetidas|mapeamento|controle|acumulando|não expiram
Beleza, vamos avançando. As seguintes 2 palavras tem relação entre elas: quantidade e repetidas.
Como conseguimos que uma permissão não seja repetida no sistema? Por exemplo, uma política de acesso para um bucket no S3 read-only seria algo assim:
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"JustRead",
"Effect":"Allow",
"Action":["s3:GetObject","s3:GetObjectVersion"],
"Resource":["arn:aws:s3:::5aexamplebucket1/*"]
}
]
}
No exemplo, a gente esta dando acesso read-only para o bucket 5aexamplebucket1
. Se a gente precisar de um acesso read-only para o bucket 5aexamplebucket2
, que tanto precisamos mudar nessa policy? Exato, só o campos Resource e Sid.
Dado que nosso problema é repetir policies e isso gera uma quantidade grande de permissões, podemos definir um template para esse tipo de policy. Então, um template genérico de uma policy que vai permitir acesso read-only a um bucket s3 ficaria:
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"JustRead-{{index .bucketName}}",
"Effect":"Allow",
"Action":["s3:GetObject","s3:GetObjectVersion"],
"Resource":["arn:aws:s3:::{{index .bucketName}}/*"]
}
]
}
Mais ja sabemos que uma policy pode ter um ou varios Statement. Então, a parte importante para nosso template e o Statement só. Vamos isolar ele e chamar de statement-template
:
{
"Sid":"JustRead-{{index .bucketName}}",
"Effect":"Allow",
"Action":["s3:GetObject","s3:GetObjectVersion"],
"Resource":["arn:aws:s3:::{{index .bucketName}}/*"]
}
Agora, cada vez que eu precise criar uma policy com permissão para acessar num bucket no S3, vamos pegar esse template e interpolar os dados, Podemos fazer isso criando a estrutura do statement-template e fazendo a interpolação programaticamente, via go-template, por exemplo.
Primeira regra de negócio que vamos definir: Para criar o Statement real, o statement-template já deveria existir e ficar em algum banco de dados para a gente conseguir pegar ele quando for preciso. Isso já va dando forma ao nosso primeiro recurso a nível de aplicação.
Dado que o template precisa ter um identificador único e explícito sobre o que representa, vamos adicionar um capo Name
para que funcione como identificador principal:
{
"Name":"bucketRead",
"Statement":[
{
"Sid":"JustRead-{{index .bucketName}}",
"Effect":"Allow",
"Action":["s3:GetObject","s3:GetObjectVersion"],
"Resource":["arn:aws:s3:::{{index .bucketName}}/*"]
}
]
}
Be-le-za! A gente pode expor esse service via API para ser acessível desde o client e permitir o usuario final criar os próprios templates caso não achar aquele com a permissão que ele precisa. Agora que definimos a unidade básica para permitir acesso aos recursos e reutilizar um mesmo template, podemos asumir que conseguimos resolver mais 2 problemas:
m̶a̶n̶u̶a̶l̶|q̶u̶a̶n̶t̶i̶d̶a̶d̶e̶|̶r̶e̶p̶e̶t̶i̶d̶a̶s̶|mapeamento|controle|acumulando|não expiram
Agora chegamos no terceiro ponto da solução: mapeamento e controle. Sabemos como gerar permissões, mais como vamos conseguir mapear elas e controlar os níveis de acesso dos usuarios?
Sabemos que podemos criar uma policy com muitos Statements ao momento de gerar as credenciais temporárias. Mais para criar esses Statements, os valores no statement-template precisam ser interpolados por os valores reais e agrupados numa estrutura simulando um IAM Policy. Será que conseguimos salvar aquele Snapshot da policy que vai ser enviada para AWS? Sim, e a gente chamou esse recurso de grant
.
O que significa um grant exatamente? É o resultado da interpolação entre um statement-template e os valores reais do recurso. O grant tem dentro dele o Statement real que vai ser agrupado na Policy, e alguns outros valores predefinidos como:ID da conta da AWS que esta dando a permissao
- O username
- Nome do template que foi usado para gerar esse grant
- Os parametros que foram enviados para interpolação
Uma vez que o grant e gerado, os dados ficam salvos no banco de dados. Dado que um grant representa um Statement, um único usuario pode ter N grants.
Resumindo, a gente até agora tem:
- statement-templates que centralizam as permissões de acesso para um recurso
- grant que consegue armazenar as informações do Statement e acesso ao recurso na AWS.
Agora, a gente consegue mapear as permissões que cada usuário vai ter e aplicar estratégias de controle de acessos. Vamos atualizar a lista de problemas:
m̶a̶n̶u̶a̶l̶|q̶u̶a̶n̶t̶i̶d̶a̶d̶e̶|̶r̶e̶p̶e̶t̶i̶d̶a̶s̶|̶m̶a̶p̶e̶a̶m̶e̶n̶t̶o̶|̶c̶o̶n̶t̶r̶o̶l̶e̶|acumulando|não expiram
Finalmente, acumulando e não expiram. Vemos aqui que ambas tem relação uma com outra.
Na nossa estrutura atual, um usuário pode ter N permissões. A gente tem mapeamento e controle, porém como a gente consegue tirar a permissão de um cara que não precisa mexer mais com o recurso?
Vamos por um exemplo: Macedonio trabalha no time A, que faz desenvolvimento usando os recursos do time A: bucket-A
, sqs-topic-A
, ec2-instance-A
, etc. Depois de um tempo, ele foi promovido para o time B, que mexe com os recursos próprios do time B: bucket-B
, sqs-topic-B
, ec2-instance-B
, etc. Dado que ele é um cara muito bom, é promovido para área de planejamento, onde ele tem acesso ao bucket-planejamento com informações confidenciais sobre os trabalhos que fazem o time A e B. Quantos acessos você acha que Macedonio tem até o momento?
Sim, dado que ninguém tirou permissão dele manualmente, o Macedonio acumulou permissões e tem acesso ao quase 100% dos recursos da empresa.
Bom, as credenciais são temporárias, então ele vai perder acesso depois o timeout da sessão acabar…resolvido? Ehmmmm…..
Unindo todos os pontos
Para atacar o problema de acumulação de permissões, precisamos definir como elas vão ser atribuidas para o usuário. Sabemos que usuário atualmente já tem no banco de dados um set de grants com o nome dele. Como ele vai pegar essas credenciais?
O último ponto aqui e o processo de login. Quando usuário vai logar numa conta da AWS, ele vai enviar alguns dados como:
- Username
- ID da conta na AWS
Esses dados vão ser usados para gerar as credenciais via STS. Porém ainda falta a parte mais importante: Criar a Policy.
O sistema vai procurar entre todos os grants que façam match com o nome de usuário e ID da conta na AWS. Uma vez com todos esses valores, vamos pegar o Statement de cada grant com mas informações interpoladas e jogar dentro de uma estrutura simulando uma Policy. A gente envia esses dados para o STS e já temos as credenciais para fazer login com as permissões configuradas.
Se aquela sessão expira, o que pode impedir o usuario fazer login novamente e pegar os mesmos grant que a gente tem no banco de dados?
Algumas soluções que chegaram na mesa para discussão:
Apagar os grants assim que foram jogados para uma policy.
Funciona, porém se um usuario precisar mexer com o recurso por mais tempo que o permitido pela sessão, vai precisar criar grant e fazer todo processo novamente, tirando produtividade.
Criar um cron-job para apagar os grants com mais de “x” dias.
Não temos ainda essa métrica para definir que depois de “x” tempo o usuario não vai precisar daquela grant.
Chegaram algumas ideias mais, porém todas elas focavam no mesmo objetivo: Apagar os grants. Como conseguimos fazer isso de forma automática? A resposta esta no banco de dados: DynamoDB.
A gente escolheu usar o DynamoDB pela simplicidade de provisionar um banco de dados para nosso projeto e alem disso, o TTL. O que isso? TTL e um campo que você consegue configurar no documento para que aquele registro seja apagado do banco de dados após o valor especificado no campo TTL expirar. Quem que vai definir aquele TTL? O usuário mesmo.
Agora, a gente consegue apagar automaticamente os grants do banco de dados e garantir assim que usuario não vai conseguir acumular permissões ao longo do tempo, além de garantir o princípio de least privilege para nossos recursos.
Apresentamos oficialmente, nosso querido gate-guardian
. O diagrama da solução fica assim:
Finalmente:
m̶a̶n̶u̶a̶l̶|q̶u̶a̶n̶t̶i̶d̶a̶d̶e̶|̶r̶e̶p̶e̶t̶i̶d̶a̶s̶|̶m̶a̶p̶i̶a̶m̶e̶n̶t̶o̶|̶c̶o̶n̶t̶r̶o̶l̶e̶|̶a̶c̶u̶m̶u̶l̶a̶n̶d̶o̶|n̶a̶o̶ ̶e̶x̶p̶i̶r̶a̶m̶
Aprendizados durante desenvolvimento do projeto
Durante todo o processo de desenvolvimento, conseguimos identificar pontos fortes a serem repetidos alguns pontos a serem melhorados.
Fora do comprometimento do time e iniciativa, acho que a principal força e saber ouvir, aceitar nossos erros e trabalhar neles. Nossos processos de revisão de código são rigorosos, pois devemos preparar nossos sistemas para serem escaláveis e resilientes a longo prazo. Às vezes chegamos a um ponto em que precisamos fazer uma refatoração radical na arquitetura, mais uma boa comunicação e abertura para novas ideias são a chave para que o produto seja entregue ou não dentro do prazo.
Por outro lado, aprendemos a olhar para o produto da perspectiva do usuário final. Como uma equipe de SRE comum não possui a figura do PO, tivemos que assumir essa função e nos manter em constante comunicação com nossos usuários finais, entre os pontos de melhoria descobrimos que com uma versão mínima do produto poderíamos testar com diferentes grupos de usuários e validar se nossas hipóteses estão corretas ou precisamos mudar a rota.
Nesse momento, o gate-guardian está no ar sendo utilizado por todo equipe de engenharia do QuintoAndar. Antes de sair do nosso forno e ir para prod, a gente teve que implementar novas estrategias para monitorar, remover permissões dependendo do alguns eventos do sistema e replicar automaticamente todos os processos entre nossas contas, temas muito legais que vão ser tratados num próximo artigo.
Se você curtiu o que a gente fez e quer formar parte desse time incrível, pode olhar todas nossas posições abertas aqui.
Agradeço ao time SRE por me ajudar com a correção e redação deste artigo, pois minha língua nativa não é o português. Obrigado gente!