diff --git a/README.md b/README.md index f383011..5876f77 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,24 @@ - [borgmatic](roles/borgmatic/README.md) - [curl_scheduled](roles/curl_scheduled/README.md) - [dnsmasq](roles/dnsmasq/README.md) +- [docker_ubuntu](roles/docker_ubuntu/README.md) - [grafana](roles/grafana/README.md) - [ipmi_exporter](roles/ipmi_exporter/README.md) - [karma](roles/karma/README.md) - [loki](roles/loki/README.md) +- [mkfs](roles/mkfs/README.md) - [mount_device](roles/mount_device/README.md) - [nginx](roles/nginx/README.md) +- [node_exporter](roles/node_exporter/README.md) - [prometheus](roles/prometheus/README.md) +- [promtail](roles/promtail/README.md) - [rclone_yandex](roles/rclone_yandex/README.md) - [rustdesk](roles/rustdesk/README.md) - [sftp_share](roles/sftp_share/README.md) - [smartctl_exporter](roles/smartctl_exporter/README.md) +- [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) \ No newline at end of file +- [wg_spoke](roles/wg_spoke/README.md) diff --git a/ci/requirements.txt b/ci/requirements.txt index 456b2b7..fcd67d5 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1 +1,2 @@ ansible-core==2.18.11 +passlib==1.7.4 diff --git a/galaxy.yml b/galaxy.yml index a760250..dd3e469 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: genlab name: common -version: 0.18.0 +version: 0.25.0 readme: README.md authors: - Alexander Gorelyshev (corvus-migratorius@proton.me) diff --git a/roles/docker_ubuntu/README.md b/roles/docker_ubuntu/README.md new file mode 100644 index 0000000..8760978 --- /dev/null +++ b/roles/docker_ubuntu/README.md @@ -0,0 +1,41 @@ +docker_ubuntu +========= + +Install Docker ecosystem on Ubuntu. + +[![lint](https://github.com/corvus-migratorius/ansible-docker-ubuntu/actions/workflows/lint.yaml/badge.svg)](https://github.com/corvus-migratorius/ansible-docker-ubuntu/actions/workflows/lint.yaml) + +[![molecule](https://github.com/corvus-migratorius/ansible-docker-ubuntu/actions/workflows/molecule.yaml/badge.svg)](https://github.com/corvus-migratorius/ansible-docker-ubuntu/actions/workflows/molecule.yaml) + +Requirements +------------ + +None + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.common.docker_ubuntu +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/docker_ubuntu/defaults/main.yml b/roles/docker_ubuntu/defaults/main.yml new file mode 100644 index 0000000..85de886 --- /dev/null +++ b/roles/docker_ubuntu/defaults/main.yml @@ -0,0 +1,5 @@ +--- +docker_ubuntu_version: "27.3.1" +docker_ubuntu_registry_mirrors: + - https://docker.io + - https://mirror.gcr.io diff --git a/roles/docker_ubuntu/handlers/main.yml b/roles/docker_ubuntu/handlers/main.yml new file mode 100644 index 0000000..73b8719 --- /dev/null +++ b/roles/docker_ubuntu/handlers/main.yml @@ -0,0 +1,11 @@ +--- +# handlers file for playbooks/roles/docker_ubuntu +- name: "Restart Docker daemon" + ansible.builtin.systemd_service: + name: docker + state: restarted + daemon_reload: true + +- name: "Sanity check" # noqa: no-changed-when + ansible.builtin.command: + cmd: "docker run --rm --pull=always hello-world" diff --git a/roles/docker_ubuntu/meta/main.yml b/roles/docker_ubuntu/meta/main.yml new file mode 100644 index 0000000..f6eca81 --- /dev/null +++ b/roles/docker_ubuntu/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: docker_ubuntu + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "Install Docker ecosystem on Ubuntu" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: ["docker"] + +dependencies: [] diff --git a/roles/docker_ubuntu/molecule/default/converge.yml b/roles/docker_ubuntu/molecule/default/converge.yml new file mode 100644 index 0000000..d3e24f2 --- /dev/null +++ b/roles/docker_ubuntu/molecule/default/converge.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.docker_ubuntu + docker_ubuntu_version: "27.3.1" + docker_ubuntu_registry_mirrors: + - https://docker.io + - https://mirror.gcr.io diff --git a/roles/docker_ubuntu/molecule/default/molecule.yml b/roles/docker_ubuntu/molecule/default/molecule.yml new file mode 100644 index 0000000..fd2f06d --- /dev/null +++ b/roles/docker_ubuntu/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-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 . diff --git a/roles/docker_ubuntu/molecule/default/verify.yml b/roles/docker_ubuntu/molecule/default/verify.yml new file mode 100644 index 0000000..a384184 --- /dev/null +++ b/roles/docker_ubuntu/molecule/default/verify.yml @@ -0,0 +1,31 @@ +--- +- name: Verify + hosts: all + gather_facts: true + any_errors_fatal: true + + tasks: + # https://github.com/ansible/molecule/issues/3587#issuecomment-1158650179 + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Sanity check Docker by running a `hello-world` container" # noqa: no-changed-when + ansible.builtin.command: + cmd: "docker run --rm hello-world" + + - name: "Check if `Compose` is available from PATH" # noqa: no-changed-when + ansible.builtin.command: + cmd: "docker compose version" + + - name: "Get docker version" # noqa: no-changed-when + register: docker_ubuntu_current_version + ansible.builtin.command: + cmd: "docker version --format '{{ '{{' }} .Server.Version {{ '}}' }}'" + + - name: "Check if Docker has the expected version" # noqa: no-changed-when + ansible.builtin.assert: + that: "docker_ubuntu_version in docker_ubuntu_current_version.stdout" + success_msg: "Docker has the expected version ({{ docker_ubuntu_current_version.stdout }} )" + fail_msg: "Unexpected Docker version ({{ docker_ubuntu_current_version.stdout }})" diff --git a/roles/docker_ubuntu/tasks/main.yml b/roles/docker_ubuntu/tasks/main.yml new file mode 100644 index 0000000..ec14880 --- /dev/null +++ b/roles/docker_ubuntu/tasks/main.yml @@ -0,0 +1,71 @@ +--- +# REF: https://www.digitalocean.com/community/tutorials/how-to-use-ansible-to-install-and-set-up-docker-on-ubuntu-20-04 + +- name: "Install aptitude" + ansible.builtin.apt: + name: aptitude + state: present + update_cache: true + +- name: "Install required system packages" + ansible.builtin.apt: + pkg: + - apt-transport-https + - ca-certificates + - curl + - gpg-agent + - python3-pip + - python3-setuptools + - software-properties-common + - virtualenv + state: present + update_cache: true + +- name: "Add Docker GPG apt Key" + ansible.builtin.apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: "Add Docker Repository" + ansible.builtin.apt_repository: + repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable" + state: present + +- name: "Install Docker ecosystem" + notify: "Sanity check" + ansible.builtin.apt: + pkg: + - docker-ce={{ docker_ubuntu_version_full }} + - docker-ce-cli={{ docker_ubuntu_version_full }} + state: present + update_cache: true + +- name: "Ensure the 'docker' group exists" + ansible.builtin.group: + name: docker + state: present + system: true + +- name: "Install Python tools for Docker" + ansible.builtin.pip: + name: [docker] + break_system_packages: true + +- name: "Configure Docker daemon" + notify: + - "Restart Docker daemon" + - "Sanity check" + ansible.builtin.template: + src: daemon.json.j2 + dest: /etc/docker/daemon.json + owner: root + group: root + mode: "0664" + validate: "dockerd --validate --config-file=%s" + +- name: "Spin up the Docker system service" + notify: "Sanity check" + ansible.builtin.systemd: + name: docker + state: started + enabled: true diff --git a/roles/docker_ubuntu/templates/daemon.json.j2 b/roles/docker_ubuntu/templates/daemon.json.j2 new file mode 100644 index 0000000..d128e9a --- /dev/null +++ b/roles/docker_ubuntu/templates/daemon.json.j2 @@ -0,0 +1,3 @@ +{ + "registry-mirrors": {{ docker_ubuntu_registry_mirrors | to_json }} +} diff --git a/roles/docker_ubuntu/vars/main.yml b/roles/docker_ubuntu/vars/main.yml new file mode 100644 index 0000000..3075048 --- /dev/null +++ b/roles/docker_ubuntu/vars/main.yml @@ -0,0 +1,2 @@ +--- +docker_ubuntu_version_full: "5:{{ docker_ubuntu_version }}-1~ubuntu.{{ ansible_lsb.release }}~{{ ansible_lsb.codename }}" diff --git a/roles/mkfs/README.md b/roles/mkfs/README.md new file mode 100644 index 0000000..cfa7a9b --- /dev/null +++ b/roles/mkfs/README.md @@ -0,0 +1,40 @@ +Role Name +========= + +Create a filesystem on the target device (thinly wraps `community.general.filesystem` module). + +Requirements +------------ + +Uses specific tools related to the fstype for creating or resizing a filesystem (`e2fsprogs`, `xfsprogs`, etc.). + +Uses generic tools mostly related to the OS, like `blkid`. + +Role Variables +-------------- + +- `mkfs_device`: (string) device that the filesystem should be created on +- `mkfs_type`: (string) type of the filesystem to be created +- `mkfs_opts`: (string) a list of options to be passed to `mkfs` (*not* a YAML array) +- `mkfs_state`: (string) `absent` or `present` +- `mkfs_force`: (boolean) overwrite an existing fs if there is one (default: `false`) + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See: [converge.yml](molecule/default/converge.yml) + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/mkfs/defaults/main.yml b/roles/mkfs/defaults/main.yml new file mode 100644 index 0000000..3525a8d --- /dev/null +++ b/roles/mkfs/defaults/main.yml @@ -0,0 +1,5 @@ +--- +# defaults file for mkfs +mkfs_opts: "" +mkfs_state: present +mkfs_force: false diff --git a/roles/mkfs/handlers/main.yml b/roles/mkfs/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/mkfs/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/mkfs/meta/main.yml b/roles/mkfs/meta/main.yml new file mode 100644 index 0000000..1fff67d --- /dev/null +++ b/roles/mkfs/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "mkfs" + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + namespace: genlab + description: "Create a filesystem on a target device" + license: "GPL-2.0-or-later" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/mkfs/molecule/default/converge.yml b/roles/mkfs/molecule/default/converge.yml new file mode 100644 index 0000000..93e2908 --- /dev/null +++ b/roles/mkfs/molecule/default/converge.yml @@ -0,0 +1,22 @@ +--- +- name: Converge + hosts: all + + pre_tasks: + + - name: "Check if the disk file already exists" + ansible.builtin.stat: + path: /tmp/test_file + register: mkfs_disk_file + + - name: "Create a file to act as a disk if it doesn't exist" + ansible.builtin.command: dd if=/dev/zero of=/tmp/test_file bs=1000000 count=100 + when: not mkfs_disk_file.stat.exists + register: mkfs_dd_output + changed_when: mkfs_dd_output.rc != 0 + + roles: + - role: genlab.common.mkfs + mkfs_device: "/tmp/test_file" + mkfs_type: ext3 + mkfs_state: present diff --git a/roles/mkfs/molecule/default/molecule.yml b/roles/mkfs/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/mkfs/molecule/default/molecule.yml @@ -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 . diff --git a/roles/mkfs/molecule/default/verify.yml b/roles/mkfs/molecule/default/verify.yml new file mode 100644 index 0000000..b9498c4 --- /dev/null +++ b/roles/mkfs/molecule/default/verify.yml @@ -0,0 +1,25 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + - name: "Attach the file to a loop device" + ansible.builtin.command: losetup /dev/loop3 /tmp/test_file + register: mkfs_loop_device + changed_when: mkfs_loop_device.rc != 0 + + - name: "Verify the filesystem type" + ansible.builtin.command: blkid /dev/loop3 + register: mkfs_blkid_output + changed_when: mkfs_blkid_output.rc != 0 + + - name: "Ensure filesystem is ext3" + ansible.builtin.assert: + that: + - "'TYPE=\"ext3\"' in mkfs_blkid_output.stdout" + + - name: "Explore stdout" + ansible.builtin.debug: + var: mkfs_blkid_output.stdout diff --git a/roles/mkfs/tasks/main.yml b/roles/mkfs/tasks/main.yml new file mode 100644 index 0000000..d3c468f --- /dev/null +++ b/roles/mkfs/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: "Create a filesystem on the given device" + community.general.filesystem: + fstype: "{{ mkfs_type }}" + dev: "{{ mkfs_device }}" + state: "{{ mkfs_state }}" + opts: "{{ mkfs_opts }}" + force: "{{ mkfs_force }}" diff --git a/roles/mkfs/vars/main.yml b/roles/mkfs/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/mkfs/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/mount_device/requirements.yml b/roles/mount_device/requirements.yml deleted file mode 100644 index 3d5f1cd..0000000 --- a/roles/mount_device/requirements.yml +++ /dev/null @@ -1,3 +0,0 @@ -# requirements file ---- -collections: [] diff --git a/roles/node_exporter/README.md b/roles/node_exporter/README.md new file mode 100644 index 0000000..0ff7a86 --- /dev/null +++ b/roles/node_exporter/README.md @@ -0,0 +1,34 @@ +Role Name +========= + +Deploy `prometheus/node_exporter` binary as a systemd unit. + +Requirements +------------ + +None + +Role Variables +-------------- + +`node_exporter_bin_path`: where to install the binary (default: `/usr/bin`). + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See: [converge.yml](molecule/default/converge.yml) + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/node_exporter/defaults/main.yml b/roles/node_exporter/defaults/main.yml new file mode 100644 index 0000000..daf4778 --- /dev/null +++ b/roles/node_exporter/defaults/main.yml @@ -0,0 +1,2 @@ +--- +node_exporter_bin_path: /usr/bin diff --git a/roles/node_exporter/handlers/main.yml b/roles/node_exporter/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/node_exporter/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/node_exporter/meta/main.yml b/roles/node_exporter/meta/main.yml new file mode 100644 index 0000000..885b94c --- /dev/null +++ b/roles/node_exporter/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: node_exporter + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "Deploy prometheus/node_exporter binary as a systemd unit" + license: "GPL-2.0-or-later" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy"] + + galaxy_tags: ["prometheus"] + +dependencies: [] diff --git a/roles/node_exporter/molecule/default/converge.yml b/roles/node_exporter/molecule/default/converge.yml new file mode 100644 index 0000000..e0158f0 --- /dev/null +++ b/roles/node_exporter/molecule/default/converge.yml @@ -0,0 +1,6 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.node_exporter + node_exporter_ver: "1.10.1" diff --git a/roles/node_exporter/molecule/default/molecule.yml b/roles/node_exporter/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/node_exporter/molecule/default/molecule.yml @@ -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 . diff --git a/roles/node_exporter/molecule/default/verify.yml b/roles/node_exporter/molecule/default/verify.yml new file mode 100644 index 0000000..3f1bb0b --- /dev/null +++ b/roles/node_exporter/molecule/default/verify.yml @@ -0,0 +1,16 @@ +# kics-scan disable=2e8d4922-8362-4606-8c14-aa10466a1ce3 +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + - name: "Sanity check the node exporter" + retries: 5 + delay: 1 + register: node_exporter_check + failed_when: '"node_exporter_build_info" not in node_exporter_check.content' + ansible.builtin.uri: + url: "http://localhost:9100/metrics" + return_content: true diff --git a/roles/node_exporter/tasks/install.yml b/roles/node_exporter/tasks/install.yml new file mode 100644 index 0000000..aaff138 --- /dev/null +++ b/roles/node_exporter/tasks/install.yml @@ -0,0 +1,37 @@ +--- +- name: "Install | Check if node_exporter is accessible from $PATH" + changed_when: false + failed_when: false + register: node_exporter_cmd + ansible.builtin.command: + cmd: node_exporter --version + +- name: "Install | Build the exporter name" + when: "node_exporter_ver not in node_exporter_cmd.stdout" + ansible.builtin.set_fact: + node_exporter_name: "/node_exporter-{{ node_exporter_ver }}\ + .{{ ansible_system | lower }}-\ + {{ (ansible_architecture == 'x86_64') | ternary('amd64', ansible_architecture) }}" + +- name: "Install | Download the node_exporter binary (release {{ node_exporter_ver }})" + when: "node_exporter_ver not in node_exporter_cmd.stdout" + ansible.builtin.unarchive: + src: "\ + https://github.com/prometheus/node_exporter/releases/download/\ + v{{ node_exporter_ver }}\ + {{ node_exporter_name }}\ + .tar.gz" + dest: /tmp + remote_src: true + +- name: "Install | Install the downloaded binary to '{{ node_exporter_bin_path }}'" + when: "node_exporter_ver not in node_exporter_cmd.stdout" + changed_when: false + ansible.builtin.command: + cmd: install /tmp/{{ node_exporter_name }}/node_exporter -t {{ node_exporter_bin_path }} + +- name: "Install | Push the service file template" + ansible.builtin.template: + src: node_exporter.service.j2 + dest: /usr/lib/systemd/system/node_exporter.service + mode: "0660" diff --git a/roles/node_exporter/tasks/main.yml b/roles/node_exporter/tasks/main.yml new file mode 100644 index 0000000..b63f5a3 --- /dev/null +++ b/roles/node_exporter/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: "Include installation tasks" + ansible.builtin.include_tasks: "install.yml" + +- name: "Including run tasks" + ansible.builtin.include_tasks: "run.yml" diff --git a/roles/node_exporter/tasks/run.yml b/roles/node_exporter/tasks/run.yml new file mode 100644 index 0000000..2d71c2e --- /dev/null +++ b/roles/node_exporter/tasks/run.yml @@ -0,0 +1,8 @@ +--- +- name: "Enable and start the node_exporter service" + changed_when: false + ansible.builtin.systemd: + name: node_exporter + enabled: true + daemon_reload: true + state: restarted diff --git a/roles/node_exporter/templates/node_exporter.service.j2 b/roles/node_exporter/templates/node_exporter.service.j2 new file mode 100644 index 0000000..414c5c6 --- /dev/null +++ b/roles/node_exporter/templates/node_exporter.service.j2 @@ -0,0 +1,12 @@ +[Unit] +Description=Node Exporter +After=network.target + +[Service] +Type=simple +ExecStart={{ node_exporter_bin_path }}/node_exporter \ + --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/) \ + --collector.systemd + +[Install] +WantedBy=multi-user.target diff --git a/roles/node_exporter/vars/main.yml b/roles/node_exporter/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/node_exporter/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/promtail/README.md b/roles/promtail/README.md new file mode 100644 index 0000000..7dac0b4 --- /dev/null +++ b/roles/promtail/README.md @@ -0,0 +1,44 @@ +promtail +========= + +Installs promtail as systemd service. + +Requirements +------------ + +Role Variables +-------------- + +(all optional) +`promtail_version`: version to install +`promtail_loki_server`: set loki server +`promtail_loki_port`: set loki port +`custom_server_config`: path to custom config witch replace all config with your own +`custom_scrape_configs`: path to custom scrape configs + +Dependencies +------------ + +No + +Example Playbook +---------------- + + - hosts: servers + roles: + - role: promtail + promtail_version: 2.7.3 + +License +------- + +MIT + +Author Information +------------------ + +Alexander Gorelyshev and Danilkin Danila (MIPT) + +Genlab LLC + +corvus-migratorius@proton.me diff --git a/roles/promtail/defaults/main.yml b/roles/promtail/defaults/main.yml new file mode 100644 index 0000000..8a8fa39 --- /dev/null +++ b/roles/promtail/defaults/main.yml @@ -0,0 +1,7 @@ +--- +promtail_version: 2.7.3 +promtail_positions_path: /home/promtail/positions.yaml +promtail_http_port: 9080 +promtail_loki_server: localhost +promtail_loki_port: 3100 +promtail_add_var_logs: true diff --git a/roles/promtail/handlers/main.yml b/roles/promtail/handlers/main.yml new file mode 100644 index 0000000..d1c06ac --- /dev/null +++ b/roles/promtail/handlers/main.yml @@ -0,0 +1,11 @@ +--- + +- name: "Restart the Promtail daemon" + ansible.builtin.systemd: + name: promtail + state: restarted + + +- name: "Reload the Promtail daemon configuration" + ansible.builtin.systemd: + daemon_reload: true diff --git a/roles/promtail/meta/main.yml b/roles/promtail/meta/main.yml new file mode 100644 index 0000000..79d6be3 --- /dev/null +++ b/roles/promtail/meta/main.yml @@ -0,0 +1,14 @@ +galaxy_info: + role_name: promtail + namespace: genlab + author: Danilkin Danila + description: Installs Promtail + + company: Genlab LLC + license: MIT + + min_ansible_version: "2.1" + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/promtail/molecule/default/converge.yml b/roles/promtail/molecule/default/converge.yml new file mode 100644 index 0000000..ea072dd --- /dev/null +++ b/roles/promtail/molecule/default/converge.yml @@ -0,0 +1,5 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.promtail diff --git a/roles/promtail/molecule/default/molecule.yml b/roles/promtail/molecule/default/molecule.yml new file mode 100644 index 0000000..8d39b18 --- /dev/null +++ b/roles/promtail/molecule/default/molecule.yml @@ -0,0 +1,24 @@ +--- +dependency: + name: galaxy +driver: + name: docker +platforms: + - name: ubuntu + image: geerlingguy/docker-ubuntu2204-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 + playbooks: + converge: converge.yml +verifier: + name: ansible +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/promtail/molecule/default/verify.yml b/roles/promtail/molecule/default/verify.yml new file mode 100644 index 0000000..7e078c2 --- /dev/null +++ b/roles/promtail/molecule/default/verify.yml @@ -0,0 +1,20 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + # https://github.com/ansible/molecule/issues/3587#issuecomment-1158650179 + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + # kics-scan ignore-block - kics doesn't like http but it's localhost so it's not important + - name: "Sanity check the Promtail daemon" + retries: 3 + delay: 1 + register: promtail_this + failed_when: "promtail_this.content != 'Ready'" + ansible.builtin.uri: + url: "http://localhost:{{ promtail_http_port }}/ready" + return_content: true diff --git a/roles/promtail/tasks/main.yml b/roles/promtail/tasks/main.yml new file mode 100644 index 0000000..127c10c --- /dev/null +++ b/roles/promtail/tasks/main.yml @@ -0,0 +1,107 @@ +--- +- name: "Install packages" + ansible.builtin.apt: + name: [acl, unzip] + state: present + update_cache: true + cache_valid_time: 3600 + + +- name: "Create the 'promtail' group" + ansible.builtin.group: + name: promtail + state: present + + +- name: "Create the 'promtail' user" + ansible.builtin.user: + name: promtail + groups: + - promtail + - systemd-journal + - adm + state: present + system: true + + +- name: "Set facl to allow promtail user to read '/var/log' contents" + ansible.posix.acl: + path: /var/log + entity: promtail + etype: user + permissions: rX + state: present + + +- name: "Make promtail user owner of '{{ promtail_positions_path }}'" + changed_when: false + ansible.builtin.file: + path: "{{ promtail_positions_path }}" + owner: promtail + group: promtail + state: touch + mode: "0750" + + +- name: "Install the requested Promtail version" + block: + - name: "Check Promtail version" + ansible.builtin.command: /usr/bin/promtail --version + register: promtail_version_check + changed_when: false + + - name: "Assert version correctness" + notify: "Restart the Promtail daemon" + ansible.builtin.assert: + that: "promtail_version in promtail_version_check.stdout" + success_msg: "Expected Promtail version available ({{ promtail_version }})" + fail_msg: "Expected version '{{ promtail_version }}'; available is '{{ promtail_version_check.stdout }}'" + + rescue: + - name: "Install Promtail if its not present" + ansible.builtin.unarchive: + src: "https://github.com/grafana/loki/releases/download/v\ + {{ promtail_version }}/promtail-linux-amd64.zip" + dest: /usr/bin/ + remote_src: true + + - name: "Rename and set permissions for the Promtail binary" + ansible.builtin.copy: + src: "/usr/bin/promtail-linux-amd64" + dest: "/usr/bin/promtail" + owner: root + group: root + mode: "0755" + remote_src: true + + - name: "Cleanup the downloaded file" + ansible.builtin.file: + path: "/usr/bin/promtail-linux-amd64" + state: absent + + +- name: "Template the systemd unit file" + notify: "Reload the Promtail daemon configuration" + ansible.builtin.template: + src: promtail.service.j2 + dest: /etc/systemd/system/promtail.service + owner: root + group: root + mode: "0644" + + +- name: "Template Promtail config file" + notify: "Restart the Promtail daemon" + ansible.builtin.template: + src: templates/promtail.yml.j2 + dest: /usr/local/bin/config-promtail.yml + owner: root + group: root + mode: "0644" + + +- name: "Enable and start Promtail daemon" + ansible.builtin.systemd: + name: promtail + state: started + enabled: true diff --git a/roles/promtail/templates/promtail.service.j2 b/roles/promtail/templates/promtail.service.j2 new file mode 100644 index 0000000..3b7b517 --- /dev/null +++ b/roles/promtail/templates/promtail.service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=Promtail service +After=network.target + +[Service] +Type=simple +User=promtail +ExecStart=/usr/bin/promtail -config.file /usr/local/bin/config-promtail.yml + +[Install] +WantedBy=multi-user.target diff --git a/roles/promtail/templates/promtail.yml.j2 b/roles/promtail/templates/promtail.yml.j2 new file mode 100644 index 0000000..21dea49 --- /dev/null +++ b/roles/promtail/templates/promtail.yml.j2 @@ -0,0 +1,28 @@ +{% if custom_server_config is defined %} +{{ custom_server_config }} +{% else %} +server: + http_listen_port: {{ promtail_http_port }} + grpc_listen_port: 0 +{% endif %} +positions: + filename: {{ promtail_positions_path }} + +clients: + - url: "http://{{ promtail_loki_server }}:{{ promtail_loki_port }}/loki/api/v1/push" + +scrape_configs: +{% if promtail_add_var_logs %} + - job_name: system + static_configs: + - targets: + - localhost + labels: + job: varlogs + __path__: /var/log/*log + host: {{ ansible_hostname }} +{% endif %} + +{% if custom_scrape_configs is defined %} +{{ custom_scrape_configs }} +{% endif %} diff --git a/roles/promtail/vars/main.yml b/roles/promtail/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/promtail/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/sshd/README.md b/roles/sshd/README.md new file mode 100644 index 0000000..3f3962d --- /dev/null +++ b/roles/sshd/README.md @@ -0,0 +1,37 @@ +ansible-sshd +========= + +Deploy a hardened sshd server + +Requirements +------------ + +None + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.common.sshd +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml new file mode 100644 index 0000000..5354282 --- /dev/null +++ b/roles/sshd/defaults/main.yml @@ -0,0 +1,5 @@ +--- +sshd_disable_pam: false +sshd_password_auth: false +sshd_challenge_response_auth: false +sshd_gss_api_auth: false diff --git a/roles/sshd/handlers/main.yml b/roles/sshd/handlers/main.yml new file mode 100644 index 0000000..9f08055 --- /dev/null +++ b/roles/sshd/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "Restart sshd" + ansible.builtin.service: + name: ssh + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/sshd/meta/main.yml b/roles/sshd/meta/main.yml new file mode 100644 index 0000000..a365305 --- /dev/null +++ b/roles/sshd/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: sshd + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "Deploy a hardened sshd server" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/sshd/molecule/default/converge.yml b/roles/sshd/molecule/default/converge.yml new file mode 100644 index 0000000..bea06f8 --- /dev/null +++ b/roles/sshd/molecule/default/converge.yml @@ -0,0 +1,5 @@ +--- +- name: Converge + hosts: all + roles: + - role: "genlab.common.sshd" diff --git a/roles/sshd/molecule/default/molecule.yml b/roles/sshd/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/sshd/molecule/default/molecule.yml @@ -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 . diff --git a/roles/sshd/molecule/default/verify.yml b/roles/sshd/molecule/default/verify.yml new file mode 100644 index 0000000..29b09d7 --- /dev/null +++ b/roles/sshd/molecule/default/verify.yml @@ -0,0 +1,76 @@ +--- + +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + - name: Gather service facts + ansible.builtin.service_facts: + + - name: Ensure sshd is running + ansible.builtin.assert: + that: + - ansible_facts.services['ssh.service'].state == 'running' + + - name: Ensure sshd_config syntax is OK + ansible.builtin.command: sshd -t -f /etc/ssh/sshd_config + changed_when: false + + - name: Ensure main parameteres are applied + ansible.builtin.shell: | + set -o pipefail ; + sshd -T | egrep -i ' + ^protocol 2| + ^permitrootlogin no| + ^passwordauthentication no| + ^pubkeyauthentication yes + ' + args: + executable: /bin/bash + changed_when: false + + - name: Get sshd_config stats + ansible.builtin.stat: + path: /etc/ssh/sshd_config + register: sshd_conf + + - name: Ensure sshd_config file is secure + ansible.builtin.assert: + that: + - sshd_conf.stat.uid == 0 + - sshd_conf.stat.gid == 0 + - sshd_conf.stat.mode == '0600' + + - name: Create test user + ansible.builtin.user: + name: test + create_home: true + shell: /bin/bash + + - name: Create .ssh directory + ansible.builtin.file: + path: /home/test/.ssh/ + state: directory + mode: '0700' + owner: test + group: test + + - name: Generate ssh keys + community.crypto.openssh_keypair: + path: /home/test/.ssh/id_rsa + owner: test + group: test + mode: '0600' + register: sshd_key_result + + - name: Put public key to test user + ansible.posix.authorized_key: + user: test + key: "{{ sshd_key_result.public_key }}" + state: present + + - name: Test ssh connection + ansible.builtin.command: ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /home/test/.ssh/id_rsa test@localhost hostname + changed_when: false diff --git a/roles/sshd/tasks/algorithms.yml b/roles/sshd/tasks/algorithms.yml new file mode 100644 index 0000000..a43581d --- /dev/null +++ b/roles/sshd/tasks/algorithms.yml @@ -0,0 +1,42 @@ +--- +# next task requires this directory to exist for sshd -t flag +- name: Ensure /run/sshd exists + ansible.builtin.file: + path: /run/sshd + state: directory + owner: root + group: root + mode: '0755' + +# NOTE: order of preference for openssh-server ed25519 -> rsa +- name: "Algorithms | enable ed25519 authentication algorithm" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_ed25519_key' + line: 'HostKey /etc/ssh/ssh_host_ed25519_key' + validate: sshd -f %s -t + +- name: "Algorithms | enable the RSA authentication algorithm" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_rsa_key' + line: 'HostKey /etc/ssh/ssh_host_rsa_key' + validate: sshd -f %s -t + +- name: "Algorithms | disable the ECDSA algorithm (deemed to be less safe)" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_ecdsa_key' + state: absent + validate: sshd -f %s -t + +- name: "Algorithms | disable the DSA algorithm (considered to be defunct)" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_dsa_key' + state: absent + validate: sshd -f %s -t diff --git a/roles/sshd/tasks/authentication.yml b/roles/sshd/tasks/authentication.yml new file mode 100644 index 0000000..8b4e63b --- /dev/null +++ b/roles/sshd/tasks/authentication.yml @@ -0,0 +1,31 @@ +--- +- name: "Authentication | Configure SSH authentication settings" + notify: Restart sshd + loop: + - { regexp: '^#?\s*PubkeyAuthentication\s+', line: 'PubkeyAuthentication yes' } + - { regexp: '^#?\s*PasswordAuthentication\s+', line: 'PasswordAuthentication {{ sshd_password_auth | ternary("yes", "no") }}' } + - { regexp: '^#?\s*PermitEmptyPasswords\s+', line: 'PermitEmptyPasswords no' } + - { regexp: '^#?\s*ChallengeResponseAuthentication\s+', line: 'ChallengeResponseAuthentication {{ sshd_challenge_response_auth | ternary("yes", "no") }}' } + - { regexp: '^#?\s*GSSAPIAuthentication\s+', line: 'GSSAPIAuthentication {{ sshd_gss_api_auth | ternary("yes", "no") }}' } + - { + regexp: '^#?\s*AuthenticationMethods\s+', + line: "{{ 'AuthenticationMethods publickey password' if sshd_password_auth else 'AuthenticationMethods publickey' }}" + } + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + validate: /usr/sbin/sshd -t -f %s + +- name: "Check if there is an SSH config forced by cloud-init" + register: sshd_cloud_init + ansible.builtin.stat: + path: "/etc/ssh/sshd_config.d/50-cloud-init.conf" + +- name: "Authentication | override password authentication by cloud-init to '{{ sshd_password_auth | ternary('yes', 'no') }}'" + when: sshd_cloud_init.stat.exists + notify: Restart sshd + ansible.builtin.lineinfile: + path: "/etc/ssh/sshd_config.d/50-cloud-init.conf" + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication {{ sshd_password_auth | ternary("yes", "no") }}' diff --git a/roles/sshd/tasks/encryption.yml b/roles/sshd/tasks/encryption.yml new file mode 100644 index 0000000..660a369 --- /dev/null +++ b/roles/sshd/tasks/encryption.yml @@ -0,0 +1,11 @@ +--- +- name: "Encryption | remove unsafe host keys" + loop: + - /etc/ssh/ssh_host_ecdsa_key + - /etc/ssh/ssh_host_ecdsa_key.pub + - /etc/ssh/ssh_host_dsa_key + - /etc/ssh/ssh_host_dsa_key.pub + notify: Restart sshd + ansible.builtin.file: + path: "{{ item }}" + state: absent diff --git a/roles/sshd/tasks/install.yml b/roles/sshd/tasks/install.yml new file mode 100644 index 0000000..ff03595 --- /dev/null +++ b/roles/sshd/tasks/install.yml @@ -0,0 +1,13 @@ +--- +- name: "Install | install OpenSSH (RHEL flavours)" + when: ansible_os_family == "RHEL" + ansible.builtin.dnf: + name: openssh + state: installed + +- name: "Install | Install OpenSSH (Debian flavours)" + when: ansible_os_family == "Debian" + ansible.builtin.apt: + name: openssh-server + state: present + update_cache: true diff --git a/roles/sshd/tasks/main.yml b/roles/sshd/tasks/main.yml new file mode 100644 index 0000000..d35222a --- /dev/null +++ b/roles/sshd/tasks/main.yml @@ -0,0 +1,23 @@ +--- +- name: "Install an OpenSSH server" + ansible.builtin.include_tasks: "install.yml" + +- name: "Configure SSH algorithms" + ansible.builtin.include_tasks: "algorithms.yml" + +- name: "Configure authentication methods" + ansible.builtin.include_tasks: "authentication.yml" + +- name: "Configure SSH encryption keys" + ansible.builtin.include_tasks: "encryption.yml" + +- name: "Configure additional restrictions" + ansible.builtin.include_tasks: "restrictions.yml" + +- name: "Log at VERBOSE level" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?LogLevel' + line: 'LogLevel VERBOSE' + validate: sshd -f %s -t diff --git a/roles/sshd/tasks/restrictions.yml b/roles/sshd/tasks/restrictions.yml new file mode 100644 index 0000000..6d14920 --- /dev/null +++ b/roles/sshd/tasks/restrictions.yml @@ -0,0 +1,30 @@ +--- +- name: "Restrictions | Configure SSH security restrictions" + loop: + - { regexp: '^#?Protocol\s+', line: 'Protocol 2' } + - { regexp: '^PermitRootLogin yes', line: 'PermitRootLogin no' } + - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' } + - { regexp: '^#?IgnoreRhosts', line: 'IgnoreRhosts yes' } + - { regexp: '^#?DebianBanner\s+', line: 'DebianBanner no' } + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + validate: /usr/sbin/sshd -t -f %s + +- name: "Restrictions | toggle PAM" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?UsePAM' + line: "UsePAM {{ sshd_disable_pam | ternary('no', 'yes') }}" + validate: sshd -f %s -t + +- name: "Restrictions | ensure the SSHD config is restricted to the root user" + notify: Restart sshd + ansible.builtin.file: + path: /etc/ssh/sshd_config + owner: root + group: root + mode: "0600" diff --git a/roles/sshd/vars/main.yml b/roles/sshd/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/sshd/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/swapfile/README.md b/roles/swapfile/README.md new file mode 100644 index 0000000..019f927 --- /dev/null +++ b/roles/swapfile/README.md @@ -0,0 +1,39 @@ +ansible-swapfile +========= + +Create/destroy a swapfile and run swapon/-off on it. + +Massively borrowing from https://github.com/geerlingguy/ansible-role-swap. + +Requirements +------------ + +None + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.swapfile +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/swapfile/defaults/main.yml b/roles/swapfile/defaults/main.yml new file mode 100644 index 0000000..e5e29fe --- /dev/null +++ b/roles/swapfile/defaults/main.yml @@ -0,0 +1,8 @@ +--- +swapfile_path: /swapfile +swapfile_size_mb: '512' +swapfile_swappiness: '60' +swapfile_state: present +swapfile_create_command: "dd if=/dev/zero of={{ swapfile_path }} bs=1M count={{ swapfile_size_mb }}" + +swapfile_test_mode: false diff --git a/roles/swapfile/handlers/main.yml b/roles/swapfile/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/swapfile/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/swapfile/meta/main.yml b/roles/swapfile/meta/main.yml new file mode 100644 index 0000000..fde50d8 --- /dev/null +++ b/roles/swapfile/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "swapfile" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/swapfile/molecule/default/converge.yml b/roles/swapfile/molecule/default/converge.yml new file mode 100644 index 0000000..459c5ad --- /dev/null +++ b/roles/swapfile/molecule/default/converge.yml @@ -0,0 +1,19 @@ +--- +- name: Converge + hosts: all + become: true + + pre_tasks: + - name: "Update apt cache" + when: ansible_os_family == 'Debian' + ansible.builtin.apt: + update_cache: true + cache_valid_time: 600 + + roles: + - role: genlab.common.swapfile + swapfile_path: /swapfile + swapfile_size_mb: 2000 + swapfile_swappiness: 30 + swapfile_state: present + swapfile_test_mode: true diff --git a/roles/swapfile/molecule/default/molecule.yml b/roles/swapfile/molecule/default/molecule.yml new file mode 100644 index 0000000..89df07d --- /dev/null +++ b/roles/swapfile/molecule/default/molecule.yml @@ -0,0 +1,29 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + pre_build_image: true + +provisioner: + name: ansible + playbooks: + converge: ${MOLECULE_PLAYBOOK:-converge.yml} + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/swapfile/molecule/default/verify.yml b/roles/swapfile/molecule/default/verify.yml new file mode 100644 index 0000000..c54ab18 --- /dev/null +++ b/roles/swapfile/molecule/default/verify.yml @@ -0,0 +1,26 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + vars: + swapfile_path: "/swapfile" + + pre_tasks: + - name: "Ensure 'file' exists" + ansible.builtin.package: + name: [file] + state: present + + tasks: + - name: "Check swapfile file type" + register: swapfile_info + changed_when: false + ansible.builtin.command: + cmd: "file {{ swapfile_path }}" + + - name: "Verify correct file type" + ansible.builtin.assert: + that: "'swap file' in swapfile_info.stdout" + success_msg: "{{ swapfile_path }}: Indeed, a Linux swap file" + fail_msg: "{{ swapfile_path }}: not a Linux swap file ('{{ swapfile_info.stdout }}')" diff --git a/roles/swapfile/tasks/disable.yml b/roles/swapfile/tasks/disable.yml new file mode 100644 index 0000000..ba04c8a --- /dev/null +++ b/roles/swapfile/tasks/disable.yml @@ -0,0 +1,11 @@ +--- +- name: "Disable | disable swap" + changed_when: false + ansible.builtin.command: + cmd: swapoff -a + + +- name: "Disable | ensure swap file doesn't exist" + ansible.builtin.file: + path: "{{ swapfile_path }}" + state: absent diff --git a/roles/swapfile/tasks/enable.yml b/roles/swapfile/tasks/enable.yml new file mode 100644 index 0000000..e433f73 --- /dev/null +++ b/roles/swapfile/tasks/enable.yml @@ -0,0 +1,47 @@ +--- +# https://justinmontgomery.com/speed-up-stat-command-in-ansible +- name: "Enable | create the swap file" + block: + - name: "Enable | stat the swap file" + register: swapfile_this + failed_when: not swapfile_this.stat.exists + ansible.builtin.stat: + path: "{{ swapfile_path }}" + get_checksum: false + get_mime: false + get_attributes: false + + rescue: + - name: "Enable | ensure swap file exists" + register: swapfile_create + ansible.builtin.command: + cmd: "{{ swapfile_create_command }}" + creates: "{{ swapfile_path }}" + + - name: "Enable | set permissions on swap file" + ansible.builtin.file: + path: "{{ swapfile_path }}" + owner: root + group: root + mode: "0600" + + - name: "Enable | make swap" # noqa: no-handler no-changed-when + register: swapfile_mkswap_result + when: swapfile_create is changed + ansible.builtin.command: + cmd: mkswap {{ swapfile_path }} + + - name: "Enable | run swapon on the swap file => '{{ swapfile_path }}'" + when: + - swapfile_mkswap_result is changed + - not swapfile_test_mode + changed_when: false + ansible.builtin.command: + cmd: swapon {{ swapfile_path }} + + +- name: "Set swappiness to '{{ swapfile_swappiness }}'" + ansible.posix.sysctl: + name: vm.swappiness + value: "{{ swapfile_swappiness }}" + state: present diff --git a/roles/swapfile/tasks/main.yml b/roles/swapfile/tasks/main.yml new file mode 100644 index 0000000..639505c --- /dev/null +++ b/roles/swapfile/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: "Manage swap file entry in fstab" + ansible.posix.mount: + name: none + src: "{{ swapfile_path }}" + fstype: swap + opts: sw + state: "{{ swapfile_state }}" + + +- name: "Include disable swap tasks" + when: swapfile_state == 'absent' + ansible.builtin.include_tasks: disable.yml + + +- name: "Include enable swap tasks" + when: swapfile_state == 'present' + ansible.builtin.include_tasks: enable.yml diff --git a/roles/swapfile/vars/main.yml b/roles/swapfile/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/swapfile/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/users/README.md b/roles/users/README.md new file mode 100644 index 0000000..30fd61c --- /dev/null +++ b/roles/users/README.md @@ -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 diff --git a/roles/users/defaults/main.yml b/roles/users/defaults/main.yml new file mode 100644 index 0000000..755fb15 --- /dev/null +++ b/roles/users/defaults/main.yml @@ -0,0 +1,2 @@ +--- +users_default_home_root: /home diff --git a/roles/users/handlers/main.yml b/roles/users/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/users/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/users/meta/main.yml b/roles/users/meta/main.yml new file mode 100644 index 0000000..03efaab --- /dev/null +++ b/roles/users/meta/main.yml @@ -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: [] diff --git a/roles/users/molecule/default/configuration/users.yml b/roles/users/molecule/default/configuration/users.yml new file mode 100644 index 0000000..39687a1 --- /dev/null +++ b/roles/users/molecule/default/configuration/users.yml @@ -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 diff --git a/roles/users/molecule/default/converge.yml b/roles/users/molecule/default/converge.yml new file mode 100644 index 0000000..0327060 --- /dev/null +++ b/roles/users/molecule/default/converge.yml @@ -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%" diff --git a/roles/users/molecule/default/molecule.yml b/roles/users/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/users/molecule/default/molecule.yml @@ -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 . diff --git a/roles/users/molecule/default/user0.keys b/roles/users/molecule/default/user0.keys new file mode 100644 index 0000000..5a478b1 --- /dev/null +++ b/roles/users/molecule/default/user0.keys @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMh9Y+wR4LH8lWJjJXqHn76kSoTRujkab+PYwD3IReFh user0@hostname diff --git a/roles/users/molecule/default/verify.yml b/roles/users/molecule/default/verify.yml new file mode 100644 index 0000000..da1af66 --- /dev/null +++ b/roles/users/molecule/default/verify.yml @@ -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" diff --git a/roles/users/molecule/users_from_var/converge.yml b/roles/users/molecule/users_from_var/converge.yml new file mode 100644 index 0000000..4993887 --- /dev/null +++ b/roles/users/molecule/users_from_var/converge.yml @@ -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 diff --git a/roles/users/molecule/users_from_var/molecule.yml b/roles/users/molecule/users_from_var/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/users/molecule/users_from_var/molecule.yml @@ -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 . diff --git a/roles/users/molecule/users_from_var/user0.keys b/roles/users/molecule/users_from_var/user0.keys new file mode 100644 index 0000000..5a478b1 --- /dev/null +++ b/roles/users/molecule/users_from_var/user0.keys @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMh9Y+wR4LH8lWJjJXqHn76kSoTRujkab+PYwD3IReFh user0@hostname diff --git a/roles/users/molecule/users_from_var/verify.yml b/roles/users/molecule/users_from_var/verify.yml new file mode 100644 index 0000000..da1af66 --- /dev/null +++ b/roles/users/molecule/users_from_var/verify.yml @@ -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" diff --git a/roles/users/tasks/create.yml b/roles/users/tasks/create.yml new file mode 100644 index 0000000..f952d08 --- /dev/null +++ b/roles/users/tasks/create.yml @@ -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 diff --git a/roles/users/tasks/main.yml b/roles/users/tasks/main.yml new file mode 100644 index 0000000..6d3f166 --- /dev/null +++ b/roles/users/tasks/main.yml @@ -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 diff --git a/roles/users/tasks/openssh.yml b/roles/users/tasks/openssh.yml new file mode 100644 index 0000000..753bf52 --- /dev/null +++ b/roles/users/tasks/openssh.yml @@ -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 diff --git a/roles/users/templates/slice.j2 b/roles/users/templates/slice.j2 new file mode 100644 index 0000000..5a9b1f5 --- /dev/null +++ b/roles/users/templates/slice.j2 @@ -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 %} diff --git a/roles/users/vars/main.yml b/roles/users/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/users/vars/main.yml @@ -0,0 +1 @@ +---