init
This commit is contained in:
commit
883f31932e
169 changed files with 5676 additions and 0 deletions
5
ansible/roles/apache/defaults/main.yml
Normal file
5
ansible/roles/apache/defaults/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
ssl_cert_tiararodney: /etc/letsencrypt/live/tiararodney.com/fullchain.pem
|
||||
ssl_key_tiararodney: /etc/letsencrypt/live/tiararodney.com/privkey.pem
|
||||
ssl_cert_administratrix: /etc/letsencrypt/live/administratrix.io/fullchain.pem
|
||||
ssl_key_administratrix: /etc/letsencrypt/live/administratrix.io/privkey.pem
|
||||
6
ansible/roles/apache/handlers/main.yml
Normal file
6
ansible/roles/apache/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
-
|
||||
name: reload apache
|
||||
service:
|
||||
name: "{{ apache_service }}"
|
||||
state: reloaded
|
||||
2
ansible/roles/apache/meta/main.yml
Normal file
2
ansible/roles/apache/meta/main.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
dependencies: []
|
||||
18
ansible/roles/apache/tasks/deploy-reverse-proxy.yml
Normal file
18
ansible/roles/apache/tasks/deploy-reverse-proxy.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
-
|
||||
name: Ensure Apache is installed
|
||||
include_tasks: main.yml
|
||||
|
||||
-
|
||||
name: "Deploy {{ vhost_name }} reverse proxy vhost"
|
||||
template:
|
||||
src: reverse-proxy-vhost.conf.j2
|
||||
dest: "{{ apache_sites_available }}/{{ vhost_name }}.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: "Enable {{ vhost_name }} site"
|
||||
command: "{{ apache_enable_site_cmd }} {{ vhost_name }}"
|
||||
args:
|
||||
creates: "{{ apache_sites_enabled }}/{{ vhost_name }}.conf"
|
||||
notify: reload apache
|
||||
27
ansible/roles/apache/tasks/deploy-static-site.yml
Normal file
27
ansible/roles/apache/tasks/deploy-static-site.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
-
|
||||
name: Ensure Apache is installed
|
||||
include_tasks: main.yml
|
||||
|
||||
-
|
||||
name: Ensure document root exists
|
||||
file:
|
||||
path: "{{ document_root }}"
|
||||
state: directory
|
||||
owner: www-data
|
||||
group: www-data
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy vhost configuration
|
||||
template:
|
||||
src: static-site-vhost.conf.j2
|
||||
dest: "{{ apache_sites_available }}/{{ name }}.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Enable site
|
||||
command: "{{ apache_enable_site_cmd }} {{ name }}"
|
||||
args:
|
||||
creates: "{{ apache_sites_enabled }}/{{ name }}.conf"
|
||||
notify: reload apache
|
||||
83
ansible/roles/apache/tasks/install-apache.yml
Normal file
83
ansible/roles/apache/tasks/install-apache.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
-
|
||||
name: Ensure letsencrypt directory exists
|
||||
file:
|
||||
path: /etc/letsencrypt
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
-
|
||||
name: Deploy SSL certificates
|
||||
unarchive:
|
||||
src: "{{ letsencrypt_archive }}"
|
||||
dest: /etc/letsencrypt/
|
||||
when: letsencrypt_archive is defined
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Ensure SSL private keys are readable by containers
|
||||
shell: find /etc/letsencrypt -name 'privkey*.pem' -exec chmod 644 {} +
|
||||
changed_when: false
|
||||
when: letsencrypt_archive is defined
|
||||
|
||||
-
|
||||
name: Install Apache
|
||||
apt:
|
||||
name: "{{ apache_package }}"
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
-
|
||||
name: Enable Apache modules
|
||||
community.general.apache2_module:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
loop:
|
||||
- proxy
|
||||
- proxy_http
|
||||
- proxy_wstunnel
|
||||
- ssl
|
||||
- rewrite
|
||||
- headers
|
||||
- auth_basic
|
||||
- autoindex
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Disable default site
|
||||
command: "{{ apache_disable_site_cmd }} 000-default"
|
||||
args:
|
||||
removes: "{{ apache_sites_enabled }}/000-default.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Ensure tiararodney.com document root exists
|
||||
file:
|
||||
path: /var/www/tiararodney.com
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy tiararodney.com vhost
|
||||
template:
|
||||
src: 000-default-redirect.conf.j2
|
||||
dest: "{{ apache_sites_available }}/000-default-redirect.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Enable tiararodney.com redirect vhost
|
||||
command: "{{ apache_enable_site_cmd }} 000-default-redirect"
|
||||
args:
|
||||
creates: "{{ apache_sites_enabled }}/000-default-redirect.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Ensure Apache is started and enabled
|
||||
service:
|
||||
name: "{{ apache_service }}"
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
-
|
||||
name: Ensure Apache is reloaded with current config
|
||||
meta: flush_handlers
|
||||
8
ansible/roles/apache/tasks/main.yml
Normal file
8
ansible/roles/apache/tasks/main.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
-
|
||||
name: Load OS-specific variables
|
||||
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
|
||||
|
||||
-
|
||||
name: Install and configure Apache
|
||||
ansible.builtin.include_tasks: install-apache.yml
|
||||
18
ansible/roles/apache/templates/000-default-redirect.conf.j2
Normal file
18
ansible/roles/apache/templates/000-default-redirect.conf.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName tiararodney.com
|
||||
Redirect permanent / https://tiararodney.com/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName tiararodney.com
|
||||
SSLEngine on
|
||||
SSLCertificateFile {{ ssl_cert_tiararodney }}
|
||||
SSLCertificateKeyFile {{ ssl_key_tiararodney }}
|
||||
|
||||
DocumentRoot /var/www/tiararodney.com
|
||||
<Directory /var/www/tiararodney.com>
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
31
ansible/roles/apache/templates/reverse-proxy-vhost.conf.j2
Normal file
31
ansible/roles/apache/templates/reverse-proxy-vhost.conf.j2
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName {{ server_name }}
|
||||
Redirect permanent / https://{{ server_name }}/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName {{ server_name }}
|
||||
SSLEngine on
|
||||
SSLCertificateFile {{ ssl_cert }}
|
||||
SSLCertificateKeyFile {{ ssl_key }}
|
||||
|
||||
{% for loc in restricted_locations | default([]) %}
|
||||
<Location {{ loc.path }}>
|
||||
{% for ip in loc.allowed_ips %}
|
||||
Require ip {{ ip }}
|
||||
{% endfor %}
|
||||
</Location>
|
||||
{% endfor %}
|
||||
|
||||
ProxyPreserveHost on
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
RequestHeader set X-Forwarded-Ssl "on"
|
||||
{% if websocket | default(false) %}
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/(.*) ws://{{ backend_host | default('127.0.0.1') }}:{{ backend_port }}/$1 [P,L]
|
||||
{% endif %}
|
||||
ProxyPass / http://{{ backend_host | default('127.0.0.1') }}:{{ backend_port }}/
|
||||
ProxyPassReverse / http://{{ backend_host | default('127.0.0.1') }}:{{ backend_port }}/
|
||||
</VirtualHost>
|
||||
21
ansible/roles/apache/templates/static-site-vhost.conf.j2
Normal file
21
ansible/roles/apache/templates/static-site-vhost.conf.j2
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName {{ server_name }}
|
||||
Redirect permanent / https://{{ server_name }}/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName {{ server_name }}
|
||||
SSLEngine on
|
||||
SSLCertificateFile {{ ssl_cert }}
|
||||
SSLCertificateKeyFile {{ ssl_key }}
|
||||
|
||||
DocumentRoot {{ document_root }}
|
||||
<Directory {{ document_root }}>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
{% if directory_index is defined %}
|
||||
DirectoryIndex {{ directory_index }}
|
||||
{% endif %}
|
||||
</VirtualHost>
|
||||
7
ansible/roles/apache/vars/Debian.yml
Normal file
7
ansible/roles/apache/vars/Debian.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
apache_package: apache2
|
||||
apache_service: apache2
|
||||
apache_sites_available: /etc/apache2/sites-available
|
||||
apache_sites_enabled: /etc/apache2/sites-enabled
|
||||
apache_enable_site_cmd: a2ensite
|
||||
apache_disable_site_cmd: a2dissite
|
||||
9
ansible/roles/authentik/defaults/main.yml
Normal file
9
ansible/roles/authentik/defaults/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
install_dir: /opt/authentik
|
||||
bind_address: "0.0.0.0"
|
||||
port: 9000
|
||||
postgres_shared_buffers: 64MB
|
||||
postgres_work_mem: 4MB
|
||||
postgres_maintenance_work_mem: 32MB
|
||||
postgres_effective_cache_size: 256MB
|
||||
redis_maxmemory: 80mb
|
||||
0
ansible/roles/authentik/files/branding/.gitkeep
Normal file
0
ansible/roles/authentik/files/branding/.gitkeep
Normal file
4
ansible/roles/authentik/meta/main.yml
Normal file
4
ansible/roles/authentik/meta/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
dependencies:
|
||||
-
|
||||
role: docker
|
||||
92
ansible/roles/authentik/tasks/deploy-authentik.yml
Normal file
92
ansible/roles/authentik/tasks/deploy-authentik.yml
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
-
|
||||
name: Ensure install directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy environment file
|
||||
template:
|
||||
src: env.j2
|
||||
dest: "{{ install_dir }}/.env"
|
||||
|
||||
-
|
||||
name: Ensure blueprints directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}/blueprints"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy OAuth2 blueprint
|
||||
template:
|
||||
src: blueprint-oauth2.yml.j2
|
||||
dest: "{{ install_dir }}/blueprints/oauth2-applications.yaml"
|
||||
when: oauth_applications is defined and oauth_applications | length > 0
|
||||
|
||||
-
|
||||
name: Deploy enrollment blueprint
|
||||
template:
|
||||
src: blueprint-enrollment.yml.j2
|
||||
dest: "{{ install_dir }}/blueprints/enrollment.yaml"
|
||||
|
||||
-
|
||||
name: Deploy social login blueprint
|
||||
template:
|
||||
src: blueprint-social-logins.yml.j2
|
||||
dest: "{{ install_dir }}/blueprints/social-logins.yaml"
|
||||
when: social_login_sources is defined and social_login_sources | length > 0
|
||||
|
||||
-
|
||||
name: Ensure media directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}/media/public"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Copy branding assets
|
||||
copy:
|
||||
src: branding/
|
||||
dest: "{{ install_dir }}/media/public/"
|
||||
mode: "0644"
|
||||
when: branding_assets | default(false)
|
||||
|
||||
-
|
||||
name: Ensure custom-templates email directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}/custom-templates/email"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy custom email templates
|
||||
template:
|
||||
src: "email/{{ item }}.j2"
|
||||
dest: "{{ install_dir }}/custom-templates/email/{{ item }}"
|
||||
loop:
|
||||
- account-confirmation.html
|
||||
- password-reset.html
|
||||
|
||||
-
|
||||
name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
|
||||
-
|
||||
name: Start Authentik stack
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: start-compose
|
||||
vars:
|
||||
compose_project_dir: "{{ install_dir }}"
|
||||
|
||||
-
|
||||
name: Deploy Authentik backup script
|
||||
template:
|
||||
src: backup.sh.j2
|
||||
dest: /etc/restic/pre-backup.d/authentik.sh
|
||||
mode: "0755"
|
||||
4
ansible/roles/authentik/tasks/main.yml
Normal file
4
ansible/roles/authentik/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy Authentik
|
||||
ansible.builtin.include_tasks: deploy-authentik.yml
|
||||
46
ansible/roles/authentik/tasks/restore-authentik.yml
Normal file
46
ansible/roles/authentik/tasks/restore-authentik.yml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
-
|
||||
name: Set backup staging directory
|
||||
set_fact:
|
||||
_authentik_backup_dir: "{{ backup_staging_dir | default('/var/backups') }}/authentik"
|
||||
|
||||
-
|
||||
name: Stop Authentik stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: absent
|
||||
|
||||
-
|
||||
name: Restore config files
|
||||
copy:
|
||||
src: "{{ _authentik_backup_dir }}/{{ item }}"
|
||||
dest: "{{ install_dir }}/{{ item }}"
|
||||
remote_src: yes
|
||||
mode: "0600"
|
||||
loop:
|
||||
- .env
|
||||
- docker-compose.yml
|
||||
|
||||
-
|
||||
name: Start Postgres only
|
||||
command: >
|
||||
docker compose -f {{ install_dir }}/docker-compose.yml
|
||||
up -d postgres
|
||||
|
||||
-
|
||||
name: Wait for Postgres to be ready
|
||||
pause:
|
||||
seconds: 10
|
||||
|
||||
-
|
||||
name: Restore Postgres dump
|
||||
shell: >
|
||||
docker compose -f {{ install_dir }}/docker-compose.yml
|
||||
exec -T postgres psql -U authentik authentik
|
||||
< {{ _authentik_backup_dir }}/authentik.sql
|
||||
|
||||
-
|
||||
name: Start full Authentik stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: present
|
||||
7
ansible/roles/authentik/templates/backup.sh.j2
Normal file
7
ansible/roles/authentik/templates/backup.sh.j2
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
BACKUP_DIR="{{ backup_staging_dir | default('/var/backups') }}/authentik"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
docker compose -f {{ install_dir }}/docker-compose.yml exec -T postgres pg_dump -U authentik authentik > "$BACKUP_DIR/authentik.sql"
|
||||
cp "{{ install_dir }}/.env" "$BACKUP_DIR/.env"
|
||||
cp "{{ install_dir }}/docker-compose.yml" "$BACKUP_DIR/docker-compose.yml"
|
||||
326
ansible/roles/authentik/templates/blueprint-enrollment.yml.j2
Normal file
326
ansible/roles/authentik/templates/blueprint-enrollment.yml.j2
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
version: 1
|
||||
metadata:
|
||||
name: enrollment-flow
|
||||
entries:
|
||||
# --- Brand configuration ---
|
||||
- model: authentik_brands.brand
|
||||
identifiers:
|
||||
domain: authentik-default
|
||||
state: present
|
||||
attrs:
|
||||
branding_title: {{ domain }}
|
||||
{% if branding_logo is defined %}
|
||||
branding_logo: {{ branding_logo }}
|
||||
{% endif %}
|
||||
{% if branding_favicon is defined %}
|
||||
branding_favicon: {{ branding_favicon }}
|
||||
{% endif %}
|
||||
|
||||
# --- Enrollment flow ---
|
||||
- model: authentik_flows.flow
|
||||
id: enrollment-flow
|
||||
identifiers:
|
||||
slug: default-enrollment-flow
|
||||
attrs:
|
||||
name: Sign Up
|
||||
title: Create an account
|
||||
designation: enrollment
|
||||
authentication: require_unauthenticated
|
||||
|
||||
# --- Recovery flow ---
|
||||
- model: authentik_flows.flow
|
||||
id: recovery-flow
|
||||
identifiers:
|
||||
slug: default-recovery-flow
|
||||
attrs:
|
||||
name: Password Recovery
|
||||
title: Reset your password
|
||||
designation: recovery
|
||||
authentication: require_unauthenticated
|
||||
|
||||
# --- Prompt fields ---
|
||||
- model: authentik_stages_prompt.prompt
|
||||
id: field-email
|
||||
identifiers:
|
||||
name: enrollment-field-email
|
||||
attrs:
|
||||
field_key: email
|
||||
label: Email
|
||||
type: email
|
||||
required: true
|
||||
placeholder: Email
|
||||
placeholder_expression: false
|
||||
order: 0
|
||||
|
||||
- model: authentik_stages_prompt.prompt
|
||||
id: field-username
|
||||
identifiers:
|
||||
name: enrollment-field-username
|
||||
attrs:
|
||||
field_key: username
|
||||
label: Username
|
||||
type: username
|
||||
required: true
|
||||
placeholder: Username
|
||||
placeholder_expression: false
|
||||
order: 1
|
||||
|
||||
- model: authentik_stages_prompt.prompt
|
||||
id: field-password
|
||||
identifiers:
|
||||
name: enrollment-field-password
|
||||
attrs:
|
||||
field_key: password
|
||||
label: Password
|
||||
type: password
|
||||
required: true
|
||||
placeholder: Password
|
||||
placeholder_expression: false
|
||||
order: 2
|
||||
|
||||
- model: authentik_stages_prompt.prompt
|
||||
id: field-password-repeat
|
||||
identifiers:
|
||||
name: enrollment-field-password-repeat
|
||||
attrs:
|
||||
field_key: password_repeat
|
||||
label: Password (repeat)
|
||||
type: password
|
||||
required: true
|
||||
placeholder: Password (repeat)
|
||||
placeholder_expression: false
|
||||
order: 3
|
||||
|
||||
# --- Password policy ---
|
||||
- model: authentik_policies_password.passwordpolicy
|
||||
id: password-policy
|
||||
identifiers:
|
||||
name: enrollment-password-policy
|
||||
attrs:
|
||||
name: enrollment-password-policy
|
||||
length_min: 10
|
||||
amount_uppercase: 1
|
||||
amount_lowercase: 1
|
||||
amount_digits: 1
|
||||
amount_symbols: 1
|
||||
check_static_rules: true
|
||||
check_have_i_been_pwned: true
|
||||
check_zxcvbn: true
|
||||
zxcvbn_score_threshold: 3
|
||||
error_message: "Password must be at least 10 characters with uppercase, lowercase, digit, and symbol."
|
||||
|
||||
# --- Enrollment stages ---
|
||||
- model: authentik_stages_prompt.promptstage
|
||||
id: enrollment-prompt-stage
|
||||
identifiers:
|
||||
name: enrollment-prompt
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf field-email
|
||||
- !KeyOf field-username
|
||||
- !KeyOf field-password
|
||||
- !KeyOf field-password-repeat
|
||||
validation_policies:
|
||||
- !KeyOf password-policy
|
||||
|
||||
- model: authentik_stages_user_write.userwritestage
|
||||
id: enrollment-user-write
|
||||
identifiers:
|
||||
name: enrollment-user-write
|
||||
attrs:
|
||||
user_creation_mode: always_create
|
||||
create_users_as_inactive: true
|
||||
|
||||
- model: authentik_stages_email.emailstage
|
||||
id: enrollment-email-verification
|
||||
identifiers:
|
||||
name: enrollment-email-verification
|
||||
attrs:
|
||||
use_global_settings: true
|
||||
activate_user_on_success: true
|
||||
subject: Verify your email address
|
||||
template: email/account-confirmation.html
|
||||
|
||||
- model: authentik_stages_user_login.userloginstage
|
||||
id: enrollment-user-login
|
||||
identifiers:
|
||||
name: enrollment-user-login
|
||||
|
||||
# --- Enrollment flow stage bindings ---
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf enrollment-flow
|
||||
stage: !KeyOf enrollment-prompt-stage
|
||||
order: 10
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf enrollment-flow
|
||||
stage: !KeyOf enrollment-user-write
|
||||
order: 20
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf enrollment-flow
|
||||
stage: !KeyOf enrollment-email-verification
|
||||
order: 30
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf enrollment-flow
|
||||
stage: !KeyOf enrollment-user-login
|
||||
order: 100
|
||||
|
||||
# --- Recovery stages ---
|
||||
- model: authentik_stages_identification.identificationstage
|
||||
id: recovery-identification
|
||||
identifiers:
|
||||
name: recovery-identification
|
||||
attrs:
|
||||
user_fields:
|
||||
- email
|
||||
|
||||
- model: authentik_stages_email.emailstage
|
||||
id: recovery-email
|
||||
identifiers:
|
||||
name: recovery-email
|
||||
attrs:
|
||||
use_global_settings: true
|
||||
subject: Reset your password
|
||||
template: email/password-reset.html
|
||||
|
||||
- model: authentik_stages_prompt.prompt
|
||||
id: field-recovery-password
|
||||
identifiers:
|
||||
name: recovery-field-password
|
||||
attrs:
|
||||
field_key: password
|
||||
label: New Password
|
||||
type: password
|
||||
required: true
|
||||
placeholder: New Password
|
||||
placeholder_expression: false
|
||||
order: 0
|
||||
|
||||
- model: authentik_stages_prompt.prompt
|
||||
id: field-recovery-password-repeat
|
||||
identifiers:
|
||||
name: recovery-field-password-repeat
|
||||
attrs:
|
||||
field_key: password_repeat
|
||||
label: New Password (repeat)
|
||||
type: password
|
||||
required: true
|
||||
placeholder: New Password (repeat)
|
||||
placeholder_expression: false
|
||||
order: 1
|
||||
|
||||
- model: authentik_stages_prompt.promptstage
|
||||
id: recovery-password-stage
|
||||
identifiers:
|
||||
name: recovery-password-prompt
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf field-recovery-password
|
||||
- !KeyOf field-recovery-password-repeat
|
||||
validation_policies:
|
||||
- !KeyOf password-policy
|
||||
|
||||
- model: authentik_stages_user_write.userwritestage
|
||||
id: recovery-user-write
|
||||
identifiers:
|
||||
name: recovery-user-write
|
||||
attrs:
|
||||
user_creation_mode: never_create
|
||||
|
||||
- model: authentik_stages_user_login.userloginstage
|
||||
id: recovery-user-login
|
||||
identifiers:
|
||||
name: recovery-user-login
|
||||
|
||||
# --- Recovery flow stage bindings ---
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf recovery-flow
|
||||
stage: !KeyOf recovery-identification
|
||||
order: 10
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf recovery-flow
|
||||
stage: !KeyOf recovery-email
|
||||
order: 20
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf recovery-flow
|
||||
stage: !KeyOf recovery-password-stage
|
||||
order: 30
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf recovery-flow
|
||||
stage: !KeyOf recovery-user-write
|
||||
order: 40
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf recovery-flow
|
||||
stage: !KeyOf recovery-user-login
|
||||
order: 100
|
||||
|
||||
# --- Unenrollment (account deletion) flow ---
|
||||
- model: authentik_flows.flow
|
||||
id: unenrollment-flow
|
||||
identifiers:
|
||||
slug: default-unenrollment-flow
|
||||
attrs:
|
||||
name: Delete Account
|
||||
title: Delete your account
|
||||
designation: unenrollment
|
||||
|
||||
- model: authentik_stages_consent.consentstage
|
||||
id: unenrollment-consent
|
||||
identifiers:
|
||||
name: unenrollment-consent
|
||||
attrs:
|
||||
mode: always_require
|
||||
|
||||
- model: authentik_stages_user_delete.userdeletestage
|
||||
id: unenrollment-user-delete
|
||||
identifiers:
|
||||
name: unenrollment-user-delete
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf unenrollment-flow
|
||||
stage: !KeyOf unenrollment-consent
|
||||
order: 10
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf unenrollment-flow
|
||||
stage: !KeyOf unenrollment-user-delete
|
||||
order: 20
|
||||
|
||||
# --- Set recovery and unenrollment flows on brand ---
|
||||
- model: authentik_brands.brand
|
||||
identifiers:
|
||||
domain: authentik-default
|
||||
state: present
|
||||
attrs:
|
||||
flow_recovery: !KeyOf recovery-flow
|
||||
flow_unenrollment: !KeyOf unenrollment-flow
|
||||
|
||||
{% if social_login_sources is not defined or social_login_sources | length == 0 %}
|
||||
# --- Bind enrollment flow to the default login identification stage ---
|
||||
- model: authentik_stages_identification.identificationstage
|
||||
identifiers:
|
||||
name: default-authentication-identification
|
||||
state: present
|
||||
attrs:
|
||||
user_fields:
|
||||
- email
|
||||
- username
|
||||
enrollment_flow: !KeyOf enrollment-flow
|
||||
{% endif %}
|
||||
67
ansible/roles/authentik/templates/blueprint-oauth2.yml.j2
Normal file
67
ansible/roles/authentik/templates/blueprint-oauth2.yml.j2
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
version: 1
|
||||
metadata:
|
||||
name: oauth2-applications
|
||||
entries:
|
||||
{% for app in oauth_applications %}
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
identifiers:
|
||||
name: {{ app.name }}
|
||||
state: present
|
||||
attrs:
|
||||
name: {{ app.name }}
|
||||
client_type: {{ app.client_type | default('confidential') }}
|
||||
client_id: {{ app.client_id }}
|
||||
{% if app.client_secret is defined %}
|
||||
client_secret: {{ app.client_secret }}
|
||||
{% endif %}
|
||||
authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||
redirect_uris:
|
||||
{% for uri in app.redirect_uris %}
|
||||
- url: "{{ uri }}"
|
||||
matching_mode: strict
|
||||
{% endfor %}
|
||||
property_mappings:
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||
|
||||
- model: authentik_core.application
|
||||
identifiers:
|
||||
slug: {{ app.slug }}
|
||||
state: present
|
||||
attrs:
|
||||
name: {{ app.name }}
|
||||
slug: {{ app.slug }}
|
||||
provider: !Find [authentik_providers_oauth2.oauth2provider, [name, {{ app.name }}]]
|
||||
policy_engine_mode: any
|
||||
|
||||
{% endfor %}
|
||||
- model: authentik_policies_expression.expressionpolicy
|
||||
identifiers:
|
||||
name: enforce-unique-email
|
||||
state: present
|
||||
attrs:
|
||||
name: enforce-unique-email
|
||||
expression: |
|
||||
from authentik.core.models import User
|
||||
email = request.context.get("prompt_data", {}).get("email", "")
|
||||
if not email:
|
||||
return True
|
||||
if User.objects.filter(email=email).exists():
|
||||
ak_message("This email address is already in use.")
|
||||
return False
|
||||
return True
|
||||
execution_logging: true
|
||||
|
||||
- model: authentik_stages_prompt.promptstage
|
||||
identifiers:
|
||||
name: default-source-enrollment-prompt
|
||||
state: present
|
||||
attrs:
|
||||
fields:
|
||||
- !Find [authentik_stages_prompt.prompt, [name, default-source-enrollment-field-username]]
|
||||
validation_policies:
|
||||
- !Find [authentik_policies_expression.expressionpolicy, [name, enforce-unique-email]]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
version: 1
|
||||
metadata:
|
||||
name: social-login-sources
|
||||
entries:
|
||||
{% for source in social_login_sources %}
|
||||
- model: authentik_sources_oauth.oauthsource
|
||||
id: source-{{ source.slug }}
|
||||
identifiers:
|
||||
slug: {{ source.slug }}
|
||||
state: present
|
||||
attrs:
|
||||
name: {{ source.name }}
|
||||
slug: {{ source.slug }}
|
||||
provider_type: {{ source.provider_type }}
|
||||
consumer_key: {{ source.client_id }}
|
||||
consumer_secret: {{ source.client_secret }}
|
||||
authentication_flow: !Find [authentik_flows.flow, [slug, default-source-authentication]]
|
||||
enrollment_flow: !Find [authentik_flows.flow, [slug, default-source-enrollment]]
|
||||
|
||||
{% endfor %}
|
||||
# --- Add social login sources to the login identification stage ---
|
||||
- model: authentik_stages_identification.identificationstage
|
||||
identifiers:
|
||||
name: default-authentication-identification
|
||||
state: present
|
||||
attrs:
|
||||
user_fields:
|
||||
- email
|
||||
- username
|
||||
enrollment_flow: !Find [authentik_flows.flow, [slug, default-enrollment-flow]]
|
||||
recovery_flow: !Find [authentik_flows.flow, [slug, default-recovery-flow]]
|
||||
sources:
|
||||
{% for source in social_login_sources %}
|
||||
- !KeyOf source-{{ source.slug }}
|
||||
{% endfor %}
|
||||
51
ansible/roles/authentik/templates/docker-compose.yml.j2
Normal file
51
ansible/roles/authentik/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "${AUTHENTIK_POSTGRESQL__PASSWORD}"
|
||||
POSTGRES_USER: "authentik"
|
||||
POSTGRES_DB: "authentik"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
command: >
|
||||
postgres -c shared_buffers={{ postgres_shared_buffers }}
|
||||
-c work_mem={{ postgres_work_mem }}
|
||||
-c maintenance_work_mem={{ postgres_maintenance_work_mem }}
|
||||
-c effective_cache_size={{ postgres_effective_cache_size }}
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
command: ["redis-server", "--maxmemory", "{{ redis_maxmemory }}", "--maxmemory-policy", "allkeys-lru"]
|
||||
restart: unless-stopped
|
||||
|
||||
server:
|
||||
image: ghcr.io/goauthentik/server:{{ version }}
|
||||
command: server
|
||||
env_file: .env
|
||||
ports:
|
||||
- "{{ bind_address }}:{{ port }}:9000"
|
||||
volumes:
|
||||
- ./blueprints:/blueprints/custom:ro
|
||||
- ./media:/media:ro
|
||||
- ./custom-templates:/templates:ro
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
worker:
|
||||
image: ghcr.io/goauthentik/server:{{ version }}
|
||||
command: worker
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./blueprints:/blueprints/custom:ro
|
||||
- ./media:/media:ro
|
||||
- ./custom-templates:/templates:ro
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{% raw %}{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% blocktrans with domain=branding_title %}Verify your email - {{ domain }}{% endblocktrans %}</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; background-color: #f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.card { background-color: #ffffff; border-radius: 8px; padding: 40px; margin-top: 20px; }
|
||||
.btn { display: inline-block; background-color: #4050b5; color: #ffffff; text-decoration: none; padding: 12px 32px; border-radius: 6px; font-weight: 600; }
|
||||
.footer { text-align: center; color: #888; font-size: 13px; margin-top: 24px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>{% blocktrans with domain=branding_title %}Welcome to {{ domain }}{% endblocktrans %}</h1>
|
||||
<p>{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}Please click the button below to verify your email address and activate your account.{% endblocktrans %}</p>
|
||||
<p style="text-align: center; margin: 32px 0;">
|
||||
<a href="{{ url }}" class="btn">{% trans "Verify Email" %}</a>
|
||||
</p>
|
||||
<p>{% blocktrans with expiry=expires|naturaltime %}This link will expire {{ expiry }}.{% endblocktrans %}</p>
|
||||
<p style="color: #888; font-size: 13px;">{% blocktrans %}If you did not create an account, you can safely ignore this email.{% endblocktrans %}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>{{ branding_title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>{% endraw %}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{% raw %}{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% blocktrans with domain=branding_title %}Reset your password - {{ domain }}{% endblocktrans %}</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; background-color: #f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.card { background-color: #ffffff; border-radius: 8px; padding: 40px; margin-top: 20px; }
|
||||
.btn { display: inline-block; background-color: #4050b5; color: #ffffff; text-decoration: none; padding: 12px 32px; border-radius: 6px; font-weight: 600; }
|
||||
.footer { text-align: center; color: #888; font-size: 13px; margin-top: 24px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>{% trans "Reset your password" %}</h1>
|
||||
<p>{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}</p>
|
||||
<p>{% blocktrans with domain=branding_title %}A password reset was requested for your account on {{ domain }}.{% endblocktrans %}</p>
|
||||
<p style="text-align: center; margin: 32px 0;">
|
||||
<a href="{{ url }}" class="btn">{% trans "Reset Password" %}</a>
|
||||
</p>
|
||||
<p>{% blocktrans with expiry=expires|naturaltime %}This link will expire {{ expiry }}.{% endblocktrans %}</p>
|
||||
<p style="color: #888; font-size: 13px;">{% blocktrans %}If you did not request a password reset, you can safely ignore this email.{% endblocktrans %}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>{{ branding_title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>{% endraw %}
|
||||
19
ansible/roles/authentik/templates/env.j2
Normal file
19
ansible/roles/authentik/templates/env.j2
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
AUTHENTIK_SECRET_KEY={{ secret_key }}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD={{ pg_password }}
|
||||
AUTHENTIK_POSTGRESQL__HOST=postgres
|
||||
AUTHENTIK_POSTGRESQL__USER=authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME=authentik
|
||||
AUTHENTIK_REDIS__HOST=redis
|
||||
AUTHENTIK_HOST={{ domain }}
|
||||
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS=10.0.0.0/24,172.16.0.0/12
|
||||
AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true
|
||||
AUTHENTIK_DISABLE_UPDATE_CHECK=true
|
||||
AUTHENTIK_ERROR_REPORTING__ENABLED=false
|
||||
{% if smtp_host is defined %}
|
||||
AUTHENTIK_EMAIL__HOST={{ smtp_host }}
|
||||
AUTHENTIK_EMAIL__PORT={{ smtp_port | default(587) }}
|
||||
AUTHENTIK_EMAIL__USERNAME={{ smtp_username }}
|
||||
AUTHENTIK_EMAIL__PASSWORD={{ smtp_password }}
|
||||
AUTHENTIK_EMAIL__USE_TLS=true
|
||||
AUTHENTIK_EMAIL__FROM={{ smtp_from | default(smtp_username) }}
|
||||
{% endif %}
|
||||
9
ansible/roles/bugzilla/defaults/main.yml
Normal file
9
ansible/roles/bugzilla/defaults/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
install_dir: /opt/bugzilla
|
||||
bugzilla_dir: /opt/bugzilla/bugzilla
|
||||
bugzilla_download_url: "https://ftp.mozilla.org/pub/webtools/bugzilla/5.0-branch/bugzilla-{{ version }}.tar.gz"
|
||||
db_port: 5433
|
||||
db_name: bugzilla
|
||||
db_user: bugzilla
|
||||
ssl_cert: /etc/letsencrypt/live/tiararodney.com/fullchain.pem
|
||||
ssl_key: /etc/letsencrypt/live/tiararodney.com/privkey.pem
|
||||
6
ansible/roles/bugzilla/meta/main.yml
Normal file
6
ansible/roles/bugzilla/meta/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
dependencies:
|
||||
-
|
||||
role: docker
|
||||
-
|
||||
role: apache
|
||||
160
ansible/roles/bugzilla/tasks/deploy-bugzilla.yml
Normal file
160
ansible/roles/bugzilla/tasks/deploy-bugzilla.yml
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
---
|
||||
-
|
||||
name: Include OS-specific variables
|
||||
include_vars: "{{ ansible_os_family }}.yml"
|
||||
|
||||
-
|
||||
name: Ensure install directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Install Bugzilla Perl dependencies
|
||||
apt:
|
||||
name: "{{ bugzilla_packages }}"
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
-
|
||||
name: Enable Apache modules for Bugzilla
|
||||
community.general.apache2_module:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
loop:
|
||||
- cgid
|
||||
- expires
|
||||
- auth_openidc
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
|
||||
-
|
||||
name: Start bugzilla database
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: start-compose
|
||||
vars:
|
||||
compose_project_dir: "{{ install_dir }}"
|
||||
|
||||
-
|
||||
name: Download Bugzilla
|
||||
unarchive:
|
||||
src: "{{ bugzilla_download_url }}"
|
||||
dest: "{{ install_dir }}"
|
||||
remote_src: yes
|
||||
creates: "{{ install_dir }}/bugzilla-{{ version }}"
|
||||
|
||||
-
|
||||
name: Symlink versioned directory to bugzilla_dir
|
||||
file:
|
||||
src: "{{ install_dir }}/bugzilla-{{ version }}"
|
||||
dest: "{{ bugzilla_dir }}"
|
||||
state: link
|
||||
when: bugzilla_dir != install_dir + '/bugzilla-' + version
|
||||
|
||||
-
|
||||
name: Deploy localconfig
|
||||
template:
|
||||
src: localconfig.j2
|
||||
dest: "{{ bugzilla_dir }}/localconfig"
|
||||
mode: "0640"
|
||||
group: www-data
|
||||
|
||||
-
|
||||
name: Deploy checksetup answers file
|
||||
template:
|
||||
src: checksetup-answers.j2
|
||||
dest: "{{ install_dir }}/checksetup-answers.pl"
|
||||
mode: "0600"
|
||||
|
||||
-
|
||||
name: Wait for PostgreSQL to be ready
|
||||
wait_for:
|
||||
host: 127.0.0.1
|
||||
port: "{{ db_port }}"
|
||||
delay: 2
|
||||
timeout: 30
|
||||
|
||||
-
|
||||
name: Run Bugzilla checksetup
|
||||
command:
|
||||
cmd: "perl checksetup.pl {{ install_dir }}/checksetup-answers.pl"
|
||||
chdir: "{{ bugzilla_dir }}"
|
||||
register: checksetup_result
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: checksetup_result.rc == 0
|
||||
|
||||
-
|
||||
name: Run Bugzilla checksetup again to generate params.json
|
||||
command:
|
||||
cmd: "perl checksetup.pl {{ install_dir }}/checksetup-answers.pl"
|
||||
chdir: "{{ bugzilla_dir }}"
|
||||
creates: "{{ bugzilla_dir }}/data/params.json"
|
||||
|
||||
-
|
||||
name: Configure Bugzilla Env auth login class
|
||||
replace:
|
||||
path: "{{ bugzilla_dir }}/data/params.json"
|
||||
regexp: '"user_info_class"\s*:\s*"CGI"'
|
||||
replace: '"user_info_class" : "Env,CGI"'
|
||||
when: oauth_client_id is defined
|
||||
|
||||
-
|
||||
name: Configure Bugzilla Env auth email variable
|
||||
replace:
|
||||
path: "{{ bugzilla_dir }}/data/params.json"
|
||||
regexp: '"auth_env_email"\s*:\s*""'
|
||||
replace: '"auth_env_email" : "OIDC_CLAIM_email"'
|
||||
when: oauth_client_id is defined
|
||||
|
||||
-
|
||||
name: Configure Bugzilla Env auth realname variable
|
||||
replace:
|
||||
path: "{{ bugzilla_dir }}/data/params.json"
|
||||
regexp: '"auth_env_realname"\s*:\s*""'
|
||||
replace: '"auth_env_realname" : "OIDC_CLAIM_name"'
|
||||
when: oauth_client_id is defined
|
||||
|
||||
-
|
||||
name: Set Bugzilla file ownership
|
||||
file:
|
||||
path: "{{ install_dir }}/bugzilla-{{ version }}"
|
||||
state: directory
|
||||
owner: www-data
|
||||
group: www-data
|
||||
recurse: yes
|
||||
|
||||
-
|
||||
name: Deploy bugzilla vhost
|
||||
template:
|
||||
src: bugzilla-vhost.conf.j2
|
||||
dest: "{{ apache_sites_available }}/bugzilla.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Enable bugzilla site
|
||||
command: "{{ apache_enable_site_cmd }} bugzilla"
|
||||
args:
|
||||
creates: "{{ apache_sites_enabled }}/bugzilla.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Deploy bugzilla backup script
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: deploy-backup
|
||||
vars:
|
||||
backup_name: bugzilla
|
||||
backup_hook_dir: /etc/restic/pre-backup.d
|
||||
backup_volumes:
|
||||
- bugzilla_postgres_data
|
||||
backup_files:
|
||||
- "{{ install_dir }}/docker-compose.yml"
|
||||
- "{{ bugzilla_dir }}/localconfig"
|
||||
4
ansible/roles/bugzilla/tasks/main.yml
Normal file
4
ansible/roles/bugzilla/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy bugzilla
|
||||
ansible.builtin.include_tasks: deploy-bugzilla.yml
|
||||
42
ansible/roles/bugzilla/tasks/restore.yml
Normal file
42
ansible/roles/bugzilla/tasks/restore.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
-
|
||||
name: Set backup staging directory
|
||||
set_fact:
|
||||
_bugzilla_backup_dir: "{{ backup_staging_dir | default('/var/backups') }}/bugzilla"
|
||||
|
||||
-
|
||||
name: Stop bugzilla database
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: absent
|
||||
|
||||
-
|
||||
name: Restore docker-compose file
|
||||
copy:
|
||||
src: "{{ _bugzilla_backup_dir }}/docker-compose.yml"
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
remote_src: yes
|
||||
mode: "0600"
|
||||
|
||||
-
|
||||
name: Restore localconfig
|
||||
copy:
|
||||
src: "{{ _bugzilla_backup_dir }}/localconfig"
|
||||
dest: "{{ bugzilla_dir }}/localconfig"
|
||||
remote_src: yes
|
||||
mode: "0640"
|
||||
group: www-data
|
||||
|
||||
-
|
||||
name: Restore bugzilla postgres volume
|
||||
command: >
|
||||
docker run --rm
|
||||
-v bugzilla_postgres_data:/data
|
||||
-v {{ _bugzilla_backup_dir }}:/backup
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/postgres_data.tar.gz -C /data"
|
||||
|
||||
-
|
||||
name: Start bugzilla database
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: present
|
||||
52
ansible/roles/bugzilla/templates/bugzilla-vhost.conf.j2
Normal file
52
ansible/roles/bugzilla/templates/bugzilla-vhost.conf.j2
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName {{ domain }}
|
||||
Redirect permanent / https://{{ domain }}/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName {{ domain }}
|
||||
SSLEngine on
|
||||
SSLCertificateFile {{ ssl_cert }}
|
||||
SSLCertificateKeyFile {{ ssl_key }}
|
||||
|
||||
{% if oauth_client_id is defined %}
|
||||
OIDCProviderIssuer {{ oauth_issuer_url }}
|
||||
OIDCProviderAuthorizationEndpoint {{ oauth_authorize_url }}
|
||||
OIDCProviderTokenEndpoint {{ oauth_token_url }}
|
||||
OIDCProviderTokenEndpointAuth client_secret_post
|
||||
OIDCProviderUserInfoEndpoint {{ oauth_userinfo_url }}
|
||||
OIDCProviderJwksUri {{ oauth_jwks_url }}
|
||||
OIDCClientID {{ oauth_client_id }}
|
||||
OIDCClientSecret {{ oauth_client_secret }}
|
||||
OIDCRedirectURI https://{{ domain }}/oidc-callback
|
||||
OIDCCryptoPassphrase {{ oauth_crypto_passphrase }}
|
||||
OIDCScope "openid profile email"
|
||||
OIDCRemoteUserClaim preferred_username
|
||||
OIDCPassClaimsAs environment
|
||||
OIDCSSLValidateServer Off
|
||||
OIDCProviderEndSessionEndpoint {{ oauth_issuer_url }}/end-session/
|
||||
|
||||
<Location />
|
||||
AuthType openid-connect
|
||||
Require valid-user
|
||||
</Location>
|
||||
|
||||
<Location /oidc-callback>
|
||||
AuthType openid-connect
|
||||
Require valid-user
|
||||
</Location>
|
||||
|
||||
RedirectMatch ^/logout$ /oidc-callback?logout=https%3A%2F%2F{{ domain }}%2F
|
||||
{% endif %}
|
||||
|
||||
DocumentRoot {{ bugzilla_dir }}
|
||||
<Directory {{ bugzilla_dir }}>
|
||||
AddHandler cgi-script .cgi
|
||||
Options +ExecCGI +FollowSymLinks
|
||||
DirectoryIndex index.cgi index.html
|
||||
AllowOverride All
|
||||
{% if oauth_client_id is not defined %}
|
||||
Require all granted
|
||||
{% endif %}
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
4
ansible/roles/bugzilla/templates/checksetup-answers.j2
Normal file
4
ansible/roles/bugzilla/templates/checksetup-answers.j2
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
$answer{'ADMIN_EMAIL'} = '{{ admin_email }}';
|
||||
$answer{'ADMIN_PASSWORD'} = '{{ admin_pwd }}';
|
||||
$answer{'ADMIN_REALNAME'} = '{{ admin_name | default("Admin") }}';
|
||||
$answer{'NO_PAUSE'} = 1;
|
||||
15
ansible/roles/bugzilla/templates/docker-compose.yml.j2
Normal file
15
ansible/roles/bugzilla/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: {{ db_name }}
|
||||
POSTGRES_USER: {{ db_user }}
|
||||
POSTGRES_PASSWORD: "{{ db_password }}"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:{{ db_port }}:5432"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
15
ansible/roles/bugzilla/templates/localconfig.j2
Normal file
15
ansible/roles/bugzilla/templates/localconfig.j2
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
$db_driver = 'Pg';
|
||||
$db_host = '127.0.0.1';
|
||||
$db_port = {{ db_port }};
|
||||
$db_name = '{{ db_name }}';
|
||||
$db_user = '{{ db_user }}';
|
||||
$db_pass = '{{ db_password }}';
|
||||
$webservergroup = 'www-data';
|
||||
{% if smtp_host is defined %}
|
||||
$smtp_server = '{{ smtp_host }}';
|
||||
$smtp_port = {{ smtp_port | default(587) }};
|
||||
$smtp_username = '{{ smtp_username }}';
|
||||
$smtp_password = '{{ smtp_password }}';
|
||||
$smtp_ssl = 'On';
|
||||
$mailfrom = '{{ smtp_from | default(smtp_username) }}';
|
||||
{% endif %}
|
||||
31
ansible/roles/bugzilla/vars/Debian.yml
Normal file
31
ansible/roles/bugzilla/vars/Debian.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
bugzilla_packages:
|
||||
- build-essential
|
||||
- libgd-dev
|
||||
- libappconfig-perl
|
||||
- libdate-calc-perl
|
||||
- libtemplate-perl
|
||||
- libmime-tools-perl
|
||||
- libdbi-perl
|
||||
- libdbd-pg-perl
|
||||
- libcgi-pm-perl
|
||||
- libmath-random-isaac-perl
|
||||
- libapache2-mod-perl2
|
||||
- libchart-perl
|
||||
- libxml-twig-perl
|
||||
- libgd-perl
|
||||
- libgd-graph-perl
|
||||
- libtemplate-plugin-gd-perl
|
||||
- libsoap-lite-perl
|
||||
- libhtml-scrubber-perl
|
||||
- libemail-sender-perl
|
||||
- libemail-mime-perl
|
||||
- libjson-xs-perl
|
||||
- libencode-detect-perl
|
||||
- libtheschwartz-perl
|
||||
- libdaemon-generic-perl
|
||||
- libdatetime-perl
|
||||
- libdatetime-timezone-perl
|
||||
- libemail-address-perl
|
||||
- perlmagick
|
||||
- libapache2-mod-auth-openidc
|
||||
5
ansible/roles/comentario/defaults/main.yml
Normal file
5
ansible/roles/comentario/defaults/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
install_dir: /opt/comentario
|
||||
port: 8060
|
||||
ssl_cert: /etc/letsencrypt/live/tiararodney.com/fullchain.pem
|
||||
ssl_key: /etc/letsencrypt/live/tiararodney.com/privkey.pem
|
||||
6
ansible/roles/comentario/meta/main.yml
Normal file
6
ansible/roles/comentario/meta/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
dependencies:
|
||||
-
|
||||
role: docker
|
||||
-
|
||||
role: apache
|
||||
52
ansible/roles/comentario/tasks/deploy-comentario.yml
Normal file
52
ansible/roles/comentario/tasks/deploy-comentario.yml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
-
|
||||
name: Ensure install directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy secrets file
|
||||
template:
|
||||
src: secrets.yaml.j2
|
||||
dest: "{{ install_dir }}/secrets.yaml"
|
||||
mode: "0600"
|
||||
|
||||
-
|
||||
name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
|
||||
-
|
||||
name: Start comentario stack
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: start-compose
|
||||
vars:
|
||||
compose_project_dir: "{{ install_dir }}"
|
||||
|
||||
-
|
||||
name: Deploy comentario vhost
|
||||
include_role:
|
||||
name: apache
|
||||
tasks_from: deploy-reverse-proxy
|
||||
vars:
|
||||
vhost_name: comentario
|
||||
server_name: "{{ domain }}"
|
||||
backend_port: "{{ port }}"
|
||||
|
||||
-
|
||||
name: Deploy comentario backup script
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: deploy-backup
|
||||
vars:
|
||||
backup_name: comentario
|
||||
backup_hook_dir: /etc/restic/pre-backup.d
|
||||
backup_volumes:
|
||||
- comentario_comentario_postgres_data
|
||||
backup_files:
|
||||
- "{{ install_dir }}/docker-compose.yml"
|
||||
- "{{ install_dir }}/secrets.yaml"
|
||||
4
ansible/roles/comentario/tasks/main.yml
Normal file
4
ansible/roles/comentario/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy comentario
|
||||
ansible.builtin.include_tasks: deploy-comentario.yml
|
||||
41
ansible/roles/comentario/tasks/restore.yml
Normal file
41
ansible/roles/comentario/tasks/restore.yml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
-
|
||||
name: Set backup staging directory
|
||||
set_fact:
|
||||
_comentario_backup_dir: "{{ backup_staging_dir | default('/var/backups') }}/comentario"
|
||||
|
||||
-
|
||||
name: Stop comentario stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: absent
|
||||
|
||||
-
|
||||
name: Restore docker-compose file
|
||||
copy:
|
||||
src: "{{ _comentario_backup_dir }}/docker-compose.yml"
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
remote_src: yes
|
||||
mode: "0600"
|
||||
|
||||
-
|
||||
name: Restore secrets file
|
||||
copy:
|
||||
src: "{{ _comentario_backup_dir }}/secrets.yaml"
|
||||
dest: "{{ install_dir }}/secrets.yaml"
|
||||
remote_src: yes
|
||||
mode: "0600"
|
||||
|
||||
-
|
||||
name: Restore comentario postgres volume
|
||||
command: >
|
||||
docker run --rm
|
||||
-v comentario_comentario_postgres_data:/data
|
||||
-v {{ _comentario_backup_dir }}:/backup
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/comentario_postgres_data.tar.gz -C /data"
|
||||
|
||||
-
|
||||
name: Start comentario stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: present
|
||||
26
ansible/roles/comentario/templates/docker-compose.yml.j2
Normal file
26
ansible/roles/comentario/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: comentario
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
comentario:
|
||||
image: registry.gitlab.com/comentario/comentario:{{ version }}
|
||||
ports:
|
||||
- "127.0.0.1:{{ port }}:80"
|
||||
environment:
|
||||
BASE_URL: https://{{ domain }}
|
||||
SECRETS_FILE: /secrets.yaml
|
||||
volumes:
|
||||
- ./secrets.yaml:/secrets.yaml:ro
|
||||
depends_on:
|
||||
- postgres
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
27
ansible/roles/comentario/templates/secrets.yaml.j2
Normal file
27
ansible/roles/comentario/templates/secrets.yaml.j2
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
postgres:
|
||||
host: postgres
|
||||
port: 5432
|
||||
database: comentario
|
||||
username: postgres
|
||||
password: postgres
|
||||
{% if smtp_host is defined %}
|
||||
smtp:
|
||||
host: {{ smtp_host }}
|
||||
port: {{ smtp_port | default(587) }}
|
||||
username: {{ smtp_username }}
|
||||
password: {{ smtp_password }}
|
||||
from: {{ smtp_from | default(smtp_username) }}
|
||||
{% endif %}
|
||||
{% if oauth_client_id is defined %}
|
||||
idp:
|
||||
oidc:
|
||||
- id: authentik
|
||||
name: Authentik
|
||||
url: {{ oauth_issuer_url }}
|
||||
key: {{ oauth_client_id }}
|
||||
secret: {{ oauth_client_secret }}
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
{% endif %}
|
||||
5
ansible/roles/conversejs/defaults/main.yml
Normal file
5
ansible/roles/conversejs/defaults/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
install_dir: /var/www/chat.tiararodney.com
|
||||
ssl_cert: /etc/letsencrypt/live/tiararodney.com/fullchain.pem
|
||||
ssl_key: /etc/letsencrypt/live/tiararodney.com/privkey.pem
|
||||
prosody_port: 5280
|
||||
4
ansible/roles/conversejs/meta/main.yml
Normal file
4
ansible/roles/conversejs/meta/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
dependencies:
|
||||
-
|
||||
role: apache
|
||||
49
ansible/roles/conversejs/tasks/deploy-conversejs.yml
Normal file
49
ansible/roles/conversejs/tasks/deploy-conversejs.yml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
-
|
||||
name: Ensure converse.js document root exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
owner: www-data
|
||||
group: www-data
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Download converse.js release
|
||||
unarchive:
|
||||
src: "https://github.com/conversejs/converse.js/releases/download/v{{ version }}/converse.js-{{ version }}.tgz"
|
||||
dest: "{{ install_dir }}"
|
||||
remote_src: yes
|
||||
extra_opts: ["--strip-components=1"]
|
||||
owner: www-data
|
||||
group: www-data
|
||||
|
||||
-
|
||||
name: Download libsignal-protocol for OMEMO
|
||||
get_url:
|
||||
url: "https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js"
|
||||
dest: "{{ install_dir }}/dist/libsignal-protocol.min.js"
|
||||
owner: www-data
|
||||
group: www-data
|
||||
|
||||
-
|
||||
name: Deploy converse.js index page
|
||||
template:
|
||||
src: index.html.j2
|
||||
dest: "{{ install_dir }}/index.html"
|
||||
owner: www-data
|
||||
group: www-data
|
||||
|
||||
-
|
||||
name: Deploy chat vhost
|
||||
template:
|
||||
src: vhost.conf.j2
|
||||
dest: "{{ apache_sites_available }}/chat.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Enable chat site
|
||||
command: "{{ apache_enable_site_cmd }} chat"
|
||||
args:
|
||||
creates: "{{ apache_sites_enabled }}/chat.conf"
|
||||
notify: reload apache
|
||||
4
ansible/roles/conversejs/tasks/main.yml
Normal file
4
ansible/roles/conversejs/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy conversejs
|
||||
ansible.builtin.include_tasks: deploy-conversejs.yml
|
||||
195
ansible/roles/conversejs/templates/index.html.j2
Normal file
195
ansible/roles/conversejs/templates/index.html.j2
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Chat - {{ domain }}</title>
|
||||
<link rel="stylesheet" href="dist/converse.min.css">
|
||||
<script src="dist/libsignal-protocol.min.js"></script>
|
||||
{% if oauth_client_id is defined %}
|
||||
<style>
|
||||
#oauth-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: #2e3436;
|
||||
}
|
||||
#oauth-login a {
|
||||
padding: 16px 32px;
|
||||
background: #3584e4;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#oauth-login a:hover { background: #1c71d8; }
|
||||
</style>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
{% if oauth_client_id is defined %}
|
||||
<div id="oauth-login">
|
||||
<a id="login-btn" href="#">Login with Authentik</a>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const CLIENT_ID = '{{ oauth_client_id }}';
|
||||
const AUTHORIZE_URL = '{{ oauth_authorize_url }}';
|
||||
const TOKEN_URL = '{{ oauth_token_url }}';
|
||||
const REDIRECT_URI = window.location.origin + window.location.pathname;
|
||||
const DOMAIN = '{{ domain }}';
|
||||
|
||||
function generateCodeVerifier() {
|
||||
const arr = new Uint8Array(32);
|
||||
crypto.getRandomValues(arr);
|
||||
return btoa(String.fromCharCode(...arr))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
async function generateCodeChallenge(verifier) {
|
||||
const data = new TextEncoder().encode(verifier);
|
||||
const hash = await crypto.subtle.digest('SHA-256', data);
|
||||
return btoa(String.fromCharCode(...new Uint8Array(hash)))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
async function startOAuth() {
|
||||
const state = generateCodeVerifier();
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
||||
sessionStorage.setItem('oauth_state', state);
|
||||
sessionStorage.setItem('oauth_code_verifier', codeVerifier);
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: 'openid profile email',
|
||||
state: state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256'
|
||||
});
|
||||
window.location.href = AUTHORIZE_URL + '?' + params.toString();
|
||||
}
|
||||
|
||||
async function exchangeCode(code) {
|
||||
const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
|
||||
const resp = await fetch(TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: CLIENT_ID,
|
||||
code: code,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
code_verifier: codeVerifier
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
sessionStorage.removeItem('oauth_code_verifier');
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
function parseJwt(token) {
|
||||
const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
return JSON.parse(atob(base64));
|
||||
}
|
||||
|
||||
function loadConverse() {
|
||||
return new Promise(function(resolve) {
|
||||
var s = document.createElement('script');
|
||||
s.src = 'dist/converse.min.js';
|
||||
s.onload = resolve;
|
||||
document.body.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
function isTokenExpired(token) {
|
||||
try {
|
||||
const claims = parseJwt(token);
|
||||
return claims.exp && (claims.exp * 1000) < Date.now();
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function initConverse(token) {
|
||||
if (isTokenExpired(token)) {
|
||||
sessionStorage.removeItem('oauth_token');
|
||||
startOAuth();
|
||||
return;
|
||||
}
|
||||
document.getElementById('oauth-login').style.display = 'none';
|
||||
const claims = parseJwt(token);
|
||||
const jid = claims.preferred_username + '@' + DOMAIN;
|
||||
loadConverse().then(function() {
|
||||
converse.initialize({
|
||||
bosh_service_url: 'https://' + DOMAIN + '/http-bind',
|
||||
websocket_url: 'wss://' + DOMAIN + '/xmpp-websocket',
|
||||
view_mode: 'fullscreen',
|
||||
authentication: 'login',
|
||||
locked_domain: DOMAIN,
|
||||
muc_domain: 'conference.' + DOMAIN,
|
||||
locked_muc_domain: 'hidden',
|
||||
muc_instant_rooms: true,
|
||||
muc_show_logs_before_join: true,
|
||||
visible_toolbar_buttons: { toggle_occupants: true },
|
||||
jid: jid,
|
||||
password: token,
|
||||
auto_login: true,
|
||||
keepalive: true,
|
||||
omemo_default: true
|
||||
});
|
||||
converse.listen.on('logout', function() {
|
||||
sessionStorage.removeItem('oauth_token');
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get('code');
|
||||
const state = params.get('state');
|
||||
|
||||
if (code && state === sessionStorage.getItem('oauth_state')) {
|
||||
history.replaceState(null, '', window.location.pathname);
|
||||
exchangeCode(code).then(function(token) {
|
||||
if (token) {
|
||||
sessionStorage.setItem('oauth_token', token);
|
||||
initConverse(token);
|
||||
}
|
||||
});
|
||||
} else if (sessionStorage.getItem('oauth_token')) {
|
||||
initConverse(sessionStorage.getItem('oauth_token'));
|
||||
} else {
|
||||
document.getElementById('login-btn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
startOAuth();
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% else %}
|
||||
<script src="dist/converse.min.js"></script>
|
||||
<script>
|
||||
converse.initialize({
|
||||
bosh_service_url: 'https://{{ domain }}/http-bind',
|
||||
websocket_url: 'wss://{{ domain }}/xmpp-websocket',
|
||||
view_mode: 'fullscreen',
|
||||
authentication: 'login',
|
||||
locked_domain: '{{ domain }}',
|
||||
muc_domain: 'conference.{{ domain }}',
|
||||
locked_muc_domain: 'hidden',
|
||||
muc_instant_rooms: false,
|
||||
muc_show_logs_before_join: true,
|
||||
visible_toolbar_buttons: {
|
||||
toggle_occupants: true
|
||||
},
|
||||
omemo_default: true
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
31
ansible/roles/conversejs/templates/vhost.conf.j2
Normal file
31
ansible/roles/conversejs/templates/vhost.conf.j2
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName {{ domain }}
|
||||
Redirect permanent / https://{{ domain }}/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName {{ domain }}
|
||||
SSLEngine on
|
||||
SSLCertificateFile {{ ssl_cert }}
|
||||
SSLCertificateKeyFile {{ ssl_key }}
|
||||
|
||||
DocumentRoot {{ install_dir }}
|
||||
<Directory {{ install_dir }}>
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Proxy BOSH requests to Prosody
|
||||
ProxyPreserveHost on
|
||||
ProxyPass /http-bind http://127.0.0.1:{{ prosody_port }}/http-bind
|
||||
ProxyPassReverse /http-bind http://127.0.0.1:{{ prosody_port }}/http-bind
|
||||
|
||||
# Proxy WebSocket requests to Prosody
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/xmpp-websocket(.*) ws://127.0.0.1:{{ prosody_port }}/xmpp-websocket$1 [P,L]
|
||||
ProxyPass /xmpp-websocket http://127.0.0.1:{{ prosody_port }}/xmpp-websocket
|
||||
ProxyPassReverse /xmpp-websocket http://127.0.0.1:{{ prosody_port }}/xmpp-websocket
|
||||
</VirtualHost>
|
||||
5
ansible/roles/devpi/defaults/main.yml
Normal file
5
ansible/roles/devpi/defaults/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
install_dir: /opt/devpi
|
||||
port: 3141
|
||||
ssl_cert: /etc/letsencrypt/live/tiararodney.com/fullchain.pem
|
||||
ssl_key: /etc/letsencrypt/live/tiararodney.com/privkey.pem
|
||||
11
ansible/roles/devpi/files/Dockerfile
Normal file
11
ansible/roles/devpi/files/Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
RUN pip install --no-cache-dir devpi-server devpi-web
|
||||
|
||||
RUN devpi-init --serverdir /data || true
|
||||
|
||||
EXPOSE 3141
|
||||
|
||||
VOLUME /data
|
||||
|
||||
CMD ["devpi-server", "--serverdir", "/data", "--host", "0.0.0.0", "--port", "3141"]
|
||||
6
ansible/roles/devpi/meta/main.yml
Normal file
6
ansible/roles/devpi/meta/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
dependencies:
|
||||
-
|
||||
role: docker
|
||||
-
|
||||
role: apache
|
||||
51
ansible/roles/devpi/tasks/deploy-devpi.yml
Normal file
51
ansible/roles/devpi/tasks/deploy-devpi.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
-
|
||||
name: Ensure install directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Copy Dockerfile
|
||||
copy:
|
||||
src: Dockerfile
|
||||
dest: "{{ install_dir }}/Dockerfile"
|
||||
|
||||
-
|
||||
name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
|
||||
-
|
||||
name: Start devpi stack
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: start-compose
|
||||
vars:
|
||||
compose_project_dir: "{{ install_dir }}"
|
||||
compose_build: policy
|
||||
|
||||
-
|
||||
name: Deploy devpi vhost
|
||||
include_role:
|
||||
name: apache
|
||||
tasks_from: deploy-reverse-proxy
|
||||
vars:
|
||||
vhost_name: devpi
|
||||
server_name: "{{ hostname }}"
|
||||
backend_port: "{{ port }}"
|
||||
|
||||
-
|
||||
name: Deploy devpi backup script
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: deploy-backup
|
||||
vars:
|
||||
backup_name: devpi
|
||||
backup_hook_dir: /etc/restic/pre-backup.d
|
||||
backup_volumes:
|
||||
- devpi_devpi_data
|
||||
backup_files:
|
||||
- "{{ install_dir }}/docker-compose.yml"
|
||||
4
ansible/roles/devpi/tasks/main.yml
Normal file
4
ansible/roles/devpi/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy devpi
|
||||
ansible.builtin.include_tasks: deploy-devpi.yml
|
||||
33
ansible/roles/devpi/tasks/restore.yml
Normal file
33
ansible/roles/devpi/tasks/restore.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
-
|
||||
name: Set backup staging directory
|
||||
set_fact:
|
||||
_devpi_backup_dir: "{{ backup_staging_dir | default('/var/backups') }}/devpi"
|
||||
|
||||
-
|
||||
name: Stop devpi stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: absent
|
||||
|
||||
-
|
||||
name: Restore docker-compose file
|
||||
copy:
|
||||
src: "{{ _devpi_backup_dir }}/docker-compose.yml"
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
remote_src: yes
|
||||
mode: "0600"
|
||||
|
||||
-
|
||||
name: Restore devpi data volume
|
||||
command: >
|
||||
docker run --rm
|
||||
-v devpi_devpi_data:/data
|
||||
-v {{ _devpi_backup_dir }}:/backup
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/devpi_data.tar.gz -C /data"
|
||||
|
||||
-
|
||||
name: Start devpi stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: present
|
||||
11
ansible/roles/devpi/templates/docker-compose.yml.j2
Normal file
11
ansible/roles/devpi/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
services:
|
||||
devpi:
|
||||
build: .
|
||||
ports:
|
||||
- "127.0.0.1:{{ port }}:3141"
|
||||
volumes:
|
||||
- devpi_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
devpi_data:
|
||||
7
ansible/roles/dnsmasq/defaults/main.yml
Normal file
7
ansible/roles/dnsmasq/defaults/main.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
dns_listen_address: "10.0.0.1"
|
||||
dns_port: 53
|
||||
dns_upstream:
|
||||
- "1.1.1.1"
|
||||
- "1.0.0.1"
|
||||
dns_records: []
|
||||
6
ansible/roles/dnsmasq/handlers/main.yml
Normal file
6
ansible/roles/dnsmasq/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
-
|
||||
name: restart dnsmasq
|
||||
service:
|
||||
name: dnsmasq
|
||||
state: restarted
|
||||
20
ansible/roles/dnsmasq/tasks/main.yml
Normal file
20
ansible/roles/dnsmasq/tasks/main.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
-
|
||||
name: Install dnsmasq
|
||||
apt:
|
||||
name: dnsmasq
|
||||
state: present
|
||||
|
||||
-
|
||||
name: Deploy dnsmasq configuration
|
||||
template:
|
||||
src: dnsmasq.conf.j2
|
||||
dest: /etc/dnsmasq.d/local.conf
|
||||
notify: restart dnsmasq
|
||||
|
||||
-
|
||||
name: Ensure dnsmasq is running
|
||||
service:
|
||||
name: dnsmasq
|
||||
state: started
|
||||
enabled: yes
|
||||
11
ansible/roles/dnsmasq/templates/dnsmasq.conf.j2
Normal file
11
ansible/roles/dnsmasq/templates/dnsmasq.conf.j2
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
listen-address={{ dns_listen_address }}
|
||||
port={{ dns_port }}
|
||||
bind-interfaces
|
||||
no-resolv
|
||||
no-hosts
|
||||
{% for server in dns_upstream %}
|
||||
server={{ server }}
|
||||
{% endfor %}
|
||||
{% for record in dns_records %}
|
||||
address=/{{ record.domain }}/{{ record.ip }}
|
||||
{% endfor %}
|
||||
11
ansible/roles/docker/handlers/main.yml
Normal file
11
ansible/roles/docker/handlers/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
-
|
||||
name: restart containerd
|
||||
service:
|
||||
name: containerd
|
||||
state: restarted
|
||||
-
|
||||
name: restart docker
|
||||
service:
|
||||
name: docker
|
||||
state: restarted
|
||||
2
ansible/roles/docker/meta/main.yml
Normal file
2
ansible/roles/docker/meta/main.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
dependencies: []
|
||||
29
ansible/roles/docker/tasks/configure-mirror.yml
Normal file
29
ansible/roles/docker/tasks/configure-mirror.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
-
|
||||
name: Add registry mirror host entries
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
regexp: "{{ item.mirror | urlsplit('hostname') | regex_escape }}"
|
||||
line: "{{ registry_mirror_ip }} {{ item.mirror | urlsplit('hostname') }}"
|
||||
loop: "{{ registry_mirrors }}"
|
||||
when: registry_mirror_ip is defined
|
||||
|
||||
-
|
||||
name: Configure Docker Hub registry mirror
|
||||
copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
content: |
|
||||
{
|
||||
"registry-mirrors": [
|
||||
{% for item in registry_mirrors if item.upstream == 'docker.io' %}
|
||||
"{{ item.mirror }}"{% if not loop.last %},{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
]
|
||||
}
|
||||
mode: "0644"
|
||||
notify: restart docker
|
||||
|
||||
-
|
||||
name: Ensure Docker is restarted if mirror changed
|
||||
meta: flush_handlers
|
||||
7
ansible/roles/docker/tasks/deploy-backup.yml
Normal file
7
ansible/roles/docker/tasks/deploy-backup.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
-
|
||||
name: "Deploy {{ backup_name }} docker volume backup script"
|
||||
template:
|
||||
src: backup-docker-volumes.sh.j2
|
||||
dest: "{{ backup_hook_dir }}/{{ backup_name }}.sh"
|
||||
mode: "0755"
|
||||
34
ansible/roles/docker/tasks/install-docker.yml
Normal file
34
ansible/roles/docker/tasks/install-docker.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
-
|
||||
name: Install Docker prerequisites
|
||||
apt:
|
||||
name: "{{ docker_prerequisites }}"
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
-
|
||||
name: Add Docker GPG key
|
||||
apt_key:
|
||||
url: "{{ docker_gpg_url }}"
|
||||
state: present
|
||||
|
||||
-
|
||||
name: Add Docker repository
|
||||
apt_repository:
|
||||
repo: "{{ docker_repo }}"
|
||||
state: present
|
||||
|
||||
-
|
||||
name: Install Docker Engine and Compose plugin
|
||||
apt:
|
||||
name: "{{ docker_packages }}"
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
-
|
||||
name: Ensure Docker service is running
|
||||
service:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
13
ansible/roles/docker/tasks/main.yml
Normal file
13
ansible/roles/docker/tasks/main.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
-
|
||||
name: Load OS-specific variables
|
||||
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
|
||||
|
||||
-
|
||||
name: Install and configure Docker
|
||||
ansible.builtin.include_tasks: install-docker.yml
|
||||
|
||||
-
|
||||
name: Configure registry mirrors
|
||||
ansible.builtin.include_tasks: configure-mirror.yml
|
||||
when: registry_mirrors is defined
|
||||
13
ansible/roles/docker/tasks/start-compose.yml
Normal file
13
ansible/roles/docker/tasks/start-compose.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
-
|
||||
name: "Create {{ compose_project_dir }} directory"
|
||||
file:
|
||||
path: "{{ compose_project_dir }}"
|
||||
state: directory
|
||||
|
||||
-
|
||||
name: "Start docker compose stack in {{ compose_project_dir }}"
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ compose_project_dir }}"
|
||||
state: present
|
||||
build: "{{ compose_build | default(omit) }}"
|
||||
10
ansible/roles/docker/templates/backup-docker-volumes.sh.j2
Normal file
10
ansible/roles/docker/templates/backup-docker-volumes.sh.j2
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
BACKUP_DIR="{{ backup_staging_dir | default('/var/backups') }}/{{ backup_name }}"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
{% for vol in backup_volumes | default([]) %}
|
||||
docker run --rm -v {{ vol }}:/data:ro -v "$BACKUP_DIR":/backup alpine sh -c "tar czf /backup/{{ vol }}.tar.gz -C /data . || [ \$? -eq 1 ]"
|
||||
{% endfor %}
|
||||
{% for f in backup_files | default([]) %}
|
||||
cp "{{ f }}" "$BACKUP_DIR/"
|
||||
{% endfor %}
|
||||
15
ansible/roles/docker/vars/Debian.yml
Normal file
15
ansible/roles/docker/vars/Debian.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
docker_prerequisites:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
docker_packages:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
docker_repo: "deb [arch=amd64] https://download.docker.com/linux/debian {{ ansible_facts['distribution_release'] }} stable"
|
||||
docker_gpg_url: "https://download.docker.com/linux/debian/gpg"
|
||||
5
ansible/roles/docker_registry/defaults/main.yml
Normal file
5
ansible/roles/docker_registry/defaults/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
install_dir: /opt/docker-registry
|
||||
port: 5050
|
||||
ssl_cert: /etc/letsencrypt/live/tiararodney.com/fullchain.pem
|
||||
ssl_key: /etc/letsencrypt/live/tiararodney.com/privkey.pem
|
||||
11
ansible/roles/docker_registry/handlers/main.yml
Normal file
11
ansible/roles/docker_registry/handlers/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
-
|
||||
name: restart docker-registry
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: restarted
|
||||
-
|
||||
name: reload apache
|
||||
service:
|
||||
name: "{{ apache_service }}"
|
||||
state: reloaded
|
||||
61
ansible/roles/docker_registry/tasks/deploy-registry.yml
Normal file
61
ansible/roles/docker_registry/tasks/deploy-registry.yml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
-
|
||||
name: Ensure install directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy registry configuration
|
||||
template:
|
||||
src: config.yml.j2
|
||||
dest: "{{ install_dir }}/config.yml"
|
||||
notify: restart docker-registry
|
||||
|
||||
-
|
||||
name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
|
||||
-
|
||||
name: Start registry stack
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: start-compose
|
||||
vars:
|
||||
compose_project_dir: "{{ install_dir }}"
|
||||
|
||||
-
|
||||
name: Load Apache variables
|
||||
include_vars:
|
||||
file: "{{ role_path }}/../apache/vars/{{ ansible_os_family }}.yml"
|
||||
|
||||
-
|
||||
name: Deploy registry vhost
|
||||
template:
|
||||
src: vhost.conf.j2
|
||||
dest: "{{ apache_sites_available }}/docker-registry-{{ hostname | regex_replace('\\..*', '') }}.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Enable registry vhost
|
||||
command: "{{ apache_enable_site_cmd }} docker-registry-{{ hostname | regex_replace('\\..*', '') }}"
|
||||
args:
|
||||
creates: "{{ apache_sites_enabled }}/docker-registry-{{ hostname | regex_replace('\\..*', '') }}.conf"
|
||||
notify: reload apache
|
||||
|
||||
-
|
||||
name: Deploy registry backup script
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: deploy-backup
|
||||
vars:
|
||||
backup_name: docker-registry
|
||||
backup_hook_dir: /etc/restic/pre-backup.d
|
||||
backup_volumes:
|
||||
- docker-registry_registry_data
|
||||
backup_files:
|
||||
- "{{ install_dir }}/docker-compose.yml"
|
||||
- "{{ install_dir }}/config.yml"
|
||||
4
ansible/roles/docker_registry/tasks/main.yml
Normal file
4
ansible/roles/docker_registry/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy registry
|
||||
ansible.builtin.include_tasks: deploy-registry.yml
|
||||
22
ansible/roles/docker_registry/tasks/restore-registry.yml
Normal file
22
ansible/roles/docker_registry/tasks/restore-registry.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
-
|
||||
name: Check if registry backup archive exists
|
||||
stat:
|
||||
path: /var/backups/docker-registry/docker-registry_registry_data.tar.gz
|
||||
register: registry_backup
|
||||
|
||||
-
|
||||
name: Restore registry volume from backup
|
||||
command: >
|
||||
docker run --rm
|
||||
-v docker-registry_registry_data:/data
|
||||
-v /var/backups/docker-registry:/backup:ro
|
||||
alpine sh -c "tar xzf /backup/docker-registry_registry_data.tar.gz -C /data"
|
||||
when: registry_backup.stat.exists
|
||||
|
||||
-
|
||||
name: Restart registry after restore
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: restarted
|
||||
when: registry_backup.stat.exists
|
||||
13
ansible/roles/docker_registry/templates/config.yml.j2
Normal file
13
ansible/roles/docker_registry/templates/config.yml.j2
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
version: 0.1
|
||||
log:
|
||||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
delete:
|
||||
enabled: true
|
||||
http:
|
||||
addr: :5000
|
||||
proxy:
|
||||
remoteurl: {{ remote_url | default('https://registry-1.docker.io') }}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
services:
|
||||
registry:
|
||||
image: registry:2
|
||||
ports:
|
||||
- "127.0.0.1:{{ port }}:5000"
|
||||
volumes:
|
||||
- registry_data:/var/lib/registry
|
||||
- ./config.yml:/etc/docker/registry/config.yml:ro
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
registry_data:
|
||||
28
ansible/roles/docker_registry/templates/vhost.conf.j2
Normal file
28
ansible/roles/docker_registry/templates/vhost.conf.j2
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<VirtualHost *:80>
|
||||
ServerName {{ hostname }}
|
||||
Redirect permanent / https://{{ hostname }}/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName {{ hostname }}
|
||||
SSLEngine on
|
||||
SSLCertificateFile {{ ssl_cert }}
|
||||
SSLCertificateKeyFile {{ ssl_key }}
|
||||
|
||||
# Return an empty OCI index for referrers requests.
|
||||
# registry:2 does not support the OCI referrers API and proxies
|
||||
# the request to upstream which may return HTML error pages,
|
||||
# causing Docker 29+ to fail with "failed to decode referrers index".
|
||||
RewriteEngine on
|
||||
RewriteRule "^/v2/.*/referrers/" - [R=200,L,E=REFERRERS:1]
|
||||
Header always set Content-Type "application/vnd.oci.image.index.v1+json" env=REFERRERS
|
||||
<LocationMatch "^/v2/.*/referrers/">
|
||||
ErrorDocument 200 '{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[]}'
|
||||
</LocationMatch>
|
||||
|
||||
ProxyPreserveHost on
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
RequestHeader set X-Forwarded-Ssl "on"
|
||||
ProxyPass / http://127.0.0.1:{{ port }}/
|
||||
ProxyPassReverse / http://127.0.0.1:{{ port }}/
|
||||
</VirtualHost>
|
||||
3
ansible/roles/host/defaults/main.yml
Normal file
3
ansible/roles/host/defaults/main.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
admin_user: tiara
|
||||
admin_shell: /bin/bash
|
||||
11
ansible/roles/host/handlers/main.yml
Normal file
11
ansible/roles/host/handlers/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
-
|
||||
name: restart sshd
|
||||
service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
-
|
||||
name: restart zramswap
|
||||
systemd:
|
||||
name: zramswap
|
||||
state: restarted
|
||||
2
ansible/roles/host/meta/main.yml
Normal file
2
ansible/roles/host/meta/main.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
dependencies: []
|
||||
13
ansible/roles/host/tasks/main.yml
Normal file
13
ansible/roles/host/tasks/main.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
-
|
||||
name: Load OS-specific variables
|
||||
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
|
||||
|
||||
-
|
||||
name: Set up admin user
|
||||
ansible.builtin.include_tasks: setup-admin.yml
|
||||
when: ssh_pubkey_dir is defined
|
||||
|
||||
-
|
||||
name: Set up base system
|
||||
ansible.builtin.include_tasks: setup-base.yml
|
||||
35
ansible/roles/host/tasks/setup-admin.yml
Normal file
35
ansible/roles/host/tasks/setup-admin.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
-
|
||||
name: Create admin user
|
||||
user:
|
||||
name: "{{ admin_user }}"
|
||||
shell: "{{ admin_shell }}"
|
||||
groups: sudo
|
||||
append: yes
|
||||
create_home: yes
|
||||
|
||||
-
|
||||
name: Allow admin user passwordless sudo
|
||||
copy:
|
||||
dest: "/etc/sudoers.d/{{ admin_user }}"
|
||||
content: "{{ admin_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
||||
mode: "0440"
|
||||
validate: "visudo -cf %s"
|
||||
|
||||
-
|
||||
name: Find SSH public keys
|
||||
find:
|
||||
paths: "{{ ssh_pubkey_dir }}"
|
||||
patterns: "*.pub"
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
register: ssh_pubkeys
|
||||
|
||||
-
|
||||
name: Deploy SSH authorized keys
|
||||
authorized_key:
|
||||
user: "{{ admin_user }}"
|
||||
key: "{{ lookup('file', item.path) }}"
|
||||
loop: "{{ ssh_pubkeys.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
69
ansible/roles/host/tasks/setup-base.yml
Normal file
69
ansible/roles/host/tasks/setup-base.yml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
-
|
||||
name: Update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
cache_valid_time: 0
|
||||
|
||||
-
|
||||
name: Install base packages
|
||||
apt:
|
||||
name: "{{ host_base_packages }}"
|
||||
state: present
|
||||
|
||||
-
|
||||
name: Disable SSH password authentication
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "^#?PasswordAuthentication"
|
||||
line: "PasswordAuthentication no"
|
||||
notify: restart sshd
|
||||
|
||||
-
|
||||
name: Disable SSH root login
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "^#?PermitRootLogin"
|
||||
line: "PermitRootLogin no"
|
||||
notify: restart sshd
|
||||
|
||||
-
|
||||
name: Allow SSH through UFW
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "22"
|
||||
proto: tcp
|
||||
|
||||
-
|
||||
name: Allow additional UFW ports
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ item.port }}"
|
||||
proto: "{{ item.proto | default('tcp') }}"
|
||||
from_ip: "{{ item.from | default('any') }}"
|
||||
loop: "{{ ufw_allow | default([]) }}"
|
||||
|
||||
-
|
||||
name: Enable UFW with default deny
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
default: deny
|
||||
direction: incoming
|
||||
|
||||
-
|
||||
name: Configure fail2ban backend
|
||||
copy:
|
||||
dest: /etc/fail2ban/jail.local
|
||||
content: |
|
||||
[DEFAULT]
|
||||
backend = {{ fail2ban_backend }}
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
-
|
||||
name: Ensure fail2ban is running
|
||||
service:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
enabled: yes
|
||||
37
ansible/roles/host/tasks/setup-swap.yml
Normal file
37
ansible/roles/host/tasks/setup-swap.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
-
|
||||
name: Ensure swap exists
|
||||
command: fallocate -l {{ swap_size | default('2G') }} /swapfile
|
||||
args:
|
||||
creates: /swapfile
|
||||
|
||||
-
|
||||
name: Set swap permissions
|
||||
file:
|
||||
path: /swapfile
|
||||
mode: '0600'
|
||||
|
||||
-
|
||||
name: Make swap
|
||||
command: mkswap /swapfile
|
||||
args:
|
||||
creates: /swapfile.swap
|
||||
|
||||
-
|
||||
name: Mark swapfile as initialized
|
||||
file:
|
||||
path: /swapfile.swap
|
||||
state: touch
|
||||
|
||||
-
|
||||
name: Enable swap
|
||||
command: swapon /swapfile
|
||||
register: swap_on
|
||||
failed_when: false
|
||||
|
||||
-
|
||||
name: Add swap to fstab
|
||||
lineinfile:
|
||||
path: /etc/fstab
|
||||
line: "/swapfile none swap sw 0 0"
|
||||
state: present
|
||||
37
ansible/roles/host/tasks/setup-zram.yml
Normal file
37
ansible/roles/host/tasks/setup-zram.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
-
|
||||
name: Install zram-tools
|
||||
apt:
|
||||
name: zram-tools
|
||||
state: present
|
||||
|
||||
-
|
||||
name: Configure zram
|
||||
copy:
|
||||
dest: /etc/default/zramswap
|
||||
content: |
|
||||
ALGO={{ zram_algorithm | default('zstd') }}
|
||||
PERCENT={{ zram_percent | default(50) }}
|
||||
PRIORITY={{ zram_priority | default(100) }}
|
||||
mode: '0644'
|
||||
notify: restart zramswap
|
||||
|
||||
-
|
||||
name: Enable zramswap service
|
||||
systemd:
|
||||
name: zramswap
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
-
|
||||
name: Disable file-backed swap if present
|
||||
command: swapoff /swapfile
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
-
|
||||
name: Remove file-backed swap from fstab
|
||||
lineinfile:
|
||||
path: /etc/fstab
|
||||
line: "/swapfile none swap sw 0 0"
|
||||
state: absent
|
||||
9
ansible/roles/host/vars/Debian.yml
Normal file
9
ansible/roles/host/vars/Debian.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
host_base_packages:
|
||||
- fail2ban
|
||||
- unattended-upgrades
|
||||
- ufw
|
||||
- vim
|
||||
- curl
|
||||
- rsync
|
||||
fail2ban_backend: systemd
|
||||
5
ansible/roles/kellnr/defaults/main.yml
Normal file
5
ansible/roles/kellnr/defaults/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
install_dir: /opt/kellnr
|
||||
port: 8000
|
||||
ssl_cert: /etc/letsencrypt/live/tiararodney.com/fullchain.pem
|
||||
ssl_key: /etc/letsencrypt/live/tiararodney.com/privkey.pem
|
||||
6
ansible/roles/kellnr/meta/main.yml
Normal file
6
ansible/roles/kellnr/meta/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
dependencies:
|
||||
-
|
||||
role: docker
|
||||
-
|
||||
role: apache
|
||||
44
ansible/roles/kellnr/tasks/deploy-kellnr.yml
Normal file
44
ansible/roles/kellnr/tasks/deploy-kellnr.yml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
-
|
||||
name: Ensure install directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
|
||||
-
|
||||
name: Start kellnr stack
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: start-compose
|
||||
vars:
|
||||
compose_project_dir: "{{ install_dir }}"
|
||||
|
||||
-
|
||||
name: Deploy kellnr vhost
|
||||
include_role:
|
||||
name: apache
|
||||
tasks_from: deploy-reverse-proxy
|
||||
vars:
|
||||
vhost_name: kellnr
|
||||
server_name: "{{ hostname }}"
|
||||
backend_port: "{{ port }}"
|
||||
|
||||
-
|
||||
name: Deploy kellnr backup script
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: deploy-backup
|
||||
vars:
|
||||
backup_name: kellnr
|
||||
backup_hook_dir: /etc/restic/pre-backup.d
|
||||
backup_volumes:
|
||||
- kellnr_kellnr_data
|
||||
backup_files:
|
||||
- "{{ install_dir }}/docker-compose.yml"
|
||||
4
ansible/roles/kellnr/tasks/main.yml
Normal file
4
ansible/roles/kellnr/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy kellnr
|
||||
ansible.builtin.include_tasks: deploy-kellnr.yml
|
||||
33
ansible/roles/kellnr/tasks/restore.yml
Normal file
33
ansible/roles/kellnr/tasks/restore.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
-
|
||||
name: Set backup staging directory
|
||||
set_fact:
|
||||
_kellnr_backup_dir: "{{ backup_staging_dir | default('/var/backups') }}/kellnr"
|
||||
|
||||
-
|
||||
name: Stop kellnr stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: absent
|
||||
|
||||
-
|
||||
name: Restore docker-compose file
|
||||
copy:
|
||||
src: "{{ _kellnr_backup_dir }}/docker-compose.yml"
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
remote_src: yes
|
||||
mode: "0600"
|
||||
|
||||
-
|
||||
name: Restore kellnr data volume
|
||||
command: >
|
||||
docker run --rm
|
||||
-v kellnr_kellnr_data:/data
|
||||
-v {{ _kellnr_backup_dir }}:/backup
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/kellnr_data.tar.gz -C /data"
|
||||
|
||||
-
|
||||
name: Start kellnr stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: present
|
||||
16
ansible/roles/kellnr/templates/docker-compose.yml.j2
Normal file
16
ansible/roles/kellnr/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
services:
|
||||
kellnr:
|
||||
image: ghcr.io/kellnr/kellnr:{{ version }}
|
||||
ports:
|
||||
- "127.0.0.1:{{ port }}:8000"
|
||||
environment:
|
||||
KELLNR_ORIGIN__HOSTNAME: "{{ hostname }}"
|
||||
KELLNR_ORIGIN__PORT: "443"
|
||||
KELLNR_ORIGIN__PROTOCOL: "https"
|
||||
KELLNR_AUTH__ADMIN_PWD: "{{ admin_pwd }}"
|
||||
volumes:
|
||||
- kellnr_data:/var/lib/kellnr
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
kellnr_data:
|
||||
10
ansible/roles/prosody/defaults/main.yml
Normal file
10
ansible/roles/prosody/defaults/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
install_dir: /opt/prosody
|
||||
bind_address: "0.0.0.0"
|
||||
bosh_port: 5280
|
||||
c2s_port: 5222
|
||||
s2s_port: 5269
|
||||
proxy65_port: 5000
|
||||
proxy65_address: "{{ bind_address }}"
|
||||
http_upload_file_size_limit: 10485760
|
||||
http_upload_expire_after: 604800
|
||||
153
ansible/roles/prosody/files/mod_auth_oauth_external.lua
Normal file
153
ansible/roles/prosody/files/mod_auth_oauth_external.lua
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
-- Based on https://hg.prosody.im/prosody-modules/file/tip/mod_auth_oauth_external/mod_auth_oauth_external.lua
|
||||
-- Patched to store email from userinfo in accounts store for mod_offline_email
|
||||
local http = require "net.http";
|
||||
local async = require "util.async";
|
||||
local jid = require "util.jid";
|
||||
local json = require "util.json";
|
||||
local sasl = require "util.sasl";
|
||||
|
||||
local issuer_identity = module:get_option_string("oauth_external_issuer");
|
||||
local oidc_discovery_url = module:get_option_string("oauth_external_discovery_url",
|
||||
issuer_identity and issuer_identity .. "/.well-known/oauth-authorization-server" or nil);
|
||||
local validation_endpoint = module:get_option_string("oauth_external_validation_endpoint");
|
||||
local token_endpoint = module:get_option_string("oauth_external_token_endpoint");
|
||||
|
||||
local username_field = module:get_option_string("oauth_external_username_field", "preferred_username");
|
||||
local allow_plain = module:get_option_boolean("oauth_external_resource_owner_password", true);
|
||||
|
||||
local client_id = module:get_option_string("oauth_external_client_id");
|
||||
local client_secret = module:get_option_string("oauth_external_client_secret");
|
||||
local scope = module:get_option_string("oauth_external_scope", "openid");
|
||||
|
||||
local accounts = module:open_store("accounts");
|
||||
|
||||
local host = module.host;
|
||||
local provider = {};
|
||||
|
||||
local function not_implemented()
|
||||
return nil, "method not implemented"
|
||||
end
|
||||
|
||||
provider.test_password = not_implemented;
|
||||
provider.get_password = not_implemented;
|
||||
provider.set_password = not_implemented;
|
||||
provider.create_user = not_implemented;
|
||||
|
||||
function provider.delete_user(username)
|
||||
return accounts:set(username, nil);
|
||||
end
|
||||
|
||||
function provider.get_account_info(username)
|
||||
local account, err = accounts:get(username);
|
||||
if not account then return nil, err or "Account not available"; end
|
||||
return {
|
||||
created = account.created;
|
||||
password_updated = account.updated;
|
||||
enabled = not account.disabled;
|
||||
};
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
local account, err = accounts:get(username);
|
||||
if err then
|
||||
return nil, err;
|
||||
elseif account then
|
||||
return true;
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function provider.users()
|
||||
return accounts:users();
|
||||
end
|
||||
|
||||
local function save_account(username, response)
|
||||
local account_data = { exists = true };
|
||||
if type(response) == "table" and type(response.email) == "string" then
|
||||
account_data.email = response.email;
|
||||
end
|
||||
accounts:set(username, account_data);
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler()
|
||||
local profile = {};
|
||||
profile.http_client = http.default:new({ connection_pooling = true });
|
||||
local extra = { oidc_discovery_url = oidc_discovery_url };
|
||||
if token_endpoint and allow_plain then
|
||||
local map_username = function (username, _realm) return username; end;
|
||||
function profile:plain_test(username, password, realm)
|
||||
username = jid.unescape(username);
|
||||
local tok, err = async.wait_for(self.profile.http_client:request(token_endpoint, {
|
||||
headers = { ["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"; ["Accept"] = "application/json" };
|
||||
body = http.formencode({
|
||||
grant_type = "password";
|
||||
client_id = client_id;
|
||||
client_secret = client_secret;
|
||||
username = map_username(username, realm);
|
||||
password = password;
|
||||
scope = scope;
|
||||
});
|
||||
}))
|
||||
if err or not (tok.code >= 200 and tok.code < 300) then
|
||||
return false, nil;
|
||||
end
|
||||
local token_resp = json.decode(tok.body);
|
||||
if not token_resp or string.lower(token_resp.token_type or "") ~= "bearer" then
|
||||
return false, nil;
|
||||
end
|
||||
if not validation_endpoint then
|
||||
self.username = jid.escape(username);
|
||||
self.token_info = token_resp;
|
||||
save_account(self.username, nil);
|
||||
return true, true;
|
||||
end
|
||||
local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint,
|
||||
{ headers = { ["Authorization"] = "Bearer " .. token_resp.access_token; ["Accept"] = "application/json" } }));
|
||||
if err then
|
||||
return false, nil;
|
||||
end
|
||||
if not (ret.code >= 200 and ret.code < 300) then
|
||||
return false, nil;
|
||||
end
|
||||
local response = json.decode(ret.body);
|
||||
if type(response) ~= "table" then
|
||||
return false, nil, nil;
|
||||
elseif type(response[username_field]) ~= "string" then
|
||||
return false, nil, nil;
|
||||
end
|
||||
self.username = jid.escape(response[username_field]);
|
||||
self.token_info = response;
|
||||
save_account(self.username, response);
|
||||
return true, true;
|
||||
end
|
||||
end
|
||||
if validation_endpoint then
|
||||
function profile:oauthbearer(token)
|
||||
if token == "" then
|
||||
return false, nil, extra;
|
||||
end
|
||||
|
||||
local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, {
|
||||
headers = { ["Authorization"] = "Bearer " .. token; ["Accept"] = "application/json" };
|
||||
}));
|
||||
if err then
|
||||
return false, nil, extra;
|
||||
end
|
||||
local response = ret and json.decode(ret.body);
|
||||
if not (ret.code >= 200 and ret.code < 300) then
|
||||
return false, nil, response or extra;
|
||||
end
|
||||
if type(response) ~= "table" or type(response[username_field]) ~= "string" then
|
||||
return false, nil, nil;
|
||||
end
|
||||
|
||||
local username = jid.escape(response[username_field]);
|
||||
save_account(username, response);
|
||||
return username, true, response;
|
||||
end
|
||||
end
|
||||
return sasl.new(host, profile);
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
-- Strip PLAIN from SASL mechanisms for BOSH and WebSocket connections.
|
||||
-- Direct c2s connections keep PLAIN for native XMPP client auth.
|
||||
|
||||
module:hook("stream-features", function(event)
|
||||
local origin, features = event.origin, event.features;
|
||||
|
||||
local is_bosh = origin.bosh_version ~= nil;
|
||||
local is_websocket = origin.websocket_request ~= nil;
|
||||
|
||||
if not (is_bosh or is_websocket) then
|
||||
return;
|
||||
end
|
||||
|
||||
for i, child in ipairs(features.tags) do
|
||||
if child.name == "mechanisms" then
|
||||
local dominated = {};
|
||||
for j, mech in ipairs(child.tags) do
|
||||
if mech:get_text() == "PLAIN" then
|
||||
table.insert(dominated, j);
|
||||
end
|
||||
end
|
||||
for k = #dominated, 1, -1 do
|
||||
local idx = dominated[k];
|
||||
table.remove(child.tags, idx);
|
||||
for m = #child, 1, -1 do
|
||||
if type(child[m]) == "table" and child[m].name == "mechanism" and child[m]:get_text() == "PLAIN" then
|
||||
table.remove(child, m);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
break;
|
||||
end
|
||||
end
|
||||
end, -1);
|
||||
16
ansible/roles/prosody/files/mod_session_timeout.lua
Normal file
16
ansible/roles/prosody/files/mod_session_timeout.lua
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- Disconnect c2s sessions after a configurable timeout to force re-authentication.
|
||||
-- This ensures that expired credentials (e.g. app passwords) are caught promptly.
|
||||
|
||||
local timeout = module:get_option_number("session_timeout", 1800); -- default 30 minutes
|
||||
|
||||
module:hook("resource-bind", function(event)
|
||||
local session = event.session;
|
||||
if not session then return; end
|
||||
|
||||
session._timeout_timer = module:add_timer(timeout, function()
|
||||
if session.type == "c2s" and not session.destroyed then
|
||||
module:log("info", "Session timeout for %s, forcing re-authentication", session.full_jid);
|
||||
session:close({ condition = "policy-violation", text = "Session expired, please reconnect" });
|
||||
end
|
||||
end);
|
||||
end);
|
||||
4
ansible/roles/prosody/meta/main.yml
Normal file
4
ansible/roles/prosody/meta/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
dependencies:
|
||||
-
|
||||
role: docker
|
||||
84
ansible/roles/prosody/tasks/deploy-prosody.yml
Normal file
84
ansible/roles/prosody/tasks/deploy-prosody.yml
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
-
|
||||
name: Ensure install directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
-
|
||||
name: Ensure modules directory exists
|
||||
file:
|
||||
path: "{{ install_dir }}/modules"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: oauth_client_id is defined or default_contacts is defined or smtp_host is defined or session_timeout is defined
|
||||
|
||||
-
|
||||
name: Deploy mod_auth_oauth_external
|
||||
copy:
|
||||
src: mod_auth_oauth_external.lua
|
||||
dest: "{{ install_dir }}/modules/mod_auth_oauth_external.lua"
|
||||
when: oauth_client_id is defined
|
||||
|
||||
-
|
||||
name: Deploy mod_sasl_oauthbearer_only_bosh
|
||||
copy:
|
||||
src: mod_sasl_oauthbearer_only_bosh.lua
|
||||
dest: "{{ install_dir }}/modules/mod_sasl_oauthbearer_only_bosh.lua"
|
||||
when: oauth_client_id is defined
|
||||
|
||||
-
|
||||
name: Deploy mod_default_contacts
|
||||
template:
|
||||
src: mod_default_contacts.lua.j2
|
||||
dest: "{{ install_dir }}/modules/mod_default_contacts.lua"
|
||||
when: default_contacts is defined
|
||||
|
||||
-
|
||||
name: Deploy mod_session_timeout
|
||||
copy:
|
||||
src: mod_session_timeout.lua
|
||||
dest: "{{ install_dir }}/modules/mod_session_timeout.lua"
|
||||
when: session_timeout is defined
|
||||
|
||||
-
|
||||
name: Deploy mod_offline_email
|
||||
template:
|
||||
src: mod_offline_email.lua.j2
|
||||
dest: "{{ install_dir }}/modules/mod_offline_email.lua"
|
||||
when: smtp_host is defined
|
||||
|
||||
-
|
||||
name: Deploy prosody configuration
|
||||
template:
|
||||
src: prosody.cfg.lua.j2
|
||||
dest: "{{ install_dir }}/prosody.cfg.lua"
|
||||
|
||||
-
|
||||
name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ install_dir }}/docker-compose.yml"
|
||||
|
||||
-
|
||||
name: Start prosody stack
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: start-compose
|
||||
vars:
|
||||
compose_project_dir: "{{ install_dir }}"
|
||||
|
||||
-
|
||||
name: Deploy prosody backup script
|
||||
include_role:
|
||||
name: docker
|
||||
tasks_from: deploy-backup
|
||||
vars:
|
||||
backup_name: prosody
|
||||
backup_hook_dir: /etc/restic/pre-backup.d
|
||||
backup_volumes:
|
||||
- prosody_prosody_data
|
||||
backup_files:
|
||||
- "{{ install_dir }}/docker-compose.yml"
|
||||
- "{{ install_dir }}/prosody.cfg.lua"
|
||||
4
ansible/roles/prosody/tasks/main.yml
Normal file
4
ansible/roles/prosody/tasks/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
-
|
||||
name: Deploy prosody
|
||||
ansible.builtin.include_tasks: deploy-prosody.yml
|
||||
36
ansible/roles/prosody/tasks/restore.yml
Normal file
36
ansible/roles/prosody/tasks/restore.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
-
|
||||
name: Set backup staging directory
|
||||
set_fact:
|
||||
_prosody_backup_dir: "{{ backup_staging_dir | default('/var/backups') }}/prosody"
|
||||
|
||||
-
|
||||
name: Stop prosody stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: absent
|
||||
|
||||
-
|
||||
name: Restore config files
|
||||
copy:
|
||||
src: "{{ _prosody_backup_dir }}/{{ item }}"
|
||||
dest: "{{ install_dir }}/{{ item }}"
|
||||
remote_src: yes
|
||||
mode: "0600"
|
||||
loop:
|
||||
- docker-compose.yml
|
||||
- prosody.cfg.lua
|
||||
|
||||
-
|
||||
name: Restore prosody data volume
|
||||
command: >
|
||||
docker run --rm
|
||||
-v prosody_prosody_data:/data
|
||||
-v {{ _prosody_backup_dir }}:/backup
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/prosody_data.tar.gz -C /data"
|
||||
|
||||
-
|
||||
name: Start prosody stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ install_dir }}"
|
||||
state: present
|
||||
18
ansible/roles/prosody/templates/docker-compose.yml.j2
Normal file
18
ansible/roles/prosody/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
services:
|
||||
prosody:
|
||||
image: prosodyim/prosody:{{ version }}
|
||||
network_mode: host
|
||||
volumes:
|
||||
- prosody_data:/var/lib/prosody
|
||||
- ./prosody.cfg.lua:/etc/prosody/prosody.cfg.lua:ro
|
||||
{% if oauth_client_id is defined or default_contacts is defined %}
|
||||
- ./modules:/usr/lib/prosody/custom-modules:ro
|
||||
{% endif %}
|
||||
{% if ssl_cert is defined %}
|
||||
- {{ ssl_cert }}:/etc/prosody/certs/fullchain.pem:ro
|
||||
- {{ ssl_key }}:/etc/prosody/certs/privkey.pem:ro
|
||||
{% endif %}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
prosody_data:
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue