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: trueonauthorized_keyremoves any key not in the list, which is how you actually revoke access rather than accumulating keys.update_password: on_createstops a run from resetting a password a user has since changed.- Generate the password hash with the
password_hashfilter 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: truekeeps 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" }| File | Safe edit method | Validator |
|---|---|---|
/etc/sudoers | A new file in sudoers.d | visudo -cf %s |
/etc/ssh/sshd_config | A drop-in in sshd_config.d | sshd -t -f %s |
/etc/sysctl.conf | A file in sysctl.d | The module's own reload |
/etc/fstab | Never with lineinfile | mount -a before rebooting |
authorized_keys | The authorized_key module | n/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.
Related
Managing files, packages and services Testing automation: lint, check mode and Molecule
Last refreshed 2026-09-18.