CORS/CSP hardening & centralization

- Add reusable Nginx include: roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2
  (dynamic ACAO/credentials/methods/headers via role vars)
- Set global 'Vary: Origin' in nginx.conf.j2 to prevent cache poisoning
- CSP: allow Simple Icons via connect-src when feature is enabled
- Front proxy: rename vars to lowercase + flush handlers after config deploy
- Desktop: gate & load Simple Icons role; inject brand logos when enabled
- Bluesky + Logout: replace inline CORS with centralized include
- Simpleicons: public CORS (ACAO='*', no credentials), keep GET/OPTIONS, allow headers
- Taiga: adjust canonical domain to taiga.kanban.{{ PRIMARY_DOMAIN }}
- LibreTranslate: remove unused images/versions keys

Fixes: https://open.project.infinito.nexus/projects/cymais/work_packages/342/activity
Discussion: https://chatgpt.com/share/68da5e27-ffd4-800f-91a3-0ef103058d44
This commit is contained in:
2025-09-29 12:23:58 +02:00
parent c06d1c4d17
commit aa19a97ed6
15 changed files with 89 additions and 48 deletions

View File

@@ -158,26 +158,31 @@ class FilterModule(object):
for directive in directives:
tokens = ["'self'"]
# 1) Load flags (includes defaults from get_csp_flags)
# Load flags (includes defaults from get_csp_flags)
flags = self.get_csp_flags(applications, application_id, directive)
tokens += flags
# 2) Allow fetching from internal CDN by default for selected directives
# Allow fetching from internal CDN by default for selected directives
if directive in ['script-src-elem', 'connect-src', 'style-src-elem']:
tokens.append(get_url(domains, 'web-svc-cdn', web_protocol))
# 3) Matomo integration if feature is enabled
# Matomo integration if feature is enabled
if directive in ['script-src-elem', 'connect-src']:
if self.is_feature_enabled(applications, matomo_feature_name, application_id):
tokens.append(get_url(domains, 'web-app-matomo', web_protocol))
# 4) ReCaptcha integration (scripts + frames) if feature is enabled
# Simpleicons integration if feature is enabled
if directive in ['connect-src']:
if self.is_feature_enabled(applications, 'simpleicons', application_id):
tokens.append(get_url(domains, 'web-svc-simpleicons', web_protocol))
# ReCaptcha integration (scripts + frames) if feature is enabled
if self.is_feature_enabled(applications, 'recaptcha', application_id):
if directive in ['script-src-elem', 'frame-src']:
tokens.append('https://www.gstatic.com')
tokens.append('https://www.google.com')
# 5) Frame ancestors handling (desktop + logout support)
# Frame ancestors handling (desktop + logout support)
if directive == 'frame-ancestors':
if self.is_feature_enabled(applications, 'desktop', application_id):
# Allow being embedded by the desktop app domain (and potentially its parent)
@@ -189,10 +194,10 @@ class FilterModule(object):
tokens.append(get_url(domains, 'web-svc-logout', web_protocol))
tokens.append(get_url(domains, 'web-app-keycloak', web_protocol))
# 6) Custom whitelist entries
# Custom whitelist entries
tokens += self.get_csp_whitelist(applications, application_id, directive)
# 7) Add inline content hashes ONLY if final tokens do NOT include 'unsafe-inline'
# Add inline content hashes ONLY if final tokens do NOT include 'unsafe-inline'
# (Check tokens, not flags, to include defaults and later modifications.)
if "'unsafe-inline'" not in tokens:
for snippet in self.get_csp_inline_content(applications, application_id, directive):
@@ -201,7 +206,7 @@ class FilterModule(object):
# Append directive
parts.append(f"{directive} {' '.join(tokens)};")
# 8) Static img-src directive (kept permissive for data/blob and any host)
# Static img-src directive (kept permissive for data/blob and any host)
parts.append("img-src * data: blob:;")
return ' '.join(parts)

View File

