Ansible: Criptografando Senhas de Usuários

As vezes queremos gerar uma senha fixa para usuários, a qual vamos reutilizar em várias máquinas.

Um exemplo típico é criar usuário administrador, capaz de realizar login com senha. Dessa forma, caso ocorra algum problema com o servidor SSH ou perca a sua chave privada junto com o seu HD, ainda é possível fazer login direto na máquina, por VNC ou por outros meios de acesso remoto.

Porém essa é a senha de administrador de muitos computadores e não queremos colocá-la diretamente em scripts de configuração, como os do Ansible, onde podem ser lidas diretamente.

Podemos, no entanto, criar uma versão encriptada da senha (a mesma usada no arquivo /etc/shadow) e aí sim colocá-la no script.

O código abaixo cria uma senha segura 24 caracteres utilizando pwgen. A opção -s indica que a senha deve ser completamente aleatória.

$ pwgen -s 24 1
> VONga6jUFoXCMQNhDlHgTHQY

Por fim usamos openssl passwd para encriptar a senha gerada Usamos a opção -6 para criptografar a senha com SHA512.

$ openssl passwd -6 'VONga6jUFoXCMQNhDlHgTHQY'
> $6$80Ll7KOwC/XDQVgZ$NwTjYG8cXcMTr9NI1EbtaX7OEAIBq3ULUSJPmpjtk/vUiyB9duYO3aNqmWmBnS.mjgaN37/mo34R8COzNrFBh0

Dessa forma, a preciosa senha está segura, longe de olhos curiosos, e podemos colocá-la no script de criação de usuários.

# Arquivo:  tasks/default-tasks.yml
---
- name: Criar usuário admin com senha
  ansible.builtin.user:
    name: admin
    comment: Usuário Administrador
    password: "$6$80Ll7KOwC/XDQVgZ$NwTjYG8cXcMTr9NI1EbtaX7OEAIBq3ULUSJPmpjtk/vUiyB9duYO3aNqmWmBnS.mjgaN37/mo34R8COzNrFBh0"

Ansible: Testando Playbooks

Ansible é uma excelente ferramenta para automatização de tarefas.

Cria-se uma lista de computadores e instalando neles um servidor SSH e Python 3, é possível automatizar a instalação de programas, alteração de configurações e, o meu favorito, atualização dos computadores.

No maior estilo do TDD (Test Driven Development), muitas vezes, queremos ter certeza que algo está funcionando antes de prosseguir com a execução do playbook.

Exemplos destes casos são:

  • A nova configuração funciona?
  • O usuário realmente pode (não pode) usar usar sudo?
  • Login do usuário root está mesmo desativado?
  • O firewall liberou/bloqueou a porta HTTP?

Podemos criar testes como esses com certa facilidade e inclusive podemos escolher entre a máquina remota ou local para executar os comandos, o que é essencial para alguns testes.

Abaixo mostro dois exemplos, testando as permissões de um usuário do servidor remoto e testando se o computador local pode fazer login como root.

# Arquivo: tasks/testes.yml
---
- name: TESTE Verificar se o usuário 'admin' pode usar SUDO sem senha
  # Teste no servidor remoto:
  # - su admin -c "xxx": executa o comando xxx como admin;
  # - whoami: imprime o nome do usuário; e
  # - Esperamos que imprima 'admin' (sem o sudo) e em seguida
  #   imprima 'root' (com sudo).
  ansible.builtin.command: |
    su admin -c "whoami; sudo whoami"
  register: result
  changed_when: false
  failed_when: >
    result.stdout_lines[0] != 'admin'
    or result.stdout_lines[1] != 'root'
  timeout: 5

