Desenvolvimento

11 jan, 2019

Protegendo seus dados: utilizando variáveis de ambiente

Publicidade

Um problema recorrente em desenvolvimento de software é o armazenamento de informações sensíveis durante a execução da aplicação. Como podemos armazenar dados, como senhas de criptografia, salts, tokens, chaves privadas e outras informações sem comprometer a segurança de nossas aplicações?

Existem milhares de soluções possíveis para esta pergunta. Algumas mais seguras, mas que exigem uma certa preparação anterior, como a criação e criptografia de chaves RSA, e outras mais simples, mas ao mesmo tempo, um pouco mais vulneráveis a explorações de pessoas mal intencionadas.

O que vamos ver aqui hoje está no meio dos dois. Não é tão complicado de se fazer, mas ao mesmo tempo não é o mais seguro que pode ser feito. Estamos falando de variáveis de ambiente.

Variáveis do que?

Apesar de ser um tópico bastante antigo, variáveis de ambiente ainda parecem coisa de outro mundo para alguns desenvolvedores, simplesmente pelo fato de que elas sempre são abordadas em relação a alguma infraestrutura, então, enquanto iniciantes e intermediários, ficamos com dúvidas sobre este tópico ter relação direta com nosso desenvolvimento, e podemos afirmar com clareza: tem, sim.

Variáveis de ambiente são variáveis como aquelas que criamos em nossas aplicações. Porém, ao invés de existirem dentro do sistema que estamos construindo, elas moram diretamente dentro do sistema operacional da máquina que está executando nossa aplicação.

Por essa razão, elas também são compartilhadas com diversas outras aplicações que estão rodando na mesma máquina e também com o SO em si, uma vez que ele também se utiliza dessas variáveis para inferir informações importantes do seu computador. Vamos a um exemplo:

Se você é um usuário de qualquer sistema operacional, já deve ter se deparado com a seguinte frase:

  • “Adicione o caminho na sua variável PATH”

Inclusive, as antigas instalações do Node, PHP e outras ferramentas de desenvolvimento exigiam que fizéssemos isso. Tudo por um motivo simples: executar o binário da aplicação.

Quando instalamos alguma ferramenta de linha de comando, por exemplo, o NodeJS, ganhamos alguns comandos, como: node e npm. Estes comandos nada mais são do que binários que estão guardados em algum diretório da máquina. E como o SO faz para descobrir este local?

Ele olha todos os caminhos na sua variável PATH e busca pelo binário que você digitou.

Podemos tirar vantagem desta funcionalidade interna de sistemas operacionais para armazenarmos nossos dados de forma um pouco mais segura.

Por que são seguras

Quando pensamos em informações sensíveis, a primeira coisa que nos vem em mente é tirar esses dados do código, não é mesmo? Afinal, qualquer informação que for “hardcoded” no nosso código é passível de recuperação, seja seu sistema interpretado – o que torna tudo muito mais fácil, já que o código é legível sem precisar de nenhum tipo de tratamento, como o JS – ou então compilado, o que torna as coisas mais difíceis porque temos que descompilar o código, mas ainda assim é possível.

As alternativas que temos geralmente envolvem a criação de algum arquivo no sistema operacional que contém essa informação e então a posterior leitura dela, mas salvar informações em arquivos não as torna mais seguras do que uma variável de ambiente, e você tem um lado ruim de ter que implementar de qualquer forma uma biblioteca de acesso a disco, o que reduz drasticamente o desempenho da sua aplicação.

Variáveis de ambiente são boas substitutas para esses casos porque, primeiro, conseguem armazenar dados diretamente no host da aplicação, e não na aplicação em si. Portanto, estamos retirando a informação sensível do nosso código e colocando ela em outro lugar, que é de muito mais difícil acesso, e segundo, porque estamos armazenando esses dados na memória, e não em um arquivo.

Quando o sistema operacional é carregado pela primeira vez, ele já armazena todos os valores padrões em sua memória e os edita sob demanda, portanto, a aplicação se torna bem mais rápida.

Por que não são tão seguras

Existem vários artigos falando muito bem sobre variáveis de ambiente e, de fato, elas provém uma ótima forma de armazenamento de informações. Porém, como tudo em tecnologia, ela não é a bala de prata para todas as situações.

E é importante sabermos o que podemos perder com o que estamos usando antes de partirmos para este tipo de solução.

