feat: initial commit

This commit is contained in:
Iain Learmonth 2025-06-02 14:55:56 +01:00
commit 072a1ed764
36 changed files with 1089 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.retry
.ansible/

53
README.md Normal file
View file

@ -0,0 +1,53 @@
<h1 align="center">
<br>
<img src="https://guardianproject.dev/irl/.profile/raw/branch/main/header.png" alt="irl.xyz">
<br>
</h1>
<p align="center">
<a href="https://docs.ansible.com/">
<img alt="Language: Ansible" src="https://img.shields.io/badge/Language-Ansible-CC0001?style=flat-square&label=Language">
</a>
<a href="https://opensource.org/licenses/BSD-2-Clause">
<img alt="Licence: BSD 2-Clause" src="https://img.shields.io/badge/License-BSD%202--Clause-orange?style=flat-square">
</a>
<img alt="Lifecycle: Experimental" src="https://img.shields.io/badge/Lifecycle-Experimental-339999?style=flat-square">
</p>
## 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.

View file

@ -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 }}"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
[Network]
NetworkName=forgejo

View file

@ -0,0 +1,2 @@
[Network]
NetworkName=frontend

View file

@ -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

View file

@ -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;
}
}

View file

@ -0,0 +1,3 @@
---
podman_host_minimum_unpriv_port: "22"
podman_host_rootless_users: ["podman"]

View file

@ -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 }}"

View file

@ -0,0 +1,4 @@
# {{ ansible_managed }}
{% for username in podman_host_rootless_users %}
{{ username }}:{{ 100000 + ((loop.index - 1) * 65536) }}:65536
{% endfor %}

View file

@ -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

View file

@ -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 }}"

View file

@ -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

View file

@ -0,0 +1,5 @@
[Network]
Driver=bridge
[Install]
WantedBy=default.target

View file

@ -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

View file

@ -0,0 +1,5 @@
[Network]
Driver=bridge
[Install]
WantedBy=default.target

View file

@ -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

View file

@ -0,0 +1,5 @@
[Network]
Driver=bridge
[Install]
WantedBy=default.target

View file

@ -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;
}
}

View file

@ -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

View file

@ -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:

View file

@ -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 }}"

View file

@ -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 }}"

View file

@ -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

View file

@ -0,0 +1,9 @@
[Unit]
Description=Timer for certbot renewals
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target

View file

@ -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;
}
}

View file

@ -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

View file

@ -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: []

View file

@ -0,0 +1,6 @@
---
- name: reload sshd
service:
name: sshd
state: reloaded
become: true

View file

@ -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

View file

@ -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

View file

@ -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 }}"

35
roles/vps/tasks/main.yml Normal file
View file

@ -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"