diff --git a/Makefile b/Makefile index 6c1f6e2c..7daff935 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Makefile for j2render TEMPLATE=./templates/vars/applications.yml.j2 -OUTPUT=./group_vars/all/07_applications.yml +OUTPUT=./group_vars/all/11_applications.yml build: @echo "🔧 Building rendered file from $(TEMPLATE)..." diff --git a/docs/guides/developer/Role_Creation.md b/docs/guides/developer/Role_Creation.md index 60f14b09..75f13dba 100644 --- a/docs/guides/developer/Role_Creation.md +++ b/docs/guides/developer/Role_Creation.md @@ -4,12 +4,12 @@ This guide will walk you through the steps to add a new Docker role for a servic --- -### **1. Define the Application Configuration in `group_vars/all/07_applications.yml`** +### **1. Define the Application Configuration in `templates/vars/applications.yml.j2`** -First, you'll need to add the default configuration for your new service under the `defaults_applications` section in `group_vars/all/07_applications.yml`. +First, you'll need to add the default configuration for your new service under the `defaults_applications` section in `templates/vars/applications.yml.j2`. #### **Steps:** -- Open `group_vars/all/07_applications.yml` +- Open `templates/vars/applications.yml.j2` - Add the configuration for `my_service` under the `defaults_applications` section. ```yaml @@ -178,7 +178,7 @@ Once you have defined the Docker role, configuration settings, and other necessa ### **Conclusion** By following this guide, you have successfully added a new Dockerized service (`my_service`) to the CyMaIS platform. You have: -- Configured the service settings in `group_vars/all/07_applications.yml` +- Configured the service settings in `templates/vars/applications.yml.j2` - Added the domain for the service in `group_vars/all/03_domains.yml` - Set the `application_id` in `vars/main.yml` - Created the necessary Docker role for managing `my_service`. diff --git a/filter_plugins/configuration_filters.py b/filter_plugins/configuration_filters.py index f57400cd..61c5dd06 100644 --- a/filter_plugins/configuration_filters.py +++ b/filter_plugins/configuration_filters.py @@ -1,37 +1,10 @@ -def get_oauth2_enabled(applications, application_id): - # Retrieve the application dictionary based on the ID +def is_feature_enabled(applications, feature:str, application_id:str)->bool: app = applications.get(application_id, {}) - # Retrieve the value for oauth2_proxy.enabled, default is False - enabled = app.get('features', {}).get('oauth2', False) + enabled = app.get('features', {}).get(feature, False) return bool(enabled) -def get_oidc_enabled(applications, application_id): - # Retrieve the application dictionary based on the ID - app = applications.get(application_id, {}) - # Retrieve the value for oidc.enabled, default is False - enabled = app.get('features', {}).get('oidc', False) - return bool(enabled) - -def get_features_iframe(applications, application_id): - app = applications.get(application_id, {}) - enabled = app.get('features', {}).get('iframe', False) - return bool(enabled) - -def get_database_central_storage(applications, application_id): - """ - Retrieve the type of the database from the application dictionary. - The expected key structure is: applications[application_id]['database']['central_storage']. - If not defined, None is returned. - """ - app = applications.get(application_id, {}) - db_type = app.get('features', {}).get('database', False) - return db_type - class FilterModule(object): def filters(self): return { - 'get_oidc_enabled': get_oidc_enabled, - 'get_oauth2_enabled': get_oauth2_enabled, - 'get_database_central_storage': get_database_central_storage, - 'get_features_iframe': get_features_iframe, + 'is_feature_enabled': is_feature_enabled, } \ No newline at end of file diff --git a/group_vars/all/00_general.yml b/group_vars/all/00_general.yml index 434554d2..8caa5801 100644 --- a/group_vars/all/00_general.yml +++ b/group_vars/all/00_general.yml @@ -22,48 +22,6 @@ primary_domain_tld: "localhost" # Top Le primary_domain_sld: "cymais" # Second Level Domain of the server primary_domain: "{{primary_domain_sld}}.{{primary_domain_tld}}" # Primary Domain of the server -# Helper Variables - -# Helper Variables for administrator -_users_administrator_username: "{{ users.administrator.username | default('administrator') }}" -_users_administrator_email: "{{ users.administrator.email | default(_users_administrator_username ~ '@' ~ primary_domain) }}" - -# Helper Variables for bounce -_users_bounce_username: "{{ users.bounce.username | default('bounce') }}" -_users_bounce_email: "{{ users.bounce.email | default(_users_bounce_username ~ '@' ~ primary_domain) }}" - -# Helper Variables for no-reply -_users_no_reply_username: "{{ users['no-reply'].username | default('no-reply') }}" -_users_no_reply_email: "{{ users['no-reply'].email | default(_users_no_reply_username ~ '@' ~ primary_domain) }}" - -# Administrator -default_users: - administrator: - username: "{{_users_administrator_username}}" # Username of the administrator - email: "{{_users_administrator_email}}" # Email of the administrator - password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file - uid: 1001 # Posix User ID - gid: 1001 # Posix Group ID - is_admin: true # Define as admin user - - bounce: - username: "{{ _users_bounce_username }}" # Bounce-handler account username - email: "{{ _users_bounce_email }}" # Email address for handling bounces - password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file - uid: 1002 # Posix User ID for bounce - gid: 1002 # Posix Group ID for bounce - - no-reply: - username: "{{ _users_no_reply_username }}" # No-reply account username - email: "{{ _users_no_reply_email }}" # Email address for outgoing no-reply mails - password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file - uid: 1003 # Posix User ID for no-reply - gid: 1003 # Posix Group ID for no-reply - - -# Test Email -test_email: "test@{{primary_domain}}" - # Server Tact Variables ## Ours in which the server is "awake" (100% working). Rest of the time is reserved for maintanance diff --git a/group_vars/all/08_calendar.yml b/group_vars/all/07_calendar.yml similarity index 95% rename from group_vars/all/08_calendar.yml rename to group_vars/all/07_calendar.yml index 5e890569..ac23dcc2 100644 --- a/group_vars/all/08_calendar.yml +++ b/group_vars/all/07_calendar.yml @@ -6,6 +6,7 @@ on_calendar_health_disc_space: "*-*-* 06,12,18,00:00:00" on_calendar_health_docker_container: "*-*-* {{ hours_server_awake }}:00:00" # Check once per hour if the docker containers are healthy on_calendar_health_docker_volumes: "*-*-* {{ hours_server_awake }}:15:00" # Check once per hour if the docker volumes are healthy on_calendar_health_nginx: "*-*-* {{ hours_server_awake }}:45:00" # Check once per hour if all webservices are available +on_calendar_health_msmtp: "*-*-* 00:00:00" # Check once per day SMTP Server ## Schedule for Cleanup Tasks on_calendar_cleanup_backups: "*-*-* 00,06,12,18:30:00" # Cleanup backups every 6 hours, MUST be called before disc space cleanup diff --git a/group_vars/all/09_ports.yml b/group_vars/all/08_ports.yml similarity index 100% rename from group_vars/all/09_ports.yml rename to group_vars/all/08_ports.yml diff --git a/group_vars/all/10_networks.yml b/group_vars/all/09_networks.yml similarity index 100% rename from group_vars/all/10_networks.yml rename to group_vars/all/09_networks.yml diff --git a/group_vars/all/10_users.yml b/group_vars/all/10_users.yml new file mode 100644 index 00000000..6c1dcbc1 --- /dev/null +++ b/group_vars/all/10_users.yml @@ -0,0 +1,53 @@ +# Helper Variables + +# Helper Variables for administrator +_users_administrator_username: "{{ users.administrator.username | default('administrator') }}" +_users_administrator_email: "{{ users.administrator.email | default(_users_administrator_username ~ '@' ~ primary_domain) }}" + +# Helper Variables for bounce +_users_bounce_username: "{{ users.bounce.username | default('bounce') }}" +_users_bounce_email: "{{ users.bounce.email | default(_users_bounce_username ~ '@' ~ primary_domain) }}" + +# Helper Variables for no-reply +_users_no_reply_username: "{{ users['no-reply'].username | default('no-reply') }}" +_users_no_reply_email: "{{ users['no-reply'].email | default(_users_no_reply_username ~ '@' ~ primary_domain) }}" + +# Helper Variables for blackhole +_users_blackhole_username: "{{ users.blackhole.username | default('no-reply') }}" +_users_blackhole_email: "{{ users.blackhole.email | default(_users_blackhole_username ~ '@' ~ primary_domain) }}" + +# Administrator +default_users: + + # Credentials will be used as administration credentials for all applications and the system + administrator: + username: "{{_users_administrator_username}}" # Username of the administrator + email: "{{_users_administrator_email}}" # Email of the administrator + password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file + uid: 1001 # Posix User ID + gid: 1001 # Posix Group ID + is_admin: true # Define as admin user + + # Account for Newsletter bouncing + bounce: + username: "{{ _users_bounce_username }}" # Bounce-handler account username + email: "{{ _users_bounce_email }}" # Email address for handling bounces + password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file + uid: 1002 # Posix User ID for bounce + gid: 1002 # Posix Group ID for bounce + + # User to send System Emails from + no-reply: + username: "{{ _users_no_reply_username }}" # No-reply account username + email: "{{ _users_no_reply_email }}" # Email address for outgoing no-reply mails + password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file + uid: 1003 # Posix User ID for no-reply + gid: 1003 # Posix Group ID for no-reply + + # Emails etc, what you send to this user will be forgetten + blackhole: + username: "{{ _users_blackhole_username }}" # Blackhole account username + email: "{{ _users_blackhole_email }}" # Email address to which emails can be send which well be forgetten + password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file + uid: 1004 # Posix User ID for bounce + gid: 1004 # Posix Group ID for bounce \ No newline at end of file diff --git a/group_vars/all/11_iam.yml b/group_vars/all/12_iam.yml similarity index 100% rename from group_vars/all/11_iam.yml rename to group_vars/all/12_iam.yml diff --git a/group_vars/all/12_storage.yml b/group_vars/all/13_storage.yml similarity index 100% rename from group_vars/all/12_storage.yml rename to group_vars/all/13_storage.yml diff --git a/group_vars/all/13_design.yml b/group_vars/all/14_design.yml similarity index 100% rename from group_vars/all/13_design.yml rename to group_vars/all/14_design.yml diff --git a/group_vars/all/14_service_provider.yml b/group_vars/all/15_about.yml similarity index 100% rename from group_vars/all/14_service_provider.yml rename to group_vars/all/15_about.yml diff --git a/roles/backup-remote-to-local/templates/backups-remote-to-local.sh.j2 b/roles/backup-remote-to-local/templates/backups-remote-to-local.sh.j2 index facae816..654f1dd1 100644 --- a/roles/backup-remote-to-local/templates/backups-remote-to-local.sh.j2 +++ b/roles/backup-remote-to-local/templates/backups-remote-to-local.sh.j2 @@ -1,8 +1,8 @@ #!/bin/bash # Pulls the remote backups from multiple hosts -hosts="{{pull_remote_backups}}"; +hosts="{{ pull_remote_backups | join(' ') }}"; errors=0 for host in $hosts; do - bash {{docker_backup_remote_to_local_folder}}backup-remote-to-local.sh $host || ((errors+=1)); + bash {{ docker_backup_remote_to_local_folder }}backup-remote-to-local.sh $host || ((errors+=1)); done; exit $errors; diff --git a/roles/docker-compose/templates/services/msmtp_curl_test.yml.j2 b/roles/docker-compose/templates/services/msmtp_curl_test.yml.j2 index a328433a..b017f676 100644 --- a/roles/docker-compose/templates/services/msmtp_curl_test.yml.j2 +++ b/roles/docker-compose/templates/services/msmtp_curl_test.yml.j2 @@ -16,7 +16,7 @@ - CMD-SHELL - > if [ ! -f /tmp/email_sent ]; then - echo 'Subject: testmessage from {{domains[application_id]}}\n\nSUCCESSFULL' | msmtp -t {{test_email}} && touch /tmp/email_sent; + echo 'Subject: testmessage from {{domains[application_id]}}\n\nSUCCESSFULL' | msmtp -t {{users.blackhole.email}} && touch /tmp/email_sent; fi && curl -f http://localhost:80/ || exit 1 interval: 1m diff --git a/roles/docker-keycloak/templates/import/realm.json.j2 b/roles/docker-keycloak/templates/import/realm.json.j2 index f0adae62..db0434bd 100644 --- a/roles/docker-keycloak/templates/import/realm.json.j2 +++ b/roles/docker-keycloak/templates/import/realm.json.j2 @@ -835,7 +835,7 @@ "secret": "{{oidc.client.secret}}", {%- set redirect_uris = [] %} {%- for application, domain in domains.items() %} - {%- if applications[application] is defined and (applications | get_oauth2_enabled(application) or applications | get_oidc_enabled(application)) %} + {%- if applications[application] is defined and (applications | is_feature_enabled('oauth2',application) or applications | is_feature_enabled('oidc',application)) %} {%- if domain is string %} {%- set _ = redirect_uris.append(web_protocol ~ '://' ~ domain ~ '/*') %} {%- else %} diff --git a/roles/docker-nextcloud/templates/nginx/docker.conf.j2 b/roles/docker-nextcloud/templates/nginx/docker.conf.j2 index 470c120f..d4c66ca1 100644 --- a/roles/docker-nextcloud/templates/nginx/docker.conf.j2 +++ b/roles/docker-nextcloud/templates/nginx/docker.conf.j2 @@ -75,7 +75,6 @@ http { add_header X-Robots-Tag "noindex, nofollow" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Frame-Options "SAMEORIGIN" always; - {% include 'roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2' %} # Remove X-Powered-By, which is an information leak fastcgi_hide_header X-Powered-By; diff --git a/roles/docker-oauth2-proxy/templates/container.yml.j2 b/roles/docker-oauth2-proxy/templates/container.yml.j2 index 75b5f315..81e5094e 100644 --- a/roles/docker-oauth2-proxy/templates/container.yml.j2 +++ b/roles/docker-oauth2-proxy/templates/container.yml.j2 @@ -1,4 +1,4 @@ -{% if applications | get_oauth2_enabled(application_id) %} +{% if applications | is_feature_enabled('oauth2',application_id) %} oauth2-proxy: image: quay.io/oauth2-proxy/oauth2-proxy:{{applications.oauth2_proxy.version}} restart: {{docker_restart_policy}} diff --git a/roles/docker-peertube/templates/peertube.conf.j2 b/roles/docker-peertube/templates/peertube.conf.j2 index 459c498f..4cdc94c8 100644 --- a/roles/docker-peertube/templates/peertube.conf.j2 +++ b/roles/docker-peertube/templates/peertube.conf.j2 @@ -5,7 +5,7 @@ server { {% include 'roles/nginx-modifier-all/templates/global.includes.conf.j2'%} - {% include 'roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2' %} + {% include 'roles/nginx-docker-reverse-proxy/templates/headers/content_security_policy.conf.j2' %} ## # Application diff --git a/roles/docker-portfolio/templates/config.yaml.j2 b/roles/docker-portfolio/templates/config.yaml.j2 index 07b02b3b..784265c0 100644 --- a/roles/docker-portfolio/templates/config.yaml.j2 +++ b/roles/docker-portfolio/templates/config.yaml.j2 @@ -28,7 +28,7 @@ accounts: class: fa-brands fa-mastodon url: "{{ web_protocol }}://{{ service_provider.contact.mastodon.split('@')[2] }}/@{{ service_provider.contact.mastodon.split('@')[1] }}" identifier: "{{service_provider.contact.mastodon}}" - iframe: {{ applications | get_features_iframe('mastodon') }} + iframe: {{ applications | is_feature_enabled('iframe','mastodon') }} {% endif %} {% if service_provider.contact.bluesky is defined and service_provider.contact.bluesky != "" %} @@ -52,7 +52,7 @@ accounts: class: fa-solid fa-camera identifier: "{{service_provider.contact.pixelfed}}" url: "{{ web_protocol }}://{{ service_provider.contact.pixelfed.split('@')[2] }}/@{{ service_provider.contact.pixelfed.split('@')[1] }}" - iframe: {{ applications | get_features_iframe('pixelfed') }} + iframe: {{ applications | is_feature_enabled('iframe','pixelfed') }} {% endif %} {% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %} @@ -64,7 +64,7 @@ accounts: class: fa-solid fa-video identifier: "{{service_provider.contact.peertube}}" url: "{{ web_protocol }}://{{ service_provider.contact.peertube.split('@')[2] }}/@{{ service_provider.contact.peertube.split('@')[1] }}" - iframe: {{ applications | get_features_iframe('peertube') }} + iframe: {{ applications | is_feature_enabled('iframe','peertube') }} {% endif %} {% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %} @@ -76,7 +76,7 @@ accounts: class: fa-solid fa-blog identifier: "{{service_provider.contact.wordpress}}" url: "{{ web_protocol }}://{{ service_provider.contact.wordpress.split('@')[2] }}/@{{ service_provider.contact.wordpress.split('@')[1] }}" - iframe: {{ applications | get_features_iframe('wordpress') }} + iframe: {{ applications | is_feature_enabled('iframe','wordpress') }} {% endif %} {% if service_provider.contact.source_code is defined and service_provider.contact.source_code != "" %} @@ -98,7 +98,7 @@ accounts: class: fas fa-network-wired identifier: "{{service_provider.contact.friendica}}" url: "{{ web_protocol }}://{{ service_provider.contact.friendica.split('@')[2] }}/@{{ service_provider.contact.friendica.split('@')[1] }}" - iframe: {{ applications | get_features_iframe('friendica') }} + iframe: {{ applications | is_feature_enabled('iframe','friendica') }} {% endif %} diff --git a/roles/docker-portfolio/templates/footer_menu.yaml.j2 b/roles/docker-portfolio/templates/footer_menu.yaml.j2 index d00e832a..f0903b97 100644 --- a/roles/docker-portfolio/templates/footer_menu.yaml.j2 +++ b/roles/docker-portfolio/templates/footer_menu.yaml.j2 @@ -37,13 +37,13 @@ icon: class: fa-solid fa-shield-halved url: https://{{domains.keycloak}}/admin - iframe: {{ applications | get_features_iframe('keycloak') }} + iframe: {{ applications | is_feature_enabled('iframe','keycloak') }} - name: Profile description: Update your personal admin settings icon: class: fa-solid fa-user-gear url: https://{{ domains.keycloak }}/realms/{{oidc.client.id}}/account - iframe: {{ applications | get_features_iframe('keycloak') }} + iframe: {{ applications | is_feature_enabled('iframe','keycloak') }} - name: Logout description: End your admin session securely icon: @@ -113,7 +113,7 @@ icon: class: fas fa-book url: https://{{domains.sphinx}} - iframe: {{ applications | get_features_iframe('sphinx') }} + iframe: {{ applications | is_feature_enabled('iframe','sphinx') }} {% endif %} @@ -124,7 +124,7 @@ icon: class: "fas fa-chalkboard-teacher" url: https://{{domains.presentation}} - iframe: {{ applications | get_features_iframe('presentation') }} + iframe: {{ applications | is_feature_enabled('iframe','presentation') }} {% endif %} diff --git a/roles/docker-syncope/templates/proxy.conf b/roles/docker-syncope/templates/proxy.conf index 106ed3b9..cc31f807 100644 --- a/roles/docker-syncope/templates/proxy.conf +++ b/roles/docker-syncope/templates/proxy.conf @@ -2,7 +2,7 @@ server { server_name {{domain}}; - {% if applications | get_oauth2_enabled(application_id) %} + {% if applications | is_feature_enabled('oauth2',application_id) %} {% include 'roles/docker-oauth2-proxy/templates/endpoint.conf.j2'%} {% endif %} diff --git a/roles/health-journalctl/handlers/main.yml b/roles/health-journalctl/handlers/main.yml index 83fa7f0b..4f80b4a1 100644 --- a/roles/health-journalctl/handlers/main.yml +++ b/roles/health-journalctl/handlers/main.yml @@ -1,5 +1,5 @@ -- name: "reload health-journalctl.cymais.service" +- name: reload health-msmtp.cymais.service systemd: - name: health-journalctl.cymais.service + name: health-msmtp.cymais.service enabled: yes - daemon_reload: yes + daemon_reload: yes \ No newline at end of file diff --git a/roles/health-msmtp/README.md b/roles/health-msmtp/README.md new file mode 100644 index 00000000..a75855ef --- /dev/null +++ b/roles/health-msmtp/README.md @@ -0,0 +1,21 @@ +# health-msmtp + +## Description + +This Ansible role sends periodic health check emails using **msmtp** to verify that your mail transport agent is operational. It deploys a simple script and hooks it into a systemd service and timer, with failure notifications sent via Telegram. + +## Overview + +Optimized for Archlinux, this role creates the required directory structure, installs and configures the health-check script, and integrates with the **systemd-notifier-telegram** role. It uses the **systemd-timer** role to schedule regular checks based on your customizable `OnCalendar` setting. + +## Purpose + +The **health-msmtp** role ensures that your mail transport system stays available by sending a test email at defined intervals. If the email fails, a Telegram alert is triggered, allowing you to detect and address issues before they impact users. + +## Features + +- **Directory & Script Deployment:** Sets up `health-msmtp/` and deploys a templated Bash script to send test emails via msmtp. +- **Systemd Service & Timer:** Provides `.service` and `.timer` units to run the check and schedule it automatically. +- **Failure Notifications:** Leverages **systemd-notifier-telegram** to push alerts when the script exits with an error. +- **Configurable Schedule:** Define your desired check frequency using the `on_calendar_health_msmtp` variable. +- **Email Destination:** Specify the recipient via the `users.administrator.email` variable. \ No newline at end of file diff --git a/roles/health-msmtp/handlers/main.yml b/roles/health-msmtp/handlers/main.yml new file mode 100644 index 00000000..83fa7f0b --- /dev/null +++ b/roles/health-msmtp/handlers/main.yml @@ -0,0 +1,5 @@ +- name: "reload health-journalctl.cymais.service" + systemd: + name: health-journalctl.cymais.service + enabled: yes + daemon_reload: yes diff --git a/roles/health-msmtp/meta/main.yml b/roles/health-msmtp/meta/main.yml new file mode 100644 index 00000000..5062b875 --- /dev/null +++ b/roles/health-msmtp/meta/main.yml @@ -0,0 +1,25 @@ +galaxy_info: + author: "Kevin Veen-Birkenbach" + description: "Sends periodic health check emails via msmtp" + license: "CyMaIS NonCommercial License (CNCL)" + license_url: "https://s.veen.world/cncl" + company: | + Kevin Veen-Birkenbach + Consulting & Coaching Solutions + https://www.veen.world + min_ansible_version: "2.9" + platforms: + - name: Archlinux + versions: + - rolling + galaxy_tags: + - health + - msmtp + - email + - systemd + - monitoring + repository: "https://s.veen.world/cymais" + issue_tracker_url: "https://s.veen.world/cymaisissues" + documentation: "https://s.veen.world/cymais" +dependencies: + - systemd-notifier-telegram diff --git a/roles/health-msmtp/tasks/main.yml b/roles/health-msmtp/tasks/main.yml new file mode 100644 index 00000000..fe9ecd6a --- /dev/null +++ b/roles/health-msmtp/tasks/main.yml @@ -0,0 +1,27 @@ +- name: "create {{ health_msmtp_folder }}" + file: + path: "{{ health_msmtp_folder }}" + state: directory + mode: 0755 + +- name: create health-msmtp.sh + template: + src: health-msmtp.sh.j2 + dest: "{{ health_msmtp_folder }}health-msmtp.sh" + mode: '0755' + +- name: create health-msmtp.cymais.service + template: + src: health-msmtp.service.j2 + dest: /etc/systemd/system/health-msmtp.cymais.service + notify: reload health-msmtp.cymais.service + +- name: set service_name to the name of the current role + set_fact: + service_name: "{{ role_name }}" + +- name: include role for systemd-timer for {{ service_name }} + include_role: + name: systemd-timer + vars: + on_calendar: "{{ on_calendar_health_msmtp }}" \ No newline at end of file diff --git a/roles/health-msmtp/templates/health-msmtp.service.j2 b/roles/health-msmtp/templates/health-msmtp.service.j2 new file mode 100644 index 00000000..0bfb9ed1 --- /dev/null +++ b/roles/health-msmtp/templates/health-msmtp.service.j2 @@ -0,0 +1,7 @@ +[Unit] +Description=Check msmtp liveliness +OnFailure=systemd-notifier-telegram.cymais@%n.service + +[Service] +Type=oneshot +ExecStart=/bin/bash {{ health_msmtp_folder }}health-msmtp.sh diff --git a/roles/health-msmtp/templates/health-msmtp.sh.j2 b/roles/health-msmtp/templates/health-msmtp.sh.j2 new file mode 100644 index 00000000..0982d729 --- /dev/null +++ b/roles/health-msmtp/templates/health-msmtp.sh.j2 @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Subject: $HOST is alive + +Host $HOSTNAME reports at $(date): I'm alive." | msmtp -t {{ users.administrator.email }} \ No newline at end of file diff --git a/roles/health-msmtp/vars/main.yml b/roles/health-msmtp/vars/main.yml new file mode 100644 index 00000000..7cefc91a --- /dev/null +++ b/roles/health-msmtp/vars/main.yml @@ -0,0 +1 @@ +health_msmtp_folder: "{{ path_administrator_scripts }}health-msmtp/" \ No newline at end of file diff --git a/roles/msmtp/meta/main.yml b/roles/msmtp/meta/main.yml index 5e636dd7..fb0e4eb3 100644 --- a/roles/msmtp/meta/main.yml +++ b/roles/msmtp/meta/main.yml @@ -21,4 +21,5 @@ galaxy_info: repository: "https://s.veen.world/cymais" issue_tracker_url: "https://s.veen.world/cymaisissues" documentation: "https://s.veen.world/cymais" -dependencies: [] +dependencies: + - health-msmtp diff --git a/roles/nginx-docker-reverse-proxy/templates/headers/content_security_policy.conf.j2 b/roles/nginx-docker-reverse-proxy/templates/headers/content_security_policy.conf.j2 new file mode 100644 index 00000000..92edb44a --- /dev/null +++ b/roles/nginx-docker-reverse-proxy/templates/headers/content_security_policy.conf.j2 @@ -0,0 +1,39 @@ +{%- set csp_parts = [] %} + +{# default-src: Fallback for all other directives if not explicitly defined #} +{%- set csp_parts = csp_parts + ["default-src 'self';"] %} + +{# frame-ancestors: Restricts which origins can embed this site in a frame or iframe #} +{%- set frame_ancestors = "frame-ancestors 'self'" %} +{%- if applications | is_feature_enabled('iframe', application_id) | bool %} + {%- set frame_ancestors = frame_ancestors + " " + web_protocol + "://" + primary_domain %} +{%- endif %} +{%- set csp_parts = csp_parts + [frame_ancestors + ";"] %} + +{# frame-src: Controls which URLs can be embedded as iframes #} +{%- set frame_src = "frame-src 'self'" %} +{%- if applications | is_feature_enabled('recaptcha', application_id) | bool %} + {%- set frame_src = frame_src + " https://www.google.com https://www.recaptcha.net" %} +{%- endif %} +{%- set csp_parts = csp_parts + [frame_src + ";"] %} + +{# img-src: Allow images from own domain and files deliverer. Also from Matomo if enabled. #} +{%- set img_src = "img-src 'self' " + web_protocol + "://" + domains.file_server %} +{%- if applications | is_feature_enabled('matomo', application_id) | bool %} + {%- set img_src = img_src + " " + web_protocol + "://" + domains.matomo %} +{%- endif %} +{%- set csp_parts = csp_parts + [img_src + ";"] %} + +{# script-src: Allow JavaScript from self, FontAwesome, jsDelivr, and Matomo if enabled #} +{%- set script_src = "script-src 'self' 'unsafe-inline'" %} +{%- if applications | is_feature_enabled('matomo', application_id) | bool %} + {%- set script_src = script_src + " " + domains.matomo %} +{%- endif %} +{%- set script_src = script_src + " https://kit.fontawesome.com https://cdn.jsdelivr.net" %} +{%- set csp_parts = csp_parts + [script_src + ";"] %} + +{# style-src: Allow CSS from self, FontAwesome, jsDelivr and inline styles #} +{%- set style_src = "style-src 'self' 'unsafe-inline' https://kit.fontawesome.com https://cdn.jsdelivr.net" %} +{%- set csp_parts = csp_parts + [style_src + ";"] %} + +add_header Content-Security-Policy "{{ csp_parts | join(' ') }}" always; diff --git a/roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2 b/roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2 deleted file mode 100644 index 8908fb13..00000000 --- a/roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2 +++ /dev/null @@ -1,4 +0,0 @@ -{% if applications.get(application_id, {}).get('features', {}).get('iframe', False) %} -add_header X-Frame-Options "SAMEORIGIN" always; -add_header Content-Security-Policy "frame-ancestors 'self' {{primary_domain}};" always; -{% endif %} diff --git a/roles/nginx-docker-reverse-proxy/templates/location/proxy_basic.conf.j2 b/roles/nginx-docker-reverse-proxy/templates/location/proxy_basic.conf.j2 index 94d612e6..addc6fb6 100644 --- a/roles/nginx-docker-reverse-proxy/templates/location/proxy_basic.conf.j2 +++ b/roles/nginx-docker-reverse-proxy/templates/location/proxy_basic.conf.j2 @@ -14,7 +14,7 @@ location {{location | default("/")}} proxy_set_header X-Forwarded-Port 443; proxy_set_header Accept-Encoding ""; - {% include 'roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2' %} + {% include 'roles/nginx-docker-reverse-proxy/templates/headers/content_security_policy.conf.j2' %} # WebSocket specific header proxy_http_version 1.1; diff --git a/roles/nginx-docker-reverse-proxy/templates/vhost/basic.conf.j2 b/roles/nginx-docker-reverse-proxy/templates/vhost/basic.conf.j2 index 2e59b23b..7309cc02 100644 --- a/roles/nginx-docker-reverse-proxy/templates/vhost/basic.conf.j2 +++ b/roles/nginx-docker-reverse-proxy/templates/vhost/basic.conf.j2 @@ -2,7 +2,7 @@ server { server_name {{domain}}; - {% if applications | get_oauth2_enabled(application_id) %} + {% if applications | is_feature_enabled('oauth2',application_id) %} {% include 'roles/docker-oauth2-proxy/templates/endpoint.conf.j2'%} {% endif %} @@ -15,7 +15,7 @@ server {% include 'roles/letsencrypt/templates/ssl_header.j2' %} - {% if applications | get_oauth2_enabled(application_id) %} + {% if applications | is_feature_enabled('oauth2',application_id) %} {% if applications[application_id].oauth2_proxy.location is defined %} {# Exposed and Unprotected Location #} {% include 'roles/nginx-docker-reverse-proxy/templates/location/proxy_basic.conf.j2' %} diff --git a/roles/nginx-https-get-cert/tasks/flavors/san.yml b/roles/nginx-https-get-cert/tasks/flavors/san.yml index 1fda3626..241c91ef 100644 --- a/roles/nginx-https-get-cert/tasks/flavors/san.yml +++ b/roles/nginx-https-get-cert/tasks/flavors/san.yml @@ -21,6 +21,9 @@ {{ '--mode-test' if mode_test | bool else '' }} register: certbundle_result changed_when: "'Certificate not yet due for renewal' not in certbundle_result.stdout" + failed_when: > + certbundle_result.rc != 0 + and 'too many certificates' not in certbundle_result.stderr when: run_once_san_certs is not defined - name: run the san tasks once diff --git a/roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2 b/roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2 index 1da2ddee..53f7dc67 100644 --- a/roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2 +++ b/roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2 @@ -1,5 +1,2 @@ -# Deactivate CSP header -add_header Content-Security-Policy: ""; - # sub filters to integrate matomo tracking code in nginx websites sub_filter '' ''; \ No newline at end of file diff --git a/roles/nginx-serve-files/templates/nginx.conf.j2 b/roles/nginx-serve-files/templates/nginx.conf.j2 index 7409343a..dee2a0ee 100644 --- a/roles/nginx-serve-files/templates/nginx.conf.j2 +++ b/roles/nginx-serve-files/templates/nginx.conf.j2 @@ -6,7 +6,7 @@ server {% include 'roles/nginx-modifier-all/templates/global.includes.conf.j2'%} - {% include 'roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2' %} + {% include 'roles/nginx-docker-reverse-proxy/templates/headers/content_security_policy.conf.j2' %} charset utf-8; location / diff --git a/roles/nginx-serve-html/templates/nginx.conf.j2 b/roles/nginx-serve-html/templates/nginx.conf.j2 index 2c492ac5..4e1124f8 100644 --- a/roles/nginx-serve-html/templates/nginx.conf.j2 +++ b/roles/nginx-serve-html/templates/nginx.conf.j2 @@ -6,7 +6,7 @@ server {% include 'roles/nginx-modifier-all/templates/global.includes.conf.j2'%} - {% include 'roles/nginx-docker-reverse-proxy/templates/headers/iframe.conf.j2' %} + {% include 'roles/nginx-docker-reverse-proxy/templates/headers/content_security_policy.conf.j2' %} charset utf-8; location / diff --git a/templates/docker/compose/networks.yml.j2 b/templates/docker/compose/networks.yml.j2 index 6b4bf113..e4226955 100644 --- a/templates/docker/compose/networks.yml.j2 +++ b/templates/docker/compose/networks.yml.j2 @@ -1,6 +1,6 @@ {# This template needs to be included in docker-compose.yml #} networks: -{% if applications | get_database_central_storage(application_id) | bool and database_type is defined %} +{% if applications | is_feature_enabled('database',application_id) | bool and database_type is defined %} central_{{ database_type }}: external: true {% endif %} diff --git a/templates/docker/container/networks.yml.j2 b/templates/docker/container/networks.yml.j2 index 421459e6..b6006fce 100644 --- a/templates/docker/container/networks.yml.j2 +++ b/templates/docker/container/networks.yml.j2 @@ -1,6 +1,6 @@ {# This template needs to be included in docker-compose.yml containers #} networks: -{% if applications | get_database_central_storage(application_id) | bool and database_type is defined %} +{% if applications | is_feature_enabled('database',application_id) | bool and database_type is defined %} central_{{ database_type }}: {% endif %} {% if applications[application_id].get('features', {}).get('ldap', false) | bool and applications.ldap.network.docker|bool %} diff --git a/templates/vars/applications.yml.j2 b/templates/vars/applications.yml.j2 index 693e75fc..23c24257 100644 --- a/templates/vars/applications.yml.j2 +++ b/templates/vars/applications.yml.j2 @@ -63,7 +63,7 @@ defaults_applications: {% endraw %}{{ features.render_features({ 'matomo': true, 'css': true, - 'iframe': true, + 'iframe': false, 'ldap': false, 'oidc': true, 'database': false, @@ -253,6 +253,7 @@ defaults_applications: 'iframe': true, 'ldap': true, 'database': true, + 'recaptcha': true, }) }}{% raw %} # LDAP Account Manager