- name: TESTE Negar login do usuário 'root' port SSH
  # Teste no computador local:
  # - SSH, ativando senha e desativando chave pública;
  # - whoami: um comando qualquer para ser executado; e
  # - Esperamos que a tentativa de login seja negada.
  ansible.builtin.command: >
    ssh -oPasswordAuthentication=yes -oPubkeyAuthentication=no
    -p{{ ansible_port }} [email protected]{{ ansible_host }} whoami
  register: result
  changed_when: false
  failed_when: "'Permission denied (publickey)' not in result.stderr"
  timeout: 5
  # Esta parte final definie que o teste é executado no computador local
  # (que executa o Ansible).
  # Desativamos ansible_become para que use o usuário atual, em vez de
  # usar sudo para se tornar root.
  delegate_to: localhost
  vars:
    ansible_become: false

Ansible: Variáveis Dependentes do Sistema Operacional

Ansible é uma excelente ferramenta para automatização de tarefas.

Cria-se uma lista de computadores e instalando neles um servidor SSH e Python 3, é possível automatizar a instalação de programas, alteração de configurações e, o meu favorito, atualização dos computadores.

No entanto, há coisas que são diferentes de um sistema para outro, por exemplo caminhos de arquivos de configuração e nomes de pacotes.

Para evitar criar várias e várias tarefas que fazem a mesma coisa ajustadas para cada sistema (when: ansible_distribution == ...), enchendo o playbook com código repetido, Ansible permite a importação de arquivos de variáveis com o módulo include_vars, onde podemos escolher dinamicamente o arquivo de variáveis correspondente ao sistema.

Então podemos criar um arquivo para cada um dos sistemas em uso e seguir em frente. Porém isso acaba por se tornar um pesadelo de manutenção: cada nova variável precisa ser adicionada em TODOS os arquivos de variáveis e, ao criar um arquivo de variáveis novo, é preciso atualizar todas as variáveis possíveis.

Uma alternativa interessante seria uma criar estrutura hierárquica, onde definimos valores padrão para todos, em seguida substituímos com os valores para a família, então com os valores para a distribuição e quem sabe até com valores para certas versões.

Padrão          default
                 /   \
Família      Debian  FreeBSD
              / \
Distro  Debian   Ubuntu

Além disso, é importante que a inexistência de algum desses arquivos de especialização não seja um empecilho.

Com um pouco de pesquisa e um pouco de tentativa-e-erro, e com "um pouco" quero dizer interminável, cheguei no código abaixo:

# Arquivo: tasks/default-tasks.yml
---
- name: Carrega variáveis padrão, da família e da distro
  ansible.builtin.include_vars: "{{ item }}"
  with_fileglob:
    - vars/default.yml
    - vars/family_{{ ansible_os_family }}.yml
    - vars/distro_{{ ansible_distribution }}.yml

