From d5f194b2c0d55dcb1ff30b4b45413d37ce5eb39d Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 8 May 2025 09:51:38 +0200 Subject: [PATCH] Solved certreap bugs, implemented caching for pictures, optimized CSP policies (stricter), optimized recaptcha implementation for keycloak, solved mariadb wait bug, solved nextcloud plugin bugs, optimized ignore handling of tasks --- group_vars/all/05_nginx.yml | 1 + roles/cleanup-certs/meta/main.yml | 1 - roles/cleanup-certs/tasks/main.yml | 2 +- ...ts.service.j2 => cleanup-certs.service.j2} | 2 +- .../README.md | 2 +- .../meta/main.yml | 0 .../tasks/main.yml | 0 .../tasks/remove_deprecated_nginx_configs.yml | 0 .../templates/import/realm.json.j2 | 25 ++++++ roles/docker-mariadb/tasks/main.yml | 9 +- roles/docker-nextcloud/tasks/plugin.yml | 83 +++++++++++-------- roles/docker-nextcloud/vars/plugins/bbb.yml | 2 +- .../nginx-docker-reverse-proxy/tasks/main.yml | 23 +++++ .../headers/content_security_policy.conf.j2 | 30 +++++-- .../templates/location/proxy_basic.conf.j2 | 3 + .../templates/location/proxy_cache.conf.j2 | 18 ++++ roles/nginx-https/meta/main.yml | 2 +- roles/update/tasks/main.yml | 8 +- templates/vars/features.yml.j2 | 15 ++-- 19 files changed, 162 insertions(+), 64 deletions(-) rename roles/cleanup-certs/templates/{certs.service.j2 => cleanup-certs.service.j2} (76%) rename roles/{nginx-domains-cleanup => cleanup-domains}/README.md (97%) rename roles/{nginx-domains-cleanup => cleanup-domains}/meta/main.yml (100%) rename roles/{nginx-domains-cleanup => cleanup-domains}/tasks/main.yml (100%) rename roles/{nginx-domains-cleanup => cleanup-domains}/tasks/remove_deprecated_nginx_configs.yml (100%) create mode 100644 roles/nginx-docker-reverse-proxy/tasks/main.yml create mode 100644 roles/nginx-docker-reverse-proxy/templates/location/proxy_cache.conf.j2 diff --git a/group_vars/all/05_nginx.yml b/group_vars/all/05_nginx.yml index f3a1a51f..bbd0ebe3 100644 --- a/group_vars/all/05_nginx.yml +++ b/group_vars/all/05_nginx.yml @@ -14,5 +14,6 @@ nginx: html: "/var/www/public_html/" # Path where the static homepage files are stored files: "/var/www/public_files/" # Path where the web accessable files are stored global: "/var/www/global/" # Directory containing files which will be globaly accessable + cache: "/tmp/nginx_cache/" # Directory which nginx uses to cache data user: "http" # Default nginx user in ArchLinux iframe: true # Allows applications to be loaded in iframe \ No newline at end of file diff --git a/roles/cleanup-certs/meta/main.yml b/roles/cleanup-certs/meta/main.yml index e5fdae16..c4c86b29 100644 --- a/roles/cleanup-certs/meta/main.yml +++ b/roles/cleanup-certs/meta/main.yml @@ -24,5 +24,4 @@ galaxy_info: documentation: "https://github.com/kevinveenbirkenbach/certreap#readme" dependencies: - - systemd-timer - systemd-notifier diff --git a/roles/cleanup-certs/tasks/main.yml b/roles/cleanup-certs/tasks/main.yml index afe50090..76b27220 100644 --- a/roles/cleanup-certs/tasks/main.yml +++ b/roles/cleanup-certs/tasks/main.yml @@ -2,7 +2,7 @@ include_role: name: pkgmgr-install vars: - package_name: cleanup-certs + package_name: certreap when: run_once_cleanup_certs is not defined - name: configure cleanup-certs.cymais.service diff --git a/roles/cleanup-certs/templates/certs.service.j2 b/roles/cleanup-certs/templates/cleanup-certs.service.j2 similarity index 76% rename from roles/cleanup-certs/templates/certs.service.j2 rename to roles/cleanup-certs/templates/cleanup-certs.service.j2 index 4044771f..d13de449 100644 --- a/roles/cleanup-certs/templates/certs.service.j2 +++ b/roles/cleanup-certs/templates/cleanup-certs.service.j2 @@ -4,4 +4,4 @@ OnFailure=systemd-notifier.cymais@%n.service [Service] Type=oneshot -ExecStartPre=/bin/sh -c '/usr/bin/python certreap --force' \ No newline at end of file +ExecStart=/bin/sh -c 'certreap --force' \ No newline at end of file diff --git a/roles/nginx-domains-cleanup/README.md b/roles/cleanup-domains/README.md similarity index 97% rename from roles/nginx-domains-cleanup/README.md rename to roles/cleanup-domains/README.md index c27d2124..baa1521c 100644 --- a/roles/nginx-domains-cleanup/README.md +++ b/roles/cleanup-domains/README.md @@ -1,4 +1,4 @@ -# nginx-domains-cleanup +# cleanup-domains ## Description diff --git a/roles/nginx-domains-cleanup/meta/main.yml b/roles/cleanup-domains/meta/main.yml similarity index 100% rename from roles/nginx-domains-cleanup/meta/main.yml rename to roles/cleanup-domains/meta/main.yml diff --git a/roles/nginx-domains-cleanup/tasks/main.yml b/roles/cleanup-domains/tasks/main.yml similarity index 100% rename from roles/nginx-domains-cleanup/tasks/main.yml rename to roles/cleanup-domains/tasks/main.yml diff --git a/roles/nginx-domains-cleanup/tasks/remove_deprecated_nginx_configs.yml b/roles/cleanup-domains/tasks/remove_deprecated_nginx_configs.yml similarity index 100% rename from roles/nginx-domains-cleanup/tasks/remove_deprecated_nginx_configs.yml rename to roles/cleanup-domains/tasks/remove_deprecated_nginx_configs.yml diff --git a/roles/docker-keycloak/templates/import/realm.json.j2 b/roles/docker-keycloak/templates/import/realm.json.j2 index db0434bd..540ba1f8 100644 --- a/roles/docker-keycloak/templates/import/realm.json.j2 +++ b/roles/docker-keycloak/templates/import/realm.json.j2 @@ -2666,6 +2666,17 @@ "autheticatorFlow": false, "userSetupAllowed": false }, +{%- if applications | is_feature_enabled('recaptcha', application_id) %} + { + "authenticatorConfig": "Google reCaptcha", + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, +{%- else %} { "authenticator": "registration-recaptcha-action", "authenticatorFlow": false, @@ -2674,6 +2685,7 @@ "autheticatorFlow": false, "userSetupAllowed": false }, +{%- endif %} { "authenticator": "registration-terms-and-conditions", "authenticatorFlow": false, @@ -2746,6 +2758,19 @@ } ], "authenticatorConfig": [ +{%- if applications | is_feature_enabled('recaptcha',application_id) %} + { + "id": "c6dcf381-7e39-4f7f-8d1f-631faec31b56", + "alias": "Google reCaptcha", + "config": { + "action": "register", + "useRecaptchaNet": "false", + "recaptcha.v3": "true", + "secret.key": "{{ applications[application_id].credentials.recaptcha.secret_key }}", + "site.key": "{{ applications[application_id].credentials.recaptcha.website_key }}" + } + }, +{%- endif %} { "id": "3e40f95e-d9a7-405d-b393-398bfc54c2e8", "alias": "create unique user config", diff --git a/roles/docker-mariadb/tasks/main.yml b/roles/docker-mariadb/tasks/main.yml index 15354beb..488ff95e 100644 --- a/roles/docker-mariadb/tasks/main.yml +++ b/roles/docker-mariadb/tasks/main.yml @@ -36,10 +36,11 @@ state: present when: run_once_docker_mariadb is not defined -- name: Wait for MariaDB inside the container to respond - shell: docker exec central-mariadb mysqladmin ping -h localhost --silent - register: mysql_ping - until: mysql_ping.rc == 0 +- name: Wait until the MariaDB container is healthy + community.docker.docker_container_info: + name: central-mariadb + register: db_info + until: db_info.containers[0].State.Health.Status == "healthy" retries: 30 delay: 5 when: diff --git a/roles/docker-nextcloud/tasks/plugin.yml b/roles/docker-nextcloud/tasks/plugin.yml index 3d511046..9abf7e27 100644 --- a/roles/docker-nextcloud/tasks/plugin.yml +++ b/roles/docker-nextcloud/tasks/plugin.yml @@ -16,46 +16,59 @@ when: not (plugin_value.enabled | bool) - name: install {{ plugin_key }} nextcloud plugin - command: "{{nextcloud_docker_exec_occ}} app:install {{ plugin_key }}" + command: "{{ nextcloud_docker_exec_occ }} app:install {{ plugin_key }}" register: install_result - failed_when: install_result.rc != 0 and ("already installed" not in install_result.stdout) - changed_when: install_result.rc == 0 and ("already installed" not in install_result.stdout) + failed_when: > + install_result.rc != 0 + and + ("already installed" not in install_result.stdout) + and + ("not compatible with this version of the server" not in install_result.stdout) + changed_when: > + install_result.rc == 0 + and + ("already installed" not in install_result.stdout) when: plugin_value.enabled | bool -- name: enable {{plugin_key}} nextcloud plugin - command: "{{nextcloud_docker_exec_occ}} app:enable {{plugin_key}}" - register: enable_result - changed_when: enable_result.rc == 0 and ("already enabled" not in enable_result.stdout) - when: plugin_value.enabled | bool +- block: + - name: enable {{plugin_key}} nextcloud plugin + command: "{{nextcloud_docker_exec_occ}} app:enable {{plugin_key}}" + register: enable_result + changed_when: enable_result.rc == 0 and ("already enabled" not in enable_result.stdout) -- name: Check if {{nextcloud_control_node_plugin_vars_directory}}{{ plugin_key }}.yml exists - stat: - path: "{{nextcloud_control_node_plugin_vars_directory}}{{ plugin_key }}.yml" - delegate_to: localhost - become: false - register: plugin_vars_file + - name: Check if {{nextcloud_control_node_plugin_vars_directory}}{{ plugin_key }}.yml exists + stat: + path: "{{nextcloud_control_node_plugin_vars_directory}}{{ plugin_key }}.yml" + delegate_to: localhost + become: false + register: plugin_vars_file + - name: "Load {{ plugin_key }} configuration variables" + include_vars: + file: "{{nextcloud_control_node_plugin_vars_directory}}{{ plugin_key }}.yml" + when: plugin_vars_file.stat.exists -- name: "Load {{ plugin_key }} configuration variables" - include_vars: - file: "{{nextcloud_control_node_plugin_vars_directory}}{{ plugin_key }}.yml" - when: plugin_vars_file.stat.exists - -- name: "Set {{ item.configkey }} for {{ item.appid }}" - loop: "{{ plugin_configuration }}" - command: > - {{ nextcloud_docker_exec_occ }} config:app:set {{ item.appid }} {{ item.configkey }} --value '{{ item.configvalue | to_json if item.configvalue is mapping else item.configvalue }}' - register: config_set_result - changed_when: (config_set_result.stdout is defined) and ("Config value were not updated" not in config_set_result.stdout) - when: plugin_vars_file.stat.exists + - name: "Set {{ item.configkey }} for {{ item.appid }}" + loop: "{{ plugin_configuration }}" + command: > + {{ nextcloud_docker_exec_occ }} config:app:set {{ item.appid }} {{ item.configkey }} --value '{{ item.configvalue | to_json if item.configvalue is mapping else item.configvalue }}' + register: config_set_result + changed_when: (config_set_result.stdout is defined) and ("Config value were not updated" not in config_set_result.stdout) + when: plugin_vars_file.stat.exists -- name: Check if {{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml exists - stat: - path: "{{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml" - delegate_to: localhost - become: false - register: plugin_tasks_file + - name: Check if {{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml exists + stat: + path: "{{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml" + delegate_to: localhost + become: false + register: plugin_tasks_file -- name: "include {{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml" - include_tasks: "{{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml" - when: plugin_tasks_file.stat.exists + - name: "include {{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml" + include_tasks: "{{nextcloud_control_node_plugin_tasks_directory}}{{ plugin_key }}.yml" + when: plugin_tasks_file.stat.exists + when: + - plugin_value.enabled | bool + - install_result is defined + - > + install_result.rc == 0 + or "already installed" in install_result.stdout \ No newline at end of file diff --git a/roles/docker-nextcloud/vars/plugins/bbb.yml b/roles/docker-nextcloud/vars/plugins/bbb.yml index 5847d1b4..545fbfbf 100644 --- a/roles/docker-nextcloud/vars/plugins/bbb.yml +++ b/roles/docker-nextcloud/vars/plugins/bbb.yml @@ -4,4 +4,4 @@ plugin_configuration: configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}" - appid: "bbb" configkey: "api.url" - configvalue: "{{ applications.bigbluebutton.urls.api }}" + configvalue: "{{ applications.bigbluebutton.urls.api }}" \ No newline at end of file diff --git a/roles/nginx-docker-reverse-proxy/tasks/main.yml b/roles/nginx-docker-reverse-proxy/tasks/main.yml new file mode 100644 index 00000000..327274b5 --- /dev/null +++ b/roles/nginx-docker-reverse-proxy/tasks/main.yml @@ -0,0 +1,23 @@ +- name: Remove (Cleanup) NGINX cache directory contents + become: true + file: + path: "{{ nginx.directories.cache }}" + state: absent + when: + - mode_cleanup | bool + - run_once_nginx_reverse_proxy is not defined + +- name: Ensure NGINX cache directory exists + become: true + file: + path: "{{ nginx.directories.cache }}" + state: directory + owner: http + group: http + mode: '0755' + when: run_once_nginx_reverse_proxy is not defined + +- name: run the nginx_reverse_proxy tasks once + set_fact: + run_once_nginx_reverse_proxy: true + when: run_once_nginx_reverse_proxy is not defined 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 index 92edb44a..5e9de568 100644 --- 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 @@ -3,6 +3,13 @@ {# default-src: Fallback for all other directives if not explicitly defined #} {%- set csp_parts = csp_parts + ["default-src 'self';"] %} +{# connect-src: Controls where fetch(), XHR, WebSocket etc. can connect to #} +{%- set connect_src = "connect-src 'self' https://ka-f.fontawesome.com" %} +{%- if applications | is_feature_enabled('matomo', application_id) | bool %} + {%- set connect_src = connect_src + " " + web_protocol + "://" + domains.matomo %} +{%- endif %} +{%- set csp_parts = csp_parts + [connect_src + ";"] %} + {# 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 %} @@ -13,21 +20,22 @@ {# 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" %} + {%- set frame_src = frame_src + " https://www.google.com" %} {%- 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 %} +{# img-src: Allow images. Prevent tracking by caching on server and client side. #} +{%- set img_src = "img-src * data: blob:"%} {%- 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'" %} +{# unsafe eval is set for sphinx #} +{%- set script_src = "script-src 'self' 'unsafe-eval' 'unsafe-inline'" %} {%- if applications | is_feature_enabled('matomo', application_id) | bool %} - {%- set script_src = script_src + " " + domains.matomo %} + {%- set script_src = script_src + " " + web_protocol + "://" + domains.matomo %} +{%- endif %} +{%- if applications | is_feature_enabled('recaptcha', application_id) | bool %} + {%- set script_src = script_src + " https://www.google.com" %} {%- endif %} {%- set script_src = script_src + " https://kit.fontawesome.com https://cdn.jsdelivr.net" %} {%- set csp_parts = csp_parts + [script_src + ";"] %} @@ -36,4 +44,10 @@ {%- set style_src = "style-src 'self' 'unsafe-inline' https://kit.fontawesome.com https://cdn.jsdelivr.net" %} {%- set csp_parts = csp_parts + [style_src + ";"] %} +{# font-src: Allow font-src from self, FontAwesome, jsDelivr and inline styles #} +{%- set font_src = "font-src 'self' https://kit.fontawesome.com https://cdn.jsdelivr.net" %} +{%- set csp_parts = csp_parts + [font_src + ";"] %} + add_header Content-Security-Policy "{{ csp_parts | join(' ') }}" always; +# Oppress header send by proxied application +proxy_hide_header Content-Security-Policy; \ No newline at end of file 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 addc6fb6..495a35c9 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 @@ -31,3 +31,6 @@ location {{location | default("/")}} proxy_read_timeout 900s; send_timeout 900s; } + +# Load caching +{% include 'roles/nginx-docker-reverse-proxy/templates/location/proxy_cache.conf.j2' %} \ No newline at end of file diff --git a/roles/nginx-docker-reverse-proxy/templates/location/proxy_cache.conf.j2 b/roles/nginx-docker-reverse-proxy/templates/location/proxy_cache.conf.j2 new file mode 100644 index 00000000..be838bc2 --- /dev/null +++ b/roles/nginx-docker-reverse-proxy/templates/location/proxy_cache.conf.j2 @@ -0,0 +1,18 @@ +proxy_cache_path {{ nginx.directories.cache }} levels=1:2 keys_zone=imgcache:10m inactive=60m use_temp_path=off; + +{%- if location is defined %} +location ~* ^{{ location }}.*\.(jpg|jpeg|png|gif|webp|ico|svg)$ { +{%- else %} +location ~* \.(jpg|jpeg|png|gif|webp|ico|svg)$ { +{%- endif %} + # Cache in browser + expires 30d; + add_header Cache-Control "public, max-age=2592000, immutable"; + + # Cache on reverse proxy side + proxy_pass http://127.0.0.1:{{http_port}}{{location | default("/")}}; + proxy_cache imgcache; + proxy_cache_valid 200 302 60m; + proxy_cache_valid 404 1m; + add_header X-Proxy-Cache $upstream_cache_status; +} \ No newline at end of file diff --git a/roles/nginx-https/meta/main.yml b/roles/nginx-https/meta/main.yml index 25815bc4..fa850772 100644 --- a/roles/nginx-https/meta/main.yml +++ b/roles/nginx-https/meta/main.yml @@ -1,4 +1,4 @@ dependencies: - nginx -- nginx-domains-cleanup +- cleanup-domains - letsencrypt \ No newline at end of file diff --git a/roles/update/tasks/main.yml b/roles/update/tasks/main.yml index c7c4f49e..bd73a560 100644 --- a/roles/update/tasks/main.yml +++ b/roles/update/tasks/main.yml @@ -21,9 +21,9 @@ - name: "Check if yay is installed" command: which yay - ignore_errors: yes register: yay_installed changed_when: false + failed_when: false - name: "Update with yay" include_role: @@ -32,9 +32,9 @@ - name: "Check if pip is installed" command: which pip - ignore_errors: yes register: pip_installed changed_when: false + failed_when: false - name: "Update with pip" include_role: @@ -43,9 +43,9 @@ - name: "Check if pkgmgr command is available" command: "which pkgmgr" register: pkgmgr_available - ignore_errors: yes + failed_when: false - name: "Update all repositories using pkgmgr" include_role: name: update-pkgmgr - when: pkgmgr_available.rc == 0 + when: pkgmgr_available.rc == 0 \ No newline at end of file diff --git a/templates/vars/features.yml.j2 b/templates/vars/features.yml.j2 index 0ef45b8d..9902ed77 100644 --- a/templates/vars/features.yml.j2 +++ b/templates/vars/features.yml.j2 @@ -1,13 +1,14 @@ {% macro render_features(options) %} features: {%- set feature_map = { - 'matomo': 'Enables Matomo tracking', - 'css': 'Enables custom CSS styling', - 'iframe': 'Allows embedding via iframe on landing page', - 'ldap': 'Enables LDAP integration and networking', - 'oidc': 'Enables OpenID Connect (OIDC) authentication', - 'oauth2': 'Enables OAuth2 proxy integration', - 'database': 'Enables use of central database' + 'matomo': 'Enables Matomo tracking', + 'css': 'Enables custom CSS styling', + 'iframe': 'Allows embedding via iframe on landing page', + 'ldap': 'Enables LDAP integration and networking', + 'oidc': 'Enables OpenID Connect (OIDC) authentication', + 'oauth2': 'Enables OAuth2 proxy integration', + 'database': 'Enables use of central database', + 'recaptcha': 'Enables recaptcha functionality' } %} {%- for key, comment in feature_map.items() %} {%- if key in options %}