O grande problema com variáveis de ambiente é o princípio da globalização. Em quase todas as linguagens de programação modernas, o uso de variáveis globais é desencorajado.

Isso porque a aplicação como um todo tem acesso a seus valores, ou seja, se a sua aplicação consegue ver os dados de suas variáveis globais, os módulos que você instalou também conseguem.

Se houver um módulo malicioso, ele pode roubar seus dados se ele souber aonde eles estão.

  • Isso aconteceu recentemente com uma biblioteca chamada event-stream, como você pode ler neste link.

Significa que não devemos utilizar? Claro que não. Apenas precisamos tomar cuidado com o que estamos instalando e com o que estamos armazenando em nossas variáveis.

Então por que utilizar?

Por mais que variáveis de ambiente possuam problemas, assim como todas as tecnologias, variáveis de ambiente (ou envs como são normalmente chamadas) provém um equilíbrio entre o que é bom mas muito complicado e o que não é tão bom mas é mais simples, tendo muitas das vantagens de segurança com poucas desvantagens. Por este motivo elas foram muito aceitas na comunidade de desenvolvimento como um todo.

A aceitação desse tipo de armazenamento é muito importante também, porque temos envs em todos os lugares, desde máquinas físicas e cloud, como o EC2 até clusters de contêineres como o Kubernetes. Então é muito fácil encontrar o suporte necessário para poder salvar suas informações.

Como utilizar

O jeito mais simples de acessar suas variáveis é através do seu próprio terminal. Se você estiver usando Linux, o comando env exibirá diretamente todas as variáveis de ambientes do seu sistema:

  • A contrapartida em sistemas Windows é o comando SET

Além disso, podemos buscar e exibir valores específicos dessas variáveis no terminal, em sistemas Linux utilizando echo $variavel:

  • No Windows podemos utilizar SET variavel

Porém, mais importante do que acessarmos nossas variáveis no nosso sistema operacional, é acessarmos essas variáveis em nossa aplicação.

Quando iniciamos uma aplicação no NodeJS, ela se torna um processo do sistema. As informações sobre este processo podem ser acessadas na propriedade global process e uma das propriedades deste objeto é o env. Portanto, podemos acessar os dados do objeto através do comando process.env:

Veja que este comando retorna um objeto, então temos como acessar suas propriedades através de process.env.variavel:

Para criarmos novas variáveis em sistemas Linux, podemos simplesmente executar o comando export variavel='valor'. Em Windows, podemos executar setx variavel valor. No caso do NodeJS, o objeto process.env é modificável, então basta realizar uma manipulação de objetos simples:

  • Cuidado! Criar uma variável de ambiente em process.env não fará com que ela esteja disponível para o resto do sistema operacional. Portanto, comandos como o env não irão exibi-la.

Trabalhando com arquivos .env

Até agora conseguimos buscar variáveis pré-existentes no sistema, mas como essas variáveis são criadas? Falamos do processo do NodeJS. Este processo automaticamente vai receber todas as envs setadas no SO no momento que a aplicação for executada, e já que nosso process.env é modificável, isso nos deixa com duas opções simples:

  • 1. Injetamos essas variáveis antes da execução, no sistema operacional através de comandos do shell, como o export
  • 2. Criamos essas variáveis logo no início da execução dentro do próprio Node

Ambos os processos estão corretos e podem ser utilizados de forma intercambiável (e até mesclada), já que process.env não faz a distinção de onde os valores foram obtidos.

Criando variáveis em tempo de execução

Felizmente, para nos ajudar, o NPM possui pacotes como o dotenv que carregam, em tempo de execução, os conteúdos de um arquivo .env no nosso objeto process.env, que é nossa segunda opção. Para isso, basta criarmos um arquivo na raiz do nosso projeto chamado .env com a estrutura:

variavel=valor
variavel=valor

E então podemos executar npm install dotenv e importar o módulo no nosso arquivo de entrada da aplicação – aquele que definimos como main no package.json – da seguinte forma:

require('dotenv').config()

Dessa forma poderemos acessar os valores definidos diretamente por process.env.variavel. Por exemplo, imagine que temos um arquivo de configuração .env desta forma:

DB_HOST=localhost
DB_PORT=4577
DB_USER=lucas
DB_PASS=envsarecool

Então, no arquivo index.js, teremos:

require('dotenv').config()
const db = require('alguma-lib-de-banco-de-dados')