@@ -6,10 +6,10 @@
include_role:
name: sys-util-csp-cert
- name: "Copy nginx config to '{{ FRONT_PROXY_DOMAIN_CONF_DST }}'"
- name: "Copy nginx config to '{{ front_proxy_domain_conf_dst }}'"
template:
src: "{{ FRONT_PROXY_DOMAIN_CONF_SRC }}"
dest: "{{ FRONT_PROXY_DOMAIN_CONF_DST }}"
src: "{{ front_proxy_domain_conf_src }}"
dest: "{{ front_proxy_domain_conf_dst }}"
register: nginx_conf
notify: restart openresty
@@ -29,3 +29,6 @@
- site_check.status is defined
- not site_check.status in [200,301,302]
when: not nginx_conf.changed
- name: "Restart Webserver for '{{ front_proxy_domain_conf_dst }}'"
meta: flush_handlers

View File

@@ -1,2 +1,2 @@
FRONT_PROXY_DOMAIN_CONF_DST: "{{ [ NGINX.DIRECTORIES.HTTP.SERVERS, domain ~ '.conf'] | path_join }}"
FRONT_PROXY_DOMAIN_CONF_SRC: "roles/sys-svc-proxy/templates/vhost/{{ vhost_flavour }}.conf.j2"
front_proxy_domain_conf_dst: "{{ [ NGINX.DIRECTORIES.HTTP.SERVERS, domain ~ '.conf'] | path_join }}"
front_proxy_domain_conf_src: "roles/sys-svc-proxy/templates/vhost/{{ vhost_flavour }}.conf.j2"

View File

