Ansible Vault: criptografar segredos no Git
Proteja senhas e tokens de API no repositorio Ansible: cifre um arquivo vars ou um valor inline, separe staging e producao e faça rekey sem expor segredos.
O que o Ansible Vault protege e o que não protege
O Ansible Vault cifra segredos dentro do repositório do playbook. Assim, o que o git armazena é texto cifrado em vez de uma palavra-passe em texto simples. O comando ansible-vault cifra um ficheiro inteiro ou um único valor dentro de um ficheiro, usando uma chave simétrica derivada de uma palavra-passe à sua escolha. O Ansible desencripta esse conteúdo em memória quando o play é executado. A variável comporta-se então como qualquer outra variável.
Esse modelo tem um limite claro. O Vault protege um segredo em repouso no repositório e nada mais. Quando uma tarefa é executada, o valor fica disponível em texto simples na memória, no template renderizado, nos argumentos do módulo e na saída da execução, a menos que impeça essa exposição. Todas as pessoas que podem executar o playbook têm a palavra-passe do Vault. Portanto, o Vault protege os segredos contra pessoas externas à equipa, mas não fornece controlo de acesso individual dentro da equipa.
Se ainda não escreveu um playbook, comece por um primeiro playbook Ansible para um VPS e volte quando esse playbook precisar de uma palavra-passe.
Encriptar um ficheiro inteiro ou apenas uma string?
ansible-vault encrypt substitui um ficheiro pelo texto cifrado. O ficheiro passa a conter um único bloco de texto em base64 sob uma linha de cabeçalho que começa por $ANSIBLE_VAULT. Use-o quando o ficheiro contiver apenas segredos.
ansible-vault encrypt_string encripta um valor e apresenta um fragmento YAML que pode colar num ficheiro vars normal. O nome da variável permanece legível e apenas o valor fica cifrado. Use-o quando os segredos estiverem junto de definições em texto simples.
A diferença relevante no trabalho diário está no diff. Um ficheiro vault é novamente encriptado com um salt aleatório novo sempre que o guarda, pelo que todos os bytes do texto cifrado mudam. git diff mostra então um bloco ilegível substituído por outro bloco ilegível. Um revisor não consegue saber se rodou uma palavra-passe ou se reescreveu o ficheiro. Com encrypt_string, cada segredo é o seu próprio bloco dentro de um ficheiro em texto simples. Assim, o diff mostra exatamente qual foi a variável alterada e mantém o resto do ficheiro intacto.
O formato inline tem um custo, que surge no momento da rotação: ansible-vault rekey não altera os blocos inline. Escolha o formato de ficheiro quando a lista de segredos for longa e mudar raramente. Escolha o formato inline quando o ficheiro combinar segredos com variáveis normais e quiser que a revisão de código tenha significado.
A estrutura de group_vars que mostra o que está protegido
O Ansible carrega group_vars/<group>.yml e também todos os ficheiros dentro de um diretório group_vars/<group>/. A forma com diretório é a que deve utilizar, porque permite que um único grupo contenha um ficheiro em texto simples e um ficheiro encriptado lado a lado.
inventory/
hosts.ini
group_vars/
all/
vars.yml
vault.yml
web/
vars.yml
vault.yml
host_vars/
db01/
vars.yml
vault.yml
playbooks/
site.ymlCada vault.yml está encriptado. Cada vars.yml está em texto simples. É possível ver quais valores estão protegidos sem abrir nenhum ficheiro, porque o nome do ficheiro indica isso.
A segunda parte do padrão é a indireção. Dentro do ficheiro encriptado, coloque o prefixo vault_ em todas as variáveis.
vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"Depois, faça referência a esses nomes no ficheiro em texto simples ao lado.
db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"As roles e os templates usam db_password e nunca precisam de saber de onde veio o valor. Isso mantém a separação entre um playbook e uma role clara. O vars.yml em texto simples também funciona como um índice pesquisável: grep -r vault_ group_vars/ lista todos os segredos esperados pelo repositório, sem desencriptar nada. O custo é um nome adicional por segredo. Um erro de digitação num nome vault_ aparece em tempo de execução como uma variável indefinida, e não como um erro de sintaxe.
Criptografar uma variável com encrypt_string
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
--stdin-name 'vault_db_password'Introduza o segredo e, em seguida, prima Ctrl-D. --stdin-name lê o valor da entrada padrão, o que o mantém fora do ficheiro do histórico da shell. A outra forma coloca o valor na linha de comandos, onde a shell o regista:
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
'a real password' --name 'vault_db_password'Em ambos os casos, o comando imprime um bloco YAML. Cole-o no ficheiro vars exatamente como foi impresso, porque a indentação sob a etiqueta !vault faz parte do valor.
vault_db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
6638643965323633646262656665306333616466396630323136393465356136396436383331
3131303163306665326539353837343663313762616561306534373963383531613664393332A etiqueta !vault indica ao carregador YAML que o valor escalar é texto cifrado, e não texto simples. O cabeçalho contém a versão do formato, a cifra e a etiqueta do ID do vault que o cifrou. Um valor cifrado sem um ID de vault contém um cabeçalho 1.1 sem etiqueta. Continua a funcionar, mas fornece menos informação sobre a origem da palavra-passe.
Onde fica a password do vault?
Fora do repositório. Essa é a única regra sem exceções.
--ask-vault-pass solicita a password uma vez por execução e não armazena nada. É adequado para um portátil, mas não para um cron job ou um CI runner.
Um ficheiro de passwords é um ficheiro de texto simples cuja primeira linha contém a password. Crie-o vazio com permissões restritas e depois preencha-o num editor, para que a password nunca chegue ao histórico da shell:
mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txtIndique esse ficheiro a qualquer comando com --vault-password-file:
ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
--vault-password-file ~/.ansible/vault-prod.txtRepetir essa opção em todos os comandos é fácil de esquecer. Por isso, defina-a uma vez em ansible.cfg na raiz do repositório.
[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txtA mesma configuração lê o valor da variável de ambiente ANSIBLE_VAULT_PASSWORD_FILE. É assim que um job de CI normalmente fornece a password. O job escreve a password, obtida do seu próprio armazenamento de credenciais, num ficheiro num diretório temporário, exporta a variável e elimina o ficheiro quando a execução termina. Adicione também o padrão do nome do ficheiro a .gitignore, porque o caminho em ansible.cfg é versionado e, mais cedo ou mais tarde, alguém criará o ficheiro real dentro do checkout.
Se o ficheiro de passwords for executável, o Ansible executa-o e lê a password a partir da saída padrão, em vez de ler o ficheiro como texto. Assim, é possível obter a password do vault a partir de um keyring do sistema ou de um gestor de segredos na cloud sem a escrever no disco. Um script usado através de --vault-id tem requisitos adicionais: o nome deve terminar em -client ou em -client seguido de uma extensão, deve ser executável, deve aceitar uma opção --vault-id e deve escrever a password na saída padrão.
Dois IDs de vault: staging e production
Um ID de vault é um rótulo associado a uma senha de vault, escrito como label@source. A origem é prompt, o caminho para um ficheiro de senha ou o caminho para um script cliente. Os rótulos permitem que um repositório mantenha segredos protegidos por mais de uma senha. Assim, a senha de staging não abre o ficheiro de production.
ansible-vault encrypt --vault-id staging@~/.ansible/vault-staging.txt \
group_vars/staging/vault.yml
ansible-vault encrypt --vault-id prod@~/.ansible/vault-prod.txt \
group_vars/prod/vault.ymlPasse todos os IDs de que uma execução possa precisar:
ansible-playbook playbooks/site.yml \
--vault-id staging@~/.ansible/vault-staging.txt \
--vault-id prod@~/.ansible/vault-prod.txtOu liste-os uma vez em ansible.cfg:
[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txtUm comportamento surpreende algumas pessoas. Por predefinição, o rótulo é uma indicação, não uma restrição. O Ansible tenta todos os segredos que tem atualmente contra o ficheiro até que um deles o desencripte. Por isso, um ficheiro com o rótulo staging continua a abrir se a senha de production for a chave correta. Defina vault_id_match = True em [defaults], ou a variável de ambiente ANSIBLE_VAULT_ID_MATCH, para que o Ansible use apenas o segredo cujo rótulo corresponde ao cabeçalho do ficheiro. Essa verificação precisa do cabeçalho 1.2. Por isso, aplica-se apenas a conteúdo que foi cifrado originalmente com um ID de vault.
Quando há mais de um ID carregado, ansible-vault encrypt deixa de saber com que senha deve cifrar. Indique-a com --encrypt-vault-id prod, ou defina vault_encrypt_identity em ansible.cfg para que o repositório tenha uma predefinição.
O benefício está no âmbito da implementação. Um job de CI que implementa staging recebe apenas a senha de staging. Assim, um runner comprometido não consegue ler as credenciais de production. Quando executar plays em um conjunto de servidores Linux a partir de uma única máquina de controlo, essa separação pode determinar se um incidente permanece pequeno ou se se torna muito grave.
Alterar a chave do vault quando alguém sai
A alteração da chave muda a palavra-passe do vault e volta a encriptar o conteúdo com a nova palavra-passe. Isto não desfaz nada. Qualquer pessoa que tenha tido a palavra-passe antiga ainda pode desencriptar qualquer cópia do repositório que tenha guardado, incluindo todos os commits antigos dessa cópia. Por isso, considere a palavra-passe do vault comprometida no momento em que o titular sai e faça a rotação pela ordem seguinte.
- Altere as credenciais reais nos servidores e nos serviços de terceiros. Este é o passo que revoga efetivamente o acesso.
- Coloque os novos valores nos ficheiros do vault com
ansible-vault edit. - Altere a chave de todos os ficheiros encriptados para uma nova palavra-passe do vault.
- Entregue a nova palavra-passe do vault às pessoas que ainda precisam dela, através de um canal que não seja o repositório.
ansible-vault rekey --vault-id prod@~/.ansible/vault-prod-old.txt \
--new-vault-id prod@prompt \
group_vars/prod/vault.yml host_vars/db01/vault.ymlrekey aceita vários ficheiros num único comando, e --new-vault-id prod@prompt pede a nova palavra-passe uma vez, em vez de a ler do disco. Mantenha o mesmo rótulo, a menos que tenha uma razão para o alterar, porque o rótulo é gravado no cabeçalho de todos os ficheiros que o comando reescreve.
É aqui que a forma inline tem um custo. ansible-vault rekey opera sobre ficheiros totalmente encriptados, por isso um bloco !vault dentro de um ficheiro de variáveis em texto simples não é alterado. Encontre primeiro esses blocos e depois regenere cada ficheiro com encrypt_string usando a nova palavra-passe:
grep -rl '!vault' group_vars/ host_vars/Essa é a troca completa. Os blocos inline permitem rever diffs legíveis, mas exigem uma passagem manual no momento da rotação. Os ficheiros totalmente encriptados são alterados com um único comando, mas não fornecem informação útil durante a revisão.
Por que o segredo ainda aparece na saída
O Vault termina o seu trabalho assim que o valor é desencriptado. O Ansible regista o resultado de uma tarefa, e um módulo que ecoa os seus argumentos inclui a credencial nesse registo. Uma execução detalhada, um --diff numa tarefa de template, uma tarefa falhada que despeja os seus argumentos ou um plugin de callback que escreve a saída num ficheiro conservarão o texto simples. Encriptar o ficheiro não impediu nenhum destes casos.
no_log: true é a opção de controlo. Defina-a em qualquer tarefa que receba uma credencial.
- name: Write the application environment file
ansible.builtin.template:
src: app.env.j2
dest: /etc/myapp/app.env
owner: myapp
group: myapp
mode: "0600"
no_log: trueO Ansible passa então a omitir o resultado dessa tarefa na saída. Assim, o log regista que a tarefa foi executada, sem registar o que ela processou. Defina-a especialmente em loops, porque um loop regista um resultado por item, e um loop sobre uma lista de credenciais regista a lista completa.
Existem outros quatro locais onde um segredo desencriptado pode escapar. Nenhum deles é abrangido por no_log:
- Um ficheiro gerado a partir de um template herda o
modee oownerque lhe atribuiu. Definamode: "0600"e um proprietário específico em qualquer ficheiro que contenha uma credencial. Caso contrário, o segredo ficará legível por qualquer utilizador no host de destino. - Um segredo passado a
ansible.builtin.commandouansible.builtin.shellaparece na lista de processos do host de destino enquanto o comando é executado, onde qualquer utilizador local pode lê-lo. Passe-o através de um ficheiro ou de uma variável de ambiente. - O cache de factos grava os factos recolhidos em disco na máquina de controlo. Assim, uma variável registada que contenha um segredo pode acabar num ficheiro de cache que ninguém considera sensível.
- O mesmo segredo costuma existir num segundo local, como um ficheiro de ambiente lido por um contentor. As regras desse local são diferentes, e manter as credenciais fora dos ficheiros env do Compose aborda essa parte.
no_log torna a depuração mais difícil. É precisamente essa a sua finalidade. Remova-o temporariamente num host de teste quando uma tarefa apresentar problemas e volte a defini-lo antes de a alteração chegar à produção.
Ler e editar ficheiros encriptados sem deixar texto em claro
ansible-vault view group_vars/prod/vault.yml desencripta para um pager e não escreve nada no disco. ansible-vault edit desencripta para um ficheiro temporário, abre o seu $EDITOR e volta a encriptar o ficheiro quando o fecha. Prefira ambos a ansible-vault decrypt, que deixa um ficheiro em texto claro na árvore de trabalho. Um ficheiro de cofre desencriptado adicionado acidentalmente à área de preparação é a forma mais comum de uma credencial real chegar a um repositório público.
O Git pode gerar um diff legível para ficheiros totalmente encriptados, desencriptando-os à medida que o processa:
git config --local diff.ansible-vault.textconv "ansible-vault view --vault-password-file ~/.ansible/vault-prod.txt"
printf '%s\n' 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributesCompreenda o efeito antes de ativar esta configuração. git diff passará a imprimir segredos de produção no seu terminal, o que os coloca no histórico de deslocamento e em qualquer partilha de ecrã. É uma conveniência local para uma pessoa num computador, por isso mantenha o git config local e espere que os checkouts de outras pessoas se comportem de forma diferente, a menos que configurem o mesmo.
Quando o vault deixa de ser a ferramenta adequada
O Vault é um formato de ficheiro com uma palavra-passe por etiqueta, e essa estrutura define os seus limites. Passe a utilizar um armazenamento de segredos dedicado quando qualquer uma das condições seguintes se aplicar.
- Precisa de acesso por pessoa. Todas as pessoas que executam o playbook têm a mesma palavra-passe, e os IDs do vault separam o acesso por ambiente, nunca por pessoa.
- Precisa de uma trilha de auditoria. O vault não regista quem desencriptou o quê, nem quando.
- Precisa de rotação programada. O vault não tem expiração nem versionamento, por isso nada indica que uma credencial não mudou durante dois anos.
- A própria aplicação precisa do segredo em tempo de execução. Um serviço que lê a palavra-passe da base de dados durante o arranque não deve obtê-la do repositório de deployment.
O padrão é então invertido. O Ansible deixa de armazenar segredos e passa a obtê-los em tempo de execução através de um plugin de lookup, a partir do HashiCorp Vault (um produto diferente com um nome confusamente semelhante), do gestor de segredos de um fornecedor cloud ou de um keyring na máquina de controlo. O repositório contém um caminho, o armazenamento contém o valor e o armazenamento mantém o registo de acessos. Para uma equipa pequena, um gestor de palavras-passe self-hosted com uma API, como um servidor Vaultwarden, cumpre a mesma função numa escala menor.
Uma credencial fica fora de tudo isto. A chave SSH que a máquina de controlo utiliza para aceder aos servidores não é um problema do vault, porque o Ansible precisa dela antes de qualquer play poder ser executado. Gira-a com um agent e uma passphrase, seguindo princípios como as noções básicas da gestão de chaves SSH.
FAQ
Devo encriptar o ficheiro vars inteiro ou apenas a string secreta?
Encripte o ficheiro inteiro quando este contiver apenas segredos, porque um comando roda todos os segredos e mantém o layout simples. Use ansible-vault encrypt_string quando os segredos estiverem junto de variáveis comuns, porque nesse caso apenas o valor encriptado muda no diff e um revisor consegue ver qual variável foi alterada. A desvantagem está na rotação. ansible-vault rekey abrange ficheiros inteiros e deixa os blocos !vault incorporados intactos, pelo que estes têm de ser regenerados manualmente com a nova palavra-passe.
Onde devo guardar o ficheiro de palavra-passe do Ansible Vault?
Fora do repositório, com o modo 0600, num caminho como ~/.ansible/vault-prod.txt. Indique esse caminho com --vault-password-file, defina vault_password_file em [defaults] dentro de ansible.cfg, ou defina ANSIBLE_VAULT_PASSWORD_FILE no ambiente. Na CI, faça o job escrever a palavra-passe do seu próprio armazenamento de credenciais num ficheiro temporário, exporte a variável e elimine o ficheiro quando o job terminar. Se o ficheiro for executável, o Ansible executa-o e lê a palavra-passe a partir da saída padrão, o que permite obtê-la de um keyring em vez de a armazenar no disco.
Como uso palavras-passe do vault diferentes para staging e produção?
Atribua uma etiqueta a cada palavra-passe com --vault-id staging@/path/to/file e --vault-id prod@/path/to/file e encripte os ficheiros de cada ambiente com a sua própria etiqueta. Passe ambos os IDs em tempo de execução ou liste-os em vault_identity_list dentro de [defaults]. Por predefinição, o Ansible tenta todos os segredos disponíveis até conseguir desencriptar o ficheiro. Defina vault_id_match = True se quiser que tente apenas o segredo cuja etiqueta corresponde ao cabeçalho do ficheiro. Com vários IDs carregados, selecione o usado para encriptar com --encrypt-vault-id.
O Ansible Vault impede que uma palavra-passe apareça na saída da execução?
Não. O Vault protege o segredo apenas enquanto este está armazenado no repositório. Quando uma tarefa é executada, o valor está em texto simples, e uma execução detalhada ou uma tarefa com falha pode incluí-lo no log. Adicione no_log: true a todas as tarefas que tratem de uma credencial, defina mode e owner restritivos em qualquer ficheiro que seja gerado por template e evite passar segredos como argumentos de comandos, porque estes ficam visíveis na lista de processos do host de destino enquanto o comando é executado.