Usamos include_vars, como de costume, porém junto com with_fileglob. Este geralmente é usado para encontrar vários arquivos usando coringas (wildcards) como vars/*.yml. Mas aqui não usamos coringas, damos direto o nome do arquivo. Se o arquivo não é encontrado ele é ignorado.

Abaixo estão exemplos dos arquivos de variáveis padrão e especializado.

# Arquivo: vars/default.yml
---
doas_config_file: /etc/doas.conf
# Arquivo: vars/family_FreeBSD.yml
---
doas_config_file: /usr/local/etc/doas.conf

Como testar pendrive e cartão SD no Linux

Uma situação extremamente chata é comprar um pendrive ou cartão SD falsificado.

Você coloca arquivos nele e, ao ler os dados de volta, estes estão corrompidos.

Comprei 4 pendrives de 16 GB e, para evitar essa situação inconveniente, quero testar se eles realmente possuem esse tamanho ou se são falsificados.

Para esses testes, geralmente crio um arquivo grande com dados aleatórios (de 128 MB até 1 GB) e copio ele várias vezes para dentro do pendrive e em seguida verifico se a checksum de cada um dos arquivos bate com a original.

Como testar pendrive e cartão SD no Linux

Testando 4 pendrives de uma vez com ZFS

Só que como dessa vez eu tenho 4 pendrives para testar, optei por fazer um pouco diferente.

Para este teste resolvi usar o sistema de arquivos ZFS.

O ZFS automaticamente guarda checksums de todos os arquivos armazenados e, ao lê-los valida essas checksums. Além disso, há um comando que realiza a verificação de todos os dados gravados.

Então, testar usando o ZFS me poupa o trabalho de manualmente criar checksums e de manualmente verificá-las. Basta gravar arquivos até encher os pendrives e verificar os dados.

Muito cuidado: é possível corromper o seu disco principal com os comandos utilizados.

Instalando as dependências

Realizei o teste no Ubuntu Linux 22.04.

Acredito que a única dependência sejam os utilitários do ZFS.

sudo apt install zfsutils-linux

Formatando

Primeiro listamos os discos.

$ ls -1rt /dev/sd*
> /dev/sda
> /dev/sdb
> /dev/sdc
> /dev/sdd
> /dev/sde

Para descobrir quais dos discos são os pendrives listamos os discos com o programa parted, filtrando os dados de interesse com o comando grep.

$ sudo parted -l | grep '/dev/sd'
> Disk /dev/sda: 1000GB
> Disk /dev/sdb: 15,7GB
> Disk /dev/sdc: 15,7GB
> Disk /dev/sdd: 15,7GB
> Disk /dev/sde: 15,7GB

Dica: Fique atento a qual é o seu disco principal. No meu caso é /dev/sda.

Vamos usar o ZFS para criar uma ZPOOL (conjunto de discos). Criamos uma ZPOOL chamada "teste" com os 4 pendrives em stripes (listras?).

$ sudo zpool create teste sdb sdc sdd sde

Dica: Não adicione o seu disco principal na ZPOOL.

Os pendrives já estavam formatados com FAT, então o ZFS se negou a criar a ZPOOL, o que causaria a perda de dados. Bastou adicionar -f no comando para forçar a criação, pois não tenho nada gravado neles.

Para melhorar um pouco a performance do teste configuramos as seguintes opções:

$ sudo zpool trim teste --wait
$ sudo zfs set atime=off teste
$ sudo zfs set sync=disabled teste
$ sudo zfs set compression=off teste

Sobre as opções habilitadas

Executar um "trim" no sistema de arquivos avisa o pendrive de todo o espaço livre, permitindo utilizá-lo de forma mais efetiva. Esse passo vai ser repetido no fim do teste. É possível que a operação de trim não seja suportada pelo pendrive ou cartão SD.

Além do trim, a única opção que recomendo usar na prática com ZFS é atime=off. Isso evita atualizar a data de acesso a cada leitura. Não tem porque transformar uma leitura em uma leitura-e-escrita, por isso o uso dessa opção é comum e recomendada.

As outras duas opções é só pra deixar o teste mais rápido e podem ter consequências indesejadas.

A opção sync=disabled, do ponto de vista da aplicação, pode afetar seriamente a consistência dos dados gravados. A maioria das aplicações, precisam que todas as gravações sejam síncronas: "só quero continuar quando os dados estiverem efetivamente gravados no disco." Essa opção mente para estas aplicações, tornando todas as gravações assíncronas.

Na prática, recomendo usar sync=standard, que deixa a aplicação escolher entre síncrono e assíncrono.

Já a opção compression=off serve para não comprimir os dados no disco.

Sim, outra vantagem do ZFS é que ele pode comprimir dados, automaticamente economizando o precioso espaço do disco. Porém, neste teste, compactar seria contra-produtivo porque queremos preencher o disco completamente e então varrê-lo em busca de erros.

Na prática, recomendo usar sync=lz4, que compacta muito bem as sequências de zero, que são muito comuns, e arquivos de texto.

Gravando

Uma coisa muito legal do ZFS é que ele monta tudo automaticamente. Como acabamos de criar uma ZPOOL, é óbvio que vamos utilizá-la e por isso ela já é montada pelo próprio ZFS.

Então basta logar como root (usando sudo), entrar na pasta do ZPOOL e começar a escrever arquivos.

$ sudo -i
$ cd /teste

Os arquivos que vamos escrever são arquivos aleatórios (lidos de /dev/urandom). Criamos uma variável COUNT, que conta o número de arquivos criados e, dentro de um laço while criamos arquivos de 128 MB utilizando o comando dd. Caso o comando dd falhe (ex: por falta de espaço em disco) o comando break pára o laço e aguardamos o sistema operacional sincronizar com o comando sync. Por fim, também já iniciamos o scrub (varredura) nos discos.

# Execute como root
COUNT=0
while :; do
    dd if=/dev/urandom of=file-$COUNT.bin bs=1M count=128 || break
    COUNT="$(($COUNT + 1))"
done
sync
zpool scrub teste

Para ver o andamento da gravação podemos usar os comandos abaixo. Eu prefiro o penúltimo.

$ ls -lh /teste
$ zfs list teste
$ zpool iostat teste 5
$ zpool iostat teste 5 -v

Verificando

Finalizada a gravação, o scrub (varredura) nos discos foi iniciado e o ZFS vai ler todos os dados, de todos os pendrives, e vai validar esses dados com as checksums.

No fim da gravação, logo após o comando sync já iniciamos o scrub, que pode ser iniciado novamente com o comando abaixo.

$ sudo zpool scrub teste

Podemos ver o andamento do scrub no status da ZPOOL.

$ zpool status teste
> ...
>   scan: scrub in progress since ...
>     14.0G scanned at 182M/s, 720M issued at 9.12M/s, 14.0G total
>     0B repaired, 5.01% done, no estimated completion time
> ...

Quando o scrub finalizar vemos o resultado também no status da ZPOOL.

$ zpool status teste
> ...
>   scan: scrub repaired 0B in 00:27:31 with 0 errors on ...
> ...
>     NAME      STATE     READ WRITE CKSUM
>     teste     ONLINE       0     0     0
>       sdb     ONLINE       0     0     0
>       sdc     ONLINE       0     0     0
>       sdd     ONLINE       0     0     0
>       sde     ONLINE       0     0     0
> ...

Ótimas notícias: Nenhum erro!

Limpando e exportando

Agora vamos apagar todos os arquivos e aplicar um "trim" no sistema de arquivos, instruindo ao pendrive que o espaço livre pode ser reutilizado, melhorando a longevidade do pendrive.

$ sudo rm -f /teste/file-*.bin
$ sudo zpool trim teste --wait

Estando tudo finalizado, podemos exportar (ejetar) a ZPOOL para podermos formatar os pendrives e usá-los normalmente.

$ sudo zpool export teste

Conclusão

Em 3 horas os 4 pendrives estavam testados e pude aproveitar boa parte do tempo para escrever este post.

Tamanha a conveniência, acredito que será meu novo método de testes de pendrives e cartões SD.

Copiando a suas chaves SSH do GitHub ou GitLab

As vezes queremos copiar uma chave pública SSH para um computador.

Esse é um jeito fácil de adicionar a sua própria chave ou de adicionar a chave de alguém para te ajudar com algum problema.

Tendo uma conexão com a internet, é possível fazer o download das chaves públicas SSH de qualquer usuário do GitHub ou GitLab.

Com os comandos abaixo é possível fazer o download da chave adicioná-las nas chaves permitidas para acesso SSH. Basta trocar USUARIO pelo nome do usuário desejado.

  1. Configure a máscara de permissões para 077: leitura e escrita apenas para o usuário dono.
    • umask 077
  2. Crie a pasta ~/.ssh, se ela não existir.
    • mkdir -p ~/.ssh
  3. Faça o download das chaves:
    • GitHub: curl https://github.com/USUARIO.keys >>~/.ssh/authorized_keys
    • GitLab: curl https://gitlab.com/USUARIO.keys >>~/.ssh/authorized_keys

Também podemos usar wget em vez de curl, substituindo curl por wget -O-.

Mudei um arquivo no Yocto mas o Bitbake não recompila

Quando compilando uma imagem no Yocto, às vezes o Bitbake se confunde e não consegue determinar que um arquivo mudou na receita.

Nesse ponto, recomendo limpar toda as pastas tmp e sstate-cache e recompilar do zero. No entanto, recompilar do zero pode demorar muito e podemos deixar isso para o fim do dia.

Para resolver o problema sem tomar muito tempo, basta limpar o diretório de trabalho e o cache. Digamos que esteja trabalhando no arquivo NOME.bb, utilizamos os comandos abaixo.

$ bitbake NOME -c clean
$ bitbake NOME -c cleansstate

Então basta compilar a imagem novamente.

$ bitbake IMAGEM

Minhas Preferências de Formatação

Abaixo estão algumas das minhas preferências e suas razões.

1. Endentação e aspecto geral

  • Endentar com 4 espaços;
  • Limite de 80 caracteres por linha; e
  • Tabulações proibidas.

Para mim 2 espaços é pouco e 8 espaços é muito na endentação. Quatro espaços parecem ser o meio termo adequado.

Linhas longas atrapalham a leitura do código: eu desejo um código tão legível quanto livros e jornais. Além disso, o limite de 80 caracteres permite abrir arquivos lado a lado sem necessitar de rolagem horizontal e ainda facilita revisão do código anterior e atual abertos lado a lado.

Com respeito a proibir tabulações, existe o argumento que tabulações são mais flexíveis, pois o programador pode escolher o tamanho de sua preferência. O argumento é válido, porém somente até se misturarem as tabulações e os espaços: nesse instante, a única pessoa que vê alinhado é o próprio autor do código; e todo o resto do mundo vê uma bagunça desalinhada. Usar apenas um tipo de espaço em branco elimina esse problema.

2. Alinhamento das chaves

Abertura de chaves na linha abaixo.

Isso ajuda a identificar onde acontece a abertura do bloco. Chave abrindo na própria linha permite determinar o início do bloco sem precisar procurar por ela no fim de alguma linha.

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3)
{
    return parametro1 + parametro2 - parametro3;
}

Compare abaixo a mesma função abrindo as chaves na linha. Não é tão óbvio como no caso acima.

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3) {
    return parametro1 + parametro2 - parametro3;
}

Abrindo as chaves na linha abaixo, fica óbvio onde os parâmetros terminam e onde inicia o corpo da função. O mesmo acontece em blocos de controle como if, for, etc.

Mesmo se eliminarmos qualquer informação sobre os caracteres, como é mostrado abaixo, facilmente identificamos a lista de parâmetros e o início do corpo da função.

XXXXXXXX XXXX XXX XXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
X
    XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX
X

XXXXXXXX XXXX XXX XXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX X
    XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX
X

A maior parte do tempo que passamos programando é utilizada, na verdade, lendo o código. Esta pequena escolha ajuda a encontrar a parte que importa no momento.

3. Parâmetros de funções

A lista de parâmetros de funções e a lista de argumentos de chamada de uma função devem estar todos em uma linha ou cada um em sua linha.

Dessa forma, a lista de parâmetros (ou argumentos) realmente se parece com uma lista. É fácil de contar os parâmetros e é fácil de encontrá-los.

Nesse ponto, há três possibilidades avaliadas na ordem seguinte:

  • Protótipo todo na mesma linha;
  • Lista de parâmetros toda na segunda linha; e
  • Lista de parâmetros com um parâmetro por linha.

Exemplos abaixo:

/* Protótipo inteiro cabe em uma linha: */

