mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-11-08 06:08:05 +00:00
Compare commits
3 Commits
9a4bf91276
...
57d5269b07
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d5269b07 | |||
| 1eefdea050 | |||
| 561160504e |
@@ -10,9 +10,23 @@ from module_utils.config_utils import get_app_conf
|
|||||||
from module_utils.get_url import get_url
|
from module_utils.get_url import get_url
|
||||||
|
|
||||||
|
|
||||||
|
def _dedup_preserve(seq):
|
||||||
|
"""Return a list with stable order and unique items."""
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for x in seq:
|
||||||
|
if x not in seen:
|
||||||
|
seen.add(x)
|
||||||
|
out.append(x)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
class FilterModule(object):
|
class FilterModule(object):
|
||||||
"""
|
"""
|
||||||
Custom filters for Content Security Policy generation and CSP-related utilities.
|
Jinja filters for building a robust, CSP3-aware Content-Security-Policy header.
|
||||||
|
Safari/CSP2 compatibility is ensured by merging the -elem/-attr variants into the base
|
||||||
|
directives (style-src, script-src). We intentionally do NOT mirror back into -elem/-attr
|
||||||
|
to allow true CSP3 granularity on modern browsers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def filters(self):
|
def filters(self):
|
||||||
@@ -61,11 +75,14 @@ class FilterModule(object):
|
|||||||
"""
|
"""
|
||||||
Returns CSP flag tokens (e.g., "'unsafe-eval'", "'unsafe-inline'") for a directive,
|
Returns CSP flag tokens (e.g., "'unsafe-eval'", "'unsafe-inline'") for a directive,
|
||||||
merging sane defaults with app config.
|
merging sane defaults with app config.
|
||||||
Default: 'unsafe-inline' is enabled for style-src and style-src-elem.
|
|
||||||
|
Defaults:
|
||||||
|
- For styles we enable 'unsafe-inline' by default (style-src, style-src-elem, style-src-attr),
|
||||||
|
because many apps rely on inline styles / style attributes.
|
||||||
|
- For scripts we do NOT enable 'unsafe-inline' by default.
|
||||||
"""
|
"""
|
||||||
# Defaults that apply to all apps
|
|
||||||
default_flags = {}
|
default_flags = {}
|
||||||
if directive in ('style-src', 'style-src-elem'):
|
if directive in ('style-src', 'style-src-elem', 'style-src-attr'):
|
||||||
default_flags = {'unsafe-inline': True}
|
default_flags = {'unsafe-inline': True}
|
||||||
|
|
||||||
configured = get_app_conf(
|
configured = get_app_conf(
|
||||||
@@ -76,7 +93,6 @@ class FilterModule(object):
|
|||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Merge defaults with configured flags (configured overrides defaults)
|
|
||||||
merged = {**default_flags, **configured}
|
merged = {**default_flags, **configured}
|
||||||
|
|
||||||
tokens = []
|
tokens = []
|
||||||
@@ -131,82 +147,148 @@ class FilterModule(object):
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Builds the Content-Security-Policy header value dynamically based on application settings.
|
Builds the Content-Security-Policy header value dynamically based on application settings.
|
||||||
- Flags (e.g., 'unsafe-eval', 'unsafe-inline') are read from server.csp.flags.<directive>,
|
|
||||||
with sane defaults applied in get_csp_flags (always 'unsafe-inline' for style-src and style-src-elem).
|
Key points:
|
||||||
- Inline hashes are read from server.csp.hashes.<directive>.
|
- CSP3-aware: supports base/elem/attr for styles and scripts.
|
||||||
- Whitelists are read from server.csp.whitelist.<directive>.
|
- Safari/CSP2 fallback: base directives (style-src, script-src) always include
|
||||||
- Inline hashes are added only if the final tokens do NOT include 'unsafe-inline'.
|
the union of their -elem/-attr variants.
|
||||||
|
- We do NOT mirror back into -elem/-attr; finer CSP3 rules remain effective
|
||||||
|
on modern browsers if you choose to use them.
|
||||||
|
- If the app explicitly disables a token on the *base* (e.g. style-src.unsafe-inline: false),
|
||||||
|
that token is removed from the merged base even if present in elem/attr.
|
||||||
|
- Inline hashes are added ONLY if that directive does NOT include 'unsafe-inline'.
|
||||||
|
- Whitelists/flags/hashes read from:
|
||||||
|
server.csp.whitelist.<directive>
|
||||||
|
server.csp.flags.<directive>
|
||||||
|
server.csp.hashes.<directive>
|
||||||
|
- “Smart defaults”:
|
||||||
|
* internal CDN for style/script elem and connect
|
||||||
|
* Matomo endpoints (if feature enabled) for script-elem/connect
|
||||||
|
* Simpleicons (if feature enabled) for connect
|
||||||
|
* reCAPTCHA (if feature enabled) for script-elem/frame-src
|
||||||
|
* frame-ancestors extended for desktop/logout/keycloak if enabled
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
directives = [
|
directives = [
|
||||||
'default-src', # Fallback source list for content types not explicitly listed
|
'default-src',
|
||||||
'connect-src', # Allowed URLs for XHR, WebSockets, EventSource, fetch()
|
'connect-src',
|
||||||
'frame-ancestors', # Who may embed this page
|
'frame-ancestors',
|
||||||
'frame-src', # Sources for nested browsing contexts (e.g., <iframe>)
|
'frame-src',
|
||||||
'script-src', # Sources for script execution
|
'script-src',
|
||||||
'script-src-elem', # Sources for <script> elements
|
'script-src-elem',
|
||||||
'style-src', # Sources for inline styles and <style>/<link> elements
|
'script-src-attr',
|
||||||
'style-src-elem', # Sources for <style> and <link rel="stylesheet">
|
'style-src',
|
||||||
'font-src', # Sources for fonts
|
'style-src-elem',
|
||||||
'worker-src', # Sources for workers
|
'style-src-attr',
|
||||||
'manifest-src', # Sources for web app manifests
|
'font-src',
|
||||||
'media-src', # Sources for audio and video
|
'worker-src',
|
||||||
|
'manifest-src',
|
||||||
|
'media-src',
|
||||||
]
|
]
|
||||||
|
|
||||||
parts = []
|
tokens_by_dir = {}
|
||||||
|
explicit_flags_by_dir = {}
|
||||||
|
|
||||||
for directive in directives:
|
for directive in directives:
|
||||||
|
# Collect explicit flags (to later respect explicit "False" on base during merge)
|
||||||
|
explicit_flags = get_app_conf(
|
||||||
|
applications,
|
||||||
|
application_id,
|
||||||
|
'server.csp.flags.' + directive,
|
||||||
|
False,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
explicit_flags_by_dir[directive] = explicit_flags
|
||||||
|
|
||||||
tokens = ["'self'"]
|
tokens = ["'self'"]
|
||||||
|
|
||||||
# Load flags (includes defaults from get_csp_flags)
|
# 1) Flags (with sane defaults)
|
||||||
flags = self.get_csp_flags(applications, application_id, directive)
|
flags = self.get_csp_flags(applications, application_id, directive)
|
||||||
tokens += flags
|
tokens += flags
|
||||||
|
|
||||||
# Allow fetching from internal CDN by default for selected directives
|
# 2) Internal CDN defaults 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', 'style-src'):
|
||||||
tokens.append(get_url(domains, 'web-svc-cdn', web_protocol))
|
tokens.append(get_url(domains, 'web-svc-cdn', web_protocol))
|
||||||
|
|
||||||
# Matomo integration if feature is enabled
|
# 3) Matomo (if 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))
|
||||||
|
|
||||||
# Simpleicons integration if feature is enabled
|
# 4) Simpleicons (if enabled) – typically used via connect-src (fetch)
|
||||||
if directive in ['connect-src']:
|
if directive == 'connect-src':
|
||||||
if self.is_feature_enabled(applications, 'simpleicons', application_id):
|
if self.is_feature_enabled(applications, 'simpleicons', application_id):
|
||||||
tokens.append(get_url(domains, 'web-svc-simpleicons', web_protocol))
|
tokens.append(get_url(domains, 'web-svc-simpleicons', web_protocol))
|
||||||
|
|
||||||
# ReCaptcha integration (scripts + frames) if feature is enabled
|
# 5) reCAPTCHA (if enabled) – scripts + frames
|
||||||
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')
|
||||||
|
|
||||||
# Frame ancestors handling (desktop + logout support)
|
# 6) Frame ancestors (desktop + logout)
|
||||||
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's site
|
||||||
domain = domains.get('web-app-desktop')[0]
|
domain = domains.get('web-app-desktop')[0]
|
||||||
sld_tld = ".".join(domain.split(".")[-2:]) # e.g., example.com
|
sld_tld = ".".join(domain.split(".")[-2:]) # e.g., example.com
|
||||||
tokens.append(f"{sld_tld}")
|
tokens.append(f"{sld_tld}")
|
||||||
if self.is_feature_enabled(applications, 'logout', application_id):
|
if self.is_feature_enabled(applications, 'logout', application_id):
|
||||||
# Allow embedding via logout proxy and Keycloak app
|
|
||||||
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))
|
||||||
|
|
||||||
# Custom whitelist entries
|
# 7) Custom whitelist
|
||||||
tokens += self.get_csp_whitelist(applications, application_id, directive)
|
tokens += self.get_csp_whitelist(applications, application_id, directive)
|
||||||
|
|
||||||
# Add inline content hashes ONLY if final tokens do NOT include 'unsafe-inline'
|
# 8) Inline hashes (only if this directive does NOT include 'unsafe-inline')
|
||||||
# (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):
|
||||||
tokens.append(self.get_csp_hash(snippet))
|
tokens.append(self.get_csp_hash(snippet))
|
||||||
|
|
||||||
# Append directive
|
tokens_by_dir[directive] = _dedup_preserve(tokens)
|
||||||
parts.append(f"{directive} {' '.join(tokens)};")
|
|
||||||
|
|
||||||
# Static img-src directive (kept permissive for data/blob and any host)
|
# ----------------------------------------------------------
|
||||||
|
# CSP3 families → ensure CSP2 fallback (Safari-safe)
|
||||||
|
# Merge style/script families so base contains union of elem/attr.
|
||||||
|
# Respect explicit disables on the base (e.g. unsafe-inline=False).
|
||||||
|
# Do NOT mirror back into elem/attr (keep granularity).
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _strip_if_disabled(unioned_tokens, explicit_flags, name):
|
||||||
|
"""
|
||||||
|
Remove a token (e.g. 'unsafe-inline') from the unioned token list
|
||||||
|
if it is explicitly disabled in the base directive flags.
|
||||||
|
"""
|
||||||
|
if isinstance(explicit_flags, dict) and explicit_flags.get(name) is False:
|
||||||
|
tok = f"'{name}'"
|
||||||
|
return [t for t in unioned_tokens if t != tok]
|
||||||
|
return unioned_tokens
|
||||||
|
|
||||||
|
def merge_family(base_key, elem_key, attr_key):
|
||||||
|
base = tokens_by_dir.get(base_key, [])
|
||||||
|
elem = tokens_by_dir.get(elem_key, [])
|
||||||
|
attr = tokens_by_dir.get(attr_key, [])
|
||||||
|
union = _dedup_preserve(base + elem + attr)
|
||||||
|
|
||||||
|
# Respect explicit disables on the base
|
||||||
|
explicit_base = explicit_flags_by_dir.get(base_key, {})
|
||||||
|
# The most relevant flags for script/style:
|
||||||
|
for flag_name in ('unsafe-inline', 'unsafe-eval'):
|
||||||
|
union = _strip_if_disabled(union, explicit_base, flag_name)
|
||||||
|
|
||||||
|
tokens_by_dir[base_key] = union # write back only to base
|
||||||
|
|
||||||
|
merge_family('style-src', 'style-src-elem', 'style-src-attr')
|
||||||
|
merge_family('script-src', 'script-src-elem', 'script-src-attr')
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Assemble header
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
parts = []
|
||||||
|
for directive in directives:
|
||||||
|
if directive in tokens_by_dir:
|
||||||
|
parts.append(f"{directive} {' '.join(tokens_by_dir[directive])};")
|
||||||
|
|
||||||
|
# Keep permissive img-src for data/blob + any host (as before)
|
||||||
parts.append("img-src * data: blob:;")
|
parts.append("img-src * data: blob:;")
|
||||||
|
|
||||||
return ' '.join(parts)
|
return ' '.join(parts)
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ defaults_networks:
|
|||||||
subnet: 192.168.104.32/28
|
subnet: 192.168.104.32/28
|
||||||
web-svc-coturn:
|
web-svc-coturn:
|
||||||
subnet: 192.168.104.48/28
|
subnet: 192.168.104.48/28
|
||||||
|
web-app-mini-qr:
|
||||||
|
subnet: 192.168.104.64/28
|
||||||
|
|
||||||
# /24 Networks / 254 Usable Clients
|
# /24 Networks / 254 Usable Clients
|
||||||
web-app-bigbluebutton:
|
web-app-bigbluebutton:
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ ports:
|
|||||||
web-app-flowise: 8056
|
web-app-flowise: 8056
|
||||||
web-app-minio_api: 8057
|
web-app-minio_api: 8057
|
||||||
web-app-minio_console: 8058
|
web-app-minio_console: 8058
|
||||||
|
web-app-mini-qr: 8059
|
||||||
web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
|
web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
|
||||||
public:
|
public:
|
||||||
# The following ports should be changed to 22 on the subdomain via stream mapping
|
# The following ports should be changed to 22 on the subdomain via stream mapping
|
||||||
|
|||||||
@@ -10,17 +10,6 @@
|
|||||||
|
|
||||||
lua_need_request_body on;
|
lua_need_request_body on;
|
||||||
|
|
||||||
header_filter_by_lua_block {
|
|
||||||
local ct = ngx.header.content_type or ""
|
|
||||||
if ct:lower():find("^text/html") then
|
|
||||||
ngx.ctx.is_html = true
|
|
||||||
-- IMPORTANT: body will be modified → drop Content-Length to avoid mismatches
|
|
||||||
ngx.header.content_length = nil
|
|
||||||
else
|
|
||||||
ngx.ctx.is_html = false
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
body_filter_by_lua_block {
|
body_filter_by_lua_block {
|
||||||
-- Only process HTML responses
|
-- Only process HTML responses
|
||||||
if not ngx.ctx.is_html then
|
if not ngx.ctx.is_html then
|
||||||
|
|||||||
@@ -1,2 +1,33 @@
|
|||||||
add_header Content-Security-Policy "{{ applications | build_csp_header(application_id, domains) }}" always;
|
# ===== Content Security Policy: only for documents and workers (no locations needed) =====
|
||||||
proxy_hide_header Content-Security-Policy; # Todo: Make this optional
|
|
||||||
|
# 1) Define your CSP once (Jinja: escape double quotes to be safe)
|
||||||
|
set $csp "{{ applications | build_csp_header(application_id, domains) | replace('\"','\\\"') }}";
|
||||||
|
|
||||||
|
# 2) Send CSP ONLY for document responses; also for workers via Sec-Fetch-Dest
|
||||||
|
header_filter_by_lua_block {
|
||||||
|
local ct = ngx.header.content_type or ngx.header["Content-Type"] or ""
|
||||||
|
local dest = ngx.var.http_sec_fetch_dest or ""
|
||||||
|
|
||||||
|
local lct = ct:lower()
|
||||||
|
local is_html = lct:find("^text/html") or lct:find("^application/xhtml+xml")
|
||||||
|
local is_worker = (dest == "worker") or (dest == "serviceworker")
|
||||||
|
|
||||||
|
if is_html or is_worker then
|
||||||
|
ngx.header["Content-Security-Policy"] = ngx.var.csp
|
||||||
|
else
|
||||||
|
ngx.header["Content-Security-Policy"] = nil
|
||||||
|
ngx.header["Content-Security-Policy-Report-Only"] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If you'll modify the body later, drop Content-Length on HTML
|
||||||
|
if is_html then
|
||||||
|
ngx.ctx.is_html = true
|
||||||
|
ngx.header.content_length = nil
|
||||||
|
else
|
||||||
|
ngx.ctx.is_html = false
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3) Prevent upstream/app CSP (duplicates)
|
||||||
|
proxy_hide_header Content-Security-Policy;
|
||||||
|
proxy_hide_header Content-Security-Policy-Report-Only;
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
@@ -13,7 +13,7 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ server:
|
|||||||
- https://code.jquery.com/
|
- https://code.jquery.com/
|
||||||
style-src-elem:
|
style-src-elem:
|
||||||
- https://cdn.jsdelivr.net
|
- https://cdn.jsdelivr.net
|
||||||
|
- https://kit.fontawesome.com
|
||||||
|
- https://code.jquery.com/
|
||||||
font-src:
|
font-src:
|
||||||
- https://ka-f.fontawesome.com
|
- https://ka-f.fontawesome.com
|
||||||
- https://cdn.jsdelivr.net
|
- https://cdn.jsdelivr.net
|
||||||
@@ -25,7 +27,7 @@ server:
|
|||||||
frame-src:
|
frame-src:
|
||||||
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ server:
|
|||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
|
||||||
script-src:
|
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
whitelist:
|
whitelist:
|
||||||
connect-src:
|
connect-src:
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
oauth2_proxy:
|
oauth2_proxy:
|
||||||
application: "application"
|
application: "application"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ server:
|
|||||||
aliases: []
|
aliases: []
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ server:
|
|||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
domains:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ server:
|
|||||||
aliases: []
|
aliases: []
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
frame-src:
|
frame-src:
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
aliases: []
|
aliases: []
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ server:
|
|||||||
aliases: []
|
aliases: []
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
rbac:
|
rbac:
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ server:
|
|||||||
style-src-elem:
|
style-src-elem:
|
||||||
- https://fonts.googleapis.com
|
- https://fonts.googleapis.com
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
domains:
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
connect-src:
|
connect-src:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ server:
|
|||||||
frame-ancestors:
|
frame-ancestors:
|
||||||
- "*" # No damage if it's used somewhere on other websites, it anyhow looks like art
|
- "*" # No damage if it's used somewhere on other websites, it anyhow looks like art
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -23,3 +23,5 @@
|
|||||||
- name: Build data (single async task)
|
- name: Build data (single async task)
|
||||||
include_tasks: 02_build_data.yml
|
include_tasks: 02_build_data.yml
|
||||||
when: MIG_BUILD_DATA | bool
|
when: MIG_BUILD_DATA | bool
|
||||||
|
|
||||||
|
- include_tasks: utils/run_once.yml
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
---
|
---
|
||||||
- block:
|
- include_tasks: 01_core.yml
|
||||||
- include_tasks: 01_core.yml
|
|
||||||
- include_tasks: utils/run_once.yml
|
|
||||||
name: "Setup Meta Infinite Graph"
|
|
||||||
when: run_once_web_app_mig is not defined
|
when: run_once_web_app_mig is not defined
|
||||||
|
|
||||||
26
roles/web-app-mini-qr/README.md
Normal file
26
roles/web-app-mini-qr/README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Mini-QR
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
**Mini-QR** is a lightweight, self-hosted web application for generating QR codes instantly and privately.
|
||||||
|
It provides a minimal and elegant interface to convert any text, URL, or message into a QR code — directly in your browser, without external tracking or dependencies.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Mini-QR is designed for simplicity, privacy, and speed.
|
||||||
|
It offers an ad-free interface that works entirely within your local environment, making it ideal for individuals, organizations, and educational institutions that value data sovereignty.
|
||||||
|
The app runs as a single Docker container and requires no database or backend setup, enabling secure and frictionless QR generation anywhere.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Instant QR code creation** — simply type or paste your content.
|
||||||
|
- **Privacy-friendly** — all generation happens client-side; no data leaves your server.
|
||||||
|
- **Open Source** — fully auditable and modifiable for custom integrations.
|
||||||
|
- **Responsive Design** — optimized for both desktop and mobile devices.
|
||||||
|
- **Docker-ready** — can be deployed in seconds using the official image.
|
||||||
|
|
||||||
|
## Further Resources
|
||||||
|
|
||||||
|
- 🧩 Upstream project: [lyqht/mini-qr](https://github.com/lyqht/mini-qr)
|
||||||
|
- 📦 Upstream Dockerfile: [View on GitHub](https://github.com/lyqht/mini-qr/blob/main/Dockerfile)
|
||||||
|
- 🌐 Docker Image: `ghcr.io/lyqht/mini-qr:latest`
|
||||||
2
roles/web-app-mini-qr/TODO.md
Normal file
2
roles/web-app-mini-qr/TODO.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# To-dos
|
||||||
|
- Remove clarity.ms
|
||||||
38
roles/web-app-mini-qr/config/main.yml
Normal file
38
roles/web-app-mini-qr/config/main.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
docker:
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
enabled: false
|
||||||
|
database:
|
||||||
|
enabled: false
|
||||||
|
features:
|
||||||
|
matomo: true
|
||||||
|
css: true
|
||||||
|
desktop: true
|
||||||
|
logout: false
|
||||||
|
server:
|
||||||
|
csp:
|
||||||
|
whitelist:
|
||||||
|
script-src-elem:
|
||||||
|
# Propably some tracking code
|
||||||
|
# Anyhow implemented to pass CSP checks
|
||||||
|
# @todo Remove
|
||||||
|
- https://www.clarity.ms/
|
||||||
|
- https://scripts.clarity.ms/
|
||||||
|
connect-src:
|
||||||
|
- https://q.clarity.ms
|
||||||
|
- https://n.clarity.ms
|
||||||
|
- "data:"
|
||||||
|
style-src-elem: []
|
||||||
|
font-src: []
|
||||||
|
frame-ancestors: []
|
||||||
|
flags:
|
||||||
|
style-src-attr:
|
||||||
|
unsafe-inline: true
|
||||||
|
script-src-elem:
|
||||||
|
unsafe-inline: true
|
||||||
|
script-src-attr:
|
||||||
|
unsafe-eval: true
|
||||||
|
domains:
|
||||||
|
canonical:
|
||||||
|
- "qr.{{ PRIMARY_DOMAIN }}"
|
||||||
|
aliases: []
|
||||||
27
roles/web-app-mini-qr/meta/main.yml
Normal file
27
roles/web-app-mini-qr/meta/main.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
galaxy_info:
|
||||||
|
author: "Kevin Veen-Birkenbach"
|
||||||
|
description: >
|
||||||
|
Mini-QR is a minimalist, self-hosted web application that allows users to
|
||||||
|
instantly generate QR codes in a privacy-friendly way.
|
||||||
|
license: "Infinito.Nexus NonCommercial License"
|
||||||
|
license_url: "https://s.infinito.nexus/license"
|
||||||
|
company: |
|
||||||
|
Kevin Veen-Birkenbach
|
||||||
|
Consulting & Coaching Solutions
|
||||||
|
https://www.veen.world
|
||||||
|
galaxy_tags:
|
||||||
|
- infinito
|
||||||
|
- qr
|
||||||
|
- webapp
|
||||||
|
- privacy
|
||||||
|
- utility
|
||||||
|
- education
|
||||||
|
- lightweight
|
||||||
|
repository: "https://github.com/lyqht/mini-qr"
|
||||||
|
issue_tracker_url: "https://github.com/lyqht/mini-qr/issues"
|
||||||
|
documentation: "https://github.com/lyqht/mini-qr"
|
||||||
|
logo:
|
||||||
|
class: "fa-solid fa-qrcode"
|
||||||
|
run_after: []
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
7
roles/web-app-mini-qr/tasks/01_core.yml
Normal file
7
roles/web-app-mini-qr/tasks/01_core.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
- name: "load docker, proxy for '{{ application_id }}'"
|
||||||
|
include_role:
|
||||||
|
name: sys-stk-full-stateless
|
||||||
|
vars:
|
||||||
|
docker_compose_flush_handlers: false
|
||||||
|
|
||||||
|
- include_tasks: utils/run_once.yml
|
||||||
4
roles/web-app-mini-qr/tasks/main.yml
Normal file
4
roles/web-app-mini-qr/tasks/main.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
- include_tasks: 01_core.yml
|
||||||
|
when: run_once_web_app_mini_qr is not defined
|
||||||
|
|
||||||
12
roles/web-app-mini-qr/templates/docker-compose.yml.j2
Normal file
12
roles/web-app-mini-qr/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||||
|
{% set container_port = 8080 %}
|
||||||
|
{{ application_id | get_entity_name }}:
|
||||||
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
|
image: "{{ MINI_QR_IMAGE }}:{{ MINI_QR_VERSION }}"
|
||||||
|
container_name: "{{ MINI_QR_CONTAINER }}"
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
|
||||||
|
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||||
|
|
||||||
|
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||||
12
roles/web-app-mini-qr/vars/main.yml
Normal file
12
roles/web-app-mini-qr/vars/main.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# General
|
||||||
|
application_id: web-app-mini-qr
|
||||||
|
entity_name: "{{ application_id | get_entity_name }}"
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker_compose_flush_handlers: false
|
||||||
|
docker_pull_git_repository: false
|
||||||
|
|
||||||
|
# Helper variables
|
||||||
|
MINI_QR_IMAGE: "ghcr.io/lyqht/mini-qr"
|
||||||
|
MINI_QR_VERSION: "latest"
|
||||||
|
MINI_QR_CONTAINER: "{{ entity_name }}"
|
||||||
@@ -10,7 +10,7 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ server:
|
|||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
whitelist:
|
whitelist:
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ server:
|
|||||||
# Makes sense that all of the website content is available in the navigator
|
# Makes sense that all of the website content is available in the navigator
|
||||||
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ version: "production" # @see https://nextcloud.com/blog/nex
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
|
|||||||
@@ -17,11 +17,6 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
#script-src:
|
|
||||||
# unsafe-inline: true
|
|
||||||
# unsafe-eval: true
|
|
||||||
#style-src:
|
|
||||||
# unsafe-inline: true
|
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src: []
|
font-src: []
|
||||||
connect-src: []
|
connect-src: []
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ server:
|
|||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
frame-ancestors:
|
frame-ancestors:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
frame-ancestors:
|
frame-ancestors:
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ server:
|
|||||||
aliases: []
|
aliases: []
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ server:
|
|||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ features:
|
|||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
whitelist:
|
whitelist:
|
||||||
worker-src:
|
worker-src:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ server:
|
|||||||
worker-src:
|
worker-src:
|
||||||
- "blob:"
|
- "blob:"
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ server:
|
|||||||
aliases: []
|
aliases: []
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
locations:
|
locations:
|
||||||
admin: "/admin/"
|
admin: "/admin/"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ server:
|
|||||||
frame-ancestors:
|
frame-ancestors:
|
||||||
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ server:
|
|||||||
aliases: []
|
aliases: []
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ server:
|
|||||||
csp:
|
csp:
|
||||||
whitelist: # URL's which should be whitelisted
|
whitelist: # URL's which should be whitelisted
|
||||||
script-src-elem: []
|
script-src-elem: []
|
||||||
style-src: []
|
style-src-attr: []
|
||||||
font-src: []
|
font-src: []
|
||||||
connect-src: []
|
connect-src: []
|
||||||
frame-src: []
|
frame-src: []
|
||||||
flags: # Flags which should be set
|
flags: # Flags which should be set
|
||||||
style-src:
|
style-src-attr:
|
||||||
unsafe-inline: false
|
unsafe-inline: false
|
||||||
script-src:
|
script-src-attr:
|
||||||
unsafe-inline: false
|
unsafe-inline: false
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
unsafe-inline: false
|
unsafe-inline: false
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import hashlib
|
|||||||
import base64
|
import base64
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import copy
|
||||||
|
|
||||||
sys.path.insert(
|
sys.path.insert(
|
||||||
0,
|
0,
|
||||||
@@ -322,6 +323,155 @@ class TestCspFilters(unittest.TestCase):
|
|||||||
tokens = self._get_directive_tokens(header, 'style-src')
|
tokens = self._get_directive_tokens(header, 'style-src')
|
||||||
self.assertIn("'unsafe-inline'", tokens)
|
self.assertIn("'unsafe-inline'", tokens)
|
||||||
|
|
||||||
|
def test_style_family_union_flows_into_base_only_no_mirror_back(self):
|
||||||
|
"""
|
||||||
|
Sources allowed only in style-src-elem/attr must appear in style-src (CSP2/Safari fallback),
|
||||||
|
but we do NOT mirror back base→elem/attr.
|
||||||
|
"""
|
||||||
|
apps = copy.deepcopy(self.apps)
|
||||||
|
|
||||||
|
# Add distinct sources to elem and attr only
|
||||||
|
apps['app1']['server']['csp'].setdefault('whitelist', {})
|
||||||
|
apps['app1']['server']['csp']['whitelist']['style-src-elem'] = [
|
||||||
|
'https://elem-only.example.com'
|
||||||
|
]
|
||||||
|
apps['app1']['server']['csp']['whitelist']['style-src-attr'] = [
|
||||||
|
'https://attr-only.example.com'
|
||||||
|
]
|
||||||
|
|
||||||
|
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
|
||||||
|
|
||||||
|
base_tokens = self._get_directive_tokens(header, 'style-src')
|
||||||
|
elem_tokens = self._get_directive_tokens(header, 'style-src-elem')
|
||||||
|
attr_tokens = self._get_directive_tokens(header, 'style-src-attr')
|
||||||
|
|
||||||
|
# Base must include both elem/attr sources
|
||||||
|
self.assertIn('https://elem-only.example.com', base_tokens)
|
||||||
|
self.assertIn('https://attr-only.example.com', base_tokens)
|
||||||
|
|
||||||
|
# elem keeps its own sources; we did not force-copy base back into elem/attr
|
||||||
|
# (No strict negative assertion here; just verify elem retains its own source)
|
||||||
|
self.assertIn('https://elem-only.example.com', elem_tokens)
|
||||||
|
self.assertIn('https://attr-only.example.com', attr_tokens)
|
||||||
|
|
||||||
|
def test_style_explicit_disable_inline_on_base_survives_union(self):
|
||||||
|
"""
|
||||||
|
If style-src.unsafe-inline is explicitly set to False on the base,
|
||||||
|
it must be removed from the merged base even if elem/attr include it by default.
|
||||||
|
"""
|
||||||
|
apps = copy.deepcopy(self.apps)
|
||||||
|
# Explicitly disable unsafe-inline for the base
|
||||||
|
apps['app1'].setdefault('server', {}).setdefault('csp', {}).setdefault('flags', {}).setdefault('style-src', {})
|
||||||
|
apps['app1']['server']['csp']['flags']['style-src']['unsafe-inline'] = False
|
||||||
|
|
||||||
|
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
|
||||||
|
|
||||||
|
base_tokens = self._get_directive_tokens(header, 'style-src')
|
||||||
|
elem_tokens = self._get_directive_tokens(header, 'style-src-elem')
|
||||||
|
attr_tokens = self._get_directive_tokens(header, 'style-src-attr')
|
||||||
|
|
||||||
|
# Base must NOT have 'unsafe-inline'
|
||||||
|
self.assertNotIn("'unsafe-inline'", base_tokens)
|
||||||
|
|
||||||
|
# elem/attr may still have 'unsafe-inline' by default (granularity preserved)
|
||||||
|
self.assertIn("'unsafe-inline'", elem_tokens)
|
||||||
|
self.assertIn("'unsafe-inline'", attr_tokens)
|
||||||
|
|
||||||
|
def test_script_explicit_disable_inline_on_base_survives_union(self):
|
||||||
|
"""
|
||||||
|
If script-src.unsafe-inline is explicitly set to False (default anyway),
|
||||||
|
ensure the base remains without 'unsafe-inline' even if elem/attr enable it.
|
||||||
|
"""
|
||||||
|
apps = copy.deepcopy(self.apps)
|
||||||
|
|
||||||
|
# Force elem/attr to allow unsafe-inline explicitly
|
||||||
|
apps['app1'].setdefault('server', {}).setdefault('csp', {}).setdefault('flags', {})
|
||||||
|
apps['app1']['server']['csp']['flags']['script-src-elem'] = {'unsafe-inline': True}
|
||||||
|
apps['app1']['server']['csp']['flags']['script-src-attr'] = {'unsafe-inline': True}
|
||||||
|
|
||||||
|
# Explicitly disable on base (redundant but makes intent clear)
|
||||||
|
apps['app1']['server']['csp']['flags']['script-src'] = {
|
||||||
|
'unsafe-inline': False,
|
||||||
|
'unsafe-eval': True
|
||||||
|
}
|
||||||
|
|
||||||
|
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
|
||||||
|
|
||||||
|
base_tokens = self._get_directive_tokens(header, 'script-src')
|
||||||
|
elem_tokens = self._get_directive_tokens(header, 'script-src-elem')
|
||||||
|
attr_tokens = self._get_directive_tokens(header, 'script-src-attr')
|
||||||
|
|
||||||
|
# Base: no 'unsafe-inline'
|
||||||
|
self.assertNotIn("'unsafe-inline'", base_tokens)
|
||||||
|
# But elem/attr: yes
|
||||||
|
self.assertIn("'unsafe-inline'", elem_tokens)
|
||||||
|
self.assertIn("'unsafe-inline'", attr_tokens)
|
||||||
|
|
||||||
|
# Also ensure 'unsafe-eval' remains present on the base
|
||||||
|
self.assertIn("'unsafe-eval'", base_tokens)
|
||||||
|
|
||||||
|
def test_script_family_union_includes_elem_attr_hosts_in_base(self):
|
||||||
|
"""
|
||||||
|
Hosts present only under script-src-elem/attr must appear in script-src (base).
|
||||||
|
"""
|
||||||
|
apps = copy.deepcopy(self.apps)
|
||||||
|
apps['app1']['server']['csp'].setdefault('whitelist', {})
|
||||||
|
apps['app1']['server']['csp']['whitelist']['script-src-elem'] = [
|
||||||
|
'https://elem-scripts.example.com'
|
||||||
|
]
|
||||||
|
apps['app1']['server']['csp']['whitelist']['script-src-attr'] = [
|
||||||
|
'https://attr-scripts.example.com'
|
||||||
|
]
|
||||||
|
|
||||||
|
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
|
||||||
|
|
||||||
|
base_tokens = self._get_directive_tokens(header, 'script-src')
|
||||||
|
self.assertIn('https://elem-scripts.example.com', base_tokens)
|
||||||
|
self.assertIn('https://attr-scripts.example.com', base_tokens)
|
||||||
|
|
||||||
|
def test_hash_inclusion_uses_final_base_tokens_after_union(self):
|
||||||
|
"""
|
||||||
|
Ensure hash inclusion for style-src is evaluated after family union & explicit-disable logic.
|
||||||
|
If base ends up WITHOUT 'unsafe-inline' after union, hashes must be present.
|
||||||
|
"""
|
||||||
|
apps = copy.deepcopy(self.apps)
|
||||||
|
|
||||||
|
# Explicitly disable 'unsafe-inline' on base 'style-src' so hashes can be included
|
||||||
|
apps['app1'].setdefault('server', {}).setdefault('csp', {}).setdefault('flags', {}).setdefault('style-src', {})
|
||||||
|
apps['app1']['server']['csp']['flags']['style-src']['unsafe-inline'] = False
|
||||||
|
|
||||||
|
# Provide a style-src hash
|
||||||
|
content = "body { background: #abc; }"
|
||||||
|
apps['app1']['server']['csp'].setdefault('hashes', {})['style-src'] = content
|
||||||
|
expected_hash = self.filter.get_csp_hash(content)
|
||||||
|
|
||||||
|
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
|
||||||
|
base_tokens = self._get_directive_tokens(header, 'style-src')
|
||||||
|
|
||||||
|
self.assertNotIn("'unsafe-inline'", base_tokens) # confirm no unsafe-inline
|
||||||
|
self.assertIn(expected_hash, header) # hash must be present
|
||||||
|
|
||||||
|
def test_no_unintended_mirroring_back_to_elem_attr(self):
|
||||||
|
"""
|
||||||
|
Verify that we do not mirror base tokens back into elem/attr:
|
||||||
|
add a base-only host and ensure elem/attr don't automatically get it.
|
||||||
|
"""
|
||||||
|
apps = copy.deepcopy(self.apps)
|
||||||
|
apps['app1']['server']['csp'].setdefault('whitelist', {})
|
||||||
|
# Add a base-only host
|
||||||
|
apps['app1']['server']['csp']['whitelist']['style-src'] = ['https://base-only.example.com']
|
||||||
|
|
||||||
|
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
|
||||||
|
|
||||||
|
base_tokens = self._get_directive_tokens(header, 'style-src')
|
||||||
|
elem_tokens = self._get_directive_tokens(header, 'style-src-elem')
|
||||||
|
attr_tokens = self._get_directive_tokens(header, 'style-src-attr')
|
||||||
|
|
||||||
|
self.assertIn('https://base-only.example.com', base_tokens)
|
||||||
|
# Not strictly required to assert negatives, but this ensures "no mirror back":
|
||||||
|
self.assertNotIn('https://base-only.example.com', elem_tokens)
|
||||||
|
self.assertNotIn('https://base-only.example.com', attr_tokens)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user