From 072a1ed7645d8da3711927dfba3db531a1b87f84 Mon Sep 17 00:00:00 2001 From: irl Date: Mon, 2 Jun 2025 14:55:56 +0100 Subject: [PATCH] feat: initial commit --- .gitignore | 2 + README.md | 53 +++++ roles/podman_forgejo/defaults/main.yml | 7 + roles/podman_forgejo/handlers/main.yml | 24 ++ roles/podman_forgejo/tasks/main.yml | 88 +++++++ .../templates/forgejo.container | 28 +++ .../podman_forgejo/templates/forgejo.network | 2 + .../podman_forgejo/templates/frontend.network | 2 + .../templates/mariadb.container | 15 ++ roles/podman_forgejo/templates/nginx.conf | 42 ++++ roles/podman_host/defaults/main.yml | 3 + roles/podman_host/tasks/main.yml | 69 ++++++ roles/podman_host/templates/subXid.j2 | 4 + roles/podman_identity/defaults/main.yml | 17 ++ roles/podman_identity/handlers/main.yml | 27 +++ roles/podman_identity/tasks/main.yml | 218 ++++++++++++++++++ .../templates/frontend.network | 5 + .../templates/keycloak.container | 33 +++ .../templates/keycloak.network | 5 + .../podman_identity/templates/ldap.container | 17 ++ roles/podman_identity/templates/ldap.network | 5 + roles/podman_identity/templates/nginx.conf | 39 ++++ .../templates/postgres.container | 15 ++ roles/podman_nginx/defaults/main.yml | 6 + roles/podman_nginx/handlers/main.yml | 18 ++ roles/podman_nginx/tasks/main.yml | 110 +++++++++ .../templates/certbot-renew.container | 13 ++ .../templates/certbot-renew.timer | 9 + roles/podman_nginx/templates/nginx.conf | 17 ++ roles/podman_nginx/templates/nginx.container | 16 ++ roles/system_baseline/defaults/main.yml | 12 + roles/system_baseline/handlers/main.yml | 6 + roles/system_baseline/tasks/main.yml | 29 +++ roles/system_baseline/tasks/sshd.yml | 24 ++ roles/system_baseline/tasks/users.yml | 74 ++++++ roles/vps/tasks/main.yml | 35 +++ 36 files changed, 1089 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 roles/podman_forgejo/defaults/main.yml create mode 100644 roles/podman_forgejo/handlers/main.yml create mode 100644 roles/podman_forgejo/tasks/main.yml create mode 100644 roles/podman_forgejo/templates/forgejo.container create mode 100644 roles/podman_forgejo/templates/forgejo.network create mode 100644 roles/podman_forgejo/templates/frontend.network create mode 100644 roles/podman_forgejo/templates/mariadb.container create mode 100644 roles/podman_forgejo/templates/nginx.conf create mode 100644 roles/podman_host/defaults/main.yml create mode 100644 roles/podman_host/tasks/main.yml create mode 100644 roles/podman_host/templates/subXid.j2 create mode 100644 roles/podman_identity/defaults/main.yml create mode 100644 roles/podman_identity/handlers/main.yml create mode 100644 roles/podman_identity/tasks/main.yml create mode 100644 roles/podman_identity/templates/frontend.network create mode 100644 roles/podman_identity/templates/keycloak.container create mode 100644 roles/podman_identity/templates/keycloak.network create mode 100644 roles/podman_identity/templates/ldap.container create mode 100644 roles/podman_identity/templates/ldap.network create mode 100644 roles/podman_identity/templates/nginx.conf create mode 100644 roles/podman_identity/templates/postgres.container create mode 100644 roles/podman_nginx/defaults/main.yml create mode 100644 roles/podman_nginx/handlers/main.yml create mode 100644 roles/podman_nginx/tasks/main.yml create mode 100644 roles/podman_nginx/templates/certbot-renew.container create mode 100644 roles/podman_nginx/templates/certbot-renew.timer create mode 100644 roles/podman_nginx/templates/nginx.conf create mode 100644 roles/podman_nginx/templates/nginx.container create mode 100644 roles/system_baseline/defaults/main.yml create mode 100644 roles/system_baseline/handlers/main.yml create mode 100644 roles/system_baseline/tasks/main.yml create mode 100644 roles/system_baseline/tasks/sshd.yml create mode 100644 roles/system_baseline/tasks/users.yml create mode 100644 roles/vps/tasks/main.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7eaa6e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.retry +.ansible/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4b60d9 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +

+
+ irl.xyz +
+

+ +

+ + Language: Ansible + + + Licence: BSD 2-Clause + + Lifecycle: Experimental +

