Merge branch 'master' into role-sshd
This commit is contained in:
@@ -26,5 +26,6 @@
|
||||
- [sshd](roles/sshd/README.md)
|
||||
- [swapfile](roles/swapfile/README.md)
|
||||
- [ufw](roles/ufw/README.md)
|
||||
- [users](roles/users/README.md)
|
||||
- [wg_hub](roles/wg_hub/README.md)
|
||||
- [wg_spoke](roles/wg_spoke/README.md)
|
||||
- [wg_spoke](roles/wg_spoke/README.md)
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
ansible-core==2.18.11
|
||||
passlib==1.7.4
|
||||
|
||||
61
roles/users/README.md
Normal file
61
roles/users/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
ansible-users
|
||||
=========
|
||||
|
||||
Create user accounts according to a YAML manifest.
|
||||
|
||||
Controls:
|
||||
- username and UID
|
||||
- groupname and GID
|
||||
- homedir creation
|
||||
- GECOS field (typically full user name)
|
||||
- shell
|
||||
- SSH public key deployment to `.ssh/authorized_keys`
|
||||
- account expiration
|
||||
- memory, swap and CPU allocation limits via user slices
|
||||
|
||||
Check out [corvus-migratorius/ansible-disk-quotas](https://github.com/corvus-migratorius/ansible-disk-quotas) for controlling non-root filesystem quotas.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Target node:
|
||||
|
||||
- systemd
|
||||
- openssh
|
||||
|
||||
Controller:
|
||||
|
||||
- `passlib` (for working with user passwords)
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
`manifest_path`: a YAML file containing user definitions (see `molecule/default/users.yml` for an example)
|
||||
|
||||
`users`: a list of objects mirroring the YAML structure expected by `manifest_path`; takes precedence over it
|
||||
|
||||
`common_memory_max`: e.g. `"500M"`, optional
|
||||
|
||||
`common_swap_max`: e.g. `"2G"`, optional
|
||||
|
||||
`common_cpu_quota`: e.g. `"100%"`, optional
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
Example Playbook
|
||||
----------------
|
||||
|
||||
- Configuring users in a YAML manifest: [converge.yml](molecule/default/converge.yml)
|
||||
- Configuring user list as a variable: [converge.yml](molecule/users_from_var/converge.yml)
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
BSD
|
||||
|
||||
Author Information
|
||||
------------------
|
||||
|
||||
corvus-migratorius@proton.me
|
||||
masayganova@gmail.com
|
||||
2
roles/users/defaults/main.yml
Normal file
2
roles/users/defaults/main.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
users_default_home_root: /home
|
||||
1
roles/users/handlers/main.yml
Normal file
1
roles/users/handlers/main.yml
Normal file
@@ -0,0 +1 @@
|
||||
---
|
||||
16
roles/users/meta/main.yml
Normal file
16
roles/users/meta/main.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
galaxy_info:
|
||||
role_name: users
|
||||
namespace: genlab
|
||||
author: Alexander Gorelyshev
|
||||
company: Genlab, LLC
|
||||
description: Configure user accounts
|
||||
license: GPL-2.0-or-later
|
||||
min_ansible_version: "2.1"
|
||||
|
||||
platforms:
|
||||
- name: Ubuntu
|
||||
versions: ["jammy", "noble"]
|
||||
|
||||
galaxy_tags: []
|
||||
|
||||
dependencies: []
|
||||
22
roles/users/molecule/default/configuration/users.yml
Normal file
22
roles/users/molecule/default/configuration/users.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
users:
|
||||
- name: "user0"
|
||||
uid: 1001
|
||||
gid: 1004
|
||||
full_name: "User Zero"
|
||||
state: "present"
|
||||
groups: [sudo]
|
||||
create_home: true
|
||||
home_root: "/tmp/someplace"
|
||||
shell: "/bin/bash"
|
||||
pubkeys_file: "user0.keys"
|
||||
|
||||
- name: "user1"
|
||||
uid: 1002
|
||||
full_name: "User One"
|
||||
state: "present"
|
||||
groups: []
|
||||
create_home: true
|
||||
shell: "/bin/sh"
|
||||
expires: "2024-07-23 12:00:00"
|
||||
generate_ssh_key: true
|
||||
10
roles/users/molecule/default/converge.yml
Normal file
10
roles/users/molecule/default/converge.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Converge
|
||||
hosts: all
|
||||
|
||||
roles:
|
||||
- role: genlab.common.users
|
||||
manifest_path: "configuration/users.yml"
|
||||
common_memory_max: "500M"
|
||||
common_swap_max: "2G"
|
||||
common_cpu_quota: "100%"
|
||||
27
roles/users/molecule/default/molecule.yml
Normal file
27
roles/users/molecule/default/molecule.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
dependency:
|
||||
name: galaxy
|
||||
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ubuntu
|
||||
image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest
|
||||
pre_build_image: true
|
||||
command: ${MOLECULE_DOCKER_COMMAND:-""}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
|
||||
lint: |
|
||||
set -e
|
||||
yamllint .
|
||||
ansible-lint .
|
||||
1
roles/users/molecule/default/user0.keys
Normal file
1
roles/users/molecule/default/user0.keys
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMh9Y+wR4LH8lWJjJXqHn76kSoTRujkab+PYwD3IReFh user0@hostname
|
||||
64
roles/users/molecule/default/verify.yml
Normal file
64
roles/users/molecule/default/verify.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
gather_facts: true
|
||||
any_errors_fatal: true
|
||||
|
||||
tasks:
|
||||
- name: "Get user info from /etc/passwd"
|
||||
register: users_etc_passwd
|
||||
changed_when: false
|
||||
ansible.builtin.shell:
|
||||
cmd: set -o pipefail; cat /etc/passwd | grep user
|
||||
executable: /bin/bash
|
||||
|
||||
- name: "Get user info from /etc/shadow"
|
||||
register: users_etc_shadow
|
||||
changed_when: false
|
||||
ansible.builtin.shell:
|
||||
cmd: set -o pipefail; cat /etc/shadow | grep user
|
||||
executable: /bin/bash
|
||||
|
||||
# using a hack since here the date of last password change == date of account creation
|
||||
- name: "Verify expected account configuration"
|
||||
vars:
|
||||
expected_shadow:
|
||||
- "user0:!:{{ (ansible_date_time.epoch | int) // 86400 }}:0:99999:7:::"
|
||||
- "user1:!:{{ (ansible_date_time.epoch | int) // 86400 }}:0:99999:7::19927:"
|
||||
expected_passwd:
|
||||
- "user0:x:1001:1004:User Zero:/tmp/someplace/user0:/bin/bash"
|
||||
- "user1:x:1002:1002:User One:/home/user1:/bin/sh"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- users_etc_passwd.stdout_lines == expected_passwd
|
||||
- users_etc_shadow.stdout_lines == expected_shadow
|
||||
|
||||
- name: "Get SSH key contents for user1"
|
||||
register: users_user1_ssh_key
|
||||
changed_when: false
|
||||
ansible.builtin.command:
|
||||
cmd: cat /home/user1/.ssh/id_ed25519.pub
|
||||
|
||||
- name: "Verify the SSH key contents for user1"
|
||||
ansible.builtin.assert:
|
||||
that: "'user1@ubuntu' in users_user1_ssh_key.stdout"
|
||||
|
||||
- name: "Get the user slice drop-in for user0"
|
||||
register: users_user0_slice_dropin
|
||||
changed_when: false
|
||||
ansible.builtin.command:
|
||||
cmd: cat /etc/systemd/system/user-1001.slice.d/50-limits.conf
|
||||
|
||||
- name: "Verify drop-in contens"
|
||||
vars:
|
||||
expected:
|
||||
- "[Slice]"
|
||||
- "MemoryAccounting=1"
|
||||
- "MemoryMax=500M"
|
||||
- "MemorySwapMax=2G"
|
||||
- "CPUAccounting=1"
|
||||
- "CPUQuota=100%"
|
||||
ansible.builtin.assert:
|
||||
that: "users_user0_slice_dropin.stdout_lines == expected"
|
||||
success_msg: "Got the expected limits slice drop-in"
|
||||
fail_msg: "Unexpected limits slice drop-in contents"
|
||||
31
roles/users/molecule/users_from_var/converge.yml
Normal file
31
roles/users/molecule/users_from_var/converge.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
- name: Converge
|
||||
hosts: all
|
||||
|
||||
roles:
|
||||
- role: genlab.common.users
|
||||
manifest_path: "some-none-existent-file" # the 'users' variable is supposed to take precedence
|
||||
common_memory_max: "500M"
|
||||
common_swap_max: "2G"
|
||||
common_cpu_quota: "100%"
|
||||
users:
|
||||
- name: "user0"
|
||||
uid: 1001
|
||||
gid: 1004
|
||||
full_name: "User Zero"
|
||||
state: "present"
|
||||
groups: [sudo]
|
||||
create_home: true
|
||||
home_root: "/tmp/someplace"
|
||||
shell: "/bin/bash"
|
||||
pubkeys_file: "user0.keys"
|
||||
|
||||
- name: "user1"
|
||||
uid: 1002
|
||||
full_name: "User One"
|
||||
state: "present"
|
||||
groups: []
|
||||
create_home: true
|
||||
shell: "/bin/sh"
|
||||
expires: "2024-07-23 12:00:00"
|
||||
generate_ssh_key: true
|
||||
27
roles/users/molecule/users_from_var/molecule.yml
Normal file
27
roles/users/molecule/users_from_var/molecule.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
dependency:
|
||||
name: galaxy
|
||||
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ubuntu
|
||||
image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest
|
||||
pre_build_image: true
|
||||
command: ${MOLECULE_DOCKER_COMMAND:-""}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
|
||||
lint: |
|
||||
set -e
|
||||
yamllint .
|
||||
ansible-lint .
|
||||
1
roles/users/molecule/users_from_var/user0.keys
Normal file
1
roles/users/molecule/users_from_var/user0.keys
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMh9Y+wR4LH8lWJjJXqHn76kSoTRujkab+PYwD3IReFh user0@hostname
|
||||
64
roles/users/molecule/users_from_var/verify.yml
Normal file
64
roles/users/molecule/users_from_var/verify.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
gather_facts: true
|
||||
any_errors_fatal: true
|
||||
|
||||
tasks:
|
||||
- name: "Get user info from /etc/passwd"
|
||||
register: users_etc_passwd
|
||||
changed_when: false
|
||||
ansible.builtin.shell:
|
||||
cmd: set -o pipefail; cat /etc/passwd | grep user
|
||||
executable: /bin/bash
|
||||
|
||||
- name: "Get user info from /etc/shadow"
|
||||
register: users_etc_shadow
|
||||
changed_when: false
|
||||
ansible.builtin.shell:
|
||||
cmd: set -o pipefail; cat /etc/shadow | grep user
|
||||
executable: /bin/bash
|
||||
|
||||
# using a hack since here the date of last password change == date of account creation
|
||||
- name: "Verify expected account configuration"
|
||||
vars:
|
||||
expected_shadow:
|
||||
- "user0:!:{{ (ansible_date_time.epoch | int) // 86400 }}:0:99999:7:::"
|
||||
- "user1:!:{{ (ansible_date_time.epoch | int) // 86400 }}:0:99999:7::19927:"
|
||||
expected_passwd:
|
||||
- "user0:x:1001:1004:User Zero:/tmp/someplace/user0:/bin/bash"
|
||||
- "user1:x:1002:1002:User One:/home/user1:/bin/sh"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- users_etc_passwd.stdout_lines == expected_passwd
|
||||
- users_etc_shadow.stdout_lines == expected_shadow
|
||||
|
||||
- name: "Get SSH key contents for user1"
|
||||
register: users_user1_ssh_key
|
||||
changed_when: false
|
||||
ansible.builtin.command:
|
||||
cmd: cat /home/user1/.ssh/id_ed25519.pub
|
||||
|
||||
- name: "Verify the SSH key contents for user1"
|
||||
ansible.builtin.assert:
|
||||
that: "'user1@ubuntu' in users_user1_ssh_key.stdout"
|
||||
|
||||
- name: "Get the user slice drop-in for user0"
|
||||
register: users_user0_slice_dropin
|
||||
changed_when: false
|
||||
ansible.builtin.command:
|
||||
cmd: cat /etc/systemd/system/user-1001.slice.d/50-limits.conf
|
||||
|
||||
- name: "Verify drop-in contens"
|
||||
vars:
|
||||
expected:
|
||||
- "[Slice]"
|
||||
- "MemoryAccounting=1"
|
||||
- "MemoryMax=500M"
|
||||
- "MemorySwapMax=2G"
|
||||
- "CPUAccounting=1"
|
||||
- "CPUQuota=100%"
|
||||
ansible.builtin.assert:
|
||||
that: "users_user0_slice_dropin.stdout_lines == expected"
|
||||
success_msg: "Got the expected limits slice drop-in"
|
||||
fail_msg: "Unexpected limits slice drop-in contents"
|
||||
106
roles/users/tasks/create.yml
Normal file
106
roles/users/tasks/create.yml
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
- name: "Create | Get current user's group entity info ({{ user.name }})"
|
||||
failed_when: false
|
||||
ansible.builtin.getent:
|
||||
database: group
|
||||
key: "{{ user.name }}"
|
||||
split: ":"
|
||||
|
||||
- name: "Create | Create user group ('{{ user.name }}')" # to handle cases where GID!=UID
|
||||
when:
|
||||
- getent_group[user.name] is not defined
|
||||
ansible.builtin.group:
|
||||
name: "{{ user.name }}"
|
||||
gid: "{{ user.gid is defined | ternary(user.gid, user.uid) }}"
|
||||
state: "{{ user.state }}"
|
||||
|
||||
- name: "Create | Create user account ('{{ user.name }}')"
|
||||
vars:
|
||||
home_root: "{{ user.home_root is defined | ternary(user.home_root, users_default_home_root) }}"
|
||||
ansible.builtin.user:
|
||||
name: "{{ user.name }}"
|
||||
state: "{{ user.state }}"
|
||||
uid: "{{ user.uid }}"
|
||||
group: "{{ user.name }}"
|
||||
create_home: "{{ user.create_home | default(true) }}"
|
||||
home: "{{ home_root }}/{{ user.name }}"
|
||||
comment: "{{ user.full_name }}"
|
||||
shell: "{{ user.shell | default('/bin/bash') }}"
|
||||
groups: "{{ user.groups | default([]) }}"
|
||||
append: false
|
||||
generate_ssh_key: "{{ user.generate_ssh_key | default(false) }}"
|
||||
ssh_key_type: "{{ user.ssh_key_type | default('ed25519') }}"
|
||||
ssh_key_comment: "{{ user.name }}@{{ ansible_nodename }}"
|
||||
# password_lock: "{{ user.password_lock | default('false') }}"
|
||||
|
||||
- name: "Create | Set user account expiration date where defined ('{{ user.name }}')"
|
||||
when: user.expires is defined
|
||||
ansible.builtin.user:
|
||||
name: "{{ user.name }}"
|
||||
state: "{{ user.state }}"
|
||||
expires: "{{ (user.expires | to_datetime).strftime('%s') }}"
|
||||
|
||||
- name: "Create | Ensure no user account expiration date where undefined ('{{ user.name }}')"
|
||||
when: user.expires is undefined
|
||||
ansible.builtin.user:
|
||||
name: "{{ user.name }}"
|
||||
state: "{{ user.state }}"
|
||||
expires: "-1"
|
||||
|
||||
- name: "Create | Deploy SSH public key to 'authorized_keys' files ('{{ user.name }}')"
|
||||
when: (user.pubkeys_file is defined) and (user.pubkeys_file != "")
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ user.name }}"
|
||||
key: "{{ lookup('file', user.pubkeys_file) }}"
|
||||
key_options: "{{ user.options | default('') }}"
|
||||
exclusive: true
|
||||
|
||||
- name: "Create | Create a systemd slice directory ('{{ user.name }}')"
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/user-{{ user.uid }}.slice.d
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0750'
|
||||
|
||||
# Set user's maximum memory limit to 'memory_limit' defined in the user manifest_path.
|
||||
# If 'memory_limit' is undefined there, use 'common_memory_max' as fallback.
|
||||
# If 'common_memory_max' is also undefined, set to "", which should be ignored by the template.
|
||||
# - 'memory_max:' may use postfix like K, M, G.
|
||||
# - 'cpu_quota': "100%" for 1 full core.
|
||||
# REF https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
|
||||
# REF https://serverfault.com/a/1092803
|
||||
- name: "Create | Create/update a systemd slice limits config ('{{ user.name }}')"
|
||||
when: user.uid is defined
|
||||
register: users_limit_state
|
||||
vars:
|
||||
memory_max: "{{ (user.memory_max is defined and user.memory_max != '') | ternary(user.memory_max, common_memory_max) | default('') }}"
|
||||
swap_max: "{{ (user.swap_max is defined and user.swap_max != '') | ternary(user.swap_max, common_swap_max) | default('') }}"
|
||||
cpu_quota: "{{ (user.cpu_quota is defined and user.cpu_quota != '') | ternary(user.cpu_quota, common_cpu_quota) | default('') }}"
|
||||
ansible.builtin.template:
|
||||
src: slice.j2
|
||||
dest: "/etc/systemd/system/user-{{ user.uid }}.slice.d/50-limits.conf"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0750'
|
||||
|
||||
- name: "Create | Enable user systemd service ('{{ user.name }}')" # noqa: no-handler
|
||||
when: users_limit_state.changed
|
||||
ansible.builtin.systemd_service:
|
||||
name: user@{{ user.uid }}.service
|
||||
enabled: true
|
||||
|
||||
# User service cannot be restarted if:
|
||||
# - the account is expired (we check for that)
|
||||
# - password change was enforced
|
||||
# Sometimes restarting fails with obscure 'status=219/CGROUP', but works fine after a retry
|
||||
# Didn't debug this yet, sorry ;(
|
||||
- name: "Restart user service (systemd limits) ('{{ user.name }}')"
|
||||
when:
|
||||
- users_limit_state.changed
|
||||
- user.expires is undefined or (user.expires | to_datetime).strftime('%s') > now(fmt='%s')
|
||||
retries: 3
|
||||
ansible.builtin.systemd_service:
|
||||
name: user@{{ user.uid }}.service
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
14
roles/users/tasks/main.yml
Normal file
14
roles/users/tasks/main.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: "Include OpenSSH client installation tasks"
|
||||
ansible.builtin.include_tasks: openssh.yml
|
||||
|
||||
- name: "Include user definition vars"
|
||||
when: (manifest_path is defined) and (manifest_path != "") and users is not defined
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ manifest_path }}"
|
||||
|
||||
- name: "Create user accounts"
|
||||
loop: "{{ users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
ansible.builtin.include_tasks: create.yml
|
||||
14
roles/users/tasks/openssh.yml
Normal file
14
roles/users/tasks/openssh.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: "OpenSSH | Ensure openssh client tools are installed"
|
||||
when: ansible_os_family == "Debian"
|
||||
ansible.builtin.apt:
|
||||
name: openssh-client
|
||||
state: present
|
||||
cache_valid_time: 3000
|
||||
|
||||
- name: "OpenSSH | Ensure openssh client tools are installed"
|
||||
when: ansible_os_family == "RedHat"
|
||||
ansible.builtin.dnf:
|
||||
name: openssh-clients
|
||||
state: present
|
||||
update_cache: true
|
||||
12
roles/users/templates/slice.j2
Normal file
12
roles/users/templates/slice.j2
Normal file
@@ -0,0 +1,12 @@
|
||||
[Slice]
|
||||
{% if memory_max is defined and memory_max != "" %}
|
||||
MemoryAccounting=1
|
||||
MemoryMax={{ memory_max }}
|
||||
MemorySwapMax={{ swap_max }}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% if cpu_quota is defined and cpu_quota != "" %}
|
||||
CPUAccounting=1
|
||||
CPUQuota={{ cpu_quota }}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
1
roles/users/vars/main.yml
Normal file
1
roles/users/vars/main.yml
Normal file
@@ -0,0 +1 @@
|
||||
---
|
||||
Reference in New Issue
Block a user