db.connect({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
pass: process.env.DB_PASS
})

Ou, para ficar um pouco mais limpo, podemos utilizar desestruturação:

require('dotenv').config()
const {DB_HOST, DB_PORT, DB_USER, DB_PASS} = process.env
const db = require('alguma-lib-de-banco-de-dados')

db.connect({
host: DB_HOST,
port: DB_PORT,
user: DB_USER,
pass: DB_PASS
})

Carregando variáveis diretamente do sistema

Nossa primeira opção é um pouco mais complicada, já que precisamos criar nossas envs antes da nossa aplicação rodar. Como fazemos isso?

Existem diversas formas. Uma das mais utilizadas é a criação de um shell script (ou batch script) na raiz da aplicação, que possui somente as definições destas envs.

Então, para carregá-las, criamos um binário e incluímos ele em nossa shell, sendo executado na troca de diretórios, que faz a execução deste script.

Aparentemente isso é bastante complicado, mas existem ferramentas como o direnv que fazem exatamente isso. Após instalar o DirEnv, basta navegar para qualquer pasta com um arquivo .envrc com a seguinte estrutura:

export variavel=valor
export variavel=valor

Veja que isso não é nada mais do que um arquivo shell que será executado, mas, para impedir a execução de scripts maliciosos – uma vez que qualquer comando shell pode ser colocado dentro de um arquivo .envrc -, o DirEnv possui um comando chamado direnv allow <diretorio> que dará permissão para que o arquivo seja executado. Vamos a um exemplo:

Primeiramente criamos um arquivo .envrc com o seguinte conteúdo na raiz da nossa aplicação:

export DB_HOST=localhost
export DB_PORT=4577
export DB_USER=lucas
export DB_PASS=envsarecool

Agora executamos, dentro deste diretório, o comando direnv allow .:

Agora podemos iniciar a nossa aplicação normalmente, assumindo que teremos essas variáveis presentes em nosso process.env na hora da execução.

E em produção

Os arquivos .env e .envrc são estritamente para uso em desenvolvimento. Portanto, eles nunca devem ser enviados para seu sistema de versionamento (como o Git), então tenha certeza de incluir este arquivo na lista de arquivos ignorados da sua aplicação. Mas isso gera um problema: como nossos colegas de equipe vão saber quais variáveis eles precisam ter em suas aplicações?

É comum a criação de arquivos .env ou .envrc vazios de exemplo chamados de .env.sample e .envrc.sample, que não contêm nenhum dado sensível, apenas a estrutura do arquivo final.

Utilizando o modelo do arquivo anterior, podemos criar um arquivo .sample da seguinte forma:

DB_HOST=
DB_PORT=4577
DB_USER=
DB_PASS=

Veja que não estamos dando nenhuma informação sensível – deixamos apenas a porta do nosso banco de dados, que não é de forma alguma uma informação importante.

Mas, se dissemos que arquivos .env e .envrc não são para serem utilizados em produção, como vamos carregar nossas variáveis na hora de colocar nossa aplicação no ar? Isso vai depender da nossa plataforma.

Cada modelo de publicação vai ter sua forma de criar envs. No caso de contêineres Docker, podemos criar envs para uma imagem através de flags -e, como:

$ docker run -p 5432:3306 --name mysql -e MYSQL_DATABASE=db -e MYSQL_USER=user -e MYSQL_PASSWORD=p@ssw0rd mysql:5.7

Veja que estamos passando diversas variáveis para dentro do contêiner. Podemos, também, utilizar arquivos docker-compose.yml ou Dockerfile para criar nossas imagens já com variáveis, como podemos ver na documentação oficial.

O ponto é que, para cada sistema de publicação, para cada provedor cloud, para cada gerenciador de contêineres, você terá uma forma diferente de passar uma variável de ambiente para sua aplicação – basta uma pesquisa rápida para identificar como isso pode ser feito. Por isso utilizamos envs: porque elas são muito populares.

Conclusão

Envs podem não ser a melhor forma de armazenar um dado sensível, mas certamente estão entre o melhor de ambos os mundos quando se trata em facilidade e segurança.

Além do mais, são muito aceitas pela comunidade global e quem está utilizando não deve ter nenhum problema em utilizá-las em suas aplicações.

Em um mundo de aplicações nativas e cada vez mais conectadas, saber onde guardar dados sensíveis é essencial para a boa execução de qualquer aplicação.

Até mais!