+ +## ansible-collection-wip + +This is the home of Ansible roles and playbooks that have been put together for one reason or another, but have not yet +been polished for publication. No guarantee is made that anything here will be maintained, however some challenges may +have been overcome in producing these so there may be some lessons to learn from the YAML herein. + +Generally roles will be targeting the latest release of Debian GNU/Linux. As new releases are approaching, sometimes +I'll jump the gun and start targeting the unreleased next version as it is usually ready for production anyway. The +Debian people just like to get everything perfect before they hit the release button. + +### Usage + +Use these as examples, but I would strongly recommend you do not include these directly in your IaC. +If something here fits your needs really well, maybe [get in touch](https://irl.xyz/contact/) with me and check if I'm +planning to maintain that role or playbook. +Sometimes a bit of external motivation is the push I need to get something finished, otherwise you can always +[hire me to do it](https://www.sr2.uk/). + +### Licence + +Copyright © 2022-2025 irl. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following +disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/roles/podman_forgejo/defaults/main.yml b/roles/podman_forgejo/defaults/main.yml new file mode 100644 index 0000000..582aa72 --- /dev/null +++ b/roles/podman_forgejo/defaults/main.yml @@ -0,0 +1,7 @@ +--- +podman_forgejo_mariadb_database: forgejo +# podman_forgejo_mariadb_password: +# podman_forgejo_mariadb_root_password: +podman_forgejo_mariadb_user: forgejo +podman_forgejo_podman_rootless_user: forge +podman_forgejo_web_hostname: "{{ inventory_hostname }}" diff --git a/roles/podman_forgejo/handlers/main.yml b/roles/podman_forgejo/handlers/main.yml new file mode 100644 index 0000000..94bf0c2 --- /dev/null +++ b/roles/podman_forgejo/handlers/main.yml @@ -0,0 +1,24 @@ +--- +- name: restart forgejo + ansible.builtin.systemd_service: + name: forgejo + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_forgejo_podman_rootless_user }}" + +- name: restart mariadb + ansible.builtin.systemd_service: + name: forgejo + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_forgejo_podman_rootless_user }}" + +- name: restart sshd + service: + name: sshd + state: restarted + become: true diff --git a/roles/podman_forgejo/tasks/main.yml b/roles/podman_forgejo/tasks/main.yml new file mode 100644 index 0000000..4d565ef --- /dev/null +++ b/roles/podman_forgejo/tasks/main.yml @@ -0,0 +1,88 @@ +--- +- name: setup alternate SSH port + lineinfile: + dest: "/etc/ssh/sshd_config" + regexp: "^Port" + line: "Port 2222" + notify: restart sshd + become: true + +- name: create service configuration directories + ansible.builtin.file: + path: "/home/{{ podman_forgejo_podman_rootless_user }}/{{ item }}" + state: directory + owner: "{{ podman_forgejo_podman_rootless_user }}" + group: "{{ podman_forgejo_podman_rootless_user }}" + mode: "0755" + become: true + with_items: + - mysql + - forgejo + +- name: install podman quadlet for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_forgejo_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_forgejo_podman_rootless_user }}" + mode: "0400" + with_items: + - forgejo.container + - mariadb.container + notify: + - "restart {{ item | split('.') | first }}" + become: true + +- name: install network quadlets for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_forgejo_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_forgejo_podman_rootless_user }}" + mode: "0400" + with_items: + - frontend.network + - forgejo.network + become: true + +- name: verify quadlets are correctly defined + ansible.builtin.command: /usr/libexec/podman/quadlet -dryrun -user + register: podman_forgejo_quadlet_result + ignore_errors: true + changed_when: false + become: true + become_user: "{{ podman_forgejo_podman_rootless_user }}" + +- name: assert that the quadlet verification succeeded + ansible.builtin.assert: + that: + - podman_forgejo_quadlet_result.rc == 0 + fail_msg: "'/usr/libexec/podman/quadlet -dryrun -user' failed! Output withheld to prevent leaking secrets." + +- name: start forgejo and mariadb + ansible.builtin.systemd_service: + name: "{{ item }}" + state: started + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_forgejo_podman_rootless_user }}" + with_items: + - forgejo + - mariadb + +- name: set up nginx + ansible.builtin.include_role: + name: podman_nginx + vars: + podman_nginx_frontend_network: frontend + podman_nginx_podman_rootless_user: "{{ podman_forgejo_podman_rootless_user }}" + podman_nginx_primary_hostname: "{{ podman_forgejo_web_hostname }}" + +- name: create nginx configuration file + ansible.builtin.template: + src: nginx.conf + dest: "/home/{{ podman_forgejo_podman_rootless_user }}/nginx/nginx.conf" + owner: "{{ podman_forgejo_podman_rootless_user }}" + group: "{{ podman_forgejo_podman_rootless_user }}" + mode: "0644" + become: true + notify: restart nginx diff --git a/roles/podman_forgejo/templates/forgejo.container b/roles/podman_forgejo/templates/forgejo.container new file mode 100644 index 0000000..9d8e00c --- /dev/null +++ b/roles/podman_forgejo/templates/forgejo.container @@ -0,0 +1,28 @@ +[Unit] +Requires=mariadb.service +After=mariadb.service + +[Container] +ContainerName=forgejo +Environment=USER_UID=1000 +Environment=USER_GID=1000 +Environment=FORGEJO__database__DB_TYPE=mysql +Environment=FORGEJO__database__HOST=mariadb:3306 +Environment=FORGEJO__database__NAME={{ podman_forgejo_mariadb_database }} +Environment=FORGEJO__database__USER={{ podman_forgejo_mariadb_user }} +Environment=FORGEJO__database__PASSWD={{ podman_forgejo_mariadb_password }} +Environment=FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION=true +Environment=FORGEJO__server__LANDING_PAGE=/explore/repos +Image=codeberg.org/forgejo/forgejo:11 +Network=frontend.network +Network=forgejo.network +PublishPort=22:22 +Volume=/home/forge/forgejo:/data +Volume=/etc/timezone:/etc/timezone:ro +Volume=/etc/localtime:/etc/localtime:ro + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/podman_forgejo/templates/forgejo.network b/roles/podman_forgejo/templates/forgejo.network new file mode 100644 index 0000000..3c1d111 --- /dev/null +++ b/roles/podman_forgejo/templates/forgejo.network @@ -0,0 +1,2 @@ +[Network] +NetworkName=forgejo diff --git a/roles/podman_forgejo/templates/frontend.network b/roles/podman_forgejo/templates/frontend.network new file mode 100644 index 0000000..379c059 --- /dev/null +++ b/roles/podman_forgejo/templates/frontend.network @@ -0,0 +1,2 @@ +[Network] +NetworkName=frontend diff --git a/roles/podman_forgejo/templates/mariadb.container b/roles/podman_forgejo/templates/mariadb.container new file mode 100644 index 0000000..7926b5a --- /dev/null +++ b/roles/podman_forgejo/templates/mariadb.container @@ -0,0 +1,15 @@ +[Container] +ContainerName=mariadb +Environment=MARIADB_ROOT_PASSWORD={{ podman_forgejo_mariadb_root_password }} +Environment=MARIADB_USER={{ podman_forgejo_mariadb_user }} +Environment=MARIADB_PASSWORD={{ podman_forgejo_mariadb_password }} +Environment=MARIADB_DATABASE={{ podman_forgejo_mariadb_database }} +Image=docker.io/mariadb:11 +Network=forgejo +Volume=/home/{{ podman_forgejo_podman_rootless_user }}/mysql:/var/lib/mysql + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/podman_forgejo/templates/nginx.conf b/roles/podman_forgejo/templates/nginx.conf new file mode 100644 index 0000000..bb8002e --- /dev/null +++ b/roles/podman_forgejo/templates/nginx.conf @@ -0,0 +1,42 @@ +# {{ ansible_managed }} + +server { + listen 80; + listen [::]:80; + + server_name {{ podman_forgejo_web_hostname }}; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://{{ podman_forgejo_web_hostname }}$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name {{ podman_forgejo_web_hostname }}; + server_tokens off; + + ssl_certificate /etc/letsencrypt/live/{{ podman_forgejo_web_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ podman_forgejo_web_hostname }}/privkey.pem; + + location / { + proxy_pass http://forgejo:3000; + + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + client_max_body_size 512M; + } +} diff --git a/roles/podman_host/defaults/main.yml b/roles/podman_host/defaults/main.yml new file mode 100644 index 0000000..558c897 --- /dev/null +++ b/roles/podman_host/defaults/main.yml @@ -0,0 +1,3 @@ +--- +podman_host_minimum_unpriv_port: "22" +podman_host_rootless_users: ["podman"] diff --git a/roles/podman_host/tasks/main.yml b/roles/podman_host/tasks/main.yml new file mode 100644 index 0000000..36e5ca9 --- /dev/null +++ b/roles/podman_host/tasks/main.yml @@ -0,0 +1,69 @@ +--- +- name: set unprivileged port minimum + ansible.posix.sysctl: + name: net.ipv4.ip_unprivileged_port_start + value: "{{ podman_host_minimum_unpriv_port }}" + sysctl_set: true + sysctl_file: /etc/sysctl.d/zzz-podman-unpriv-port.conf + reload: true + become: true + +- name: create users for rootless podman + ansible.builtin.user: + name: "{{ item }}" + become: true + with_items: "{{ podman_host_rootless_users }}" + +- name: set XDG_RUNTIME_DIR in .profile for rootless users + ansible.builtin.lineinfile: + path: "/home/{{ item }}/.bashrc" + line: "export XDG_RUNTIME_DIR=/run/user/$(id -u)" + create: false + become: true + become_user: "{{ item }}" + with_items: "{{ podman_host_rootless_users }}" + +- name: enable linger for rootless users + ansible.builtin.command: + argv: + - /usr/bin/loginctl + - enable-linger + - "{{ item }}" + creates: "/var/lib/systemd/linger/{{ item }}" + become: true + with_items: "{{ podman_host_rootless_users }}" + +- name: create /etc/subuid and /etc/subgid + ansible.builtin.template: + dest: "/etc/{{ item }}" + src: subXid.j2 + with_items: + - subuid + - subgid + become: true + +- name: install podman + ansible.builtin.apt: + pkg: podman + state: latest + become: true + +- name: create quadlets directory + ansible.builtin.file: + path: "/home/{{ item }}/.config/containers/systemd" + state: directory + owner: "{{ item }}" + group: "{{ item }}" + mode: "0700" + with_items: "{{ podman_host_rootless_users }}" + become: true + +- name: enable podman auto update timer for rootless users + ansible.builtin.systemd_service: + name: podman-auto-update.timer + scope: user + state: started + enabled: true + become: true + become_user: "{{ item }}" + with_items: "{{ podman_host_rootless_users }}" diff --git a/roles/podman_host/templates/subXid.j2 b/roles/podman_host/templates/subXid.j2 new file mode 100644 index 0000000..a8022c6 --- /dev/null +++ b/roles/podman_host/templates/subXid.j2 @@ -0,0 +1,4 @@ +# {{ ansible_managed }} +{% for username in podman_host_rootless_users %} +{{ username }}:{{ 100000 + ((loop.index - 1) * 65536) }}:65536 +{% endfor %} diff --git a/roles/podman_identity/defaults/main.yml b/roles/podman_identity/defaults/main.yml new file mode 100644 index 0000000..9a262ac --- /dev/null +++ b/roles/podman_identity/defaults/main.yml @@ -0,0 +1,17 @@ +--- +podman_identity_certbot_testing: false +# podman_identity_keycloak_admin_password: +podman_identity_keycloak_admin_username: admin +podman_identity_keycloak_hostname: "{{ inventory_hostname }}" +podman_identity_keycloak_providers: + - url: https://github.com/jacekkow/keycloak-protocol-cas/releases/download/26.1.2/keycloak-protocol-cas-26.1.2.jar + sha256: de106e8b6b0018a5f121dfa04a14859743e35c5c7ef0abdb01e4a6018d1e2d84 + - url: https://github.com/sventorben/keycloak-restrict-client-auth/releases/download/v26.0.0/keycloak-restrict-client-auth.jar + sha256: 69274e7864f1356f6e14c668787be0c4b8f4d1f4ed28b4e5fa540fa71c8df472 +# podman_identity_ldap_administrator_password: +# podman_identity_ldap_directory_manager_password: +# podman_identity_ldap_database_suffix_dn: +podman_identity_podman_rootless_user: identity +podman_identity_postgres_keycloak_database: keycloak +# podman_identity_postgres_keycloak_password: +podman_identity_postgres_keycloak_username: keycloak diff --git a/roles/podman_identity/handlers/main.yml b/roles/podman_identity/handlers/main.yml new file mode 100644 index 0000000..dcb6f1e --- /dev/null +++ b/roles/podman_identity/handlers/main.yml @@ -0,0 +1,27 @@ +--- +- name: restart ldap + ansible.builtin.systemd_service: + name: ldap + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + +- name: restart postgres + ansible.builtin.systemd_service: + name: postgres + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + +- name: restart keycloak + ansible.builtin.systemd_service: + name: keycloak + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" diff --git a/roles/podman_identity/tasks/main.yml b/roles/podman_identity/tasks/main.yml new file mode 100644 index 0000000..5c29e4f --- /dev/null +++ b/roles/podman_identity/tasks/main.yml @@ -0,0 +1,218 @@ +--- +# TODO: configure ufw + +- name: create service configuration directories + ansible.builtin.file: + path: "/home/{{ podman_identity_podman_rootless_user }}/{{ item }}" + state: directory + owner: "{{ podman_identity_podman_rootless_user }}" + group: "{{ podman_identity_podman_rootless_user }}" + mode: "0755" + become: true + with_items: + - keycloak + - ldap + - postgres + +- name: download keycloak providers + ansible.builtin.get_url: + url: "{{ item.url }}" + dest: "/home/{{ podman_identity_podman_rootless_user }}/keycloak/{{ item.url | basename }}" + checksum: "sha256:{{ item.sha256 }}" + with_items: "{{ podman_identity_keycloak_providers }}" + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + notify: restart keycloak + +- name: install systemd units for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_identity_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_identity_podman_rootless_user }}" + mode: "0400" + with_items: + - ldap.container + - keycloak.container + - postgres.container + notify: + - "restart {{ item | split('.') | first }}" + become: true + +- name: install network quadlets for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_identity_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_identity_podman_rootless_user }}" + mode: "0400" + with_items: + - frontend.network + - ldap.network + - keycloak.network + become: true + +- name: verify quadlets are correctly defined + ansible.builtin.command: /usr/libexec/podman/quadlet -dryrun -user + register: podman_identity_quadlet_result + ignore_errors: true + changed_when: false + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + +- name: assert that the quadlet verification succeeded + ansible.builtin.assert: + that: + - podman_identity_quadlet_result.rc == 0 + fail_msg: "'/usr/libexec/podman/quadlet -dryrun -user' failed! Output withheld to prevent leaking secrets." + +- name: start postgres and keycloak + ansible.builtin.systemd_service: + name: "{{ item }}" + state: started + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + with_items: + - postgres + - keycloak + +- name: set up nginx + ansible.builtin.import_role: + name: podman_nginx + vars: + podman_nginx_podman_rootless_user: "{{ podman_identity_podman_rootless_user }}" + podman_nginx_primary_hostname: "{{ podman_identity_keycloak_hostname }}" + podman_nginx_frontend_network: frontend + +- name: start ldap + ansible.builtin.systemd_service: + name: ldap + state: started + scope: user + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + +- name: create nginx configuration file + ansible.builtin.template: + src: nginx.conf + dest: "/home/{{ podman_identity_podman_rootless_user }}/nginx/nginx.conf" + owner: "{{ podman_identity_podman_rootless_user }}" + group: "{{ podman_identity_podman_rootless_user }}" + mode: "0644" + become: true + notify: restart nginx + +- name: wait 30 seconds for ldap server to start + ansible.builtin.pause: + seconds: 30 + +- name: create ldap suffix + containers.podman.podman_container_exec: + name: ldap + argv: + - dsconf + - -v + - localhost + - backend + - create + - --suffix + - "{{ podman_identity_ldap_database_suffix_dn }}" + - --be-name + - "{{ podman_identity_ldap_database_backend_name }}" + - --create-suffix + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + register: podman_identity_create_suffix + ignore_errors: true + changed_when: false + tags: + - ldap + +- name: create suffix result (only when changed) + debug: + msg: "Suffix was created" + when: not podman_identity_create_suffix.failed + changed_when: not podman_identity_create_suffix.failed + +- name: ldap organisational units + community.general.ldap_entry: + dn: "ou={{ item }},{{ podman_identity_ldap_database_suffix_dn }}" + objectClass: + - top + - organizationalUnit + server_uri: ldaps://{{ inventory_hostname }}/ + bind_dn: "cn=Directory Manager" + bind_pw: "{{ podman_identity_ldap_directory_manager_password }}" + delegate_to: localhost + with_items: + - Administrators + - People + - Groups + environment: + - LDAPTLS_REQCERT: "{% if podman_identity_certbot_testing %}never{% else %}always{% endif %}" + tags: ldap + +- name: enable memberOf plugin + containers.podman.podman_container_exec: + name: ldap + argv: + - dsconf + - -v + - localhost + - -D "cn=Directory Manager" + - plugin + - memberof + - enable + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + tags: + - ldap + +- name: disable anonymous bind + containers.podman.podman_container_exec: + name: ldap + argv: + - dsconf + - -v + - localhost + - -D "cn=Directory Manager" + - config + - replace + - nsslapd-allow-anonymous-access=off + become: true + become_user: "{{ podman_identity_podman_rootless_user }}" + tags: + - ldap + +- name: ldap read-only administrator + community.general.ldap_entry: + dn: "uid=admin,ou=Administrators,{{ podman_identity_ldap_database_suffix_dn }}" + objectClass: + - top + - person + - organizationalPerson + - inetOrgPerson + attributes: + cn: admin + sn: admin + userPassword: "{{ podman_identity_ldap_administrator_password }}" + server_uri: ldaps://{{ inventory_hostname }}/ + bind_dn: "cn=Directory Manager" + bind_pw: "{{ podman_identity_ldap_directory_manager_password }}" + delegate_to: localhost + environment: + - LDAPTLS_REQCERT: "{% if podman_identity_certbot_testing %}never{% else %}always{% endif %}" + tags: ldap + +- name: ldap access control information + community.general.ldap_attrs: + dn: "{{ podman_identity_ldap_database_suffix_dn }}" + attributes: + aci: '(target="ldap:///{{ podman_identity_ldap_database_suffix_dn }}")(targetattr="*") (version 3.0; acl "readonly"; allow (search,read,compare) userdn="ldap:///uid=admin,ou=Administrators,{{ podman_identity_ldap_database_suffix_dn }}";)' + server_uri: ldaps://{{ inventory_hostname }}/ + bind_dn: "cn=Directory Manager" + bind_pw: "{{ podman_identity_ldap_directory_manager_password }}" + delegate_to: localhost + environment: + - LDAPTLS_REQCERT: "{% if podman_identity_certbot_testing %}never{% else %}always{% endif %}" + tags: ldap diff --git a/roles/podman_identity/templates/frontend.network b/roles/podman_identity/templates/frontend.network new file mode 100644 index 0000000..d11e247 --- /dev/null +++ b/roles/podman_identity/templates/frontend.network @@ -0,0 +1,5 @@ +[Network] +Driver=bridge + +[Install] +WantedBy=default.target diff --git a/roles/podman_identity/templates/keycloak.container b/roles/podman_identity/templates/keycloak.container new file mode 100644 index 0000000..2862a12 --- /dev/null +++ b/roles/podman_identity/templates/keycloak.container @@ -0,0 +1,33 @@ +[Unit] +Requires=postgres.service +After=postgres.service + +[Container] +AutoUpdate=registry +ContainerName=keycloak +Environment=\ + KC_DB=postgres \ + KC_DB_PASSWORD={{ podman_identity_postgres_keycloak_password }} \ + KC_DB_URL=jdbc:postgresql://postgres/{{ podman_identity_postgres_keycloak_database }} \ + KC_DB_USERNAME={{ podman_identity_postgres_keycloak_username }} \ + KC_HOSTNAME={{ podman_identity_keycloak_hostname }} \ + KC_HTTP_ENABLED=true \ + KC_HTTP_PORT=8080 \ + KC_PROXY_HEADERS=xforwarded \ + KC_BOOTSTRAP_ADMIN_USERNAME={{ podman_identity_keycloak_admin_username }} \ + KC_BOOTSTRAP_ADMIN_PASSWORD={{ podman_identity_keycloak_admin_password }} \ + PROXY_ADDRESS_FORWARDING=true +Exec=start +Image=quay.io/keycloak/keycloak:26.1 +Network=keycloak.network +Network=ldap.network +Network=frontend.network +{% for provider in podman_identity_keycloak_providers %} +Volume=/home/{{ podman_identity_podman_rootless_user }}/keycloak/{{ provider.url | basename }}:/opt/keycloak/providers/{{ provider.url | basename }}:ro +{% endfor %} + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/podman_identity/templates/keycloak.network b/roles/podman_identity/templates/keycloak.network new file mode 100644 index 0000000..d11e247 --- /dev/null +++ b/roles/podman_identity/templates/keycloak.network @@ -0,0 +1,5 @@ +[Network] +Driver=bridge + +[Install] +WantedBy=default.target diff --git a/roles/podman_identity/templates/ldap.container b/roles/podman_identity/templates/ldap.container new file mode 100644 index 0000000..309556f --- /dev/null +++ b/roles/podman_identity/templates/ldap.container @@ -0,0 +1,17 @@ +[Container] +ContainerName=ldap +Environment=DS_DM_PASSWORD={{ podman_identity_ldap_directory_manager_password }} +Image=quay.io/389ds/dirsrv:latest +Network=ldap.network +PublishPort=636:3636/tcp +Volume=/home/{{ podman_identity_podman_rootless_user }}/ldap:/data:rw +Volume=/home/{{ podman_identity_podman_rootless_user }}/certbot/conf/live/{{ podman_identity_keycloak_hostname }}/privkey.pem:/data/tls/server.key:ro +Volume=/home/{{ podman_identity_podman_rootless_user }}/certbot/conf/live/{{ podman_identity_keycloak_hostname }}/cert.pem:/data/tls/server.crt:ro +Volume=/home/{{ podman_identity_podman_rootless_user }}/certbot/conf/live/{{ podman_identity_keycloak_hostname }}/chain.pem:/data/tls/ca/chain.crt:ro + +[Service] +RuntimeMaxSec=604800 +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/podman_identity/templates/ldap.network b/roles/podman_identity/templates/ldap.network new file mode 100644 index 0000000..d11e247 --- /dev/null +++ b/roles/podman_identity/templates/ldap.network @@ -0,0 +1,5 @@ +[Network] +Driver=bridge + +[Install] +WantedBy=default.target diff --git a/roles/podman_identity/templates/nginx.conf b/roles/podman_identity/templates/nginx.conf new file mode 100644 index 0000000..4177926 --- /dev/null +++ b/roles/podman_identity/templates/nginx.conf @@ -0,0 +1,39 @@ +# {{ ansible_managed }} + +server { + listen 80; + listen [::]:80; + + server_name {{ podman_identity_keycloak_hostname }}; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://{{ podman_identity_keycloak_hostname }}$request_uri; + } +} + +server { + listen 443 default_server ssl; + listen [::]:443 ssl; + http2 on; + + server_name {{ podman_identity_keycloak_hostname }}; + server_tokens off; + + ssl_certificate /etc/letsencrypt/live/{{ podman_identity_keycloak_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ podman_identity_keycloak_hostname }}/privkey.pem; + + location / { + proxy_pass http://keycloak:8080/; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port 443; + } +} diff --git a/roles/podman_identity/templates/postgres.container b/roles/podman_identity/templates/postgres.container new file mode 100644 index 0000000..3f5e10b --- /dev/null +++ b/roles/podman_identity/templates/postgres.container @@ -0,0 +1,15 @@ +[Container] +AutoUpdate=registry +ContainerName=postgres +Environment=\ + POSTGRES_DB={{ podman_identity_postgres_keycloak_database }} \ + POSTGRES_PASSWORD={{ podman_identity_postgres_keycloak_password }} \ + POSTGRES_USER={{ podman_identity_postgres_keycloak_username }} \ + POSTGRES_HOST_AUTH_METHOD=scram-sha-256 \ + POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 +Image=docker.io/postgres:17.3 +Network=keycloak.network +Volume=/home/{{ podman_identity_podman_rootless_user }}/postgres:/var/lib/postgresql/data:rw + +[Install] +WantedBy=default.target diff --git a/roles/podman_nginx/defaults/main.yml b/roles/podman_nginx/defaults/main.yml new file mode 100644 index 0000000..5d6b76e --- /dev/null +++ b/roles/podman_nginx/defaults/main.yml @@ -0,0 +1,6 @@ +--- +podman_nginx_additional_hostnames: [] +podman_nginx_certbot_testing: false +# podman_nginx_frontend_network: +podman_nginx_podman_rootless_user: nginx +# podman_nginx_primary_hostname: \ No newline at end of file diff --git a/roles/podman_nginx/handlers/main.yml b/roles/podman_nginx/handlers/main.yml new file mode 100644 index 0000000..f1b17d9 --- /dev/null +++ b/roles/podman_nginx/handlers/main.yml @@ -0,0 +1,18 @@ +--- +- name: restart certbot-renew + ansible.builtin.systemd_service: + name: certbot-renew + state: started + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: restart nginx + ansible.builtin.systemd_service: + name: nginx + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" diff --git a/roles/podman_nginx/tasks/main.yml b/roles/podman_nginx/tasks/main.yml new file mode 100644 index 0000000..7d3f3c8 --- /dev/null +++ b/roles/podman_nginx/tasks/main.yml @@ -0,0 +1,110 @@ +--- +- name: create service configuration directories + ansible.builtin.file: + path: "/home/{{ podman_nginx_podman_rootless_user }}/{{ item }}" + state: directory + owner: "{{ podman_nginx_podman_rootless_user }}" + group: "{{ podman_nginx_podman_rootless_user }}" + mode: "0755" + become: true + with_items: + - .config/systemd/user + - certbot/conf + - certbot/www + - nginx + +- name: install podman quadlet for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_nginx_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_nginx_podman_rootless_user }}" + mode: "0400" + with_items: + - certbot-renew.container + - nginx.container + notify: + - "restart {{ item | split('.') | first }}" + become: true + +- name: install certbot renewal timer for rootless podman user + ansible.builtin.template: + src: "certbot-renew.timer" + dest: "/home/{{ podman_nginx_podman_rootless_user }}/.config/systemd/user/certbot-renew.timer" + owner: "{{ podman_nginx_podman_rootless_user }}" + mode: "0400" + become: true + +- name: verify quadlets are correctly defined + ansible.builtin.command: /usr/libexec/podman/quadlet -dryrun -user + register: podman_nginx_quadlet_result + ignore_errors: true + changed_when: false + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: check if certificate exists + stat: + path: "/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf/live/{{ podman_nginx_primary_hostname }}/fullchain.pem" + register: podman_nginx_cert_stat + become: yes + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: create temporary nginx configuration (no https) + ansible.builtin.template: + src: nginx.conf + dest: "/home/{{ podman_nginx_podman_rootless_user }}/nginx/nginx.conf" + owner: "{{ podman_nginx_podman_rootless_user }}" + group: "{{ podman_nginx_podman_rootless_user }}" + mode: "0644" + become: true + when: podman_nginx_cert_stat.stat.exists == false + +- name: start nginx + ansible.builtin.systemd_service: + name: nginx + state: started + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: run certbot container to create certificate + ansible.builtin.command: + cmd: > + podman run --name certbot-generate + --rm + --volume /home/{{ podman_nginx_podman_rootless_user }}/certbot/www:/var/www/certbot:rw + --volume /home/{{ podman_nginx_podman_rootless_user }}/certbot/conf:/etc/letsencrypt:rw + docker.io/certbot/certbot:latest + certonly + --register-unsafely-without-email + --agree-tos + --webroot + --webroot-path /var/www/certbot/ + -d "{{ podman_nginx_primary_hostname }}" + {% for hostname in podman_nginx_additional_hostnames %} -d "{{ hostname }}"{% endfor %} + {% if podman_nginx_certbot_testing %} --test-cert{% endif %} + when: podman_nginx_cert_stat.stat.exists == false + become: yes + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: check if certificate exists + stat: + path: "/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf/live/{{ podman_nginx_primary_hostname }}/fullchain.pem" + register: podman_nginx_cert_stat + become: yes + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: ensure certificate exists now + ansible.builtin.assert: + that: + - podman_nginx_cert_stat.stat.exists + fail_msg: "Failed to get a Lets Encrypt certificate." + +- name: start certbot renewal timer + ansible.builtin.systemd_service: + name: "certbot-renew.timer" + state: started + scope: user + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" diff --git a/roles/podman_nginx/templates/certbot-renew.container b/roles/podman_nginx/templates/certbot-renew.container new file mode 100644 index 0000000..7d28fd4 --- /dev/null +++ b/roles/podman_nginx/templates/certbot-renew.container @@ -0,0 +1,13 @@ +[Unit] +Description=Run certbot renew + +[Container] +AutoUpdate=registry +ContainerName=certbot-renew +Exec=renew +Image=docker.io/certbot/certbot:latest +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/www:/var/www/certbot +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf:/etc/letsencrypt + +[Service] +Restart=no diff --git a/roles/podman_nginx/templates/certbot-renew.timer b/roles/podman_nginx/templates/certbot-renew.timer new file mode 100644 index 0000000..65f32c1 --- /dev/null +++ b/roles/podman_nginx/templates/certbot-renew.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Timer for certbot renewals + +[Timer] +OnCalendar=daily +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/podman_nginx/templates/nginx.conf b/roles/podman_nginx/templates/nginx.conf new file mode 100644 index 0000000..aea3186 --- /dev/null +++ b/roles/podman_nginx/templates/nginx.conf @@ -0,0 +1,17 @@ +# {{ ansible_managed }} + +server { + listen 80; + listen [::]:80; + + server_name {{ podman_nginx_primary_hostname }}; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://{{ podman_nginx_primary_hostname }}$request_uri; + } +} diff --git a/roles/podman_nginx/templates/nginx.container b/roles/podman_nginx/templates/nginx.container new file mode 100644 index 0000000..fb8cafe --- /dev/null +++ b/roles/podman_nginx/templates/nginx.container @@ -0,0 +1,16 @@ +[Container] +ContainerName=nginx +Image=docker.io/nginx:latest +{% if podman_nginx_frontend_network is defined %}Network={{ podman_nginx_frontend_network }}.network{% endif +%} +PublishPort=80:80 +PublishPort=443:443 +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/www:/var/www/certbot/:ro +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf/:/etc/letsencrypt/:ro +Volume=/home/{{ podman_nginx_podman_rootless_user }}/nginx:/etc/nginx/conf.d/:ro + +[Service] +RuntimeMaxSec=604800 +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/system_baseline/defaults/main.yml b/roles/system_baseline/defaults/main.yml new file mode 100644 index 0000000..ff6ac36 --- /dev/null +++ b/roles/system_baseline/defaults/main.yml @@ -0,0 +1,12 @@ +--- +system_baseline_admin_users: + - user: irl + comment: irl + ssh_public_key: "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIJpoCJEax0XTNK6qfYfZV60euSwoc0RQ0bwFDQGMWYQnAAAABHNzaDo=" +system_baseline_admin_user_groups_debian: + - adm + - staff + - sudo + - systemd-journal +system_baseline_retired_admin_users: [] +system_baseline_service_users: [] diff --git a/roles/system_baseline/handlers/main.yml b/roles/system_baseline/handlers/main.yml new file mode 100644 index 0000000..b5768b2 --- /dev/null +++ b/roles/system_baseline/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: reload sshd + service: + name: sshd + state: reloaded + become: true diff --git a/roles/system_baseline/tasks/main.yml b/roles/system_baseline/tasks/main.yml new file mode 100644 index 0000000..3755a09 --- /dev/null +++ b/roles/system_baseline/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: upgrade debian packages (apt) + ansible.builtin.apt: + upgrade: safe + cache_valid_time: 3600 + become: true + when: ansible_distribution == 'Debian' + +- name: install system packages (dnf) + dnf: + name: "*" + state: latest + update_cache: true + become: true + when: ansible_distribution == 'AlmaLinux' + +- name: setup users + ansible.builtin.include_tasks: + file: users.yml + +- name: setup OpenSSH server + ansible.builtin.include_tasks: + file: sshd.yml + +- name: remove root authorised keys + ansible.builtin.file: + path: /root/.ssh/authorized_keys + state: absent + become: true diff --git a/roles/system_baseline/tasks/sshd.yml b/roles/system_baseline/tasks/sshd.yml new file mode 100644 index 0000000..cf26884 --- /dev/null +++ b/roles/system_baseline/tasks/sshd.yml @@ -0,0 +1,24 @@ +--- +- name: sshd PermitRootLogin=no + lineinfile: + dest: "/etc/ssh/sshd_config" + regexp: "^#?\\w*PermitRootLogin" + line: "PermitRootLogin no" + state: present + become: true + notify: "reload sshd" + +- name: sshd PasswordAuthentication=no + lineinfile: + dest: "/etc/ssh/sshd_config" + regexp: "^#?\\w*PasswordAuthentication" + line: "PasswordAuthentication no" + state: present + become: true + notify: "reload sshd" + +- name: retrieve ssh host key + fetch: + src: "/etc/ssh/ssh_host_ed25519_key.pub" + dest: "files/ssh_host_keys/{{ inventory_hostname }}_ed25519.pub" + flat: yes diff --git a/roles/system_baseline/tasks/users.yml b/roles/system_baseline/tasks/users.yml new file mode 100644 index 0000000..59f02c1 --- /dev/null +++ b/roles/system_baseline/tasks/users.yml @@ -0,0 +1,74 @@ +--- +- name: create a group for admin users + ansible.builtin.group: + name: ops + state: present + become: true + +- name: create admin users + ansible.builtin.user: + name: "{{ item.user }}" + comment: "{{ item.comment | default(item.user) }}" + group: ops + with_items: "{{ system_baseline_admin_users }}" + become: true + +- name: remove retired admin users + ansible.builtin.user: + name: "{{ item }}" + state: absent + with_items: "{{ system_baseline_retired_admin_users }}" + become: true + +- name: additional groups for admin users (Debian only) + ansible.builtin.user: + name: "{{ item.user }}" + groups: "{{ system_baseline_admin_user_groups_debian }}" + append: true + with_items: "{{ system_baseline_admin_users }}" + become: true + when: ansible_distribution == 'Debian' + +- name: install SSH keys for admin users + ansible.posix.authorized_key: + user: "{{ item.user }}" + state: present + key: "{{ item.ssh_public_key }}" + exclusive: true + with_items: "{{ system_baseline_admin_users }}" + become: true + +- name: allow passwordless sudo for sudo group (Debian only) + ansible.builtin.lineinfile: + path: /etc/sudoers + state: present + regexp: "^#?\\w*%sudo " + line: "%sudo ALL=(ALL) NOPASSWD: ALL" + validate: "/usr/sbin/visudo -cf %s" + become: true + when: ansible_distribution == 'Debian' + +- name: create a group for service users + ansible.builtin.group: + name: services + state: present + become: true + +- name: create service users + ansible.builtin.user: + name: "{{ item.user }}" + comment: "{{ item.comment | default(item.user) }}" + group: services + with_items: "{{ system_baseline_service_users }}" + become: true + +- name: enable linger for service users + ansible.builtin.command: + argv: + - /usr/bin/loginctl + - enable-linger + - "{{ item.user }}" + creates: "/var/lib/systemd/linger/{{ item.user }}" + when: "ansible_distribution == 'Debian' and (item.linger is not defined or item.linger)" + become: true + with_items: "{{ system_baseline_service_users }}" diff --git a/roles/vps/tasks/main.yml b/roles/vps/tasks/main.yml new file mode 100644 index 0000000..759a183 --- /dev/null +++ b/roles/vps/tasks/main.yml @@ -0,0 +1,35 @@ +--- +# https://support.solusvm.com/hc/en-us/articles/21334950006807-How-to-install-Guest-Tools-manually-inside-a-VM-in-SolusVM-2 + +- name: install required packages + apt: + pkg: + - qemu-guest-agent + - cloud-init + - tuned + state: latest + cache_valid_time: 3600 + become: true + when: ansible_distribution == 'Debian' + +- name: install required packages + dnf: + name: + - qemu-guest-agent + - cloud-init + - tuned + state: latest + update_cache: true + become: true + when: ansible_distribution == 'AlmaLinux' + +- name: check tuned profile + command: tuned-adm active + register: vps_tuned_profile + become: true + changed_when: false + +- name: start tuned profile + shell: tuned-adm profile virtual-guest + become: true + when: "'virtual-guest' not in vps_tuned_profile.stdout"