Users, SSH keys and secure defaults

Create users and groups, manage authorized keys, write sudoers rules safely, set permissions and sysctl, and keep Vault secrets out of the logs.

Users, groups and keys

- name: Create the application group
  ansible.builtin.group:
    name: app
    gid: 1500
    state: present

- name: Create the deploy user
  ansible.builtin.user:
    name: deploy
    uid: 1501
    group: app
    groups: [sudo]
    append: true                 # add to these groups, do not replace membership
    shell: /bin/bash
    create_home: true
    password: "{{ deploy_password | password_hash('sha512') }}"
    update_password: on_create    # do not reset a password the user has changed
    state: present

- name: Install the public keys that may log in
  ansible.posix.authorized_key:
    user: deploy
    key: "{{ lookup('file', 'keys/' + item + '.pub') }}"
    state: present
    exclusive: true              # remove keys not in this list
  loop:
    - alice
    - bob

- name: Remove a departed user's access without deleting their files
  ansible.builtin.user:
    name: "{{ item }}"
    state: absent
    remove: false
  loop: "{{ users_to_remove }}"
  • exclusive: true on authorized_key removes any key not in the list, which is how you actually revoke access rather than accumulating keys.
  • update_password: on_create stops a run from resetting a password a user has since changed.
  • Generate the password hash with the password_hash filter and a per-host salt, or the same password produces the same hash everywhere and becomes guessable from a stolen file.
  • Deleting a user without remove: true keeps their files and their uid ownership, which is usually what you want for auditing.

Sudoers and hardening

- name: Grant a narrow sudo command, validated before it lands
  ansible.builtin.copy:
    dest: /etc/sudoers.d/50-app-deploy
    content: |
      %app ALL=(root) NOPASSWD: /bin/systemctl restart app
      deploy ALL=(root) NOPASSWD: /usr/bin/journalctl -u app *
    owner: root
    group: root
    mode: "0440"
    validate: /usr/sbin/visudo -cf %s

- name: Harden SSH through a drop-in rather than editing the main file
  ansible.builtin.copy:
    dest: /etc/ssh/sshd_config.d/10-hardening.conf
    content: |
      PermitRootLogin no
      PasswordAuthentication no
      KbdInteractiveAuthentication no
      X11Forwarding no
      MaxAuthTries 3
    mode: "0644"
    validate: /usr/sbin/sshd -t -f %s
  notify: Reload sshd

- name: Kernel parameters for a database host
  ansible.posix.sysctl:
    name: "{{ item.name }}"
    value: "{{ item.value }}"
    sysctl_file: /etc/sysctl.d/90-app.conf
    reload: true
    state: present
  loop:
    - { name: vm.swappiness, value: "10" }
    - { name: net.core.somaxconn, value: "2048" }
FileSafe edit methodValidator
/etc/sudoersA new file in sudoers.dvisudo -cf %s
/etc/ssh/sshd_configA drop-in in sshd_config.dsshd -t -f %s
/etc/sysctl.confA file in sysctl.dThe module's own reload
/etc/fstabNever with lineinfilemount -a before rebooting
authorized_keysThe authorized_key modulen/a

A drop-in directory is the difference between a change you can revert by deleting a file and a change you must reconstruct by hand. On any modern systemd distribution, prefer it over editing the vendor file.

Secrets without leaks

- name: Write a secret to a file without it appearing in the output
  ansible.builtin.copy:
    content: "{{ vault_app_db_password }}"
    dest: /etc/app/db.password
    owner: app
    group: app
    mode: "0600"
  no_log: true                     # keeps the task output and the diff out of the log

- name: Use a secret in a command without exposing it in the process list
  ansible.builtin.shell:
    cmd: |
      set -o pipefail
      PGPASSWORD="$(cat /etc/app/db.password)" psql -h db.internal -c 'SELECT 1'
    executable: /bin/bash
  no_log: true
  changed_when: false
⚠️
no_log: true hides the output but a failed task still reports the module arguments in some callbacks, and a secret in a cmd string is visible in the process list while it runs. Prefer passing the secret through a file or an environment variable read from a file.

FAQ

How do I revoke a user's SSH access?
Remove their key from the list and rely on exclusive: true, or set their account to state: absent. Removing the key from the repository is not revocation until a play runs against the host.
Is <code>no_log</code> enough to protect a secret?
It prevents the value appearing in normal output, but not in a debugger, a profile task listing, or the target's process table. Keep the secret in a file with restrictive permissions and read it at the point of use.

Managing files, packages and services Testing automation: lint, check mode and Molecule

Last refreshed 2026-09-18.