int funcao_f(int parametro1, int parametro2)
{
    return parametro1 + parametro2;
}

/*
 * Protótipo não cabe em uma única linha, porém
 * a lista de parâmetros cabe inteira na segunda linha:
 */

unsigned long int funcao_g(
    unsigned long int parametro1, unsigned long int parametro2)
{
    return parametro1 + parametro2;
}

/*
 * Parâmetros não cabem todos na segunda linha, então
 * coloca-se um parâmetro por linha:
 */

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3)
{
    return parametro1 + parametro2 - parametro3;
}

4. Comentários

  • Usar o mínimo de comentários possível; e
  • Comentários multilinha devem ter um caractere inicial de alinhamento.

Comentários não são analisados pelo compilador e não precisam fazer sentido para o código funcionar. Por isso não são atualizados em conjunto com o código. Consequentemente, com o passar do tempo a semântica (o significado) do código se desvia dos comentários presentes.

Na maioria dos casos é possível fazer um código autoexplicativo através da refatoração e da melhoria de nomes de funções e nomes de variáveis.

/* executa_loop1 */

void executa_loop1(void)
{
    // Lê o valor do sinal do sensor
    int16_t val = ADC0;

    // Calcula tensão do sensor em volts
    // a partir do offset e do ganho
    float valf = (val - 512) * (24.0 / 512.0);

    // Escreve na serial
    Serial_EscreveFloat(valf);
}