@@ -0,0 +1,25 @@
# Configure CORS headers dynamically based on role variables.
# If no variable is defined, defaults are applied (e.g. same-origin).
# Discussion: https://chat.openai.com/share/2671b961-c1b0-472d-bae2-2804d0455e8a
{# Access-Control-Allow-Origin #}
{% if aca_origin is defined %}
add_header 'Access-Control-Allow-Origin' {{ aca_origin }};
{% else %}
add_header 'Access-Control-Allow-Origin' $scheme://$host always;
{% endif %}
{# Access-Control-Allow-Credentials #}
{% if aca_credentials is defined %}
add_header 'Access-Control-Allow-Credentials' {{ aca_credentials }};
{% endif %}
{# Access-Control-Allow-Methods #}
{% if aca_methods is defined %}
add_header 'Access-Control-Allow-Methods' {{ aca_methods }};
{% endif %}
{# Access-Control-Allow-Headers #}
{% if aca_headers is defined %}
add_header 'Access-Control-Allow-Headers' {{ aca_headers }};
{% endif %}

View File

@@ -58,5 +58,3 @@ server
{% endif %}
}

View File

@@ -13,6 +13,11 @@ http
default_type text/html;
{# Ensure caches (browsers, proxies, CDNs) treat responses as dependent on the Origin header
to prevent cross-domain cache poisoning issues.
Discussion: https://chat.openai.com/share/2671b961-c1b0-472d-bae2-2804d0455e8a #}
add_header 'Vary' 'Origin' always;
{# caching #}
proxy_cache_path {{ NGINX.DIRECTORIES.CACHE.GENERAL }} levels=1:2 keys_zone=cache:20m max_size=20g inactive=14d use_temp_path=off;
proxy_cache_path {{ NGINX.DIRECTORIES.CACHE.IMAGE }} levels=1:2 keys_zone=imgcache:10m inactive=60m use_temp_path=off;

View File

@@ -2,17 +2,17 @@
# Exposes a same-origin /config to avoid CORS when the social-app fetches config.
location = /config {
proxy_pass {{ BLUESKY_CONFIG_UPSTREAM_URL }};
# Nur Hostname extrahieren:
{# Just extract hostname #}
set $up_host "{{ BLUESKY_CONFIG_UPSTREAM_URL | regex_replace('^https?://', '') | regex_replace('/.*$', '') }}";
proxy_set_header Host $up_host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
# Make response clearly same-origin for browsers
{# Access Control Allow Configurations #}
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $scheme://$host always;
add_header Vary Origin always;
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
}
location = /ipcc {
@@ -23,7 +23,7 @@ location = /ipcc {
proxy_set_header Connection "";
proxy_ssl_server_name on;
{# Access Control Allow Configurations #}
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $scheme://$host always;
add_header Vary Origin always;
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
}

View File

@@ -1,3 +1,12 @@
- name: "Load brand logos role for '{{ application_id }}'"
include_role:
name: web-svc-simpleicons
vars:
flush_handlers: true
when:
- run_once_web_svc_simpleicons is not defined
- DESKTOP_SIMPLEICONS_ENABLED | bool
- name: "Validate configuration"
include_tasks: "02_validate.yml"
when: MODE_ASSERT | bool
@@ -24,25 +33,16 @@
set_fact:
portfolio_cards: "{{ lookup('docker_cards', 'roles') }}"
- name: "Load images for applications feature simpleicons is enabled "
- name: "Load Desktop Brand logos"
set_fact:
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(domains, WEB_PROTOCOL) }}"
when:
- (applications | get_app_conf(application_id, 'features.simpleicons', False))
when: DESKTOP_SIMPLEICONS_ENABLED | bool
changed_when: false
- name: Group docker cards
set_fact:
portfolio_menu_data: "{{ lookup('docker_cards_grouped', portfolio_cards, portfolio_menu_categories) }}"
- name: Debug portfolio data
debug:
msg:
portfolio_cards: "{{ portfolio_cards }}"
portfolio_menu_categories: "{{ portfolio_menu_categories}}"
portfolio_menu_data: "{{ portfolio_menu_data }}"
service_provider: "{{ service_provider }}"
when: MODE_DEBUG | bool
- name: Copy host-specific config.yaml if it exists
template:
src: "{{ DESKTOP_CONFIG_INV_PATH }}"

View File

@@ -10,6 +10,9 @@ docker_pull_git_repository: true
# Desktop
## Simpleicons
DESKTOP_SIMPLEICONS_ENABLED: "{{ applications | get_app_conf(application_id, 'features.simpleicons') }}"
## Javascript
DESKTOP_JS_CDN_URL: "{{ domains | get_url('web-svc-cdn', WEB_PROTOCOL) }}"
DESKTOP_JS_FILES: ['iframe.js','oidc.js']

View File

@@ -31,4 +31,4 @@ server:
unsafe-eval: true
domains:
canonical:
- "kanban.project.{{ PRIMARY_DOMAIN }}"
- "taiga.kanban.{{ PRIMARY_DOMAIN }}"

View File

@@ -1,8 +1,6 @@
credentials: {}
docker:
images: {} # @todo Move under services
versions: {} # @todo Move under services
services:
redis:
enabled: false # Enable Redis

View File

@@ -21,6 +21,11 @@
- name: "load docker, proxy for '{{ application_id }}'"
include_role:
name: sys-stk-full-stateless
vars:
aca_origin: "'{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}' always"
aca_credentials: "'true' always"
aca_methods: "'GET, OPTIONS' always"
aca_headers: "'Accept, Authorization' always"
- name: Create symbolic link from .env file to repository
file:

View File

@@ -1,5 +1,5 @@
location = /logout {
# Proxy to the logout service
{# Proxy to the logout service #}
proxy_pass http://127.0.0.1:{{ ports.localhost.http['web-svc-logout'] }}/logout;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -7,18 +7,15 @@ location = /logout {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
# CORS headers allow your central page to call this
add_header 'Access-Control-Allow-Origin' '{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Accept, Authorization' always;
{# CORS headers allow your central page to call this #}
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
# Disable caching absolutely
{# Disable caching absolutely #}
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
# Handle preflight
{# Handle preflight #}
if ($request_method = OPTIONS) {
return 204;
}

View File

@@ -1,8 +1,6 @@
credentials: {}
docker:
images: {} # @todo Move under services
versions: {} # @todo Move under services
services:
redis:
enabled: false # Enable Redis

View File

@@ -3,6 +3,10 @@
- name: "load docker, proxy for '{{ application_id }}'"
include_role:
name: sys-stk-full-stateless
vars:
aca_origin: "* always"
aca_methods: "'GET, OPTIONS' always"
aca_headers: "'Accept, Authorization, Content-Type' always"
- name: "Copy '{{ application_id }}' files"
template: