{"id":1736,"date":"2026-09-23T13:30:34","date_gmt":"2026-09-23T16:30:34","guid":{"rendered":"https:\/\/www.linuxpro.com.br\/?p=1736"},"modified":"2026-09-23T13:50:43","modified_gmt":"2026-09-23T16:50:43","slug":"usuarios-linux-ansible-ssh-sudo-git","status":"publish","type":"post","link":"https:\/\/www.linuxpro.com.br\/en\/2026\/09\/usuarios-linux-ansible-ssh-sudo-git\/","title":{"rendered":"Linux users with Ansible: SSH keys, sudo and history in Git"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/usuarios-linux-ansible-git.webp\" alt=\"Mascote LinuxPro gira uma chave dourada na fechadura de um rack de servidores, com os logos do Ansible e do Git na parede e o cachorro caramelo deitado ao lado da mesa\" width=\"1486\" height=\"856\" \/><\/p>\n<p>No <a href=\"\/2026\/09\/primeiros-passos-com-ansible\/\">Primeiros passos com Ansible<\/a> voc\u00ea instalou o Ansible, montou o invent\u00e1rio, rodou comandos ad-hoc e escreveu o primeiro playbook. Este post parte dali e resolve um problema que todo administrador tem: <strong>dar e tirar acesso de pessoas aos servidores<\/strong>. Conta criada \u00e0 m\u00e3o em cada m\u00e1quina vira bagun\u00e7a r\u00e1pido \u2014 ningu\u00e9m sabe quem tem sudo, a chave SSH do estagi\u00e1rio que saiu continua l\u00e1 e o \u00fanico registro \u00e9 a mem\u00f3ria de algu\u00e9m. Aqui a lista de usu\u00e1rios vira c\u00f3digo: um arquivo YAML num reposit\u00f3rio Git, aplicado pelo Ansible. Criar, bloquear ou remover algu\u00e9m passa a ser um commit, com autor, data e revis\u00e3o, e o <code>git log<\/code> vira a auditoria.<\/p>\n<p>Tudo o que aparece abaixo foi executado de verdade num laborat\u00f3rio com dois servidores \u2014 <strong>Ubuntu 24.04<\/strong> e <strong>Debian 13<\/strong> \u2014 e uma m\u00e1quina de controle Ubuntu 24.04 com o Ansible do reposit\u00f3rio oficial da distribui\u00e7\u00e3o (<code>ansible<\/code> 9.2.0, <code>ansible-core<\/code> 2.16.3). As sa\u00eddas s\u00e3o reais.<\/p>\n<h2>O que vamos montar<\/h2>\n<p>O projeto tem um arquivo com a lista de pessoas (nome, chave p\u00fablica, grupos, sudo e <em>estado<\/em>), um playbook que transforma essa lista em contas nos servidores e um template para o sudo. A m\u00e1quina de controle s\u00f3 aplica o que est\u00e1 na branch principal; o que chega l\u00e1 passou por commit e, se voc\u00ea quiser, por revis\u00e3o.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/usuarios-linux-ansible-fluxo.webp\" alt=\"Diagrama: reposit\u00f3rio Git com a lista de usu\u00e1rios, revis\u00e3o, m\u00e1quina de controle rodando ansible-playbook e servidores Ubuntu e Debian recebendo conta, chave SSH e regra de sudo\" width=\"1200\" height=\"700\" \/><\/p>\n<p>Cada pessoa passa por tr\u00eas estados, sempre declarados no YAML:<\/p>\n<ul>\n<li><code>ativo<\/code> \u2014 conta criada, chave SSH instalada, grupos e sudo aplicados;<\/li>\n<li><code>bloqueado<\/code> \u2014 ningu\u00e9m entra mais, nem por chave, mas a conta e os arquivos ficam para an\u00e1lise;<\/li>\n<li><code>removido<\/code> \u2014 conta, grupo pessoal e home apagados.<\/li>\n<\/ul>\n<h2>A estrutura do projeto no Git<\/h2>\n<p>Na m\u00e1quina de controle, crie o diret\u00f3rio e o reposit\u00f3rio. A estrutura final \u00e9 esta:<\/p>\n<pre><code class=\"language-plaintext\">usuarios-linux\/\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 ansible.cfg\n\u251c\u2500\u2500 inventory.ini\n\u251c\u2500\u2500 usuarios.yml              # o playbook\n\u251c\u2500\u2500 templates\/\n\u2502   \u2514\u2500\u2500 sudoers.j2\n\u2514\u2500\u2500 group_vars\/\n    \u2514\u2500\u2500 all\/\n        \u251c\u2500\u2500 usuarios.yml      # a lista de pessoas\n        \u2514\u2500\u2500 vault.yml         # senhas, sempre cifradas<\/code><\/pre>\n<p>O <code>ansible.cfg<\/code> aponta o invent\u00e1rio, o arquivo com a senha do vault (fora do reposit\u00f3rio) e liga o <code>become<\/code>:<\/p>\n<pre><code class=\"language-ini\">[defaults]\ninventory = inventory.ini\nvault_password_file = ~\/.vault-pass-usuarios\nnocows = true\nretry_files_enabled = false\n\n[privilege_escalation]\nbecome = true<\/code><\/pre>\n<p>O invent\u00e1rio usa um usu\u00e1rio de automa\u00e7\u00e3o, <code>deploy<\/code>, com chave SSH e sudo sem senha \u2014 o mesmo esquema do post anterior. No laborat\u00f3rio os endere\u00e7os eram cont\u00eaineres; aqui ficam IPs de documenta\u00e7\u00e3o:<\/p>\n<pre><code class=\"language-ini\">[servidores]\nsrv-ubuntu ansible_host=192.0.2.10\nsrv-debian ansible_host=192.0.2.11\n\n[servidores:vars]\nansible_user=deploy<\/code><\/pre>\n<p>E o <code>.gitignore<\/code> existe para uma coisa s\u00f3: garantir que segredo n\u00e3o entra no hist\u00f3rico. Chave <strong>privada<\/strong> e senha em texto puro nunca v\u00e3o para o Git \u2014 o que vai \u00e9 chave <strong>p\u00fablica<\/strong> e senha cifrada com <code>ansible-vault<\/code>.<\/p>\n<pre><code class=\"language-plaintext\"># segredos e chaves privadas nunca entram no reposit\u00f3rio\n.vault-pass*\nid_*\n*.pem\n*.key\n*.retry<\/code><\/pre>\n<h2>A lista de usu\u00e1rios<\/h2>\n<p><code>group_vars\/all\/usuarios.yml<\/code> \u00e9 o arquivo que as pessoas v\u00e3o editar. Cada entrada tem nome, nome completo, uma lista de chaves p\u00fablicas, grupos extras e o estado. No primeiro commit a lista come\u00e7a vazia; este \u00e9 o estado depois de conceder acesso \u00e0 Ana e ao Bruno:<\/p>\n<pre><code class=\"language-yaml\">---\n# Contas de pessoas nos servidores. Cada mudan\u00e7a aqui \u00e9 um commit revisado.\n# estado: ativo | bloqueado | removido\n# Nunca apague uma entrada: mude o estado para \"removido\".\nusuarios:\n  - nome: ana\n    nome_completo: Ana Souza\n    chaves:\n      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFheyJLtuZlL6mZue2Ha2VlsOrCpQokkeAI+MQAbIozQ ana@notebook\n    grupos: [sudo]          # sudo pedindo a senha (hash no vault)\n    estado: ativo\n\n  - nome: bruno\n    nome_completo: Bruno Lima\n    chaves:\n      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKEvt3LKu4Ue9C2ewbKyC1F41f\/\/kWyOoGLZ5RXeMJEq bruno@notebook\n    grupos: [adm]           # l\u00ea os logs em \/var\/log\n    sudo_nopasswd: true     # sudo sem senha, via \/etc\/sudoers.d\n    estado: ativo<\/code><\/pre>\n<p>As chaves foram geradas com <code>ssh-keygen -t ed25519<\/code> no notebook de cada pessoa, que manda s\u00f3 o <code>.pub<\/code>. Se ainda n\u00e3o tem intimidade com chaves SSH, veja <a href=\"\/2017\/04\/ssh-sem-senha\/\">Usando chaves de autentica\u00e7\u00e3o no SSH<\/a>.<\/p>\n<h2>Duas formas de dar sudo<\/h2>\n<p>Um usu\u00e1rio que entra s\u00f3 por chave n\u00e3o tem senha \u2014 e o sudo padr\u00e3o pede a senha <em>do usu\u00e1rio<\/em>. H\u00e1 duas sa\u00eddas, e o projeto usa as duas para mostrar a diferen\u00e7a:<\/p>\n<ul>\n<li><strong>Ana: grupo <code>sudo<\/code>, com senha.<\/strong> Tanto o Ubuntu quanto o Debian j\u00e1 trazem <code>%sudo ALL=(ALL:ALL) ALL<\/code> em <code>\/etc\/sudoers<\/code>. A senha inicial vem de um hash guardado no vault; \u00e9 o modelo mais seguro, porque roubar a chave SSH n\u00e3o basta para virar root.<\/li>\n<li><strong>Bruno: <code>NOPASSWD<\/code> via <code>\/etc\/sudoers.d\/<\/code>.<\/strong> Pr\u00e1tico para automa\u00e7\u00e3o e plant\u00e3o, mas quem tiver a chave privada dele vira root sem nenhuma barreira extra. Use para poucas pessoas, com chave protegida por passphrase, e revise essa lista com frequ\u00eancia.<\/li>\n<\/ul>\n<p>O hash da senha da Ana \u00e9 gerado com <code>openssl passwd -6<\/code> e guardado cifrado:<\/p>\n<pre><code class=\"language-bash\">openssl passwd -6                       # digita a senha, recebe o hash $6$...\necho 'senha-longa-do-vault' &gt; ~\/.vault-pass-usuarios &amp;&amp; chmod 600 ~\/.vault-pass-usuarios\ncat &gt; group_vars\/all\/vault.yml &lt;&lt;'EOF'\n---\nvault_senhas:\n  ana: \"$6$...hash gerado acima...\"\nEOF\nansible-vault encrypt group_vars\/all\/vault.yml\nhead -2 group_vars\/all\/vault.yml<\/code><\/pre>\n<pre><code class=\"language-plaintext\">Encryption successful\n$ANSIBLE_VAULT;1.1;AES256\n30613166333535333030386164623937653739393238316439323662363831393935353039613739<\/code><\/pre>\n<p>O arquivo cifrado pode ir para o Git; a senha do vault, n\u00e3o. Para mais sobre o vault, veja <a href=\"\/2026\/09\/automatizando-servidores-linux-com-ansible\/\">Automatizando Servidores Linux com Ansible<\/a>.<\/p>\n<p>A regra de sudo sem senha sai de um template. Ele s\u00f3 lista quem est\u00e1 <code>ativo<\/code> e tem <code>sudo_nopasswd: true<\/code> \u2014 ent\u00e3o bloquear algu\u00e9m j\u00e1 tira o sudo dele:<\/p>\n<pre><code class=\"language-jinja2\"># Gerenciado pelo Ansible (reposit\u00f3rio usuarios-linux). N\u00e3o edite \u00e0 m\u00e3o.\n{% for u in usuarios if u.estado == 'ativo' and u.sudo_nopasswd | default(false) %}\n{{ u.nome }} ALL=(ALL:ALL) NOPASSWD: ALL\n{% endfor %}<\/code><\/pre>\n<h2>O playbook<\/h2>\n<p><code>usuarios.yml<\/code> separa a lista pelos tr\u00eas estados e trata cada um. Por baixo, o m\u00f3dulo <code>ansible.builtin.user<\/code> chama <code>useradd<\/code>, <code>usermod<\/code> e <code>userdel<\/code> \u2014 os mesmos comandos que voc\u00ea rodaria \u00e0 m\u00e3o, s\u00f3 que em todos os servidores e sempre do mesmo jeito.<\/p>\n<pre><code class=\"language-yaml\">---\n- name: Usu\u00e1rios Linux declarados no Git\n  hosts: servidores\n  become: true\n  vars:\n    ativos: \"{{ usuarios | selectattr('estado', 'eq', 'ativo') | list }}\"\n    bloqueados: \"{{ usuarios | selectattr('estado', 'eq', 'bloqueado') | list }}\"\n    removidos: \"{{ usuarios | selectattr('estado', 'eq', 'removido') | list }}\"\n\n  tasks:\n    - name: Grupo comum da equipe\n      ansible.builtin.group:\n        name: equipe\n        state: present\n\n    - name: Contas ativas\n      ansible.builtin.user:\n        name: \"{{ item.nome }}\"\n        comment: \"{{ item.nome_completo }}\"\n        shell: \/bin\/bash\n        groups: \"{{ ['equipe'] + item.grupos | default([]) }}\"\n        append: true\n        create_home: true\n        password: \"{{ vault_senhas[item.nome] | default(omit) }}\"\n        update_password: on_create\n        expires: -1\n      loop: \"{{ ativos }}\"\n      loop_control:\n        label: \"{{ item.nome }}\"\n\n    - name: Chaves SSH autorizadas (somente as do reposit\u00f3rio)\n      ansible.posix.authorized_key:\n        user: \"{{ item.nome }}\"\n        path: \"\/home\/{{ item.nome }}\/.ssh\/authorized_keys\"  # permite o --check antes de a conta existir\n        key: \"{{ item.chaves | join('\\n') }}\"\n        exclusive: true\n      loop: \"{{ ativos }}\"\n      loop_control:\n        label: \"{{ item.nome }}\"\n\n    - name: Regras de sudo sem senha\n      ansible.builtin.template:\n        src: sudoers.j2\n        dest: \/etc\/sudoers.d\/90-usuarios-ansible\n        owner: root\n        group: root\n        mode: \"0440\"\n        validate: \/usr\/sbin\/visudo -cf %s\n\n    - name: Contas bloqueadas - trava a senha e expira a conta\n      ansible.builtin.user:\n        name: \"{{ item.nome }}\"\n        password_lock: true\n        expires: 86400  # 1970-01-02: o PAM recusa at\u00e9 o login por chave\n      loop: \"{{ bloqueados }}\"\n      loop_control:\n        label: \"{{ item.nome }}\"\n\n    - name: Contas bloqueadas - remove as chaves SSH\n      ansible.builtin.file:\n        path: \"\/home\/{{ item.nome }}\/.ssh\/authorized_keys\"\n        state: absent\n      loop: \"{{ bloqueados }}\"\n      loop_control:\n        label: \"{{ item.nome }}\"\n\n    - name: Contas bloqueadas - encerra processos e sess\u00f5es abertas\n      ansible.builtin.command: pkill -KILL -u {{ item.nome }}\n      register: pkill\n      changed_when: pkill.rc == 0\n      failed_when: pkill.rc &gt; 1\n      loop: \"{{ bloqueados }}\"\n      loop_control:\n        label: \"{{ item.nome }}\"\n\n    - name: Contas removidas - apaga usu\u00e1rio e home\n      ansible.builtin.user:\n        name: \"{{ item.nome }}\"\n        state: absent\n        remove: true\n      loop: \"{{ removidos }}\"\n      loop_control:\n        label: \"{{ item.nome }}\"<\/code><\/pre>\n<p>Os detalhes que importam:<\/p>\n<ul>\n<li><code>append: true<\/code> acrescenta os grupos listados sem tirar a pessoa de grupos que ela j\u00e1 tenha \u2014 o equivalente a <code>usermod -aG<\/code>. O <code>-G<\/code> sem <code>-a<\/code> substitui a lista inteira; veja a pegadinha disso mais abaixo.<\/li>\n<li><code>update_password: on_create<\/code> grava a senha do vault s\u00f3 na cria\u00e7\u00e3o; depois a pessoa troca a pr\u00f3pria senha com <code>passwd<\/code> e o Ansible n\u00e3o sobrescreve.<\/li>\n<li><code>expires: -1<\/code> garante conta sem data de expira\u00e7\u00e3o \u2014 \u00e9 o que desfaz um bloqueio se algu\u00e9m voltar a <code>ativo<\/code>.<\/li>\n<li><code>ansible.posix.authorized_key<\/code> com <code>exclusive: true<\/code> deixa em <code>~\/.ssh\/authorized_keys<\/code> <strong>somente<\/strong> as chaves do reposit\u00f3rio. Chave acrescentada \u00e0 m\u00e3o some na pr\u00f3xima execu\u00e7\u00e3o. O m\u00f3dulo cria <code>~\/.ssh<\/code> com modo 700 e o arquivo com 600, como o sshd exige. A collection <code>ansible.posix<\/code> vem no pacote <code>ansible<\/code> (no Ubuntu 24.04, vers\u00e3o 1.5.4); com s\u00f3 o <code>ansible-core<\/code>, instale-a com <code>ansible-galaxy collection install ansible.posix<\/code>.<\/li>\n<li>O <code>path<\/code> expl\u00edcito no <code>authorized_key<\/code> existe por um motivo pr\u00e1tico: sem ele, o <code>--check<\/code> falha para usu\u00e1rios que ainda n\u00e3o existem, com <em>\u201cEither user must exist or you must provide full path to key file in check mode\u201d<\/em>.<\/li>\n<li><code>validate: \/usr\/sbin\/visudo -cf %s<\/code> testa o arquivo de sudo <strong>antes<\/strong> de instal\u00e1-lo. Um erro de sintaxe em <code>\/etc\/sudoers.d\/<\/code> pode travar o sudo da m\u00e1quina inteira; com a valida\u00e7\u00e3o, o Ansible falha e o arquivo antigo continua l\u00e1.<\/li>\n<\/ul>\n<p>Antes do primeiro commit, rode o <a href=\"https:\/\/ansible.readthedocs.io\/projects\/lint\/\">ansible-lint<\/a> (pacote <code>ansible-lint<\/code> no Ubuntu 24.04, vers\u00e3o 6.17.2):<\/p>\n<pre><code class=\"language-bash\">ansible-lint<\/code><\/pre>\n<pre><code class=\"language-plaintext\">Passed: 0 failure(s), 0 warning(s) on 7 files. Last profile that met the validation criteria was 'production'.<\/code><\/pre>\n<h2>Primeiro commit e primeira aplica\u00e7\u00e3o<\/h2>\n<pre><code class=\"language-bash\">git init -b main\ngit add -A\ngit commit -m \"Estrutura inicial: invent\u00e1rio, playbook e lista vazia de usu\u00e1rios\"\n# ... edita group_vars\/all\/usuarios.yml e cria o vault.yml ...\ngit add -A\ngit commit -m \"Concede acesso a Ana (sudo com senha) e Bruno (sudo sem senha)\"<\/code><\/pre>\n<p>Antes de mexer em qualquer servidor, veja o que vai mudar com <code>--check --diff<\/code>:<\/p>\n<pre><code class=\"language-bash\">ansible-playbook usuarios.yml --check --diff --limit srv-ubuntu<\/code><\/pre>\n<pre><code class=\"language-plaintext\">PLAY [Usu\u00e1rios Linux declarados no Git] ****************************************\nTASK [Gathering Facts] *********************************************************\nok: [srv-ubuntu]\nTASK [Grupo comum da equipe] ***************************************************\nchanged: [srv-ubuntu]\nTASK [Contas ativas] ***********************************************************\nchanged: [srv-ubuntu] =&gt; (item=ana)\nchanged: [srv-ubuntu] =&gt; (item=bruno)\nTASK [Chaves SSH autorizadas (somente as do reposit\u00f3rio)] **********************\n--- before: \/home\/ana\/.ssh\/authorized_keys\n+++ after: \/home\/ana\/.ssh\/authorized_keys\n@@ -0,0 +1 @@\n+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFheyJLtuZlL6mZue2Ha2VlsOrCpQokkeAI+MQAbIozQ ana@notebook\nchanged: [srv-ubuntu] =&gt; (item=ana)\n--- before: \/home\/bruno\/.ssh\/authorized_keys\n+++ after: \/home\/bruno\/.ssh\/authorized_keys\n@@ -0,0 +1 @@\n+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKEvt3LKu4Ue9C2ewbKyC1F41f\/\/kWyOoGLZ5RXeMJEq bruno@notebook\nchanged: [srv-ubuntu] =&gt; (item=bruno)\nTASK [Regras de sudo sem senha] ************************************************\n--- before\n+++ after: \/root\/.ansible\/tmp\/ansible-local-369ok2z5l9m\/tmp9vnvv456\/sudoers.j2\n@@ -0,0 +1,2 @@\n+# Gerenciado pelo Ansible (reposit\u00f3rio usuarios-linux). N\u00e3o edite \u00e0 m\u00e3o.\n+bruno ALL=(ALL:ALL) NOPASSWD: ALL\nchanged: [srv-ubuntu]\nTASK [Contas bloqueadas - trava a senha e expira a conta] **********************\nTASK [Contas bloqueadas - remove as chaves SSH] ********************************\nTASK [Contas bloqueadas - encerra processos e sess\u00f5es abertas] *****************\nTASK [Contas removidas - apaga usu\u00e1rio e home] *********************************\nPLAY RECAP *********************************************************************\nsrv-ubuntu                 : ok=5    changed=4    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0<\/code><\/pre>\n<p>O diff mostra exatamente as chaves e a linha de sudo que v\u00e3o entrar. Tudo certo, aplique de verdade nos dois servidores:<\/p>\n<pre><code class=\"language-bash\">ansible-playbook usuarios.yml<\/code><\/pre>\n<pre><code class=\"language-plaintext\">PLAY [Usu\u00e1rios Linux declarados no Git] ****************************************\nTASK [Gathering Facts] *********************************************************\nok: [srv-ubuntu]\nok: [srv-debian]\nTASK [Grupo comum da equipe] ***************************************************\nchanged: [srv-debian]\nchanged: [srv-ubuntu]\nTASK [Contas ativas] ***********************************************************\nchanged: [srv-debian] =&gt; (item=ana)\nchanged: [srv-ubuntu] =&gt; (item=ana)\nchanged: [srv-debian] =&gt; (item=bruno)\nchanged: [srv-ubuntu] =&gt; (item=bruno)\nTASK [Chaves SSH autorizadas (somente as do reposit\u00f3rio)] **********************\nchanged: [srv-debian] =&gt; (item=ana)\nchanged: [srv-ubuntu] =&gt; (item=ana)\nchanged: [srv-debian] =&gt; (item=bruno)\nchanged: [srv-ubuntu] =&gt; (item=bruno)\nTASK [Regras de sudo sem senha] ************************************************\nchanged: [srv-debian]\nchanged: [srv-ubuntu]\nPLAY RECAP *********************************************************************\nsrv-debian                 : ok=5    changed=4    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0   \nsrv-ubuntu                 : ok=5    changed=4    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0<\/code><\/pre>\n<p>E rode de novo. Um playbook bem escrito \u00e9 <strong>idempotente<\/strong>: a segunda execu\u00e7\u00e3o n\u00e3o muda nada.<\/p>\n<pre><code class=\"language-plaintext\">PLAY RECAP *********************************************************************\nsrv-debian                 : ok=5    changed=0    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0\nsrv-ubuntu                 : ok=5    changed=0    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0<\/code><\/pre>\n<h2>Conferindo nos servidores<\/h2>\n<p>Os comandos cl\u00e1ssicos continuam valendo para verificar o que o Ansible fez: <code>getent<\/code> l\u00ea <code>\/etc\/passwd<\/code> e <code>\/etc\/group<\/code>, <code>id<\/code> mostra os grupos e <code>chage -l<\/code> mostra expira\u00e7\u00e3o.<\/p>\n<pre><code class=\"language-bash\">getent passwd ana bruno\nid ana; id bruno\ngetent group equipe sudo\ncat \/etc\/sudoers.d\/90-usuarios-ansible\nchage -l bruno | head -4<\/code><\/pre>\n<pre><code class=\"language-plaintext\">== ubuntu2404\nana:x:1002:1003:Ana Souza:\/home\/ana:\/bin\/bash\nbruno:x:1003:1004:Bruno Lima:\/home\/bruno:\/bin\/bash\nuid=1002(ana) gid=1003(ana) groups=1003(ana),27(sudo),1002(equipe)\nuid=1003(bruno) gid=1004(bruno) groups=1004(bruno),4(adm),1002(equipe)\nequipe:x:1002:ana,bruno\nsudo:x:27:ubuntu,ana\n# Gerenciado pelo Ansible (reposit\u00f3rio usuarios-linux). N\u00e3o edite \u00e0 m\u00e3o.\nbruno ALL=(ALL:ALL) NOPASSWD: ALL\n-r--r----- 1 root root 111 Sep 23 16:16 \/etc\/sudoers.d\/90-usuarios-ansible\nLast password change\t\t\t\t\t: Sep 23, 2026\nPassword expires\t\t\t\t\t: never\nPassword inactive\t\t\t\t\t: never\nAccount expires\t\t\t\t\t\t: never\n== debian13\nana:x:1001:1002:Ana Souza:\/home\/ana:\/bin\/bash\nbruno:x:1002:1003:Bruno Lima:\/home\/bruno:\/bin\/bash\nuid=1001(ana) gid=1002(ana) groups=1002(ana),27(sudo),1001(equipe)\nuid=1002(bruno) gid=1003(bruno) groups=1003(bruno),4(adm),1001(equipe)<\/code><\/pre>\n<p>Duas diferen\u00e7as entre as distribui\u00e7\u00f5es apareceram aqui:<\/p>\n<ul>\n<li><strong>UIDs diferentes<\/strong> \u2014 a imagem do Ubuntu 24.04 j\u00e1 tinha o usu\u00e1rio <code>ubuntu<\/code> com UID 1000, ent\u00e3o a Ana ficou com 1002 l\u00e1 e 1001 no Debian. N\u00e3o importa para login e sudo, mas importa se os servidores compartilham arquivos por NFS ou restauram backups entre si. Nesse caso, fixe <code>uid:<\/code> na entrada de cada pessoa.<\/li>\n<li><strong>Permiss\u00e3o do home<\/strong> \u2014 <code>HOME_MODE<\/code> em <code>\/etc\/login.defs<\/code> \u00e9 <code>0750<\/code> no Ubuntu 24.04 e <code>0700<\/code> no Debian 13. Outro detalhe do Debian 13: <code>ENCRYPT_METHOD<\/code> \u00e9 <code>YESCRYPT<\/code>, contra <code>SHA512<\/code> no Ubuntu; o hash <code>$6$<\/code> (SHA-512) do vault funciona nos dois.<\/li>\n<\/ul>\n<h2>Testando o login sem senha e o sudo<\/h2>\n<p>Do \u201cnotebook\u201d de cada pessoa (outro cont\u00eainer no laborat\u00f3rio), com a chave privada dela:<\/p>\n<pre><code class=\"language-bash\">ssh -i ~\/chaves\/ana ana@srv-ubuntu 'whoami; sudo -n true; sudo -S -p \"\" id -un &lt;&lt;&lt; \"a-senha-da-ana\"'\nssh -i ~\/chaves\/bruno bruno@srv-ubuntu 'whoami; sudo -n id -un'\nssh -i ~\/chaves\/ana bruno@srv-ubuntu true      # chave da Ana na conta do Bruno<\/code><\/pre>\n<pre><code class=\"language-plaintext\">ana\nsudo: a password is required\nroot\nbruno\nroot\nbruno@users-lab-ubuntu2404: Permission denied (publickey,password).<\/code><\/pre>\n<p>A Ana entrou sem senha e o sudo dela pediu a senha, como esperado; o Bruno virou root sem senha; e a chave de uma pessoa n\u00e3o abre a conta da outra. O resultado foi id\u00eantico no Debian 13. Com todos entrando por chave, o pr\u00f3ximo passo natural \u00e9 desligar <code>PasswordAuthentication<\/code> no sshd.<\/p>\n<h2>Nova pessoa: branch, revis\u00e3o e merge<\/h2>\n<p>Com o reposit\u00f3rio num servidor Git (<a href=\"\/2026\/09\/gitea-git-self-hosted-runner-tea\/\">Gitea<\/a>, GitLab ou GitHub), dar acesso vira um pull request: algu\u00e9m prop\u00f5e, outra pessoa revisa, e s\u00f3 o que entra na <code>main<\/code> \u00e9 aplicado. Aqui a Ana pede a conta da Carla, que vai ler logs no plant\u00e3o mas n\u00e3o tem sudo:<\/p>\n<pre><code class=\"language-bash\">git switch -c acesso-carla\ncat &gt;&gt; group_vars\/all\/usuarios.yml &lt;&lt;'EOF'\n\n  - nome: carla\n    nome_completo: Carla Mendes\n    chaves:\n      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBVS+xmSmHpXvCptaasmycC0QDTcPDfQ6UaLpnNx9+ai carla@notebook\n    grupos: [adm]           # s\u00f3 leitura de logs, sem sudo\n    estado: ativo\nEOF\ngit commit -am \"Cria conta da Carla (plant\u00e3o, leitura de logs)\"   # autora: Ana Souza\n# revis\u00e3o aprovada:\ngit switch main\ngit merge --no-ff -m \"Merge: acesso da Carla (revisado por Nilton)\" acesso-carla\ngit log --oneline --graph<\/code><\/pre>\n<pre><code class=\"language-plaintext\">*   7749b7e Merge: acesso da Carla (revisado por Nilton)\n|\\\n| * b49a9f8 Cria conta da Carla (plant\u00e3o, leitura de logs)\n|\/\n* 87268a9 Concede acesso a Ana (sudo com senha) e Bruno (sudo sem senha)\n* 16c8739 Estrutura inicial: invent\u00e1rio, playbook e lista vazia de usu\u00e1rios<\/code><\/pre>\n<p>Para testar o <code>exclusive: true<\/code>, antes de aplicar algu\u00e9m acrescentou \u00e0 m\u00e3o uma chave \u201cesquecida\u201d no <code>authorized_keys<\/code> do Bruno no Ubuntu. A execu\u00e7\u00e3o com <code>--diff<\/code> criou a Carla e removeu a chave intrusa:<\/p>\n<pre><code class=\"language-plaintext\">TASK [Chaves SSH autorizadas (somente as do reposit\u00f3rio)] **********************\nok: [srv-debian] =&gt; (item=ana)\nok: [srv-ubuntu] =&gt; (item=ana)\nok: [srv-debian] =&gt; (item=bruno)\n--- before: \/home\/bruno\/.ssh\/authorized_keys\n+++ after: \/home\/bruno\/.ssh\/authorized_keys\n@@ -1,2 +1 @@\n ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKEvt3LKu4Ue9C2ewbKyC1F41f\/\/kWyOoGLZ5RXeMJEq bruno@notebook\n-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHnDTe6LIwLxY8m0dTCmmv2L1d0cNQ9DxbhGnBgd7W8z intruso@fora-do-git\nchanged: [srv-ubuntu] =&gt; (item=bruno)\n--- before: \/home\/carla\/.ssh\/authorized_keys\n+++ after: \/home\/carla\/.ssh\/authorized_keys\n@@ -0,0 +1 @@\n+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBVS+xmSmHpXvCptaasmycC0QDTcPDfQ6UaLpnNx9+ai carla@notebook\nchanged: [srv-debian] =&gt; (item=carla)<\/code><\/pre>\n<p>A Carla entra, l\u00ea <code>\/var\/log<\/code> pelo grupo <code>adm<\/code> e n\u00e3o tem sudo:<\/p>\n<pre><code class=\"language-plaintext\">uid=1003(carla) gid=1004(carla) groups=1004(carla),4(adm),1001(equipe)\nsudo: a password is required<\/code><\/pre>\n<h2>Bloquear antes de remover<\/h2>\n<p>Quando algu\u00e9m sai, a tenta\u00e7\u00e3o \u00e9 apagar a conta na hora. Melhor bloquear primeiro: a pessoa perde o acesso imediatamente, mas o home, os crontabs e os arquivos continuam l\u00e1 para voc\u00ea conferir o que precisa ser transferido. S\u00f3 depois vem a remo\u00e7\u00e3o.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/usuarios-linux-ansible-ciclo.webp\" alt=\"Diagrama: ciclo de vida do usu\u00e1rio no c\u00f3digo \u2014 adicionar, aplicar, bloquear e remover \u2014 com os comandos executados e um commit Git em cada passo\" width=\"1200\" height=\"640\" \/><\/p>\n<h3>O <code>usermod -L<\/code> n\u00e3o bloqueia login por chave<\/h3>\n<p>Esse \u00e9 o erro mais comum. <code>usermod -L<\/code> (e <code>passwd -l<\/code>, e o <code>password_lock: true<\/code> do Ansible) s\u00f3 p\u00f5e um <code>!<\/code> na frente do hash em <code>\/etc\/shadow<\/code>: invalida a <strong>senha<\/strong>. Login por chave n\u00e3o usa senha. Testei cada m\u00e9todo isolado, nos dois servidores, com uma conta que s\u00f3 tinha chave SSH:<\/p>\n<pre><code class=\"language-plaintext\"># senha j\u00e1 \u00e9 \"!\" (conta criada sem senha): chave funciona\nteste:!:20719:0:99999:7:::           -&gt; login OK (Ubuntu e Debian)\n# usermod -L teste\nteste:!:20719:0:99999:7:::           -&gt; login OK (Ubuntu e Debian)\n# usermod -e 1 teste   (expira em 1970-01-02)\nteste:!:20719:0:99999:7::1:          -&gt; Connection closed (Ubuntu e Debian)\n# usermod -s \/usr\/sbin\/nologin teste\n                                      -&gt; \"This account is currently not available.\"<\/code><\/pre>\n<p>Com a expira\u00e7\u00e3o, o servidor registra o motivo \u2014 \u00e9 o m\u00f3dulo <code>pam_unix<\/code>, na fase <em>account<\/em>, que recusa mesmo depois de a chave ter sido aceita:<\/p>\n<pre><code class=\"language-plaintext\">sshd[307]: pam_unix(sshd:account): account teste has expired (account expired)\nsshd[307]: fatal: Access denied for user teste by PAM account configuration [preauth]<\/code><\/pre>\n<p>Isso vale porque Ubuntu e Debian usam <code>UsePAM yes<\/code> no sshd, e o PAM checa a data de expira\u00e7\u00e3o em qualquer m\u00e9todo de autentica\u00e7\u00e3o. A pr\u00f3pria p\u00e1gina de manual do <a href=\"https:\/\/man7.org\/linux\/man-pages\/man8\/usermod.8.html\">usermod<\/a> avisa: para travar a conta, e n\u00e3o s\u00f3 a senha, defina tamb\u00e9m a expira\u00e7\u00e3o como 1. E o shell <code>nologin<\/code> sozinho tamb\u00e9m n\u00e3o basta: ele recusa o shell interativo, mas a conex\u00e3o continua aberta para t\u00faneis \u2014 um <code>ssh -N<\/code> com a conta em <code>nologin<\/code> ficou conectado at\u00e9 eu mat\u00e1-lo.<\/p>\n<p>Por isso o estado <code>bloqueado<\/code> do playbook faz quatro coisas: trava a senha, expira a conta em 1970-01-02 (<code>expires: 86400<\/code>, um dia depois da \u00e9poca Unix), apaga o <code>authorized_keys<\/code> e mata os processos da pessoa. O template de sudo deixa de listar quem n\u00e3o est\u00e1 <code>ativo<\/code>.<\/p>\n<h3>Bloqueando o Bruno com uma sess\u00e3o aberta<\/h3>\n<p>Com o Bruno conectado no Ubuntu rodando um <code>sleep 900<\/code>:<\/p>\n<pre><code class=\"language-bash\">git diff<\/code><\/pre>\n<pre><code class=\"language-diff\">@@ -16,7 +16,7 @@ usuarios:\n       - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKEvt3LKu4Ue9C2ewbKyC1F41f\/\/kWyOoGLZ5RXeMJEq bruno@notebook\n     grupos: [adm]           # l\u00ea os logs em \/var\/log\n     sudo_nopasswd: true     # sudo sem senha, via \/etc\/sudoers.d\n-    estado: ativo\n+    estado: bloqueado      # saiu da equipe em 2026-09-23<\/code><\/pre>\n<pre><code class=\"language-bash\">git commit -am \"Bloqueia Bruno: saiu da equipe\"\nansible-playbook usuarios.yml --diff<\/code><\/pre>\n<pre><code class=\"language-plaintext\">TASK [Regras de sudo sem senha] ************************************************\n--- before: \/etc\/sudoers.d\/90-usuarios-ansible\n+++ after: \/root\/.ansible\/tmp\/ansible-local-107075lrwl0p\/tmpq25ijl7j\/sudoers.j2\n@@ -1,2 +1 @@\n # Gerenciado pelo Ansible (reposit\u00f3rio usuarios-linux). N\u00e3o edite \u00e0 m\u00e3o.\n-bruno ALL=(ALL:ALL) NOPASSWD: ALL\nchanged: [srv-debian]\n--- before: \/etc\/sudoers.d\/90-usuarios-ansible\n+++ after: \/root\/.ansible\/tmp\/ansible-local-107075lrwl0p\/tmpd2p_rq9_\/sudoers.j2\n@@ -1,2 +1 @@\n # Gerenciado pelo Ansible (reposit\u00f3rio usuarios-linux). N\u00e3o edite \u00e0 m\u00e3o.\n-bruno ALL=(ALL:ALL) NOPASSWD: ALL\nchanged: [srv-ubuntu]\nTASK [Contas bloqueadas - trava a senha e expira a conta] **********************\nchanged: [srv-debian] =&gt; (item=bruno)\nchanged: [srv-ubuntu] =&gt; (item=bruno)\nTASK [Contas bloqueadas - remove as chaves SSH] ********************************\n--- before\n+++ after\n@@ -1,4 +1,4 @@\n {\n     \"path\": \"\/home\/bruno\/.ssh\/authorized_keys\",\n-    \"state\": \"file\"\n+    \"state\": \"absent\"\n }\nchanged: [srv-debian] =&gt; (item=bruno)\n--- before\n+++ after\n@@ -1,4 +1,4 @@\n {\n     \"path\": \"\/home\/bruno\/.ssh\/authorized_keys\",\n-    \"state\": \"file\"\n+    \"state\": \"absent\"\n }\nchanged: [srv-ubuntu] =&gt; (item=bruno)\nTASK [Contas bloqueadas - encerra processos e sess\u00f5es abertas] *****************\nchanged: [srv-ubuntu] =&gt; (item=bruno)\nTASK [Contas removidas - apaga usu\u00e1rio e home] *********************************\nPLAY RECAP *********************************************************************\nsrv-debian                 : ok=8    changed=3    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   \nsrv-ubuntu                 : ok=8    changed=4    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0<\/code><\/pre>\n<p>No notebook do Bruno, a sess\u00e3o caiu na hora:<\/p>\n<pre><code class=\"language-plaintext\">Connection to users-lab-ubuntu2404 closed by remote host.<\/code><\/pre>\n<p>E, para provar que a expira\u00e7\u00e3o segura o acesso mesmo que algu\u00e9m devolva a chave \u00e0 m\u00e3o, recoloquei o <code>authorized_keys<\/code> dele e tentei de novo:<\/p>\n<pre><code class=\"language-plaintext\">bruno:!:20719:0:99999:7::1:\nAccount expires\t\t\t\t\t\t: Jan 02, 1970\nYour account has expired; please contact your system administrator.\nConnection closed by 192.168.48.2 port 22<\/code><\/pre>\n<p>Mesmo resultado no Debian 13. Antes de remover, procure o que o Bruno deixou fora do home \u2014 arquivos em <code>\/srv<\/code>, <code>\/var\/www<\/code>, crontab em <code>\/var\/spool\/cron\/crontabs<\/code>:<\/p>\n<pre><code class=\"language-bash\">ansible servidores -m ansible.builtin.command -a \"find \/ -xdev -user bruno -not -path '\/home\/bruno*'\"\nansible servidores -m ansible.builtin.command -a \"crontab -l -u bruno\"<\/code><\/pre>\n<p>Aqui um <code>FAILED<\/code> \u00e9 a resposta boa: o <code>crontab -l<\/code> sai com c\u00f3digo 1 e a mensagem <em>\u201cno crontab for bruno\u201d<\/em> quando n\u00e3o h\u00e1 crontab. No laborat\u00f3rio n\u00e3o havia nada. Fa\u00e7a tamb\u00e9m o backup do home, se ele tiver algo de valor.<\/p>\n<h2>Remover a conta<\/h2>\n<p>A remo\u00e7\u00e3o \u00e9 outro commit \u2014 e aqui a regra de ouro do projeto: <strong>n\u00e3o apague a entrada do YAML<\/strong>, mude o estado para <code>removido<\/code>.<\/p>\n<pre><code class=\"language-bash\">sed -i 's\/estado: bloqueado      # saiu\/estado: removido       # saiu\/' group_vars\/all\/usuarios.yml\ngit commit -am \"Remove a conta do Bruno (bloqueada desde 2026-09-23)\"\nansible-playbook usuarios.yml<\/code><\/pre>\n<pre><code class=\"language-plaintext\">TASK [Contas removidas - apaga usu\u00e1rio e home] *********************************\nchanged: [srv-debian] =&gt; (item=bruno)\nchanged: [srv-ubuntu] =&gt; (item=bruno)\n\nPLAY RECAP *********************************************************************\nsrv-debian                 : ok=6    changed=1    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0\nsrv-ubuntu                 : ok=6    changed=1    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0<\/code><\/pre>\n<p>O <code>state: absent<\/code> com <code>remove: true<\/code> equivale a <code>userdel -r<\/code>: apaga a conta, o grupo pessoal e o home. Nos dois servidores, o Bruno sumiu de todos os arquivos:<\/p>\n<pre><code class=\"language-plaintext\">id: 'bruno': no such user\nls: cannot access '\/home\/bruno': No such file or directory\n\/etc\/passwd:0\n\/etc\/shadow:0\n\/etc\/group:0\n\/etc\/gshadow:0\n\/etc\/sudoers.d\/90-usuarios-ansible:0<\/code><\/pre>\n<p>Se ainda houver processo do usu\u00e1rio rodando, o <code>userdel<\/code> se recusa a apagar a conta e o Ansible falha nessa tarefa com <em>\u201cuserdel: user \u2026 is currently used by process \u2026\u201d<\/em> (testei) \u2014 mais um motivo para passar pelo bloqueio, que mata os processos antes. Arquivos que ficarem fora do home passam a pertencer a um UID \u201c\u00f3rf\u00e3o\u201d, que o pr\u00f3ximo usu\u00e1rio criado pode herdar; da\u00ed a busca com <code>find -user<\/code> antes da remo\u00e7\u00e3o.<\/p>\n<p>Por que n\u00e3o apagar a entrada? Porque o Ansible s\u00f3 gerencia o que est\u00e1 na lista. Testei tirando a Carla do YAML e rodando o playbook: <code>changed=0<\/code>, e ela continuou entrando por SSH normalmente. Apagar a linha n\u00e3o remove ningu\u00e9m \u2014 s\u00f3 faz o Ansible esquecer que a conta existe.<\/p>\n<h2>Auditoria com o git log<\/h2>\n<p>Agora a pergunta \u201cquem deu acesso a quem, e quando?\u201d tem resposta em segundos:<\/p>\n<pre><code class=\"language-bash\">git log --format=\"%h %ad %an  %s\" --date=short<\/code><\/pre>\n<pre><code class=\"language-plaintext\">491d64d 2026-09-23 Nilton  Remove a conta do Bruno (bloqueada desde 2026-09-23)\n500ce95 2026-09-23 Nilton  Bloqueia Bruno: saiu da equipe\n7749b7e 2026-09-23 Nilton  Merge: acesso da Carla (revisado por Nilton)\nb49a9f8 2026-09-23 Ana Souza  Cria conta da Carla (plant\u00e3o, leitura de logs)\n87268a9 2026-09-23 Nilton  Concede acesso a Ana (sudo com senha) e Bruno (sudo sem senha)\n16c8739 2026-09-23 Nilton  Estrutura inicial: invent\u00e1rio, playbook e lista vazia de usu\u00e1rios<\/code><\/pre>\n<p>O hist\u00f3rico completo de uma pessoa, com o diff de cada mudan\u00e7a de estado:<\/p>\n<pre><code class=\"language-bash\">git log -p -G\"estado: (bloqueado|removido)\" -- group_vars\/all\/usuarios.yml<\/code><\/pre>\n<pre><code class=\"language-plaintext\">491d64d Nilton 2026-09-23 16:17:56 +0000\n    Remove a conta do Bruno (bloqueada desde 2026-09-23)\n...\n-    estado: bloqueado      # saiu da equipe em 2026-09-23\n+    estado: removido       # saiu da equipe em 2026-09-23\n...\n500ce95 Nilton 2026-09-23 16:17:28 +0000\n    Bloqueia Bruno: saiu da equipe\n...\n-    estado: ativo\n+    estado: bloqueado      # saiu da equipe em 2026-09-23<\/code><\/pre>\n<p>Quem escreveu cada linha da conta da Carla:<\/p>\n<pre><code class=\"language-bash\">git blame --date=short -L \"\/nome: carla\/,+6\" group_vars\/all\/usuarios.yml<\/code><\/pre>\n<pre><code class=\"language-plaintext\">b49a9f8d (Ana Souza 2026-09-23 21)   - nome: carla\nb49a9f8d (Ana Souza 2026-09-23 22)     nome_completo: Carla Mendes\nb49a9f8d (Ana Souza 2026-09-23 23)     chaves:\nb49a9f8d (Ana Souza 2026-09-23 24)       - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBVS+xmSmHpXvCptaasmycC0QDTcPDfQ6UaLpnNx9+ai carla@notebook\nb49a9f8d (Ana Souza 2026-09-23 25)     grupos: [adm]           # s\u00f3 leitura de logs, sem sudo\nb49a9f8d (Ana Souza 2026-09-23 26)     estado: ativo<\/code><\/pre>\n<p>E quem concedeu sudo sem senha, em qualquer ponto da hist\u00f3ria \u2014 o <code>-S<\/code> procura commits que adicionaram ou removeram o texto:<\/p>\n<pre><code class=\"language-bash\">git log -S\"sudo_nopasswd: true\" --format=\"%h %ad %an  %s\" --date=short -- group_vars\/all\/usuarios.yml<\/code><\/pre>\n<pre><code class=\"language-plaintext\">87268a9 2026-09-23 Nilton  Concede acesso a Ana (sudo com senha) e Bruno (sudo sem senha)<\/code><\/pre>\n<p>O autor de um commit \u00e9 o que a pessoa configurou no Git, ent\u00e3o vale o quanto voc\u00ea confia nele. Para auditoria s\u00e9ria, exija commits assinados (<code>git commit -S<\/code>) e proteja a branch <code>main<\/code> no servidor Git, aceitando s\u00f3 merge revisado.<\/p>\n<p>O Git n\u00e3o substitui o log do servidor, ele o complementa. O <code>useradd<\/code>, o <code>usermod<\/code> e o <code>userdel<\/code> registram cada opera\u00e7\u00e3o no journal, e o sudo registra o que o usu\u00e1rio <code>deploy<\/code> do Ansible executou:<\/p>\n<pre><code class=\"language-bash\">journalctl -t useradd -t usermod -t userdel | grep bruno<\/code><\/pre>\n<pre><code class=\"language-plaintext\">useradd[926]: new user: name=bruno, UID=1003, GID=1004, home=\/home\/bruno, shell=\/bin\/bash, from=\/dev\/pts\/1\nusermod[2253]: change user 'bruno' expiration from 'never' to '1970-01-02'\nuserdel[2901]: delete user 'bruno'\nuserdel[2901]: removed group 'bruno' owned by 'bruno'\nuserdel[2901]: removed shadow group 'bruno' owned by 'bruno'<\/code><\/pre>\n<p>O hor\u00e1rio do journal casa com o commit: d\u00e1 para amarrar cada mudan\u00e7a no servidor ao commit que a pediu.<\/p>\n<h2>Desfazendo um engano com git revert<\/h2>\n<p>Bloqueou a pessoa errada? O conserto \u00e9 o mesmo fluxo ao contr\u00e1rio. No laborat\u00f3rio, bloqueei a Carla de prop\u00f3sito, confirmei que o login dela passou a falhar e desfiz:<\/p>\n<pre><code class=\"language-bash\">git commit -am \"Bloqueia Carla\"\nansible-playbook usuarios.yml        # Carla: Permission denied (publickey,password)\ngit revert --no-edit HEAD\nansible-playbook usuarios.yml --diff<\/code><\/pre>\n<pre><code class=\"language-plaintext\">[main 456bd14] Revert \"Bloqueia Carla\"\n...\nTASK [Contas ativas] ***********************************************************\nchanged: [srv-ubuntu] =&gt; (item=carla)\nTASK [Chaves SSH autorizadas (somente as do reposit\u00f3rio)] **********************\n--- before: \/home\/carla\/.ssh\/authorized_keys\n+++ after: \/home\/carla\/.ssh\/authorized_keys\n@@ -0,0 +1 @@\n+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBVS+xmSmHpXvCptaasmycC0QDTcPDfQ6UaLpnNx9+ai carla@notebook\nchanged: [srv-ubuntu] =&gt; (item=carla)<\/code><\/pre>\n<p>A conta voltou a n\u00e3o expirar, a chave voltou e a Carla entrou de novo \u2014 e o engano e a corre\u00e7\u00e3o ficaram, os dois, no hist\u00f3rico. Um limite: para quem tem senha (como a Ana), o bloqueio tamb\u00e9m trava a senha com <code>!<\/code>, e o revert n\u00e3o destrava; nesse caso rode <code>usermod -U<\/code> ou defina uma senha nova. O playbook n\u00e3o usa <code>password_lock: false<\/code> nas contas ativas porque, em conta s\u00f3 com chave, o <code>usermod -U<\/code> se recusa (\u201cunlocking the user&#8217;s password would result in a passwordless account\u201d) e o Ansible marcaria <code>changed<\/code> em toda execu\u00e7\u00e3o.<\/p>\n<h2>Pegadinha: o append n\u00e3o tira ningu\u00e9m de um grupo<\/h2>\n<p><code>append: true<\/code> protege grupos que outras roles adicionaram (o <code>docker<\/code>, por exemplo), mas tem um custo: tirar <code>sudo<\/code> da lista de grupos da Ana <strong>n\u00e3o<\/strong> tira a Ana do grupo. Testei: com <code>grupos: []<\/code>, o playbook deu <code>changed=0<\/code> e o <code>id ana<\/code> continuou mostrando <code>27(sudo)<\/code>. Para revogar, voc\u00ea tem duas sa\u00eddas:<\/p>\n<ul>\n<li>remover o grupo explicitamente, uma vez: <code>ansible servidores -m ansible.builtin.user -a \"name=ana groups=ana,equipe append=false\"<\/code> (testado: a Ana saiu do <code>sudo<\/code>);<\/li>\n<li>ou trocar para <code>append: false<\/code> no playbook, se o YAML for a \u00fanica fonte de verdade para os grupos das pessoas \u2014 a\u00ed a lista passa a ser exata, e qualquer grupo acrescentado fora dela some.<\/li>\n<\/ul>\n<p>\u00c9 o mesmo perigo do <code>usermod -G<\/code> sem <code>-a<\/code> no terminal, s\u00f3 que ao contr\u00e1rio: l\u00e1 voc\u00ea tira grupos sem querer; aqui voc\u00ea acha que tirou e n\u00e3o tirou.<\/p>\n<h2>C\u00f3digo completo<\/h2>\n<p>O projeto virou um reposit\u00f3rio no GitHub, pronto para clonar: <a href=\"https:\/\/github.com\/jniltinho\/ansible-linux-users\">jniltinho\/ansible-linux-users<\/a>. L\u00e1 o c\u00f3digo est\u00e1 organizado numa role reutiliz\u00e1vel, <code>linux_users<\/code>, com valida\u00e7\u00e3o da lista antes de qualquer mudan\u00e7a, testes de integra\u00e7\u00e3o no Ubuntu 24.04 e no Debian 13 e CI no GitHub Actions. Para quem quer s\u00f3 os arquivos deste post, eles tamb\u00e9m est\u00e3o num <a href=\"https:\/\/gist.github.com\/jniltinho\/0ef642a14d81a315d2f47f1953b44af0\">Gist<\/a>.<\/p>\n<p>O c\u00f3digo do reposit\u00f3rio e do Gist est\u00e1 em ingl\u00eas, com os mesmos passos deste post: <code>usuarios<\/code> vira <code>linux_users_accounts<\/code> (no Gist, <code>users<\/code>), <code>estado<\/code> vira <code>state<\/code>, e os estados <code>ativo<\/code>, <code>bloqueado<\/code> e <code>removido<\/code> viram <code>active<\/code>, <code>locked<\/code> e <code>removed<\/code>. As chaves de exemplo s\u00e3o p\u00fablicas e foram geradas s\u00f3 para o laborat\u00f3rio, os IPs s\u00e3o de documenta\u00e7\u00e3o, e o arquivo do vault aparece como exemplo em texto puro: cifre o seu antes do primeiro commit.<\/p>\n<h2>Refer\u00eancias<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/builtin\/user_module.html\">M\u00f3dulo ansible.builtin.user<\/a><\/li>\n<li><a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/posix\/authorized_key_module.html\">M\u00f3dulo ansible.posix.authorized_key<\/a><\/li>\n<li><a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/builtin\/template_module.html\">M\u00f3dulo ansible.builtin.template (par\u00e2metro validate)<\/a><\/li>\n<li><a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/vault_guide\/index.html\">Guia do Ansible Vault<\/a><\/li>\n<li><a href=\"https:\/\/man7.org\/linux\/man-pages\/man8\/usermod.8.html\">usermod(8)<\/a> e <a href=\"https:\/\/man7.org\/linux\/man-pages\/man5\/shadow.5.html\">shadow(5)<\/a><\/li>\n<\/ul>\n<h2>Conclus\u00e3o<\/h2>\n<p>Com a lista de usu\u00e1rios no Git, o acesso aos servidores deixa de depender de mem\u00f3ria: cada conta tem um commit que explica por que ela existe, cada sudo tem um autor e cada sa\u00edda tem data. O Ansible garante que os servidores batem com o reposit\u00f3rio \u2014 inclusive apagando a chave que algu\u00e9m colocou \u00e0 m\u00e3o. Duas li\u00e7\u00f5es do laborat\u00f3rio para levar: bloqueie antes de remover, e lembre que <code>usermod -L<\/code> trava a senha, n\u00e3o a conta; quem barra a chave SSH \u00e9 a expira\u00e7\u00e3o. Para os fundamentos, volte ao <a href=\"\/2026\/09\/primeiros-passos-com-ansible\/\">Primeiros passos com Ansible<\/a>; para vault, roles e casos maiores, siga para <a href=\"\/2026\/09\/automatizando-servidores-linux-com-ansible\/\">Automatizando Servidores Linux com Ansible<\/a>. E, se o Git ainda \u00e9 novidade, comece por <a href=\"\/2017\/04\/git-simples-e-rapido\/\">Git simples e r\u00e1pido<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Create, lock and remove Linux users with Ansible: passwordless SSH key, sudo via group or sudoers.d, and every change as a Git commit for auditing. Tested on Ubuntu 24.04 and Debian 13.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[11,46,2,120,3],"tags":[121,122,48,479,506,507,508],"class_list":["post-1736","post","type-post","status-publish","format-standard","hentry","category-debian","category-devops","category-linux","category-servidores","category-ubuntu","tag-ansible","tag-automacao","tag-git","tag-seguranca","tag-ssh","tag-sudo","tag-usuarios"],"_links":{"self":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1736","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/comments?post=1736"}],"version-history":[{"count":3,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1736\/revisions"}],"predecessor-version":[{"id":1741,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1736\/revisions\/1741"}],"wp:attachment":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/media?parent=1736"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/categories?post=1736"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/tags?post=1736"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}