/* ---------------------------------------- */

/* executa_loop2 */

#define SENSOR_OFFSET 512
#define SENSOR_GANHO  (24.0 / 512.0)

float TensaoSensor_Volts(void);

void executa_loop2(void)
{
    Serial_EscreveFloat(TensaoSensor_Volts());
}

int16_t SinalSensor_ADC(void)
{
    return ADC0;
}

float TensaoSensor_Volts(void)
{
    return (SinalSensor_ADC() - SENSOR_OFFSET) * SENSOR_GANHO;
}
  • O primeiro comentário é removido com uma chamada da função SinalSensor_ADC().
  • O segundo comentário é removido com uma chamada da função TensaoSensor_Volts() e pelo uso das constantes SENSOR_OFFSET e SENSOR_GANHO.
  • O terceiro comentário é redundante e desnecessário.

Qual versão da função executa_loopX() você prefere ler?

Alguns comentários, por sua vez, são impossíveis de serem transformados em código. Por exemplo, o comentário a seguir mostra um diagrama de um LCD 16×2 que mostra a tensão elétrica e a corrente medida.

/*
 *   0123456789012345
 * 0 Tensão   XX,XX V 0
 * 1 Corrente Y,YYY A 1
 *   0123456789012345
 */

Sobre o caractere inicial dos comentários multilinha, alguns editores e formatadores eliminam espaços em branco iniciais, alinhando o primeiro caractere não branco de cada linha.

Dessa forma, todo o esforço de alinhamento abaixo é perdido em um piscar de olhos:

/*  0123456789012345
  0 Display 16x2     0
  1 Linha 2          1
    0123456789012345 */

O comentário acima acaba por se transformar em alguma dessas atrocidades:

/* 0123456789012345
   0 Display 16x2     0
   1 Linha 2          1
   0123456789012345 */

/* 0123456789012345
0 Display 16x2     0
1 Linha 2          1
0123456789012345 */

Colocando os asteriscos de alinhamento o problema deixa de existir.

/*
 *   0123456789012345
 * 0 Display 16x2     0
 * 1 Linha 2          1
 *   0123456789012345
 */

Considerações finais

Essas são as algumas das minhas preferências, ao menos as que eu acho mais importantes.

Todas essas já mudaram algumas vezes desde que comecei a programar e, com a experiência, são as que me parecem mais naturais e que facilitam a leitura do código.

Além disso, a maioria delas (exceto as sobre comentários), não me dão trabalho algum, pois utilizo um formatador automático. Assim posso focar no código em si e em como melhorá-lo para eliminar comentários.