mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-10-09 18:28:10 +02:00
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:
@@ -158,26 +158,31 @@ class FilterModule(object):
|
|||||||
for directive in directives:
|
for directive in directives:
|
||||||
tokens = ["'self'"]
|
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)
|
flags = self.get_csp_flags(applications, application_id, directive)
|
||||||
tokens += flags
|
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']:
|
if directive in ['script-src-elem', 'connect-src', 'style-src-elem']:
|
||||||
tokens.append(get_url(domains, 'web-svc-cdn', web_protocol))
|
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 directive in ['script-src-elem', 'connect-src']:
|
||||||
if self.is_feature_enabled(applications, matomo_feature_name, application_id):
|
if self.is_feature_enabled(applications, matomo_feature_name, application_id):
|
||||||
tokens.append(get_url(domains, 'web-app-matomo', web_protocol))
|
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 self.is_feature_enabled(applications, 'recaptcha', application_id):
|
||||||
if directive in ['script-src-elem', 'frame-src']:
|
if directive in ['script-src-elem', 'frame-src']:
|
||||||
tokens.append('https://www.gstatic.com')
|
tokens.append('https://www.gstatic.com')
|
||||||
tokens.append('https://www.google.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 directive == 'frame-ancestors':
|
||||||
if self.is_feature_enabled(applications, 'desktop', application_id):
|
if self.is_feature_enabled(applications, 'desktop', application_id):
|
||||||
# Allow being embedded by the desktop app domain (and potentially its parent)
|
# 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-svc-logout', web_protocol))
|
||||||
tokens.append(get_url(domains, 'web-app-keycloak', 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)
|
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.)
|
# (Check tokens, not flags, to include defaults and later modifications.)
|
||||||
if "'unsafe-inline'" not in tokens:
|
if "'unsafe-inline'" not in tokens:
|
||||||
for snippet in self.get_csp_inline_content(applications, application_id, directive):
|
for snippet in self.get_csp_inline_content(applications, application_id, directive):
|
||||||
@@ -201,7 +206,7 @@ class FilterModule(object):
|
|||||||
# Append directive
|
# Append directive
|
||||||
parts.append(f"{directive} {' '.join(tokens)};")
|
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:;")
|
parts.append("img-src * data: blob:;")
|
||||||
|
|
||||||
return ' '.join(parts)
|
return ' '.join(parts)
|
||||||
|
@@ -6,10 +6,10 @@
|
|||||||
include_role:
|
include_role:
|
||||||
name: sys-util-csp-cert
|
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:
|
template:
|
||||||
src: "{{ FRONT_PROXY_DOMAIN_CONF_SRC }}"
|
src: "{{ front_proxy_domain_conf_src }}"
|
||||||
dest: "{{ FRONT_PROXY_DOMAIN_CONF_DST }}"
|
dest: "{{ front_proxy_domain_conf_dst }}"
|
||||||
register: nginx_conf
|
register: nginx_conf
|
||||||
notify: restart openresty
|
notify: restart openresty
|
||||||
|
|
||||||
@@ -28,4 +28,7 @@
|
|||||||
when:
|
when:
|
||||||
- site_check.status is defined
|
- site_check.status is defined
|
||||||
- not site_check.status in [200,301,302]
|
- not site_check.status in [200,301,302]
|
||||||
when: not nginx_conf.changed
|
when: not nginx_conf.changed
|
||||||
|
|
||||||
|
- name: "Restart Webserver for '{{ front_proxy_domain_conf_dst }}'"
|
||||||
|
meta: flush_handlers
|
@@ -1,2 +1,2 @@
|
|||||||
FRONT_PROXY_DOMAIN_CONF_DST: "{{ [ NGINX.DIRECTORIES.HTTP.SERVERS, domain ~ '.conf'] | path_join }}"
|
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_src: "roles/sys-svc-proxy/templates/vhost/{{ vhost_flavour }}.conf.j2"
|
||||||
|
@@ -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 %}
|
@@ -58,5 +58,3 @@ server
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -13,6 +13,11 @@ http
|
|||||||
|
|
||||||
default_type text/html;
|
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 #}
|
{# 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.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;
|
proxy_cache_path {{ NGINX.DIRECTORIES.CACHE.IMAGE }} levels=1:2 keys_zone=imgcache:10m inactive=60m use_temp_path=off;
|
||||||
|
@@ -2,17 +2,17 @@
|
|||||||
# Exposes a same-origin /config to avoid CORS when the social-app fetches config.
|
# Exposes a same-origin /config to avoid CORS when the social-app fetches config.
|
||||||
location = /config {
|
location = /config {
|
||||||
proxy_pass {{ BLUESKY_CONFIG_UPSTREAM_URL }};
|
proxy_pass {{ BLUESKY_CONFIG_UPSTREAM_URL }};
|
||||||
# Nur Hostname extrahieren:
|
|
||||||
|
{# Just extract hostname #}
|
||||||
set $up_host "{{ BLUESKY_CONFIG_UPSTREAM_URL | regex_replace('^https?://', '') | regex_replace('/.*$', '') }}";
|
set $up_host "{{ BLUESKY_CONFIG_UPSTREAM_URL | regex_replace('^https?://', '') | regex_replace('/.*$', '') }}";
|
||||||
proxy_set_header Host $up_host;
|
proxy_set_header Host $up_host;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Connection "";
|
proxy_set_header Connection "";
|
||||||
proxy_ssl_server_name on;
|
proxy_ssl_server_name on;
|
||||||
|
|
||||||
# Make response clearly same-origin for browsers
|
{# Access Control Allow Configurations #}
|
||||||
proxy_hide_header Access-Control-Allow-Origin;
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
add_header Access-Control-Allow-Origin $scheme://$host always;
|
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
|
||||||
add_header Vary Origin always;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /ipcc {
|
location = /ipcc {
|
||||||
@@ -23,7 +23,7 @@ location = /ipcc {
|
|||||||
proxy_set_header Connection "";
|
proxy_set_header Connection "";
|
||||||
proxy_ssl_server_name on;
|
proxy_ssl_server_name on;
|
||||||
|
|
||||||
|
{# Access Control Allow Configurations #}
|
||||||
proxy_hide_header Access-Control-Allow-Origin;
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
add_header Access-Control-Allow-Origin $scheme://$host always;
|
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
|
||||||
add_header Vary Origin always;
|
|
||||||
}
|
}
|
||||||
|
@@ -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"
|
- name: "Validate configuration"
|
||||||
include_tasks: "02_validate.yml"
|
include_tasks: "02_validate.yml"
|
||||||
when: MODE_ASSERT | bool
|
when: MODE_ASSERT | bool
|
||||||
@@ -24,25 +33,16 @@
|
|||||||
set_fact:
|
set_fact:
|
||||||
portfolio_cards: "{{ lookup('docker_cards', 'roles') }}"
|
portfolio_cards: "{{ lookup('docker_cards', 'roles') }}"
|
||||||
|
|
||||||
- name: "Load images for applications feature simpleicons is enabled "
|
- name: "Load Desktop Brand logos"
|
||||||
set_fact:
|
set_fact:
|
||||||
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(domains, WEB_PROTOCOL) }}"
|
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(domains, WEB_PROTOCOL) }}"
|
||||||
when:
|
when: DESKTOP_SIMPLEICONS_ENABLED | bool
|
||||||
- (applications | get_app_conf(application_id, 'features.simpleicons', False))
|
changed_when: false
|
||||||
|
|
||||||
- name: Group docker cards
|
- name: Group docker cards
|
||||||
set_fact:
|
set_fact:
|
||||||
portfolio_menu_data: "{{ lookup('docker_cards_grouped', portfolio_cards, portfolio_menu_categories) }}"
|
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
|
- name: Copy host-specific config.yaml if it exists
|
||||||
template:
|
template:
|
||||||
src: "{{ DESKTOP_CONFIG_INV_PATH }}"
|
src: "{{ DESKTOP_CONFIG_INV_PATH }}"
|
||||||
|
@@ -10,6 +10,9 @@ docker_pull_git_repository: true
|
|||||||
|
|
||||||
# Desktop
|
# Desktop
|
||||||
|
|
||||||
|
## Simpleicons
|
||||||
|
DESKTOP_SIMPLEICONS_ENABLED: "{{ applications | get_app_conf(application_id, 'features.simpleicons') }}"
|
||||||
|
|
||||||
## Javascript
|
## Javascript
|
||||||
DESKTOP_JS_CDN_URL: "{{ domains | get_url('web-svc-cdn', WEB_PROTOCOL) }}"
|
DESKTOP_JS_CDN_URL: "{{ domains | get_url('web-svc-cdn', WEB_PROTOCOL) }}"
|
||||||
DESKTOP_JS_FILES: ['iframe.js','oidc.js']
|
DESKTOP_JS_FILES: ['iframe.js','oidc.js']
|
||||||
|
@@ -31,4 +31,4 @@ server:
|
|||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "kanban.project.{{ PRIMARY_DOMAIN }}"
|
- "taiga.kanban.{{ PRIMARY_DOMAIN }}"
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
|
|
||||||
credentials: {}
|
credentials: {}
|
||||||
docker:
|
docker:
|
||||||
images: {} # @todo Move under services
|
|
||||||
versions: {} # @todo Move under services
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
enabled: false # Enable Redis
|
enabled: false # Enable Redis
|
||||||
|
@@ -21,6 +21,11 @@
|
|||||||
- name: "load docker, proxy for '{{ application_id }}'"
|
- name: "load docker, proxy for '{{ application_id }}'"
|
||||||
include_role:
|
include_role:
|
||||||
name: sys-stk-full-stateless
|
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
|
- name: Create symbolic link from .env file to repository
|
||||||
file:
|
file:
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
location = /logout {
|
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_pass http://127.0.0.1:{{ ports.localhost.http['web-svc-logout'] }}/logout;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -7,18 +7,15 @@ location = /logout {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
# CORS headers – allow your central page to call this
|
{# CORS headers – allow your central page to call this #}
|
||||||
add_header 'Access-Control-Allow-Origin' '{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}' always;
|
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
|
||||||
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;
|
|
||||||
|
|
||||||
# Disable caching absolutely
|
{# Disable caching absolutely #}
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
||||||
add_header Pragma "no-cache" always;
|
add_header Pragma "no-cache" always;
|
||||||
add_header Expires "0" always;
|
add_header Expires "0" always;
|
||||||
|
|
||||||
# Handle preflight
|
{# Handle preflight #}
|
||||||
if ($request_method = OPTIONS) {
|
if ($request_method = OPTIONS) {
|
||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
|
|
||||||
credentials: {}
|
credentials: {}
|
||||||
docker:
|
docker:
|
||||||
images: {} # @todo Move under services
|
|
||||||
versions: {} # @todo Move under services
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
enabled: false # Enable Redis
|
enabled: false # Enable Redis
|
||||||
@@ -11,7 +9,7 @@ docker:
|
|||||||
features:
|
features:
|
||||||
matomo: false # Matomo tracking isn't necessary
|
matomo: false # Matomo tracking isn't necessary
|
||||||
css: true # Enable Global CSS Styling
|
css: true # Enable Global CSS Styling
|
||||||
desktop: true # Enable loading of app in iframe
|
desktop: true # Enable loading of app in iframe
|
||||||
ldap: false # Enable LDAP Network
|
ldap: false # Enable LDAP Network
|
||||||
central_database: false # Enable Central Database Network
|
central_database: false # Enable Central Database Network
|
||||||
recaptcha: false # Enable ReCaptcha
|
recaptcha: false # Enable ReCaptcha
|
||||||
|
@@ -3,6 +3,10 @@
|
|||||||
- name: "load docker, proxy for '{{ application_id }}'"
|
- name: "load docker, proxy for '{{ application_id }}'"
|
||||||
include_role:
|
include_role:
|
||||||
name: sys-stk-full-stateless
|
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"
|
- name: "Copy '{{ application_id }}' files"
|
||||||
template:
|
template:
|
||||||
|
Reference in New Issue
Block a user