Executive Summary
Multiple YunoHost-hosted applications (Home Assistant, Obsidian Web, Paperless-ngx, n8n) were experiencing WebSocket connection failures due to restrictive Content Security Policy (CSP) headers. The issue was caused by YunoHost’s default nginx security configuration lacking WebSocket protocol support in the connect-src directive.
Affected Applications
- Home Assistant (ha.TLD.com) - Real-time state updates failed - Installed/accessed via Redirect-ynh
- Obsidian Web (obsidian-web.TLD.com) - Live sync broken - Installed/accessed via Redirect-ynh
- Paperless-ngx (paperless.TLD.com) - Real-time features non-functional
- n8n (TLD.com/n8n) - Workflow execution updates blocked
- Any other apps requiring WebSocket connections
Technical Details
Note
I have replaced all references to my domain to TLD.com. Consider this when using this for your set up.
Root Cause
YunoHost’s default nginx security configuration (/usr/share/yunohost/conf/nginx/security.conf.inc) sets a Content-Security-Policy header that does not explicitly allow WebSocket connections:
# BROKEN CSP (missing connect-src)
more_set_headers "Content-Security-Policy: upgrade-insecure-requests;
default-src https: data: blob:;
object-src https: data: 'unsafe-inline';
style-src https: data: 'unsafe-inline';
script-src https: data: 'unsafe-inline' 'unsafe-eval';
worker-src 'self' blob:;";
Why This Breaks WebSockets
According to the Content Security Policy specification:
- When
connect-srcis not explicitly set, it falls back todefault-src default-src https: data: blob:does not includewss:(WebSocket Secure protocol)- Browsers block WebSocket connections as CSP violations
Error Signatures
Browser Console:
Connecting to 'wss://ha.TLD.com/api/websocket' violates the following
Content Security Policy directive: "default-src https: data: blob:".
Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.
Network Tab:
- WebSocket connections fail to establish
- Status: (blocked by CSP)
- No 101 Switching Protocols response
Diagnosis Steps
1. Check Current CSP Headers
# Check what CSP is being served
curl -I https://ha.TLD.com 2>&1 | grep -i "content-security-policy"
# Expected BAD output (missing connect-src):
content-security-policy: upgrade-insecure-requests; default-src https: data: blob:; ...
# Expected GOOD output (has connect-src with wss:):
content-security-policy: upgrade-insecure-requests; default-src https: data: blob:;
connect-src https: wss: data: 'self'; ...
2. Check Browser Console
Open DevTools (F12) → Console tab and look for:
- “violates the following Content Security Policy directive”
- Messages mentioning
wss://orws://connections - “connect-src” mentioned in error messages
3. Check Network Tab
Open DevTools (F12) → Network tab → Filter by WS:
- WebSocket connections should show Status: 101 (Switching Protocols)
- If blocked, they’ll show as failed or blocked by CSP
4. Identify Affected Files
# Find all CSP configurations
sudo grep -r "Content-Security-Policy" /etc/nginx/
# Check the main security config
sudo cat /etc/nginx/conf.d/security.conf.inc | grep "Content-Security-Policy"
# Check the template source
sudo cat /usr/share/yunohost/conf/nginx/security.conf.inc | grep "Content-Security-Policy"
Solution
Manual Fix
Edit both the template and active configuration files:
# 1. Edit the template (prevents regeneration issues)
sudo nano /usr/share/yunohost/conf/nginx/security.conf.inc
# 2. Edit the active config
sudo nano /etc/nginx/conf.d/security.conf.inc
# Find this line:
more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src https: data: blob:; ...
# Add after "default-src https: data: blob:":
connect-src https: wss: data: 'self';
# Final result:
more_set_headers "Content-Security-Policy: upgrade-insecure-requests;
default-src https: data: blob:;
connect-src https: wss: data: 'self';
object-src https: data: 'unsafe-inline';
style-src https: data: 'unsafe-inline';
script-src https: data: 'unsafe-inline' 'unsafe-eval';
worker-src 'self' blob:;";
# 3. Test and apply
sudo nginx -t
sudo systemctl reload nginx
Automated Fix (Recommended)
See the accompanying yunohost-csp-fix.sh script and systemd service files.
Installation:
# 1. Save the script
sudo nano /usr/local/bin/yunohost-csp-fix.sh
# (Paste the script content)
sudo chmod +x /usr/local/bin/yunohost-csp-fix.sh
# 2. Run the service installer
sudo bash install-csp-fix-service.sh
# 3. Verify it's running
sudo systemctl status yunohost-csp-fix.timer
sudo systemctl status yunohost-csp-watcher.path
# 4. Check logs
sudo tail -f /var/log/yunohost-csp-fix.log
Verification
1. Check Headers
# Should now include connect-src with wss:
curl -I https://ha.TLD.com 2>&1 | grep -i "content-security-policy"
2. Clear Browser Cache
Critical step - CSP headers are aggressively cached:
- Chrome/Edge: F12 → Network → Right-click Refresh → “Empty Cache and Hard Reload”
- Firefox: Ctrl+Shift+Delete → Cache → Clear Now
- Or: Open apps in Incognito/Private window for clean test
3. Test WebSocket Connections
Open DevTools (F12):
- Console: Should have NO CSP violation errors
- Network → WS: WebSocket connections show Status 101
- Functionality: Real-time features work (HA updates, Obsidian sync, etc.)
Persistence Considerations
YunoHost Regeneration Events
YunoHost may regenerate nginx configs during:
- App installations/removals
- Running
yunohost tools regen-conf nginx --force - YunoHost system updates
- NOT during normal reboots (no auto-regen found)
Protection Strategy
-
Always edit BOTH files:
- Template:
/usr/share/yunohost/conf/nginx/security.conf.inc - Active:
/etc/nginx/conf.d/security.conf.inc
- Template:
-
Use the automated fix:
- Runs on boot (2 minutes after)
- Runs hourly (catches manual regen-conf)
- Watches for file changes (catches automatic regen-conf)
-
Create a reminder:
- After running
yunohost tools regen-conf nginx --force - After installing new apps
- After YunoHost updates
- After running
App-Specific Overrides
Some apps may have custom CSP configurations that override the global setting:
Check for Overrides
# Find all CSP configurations
sudo find /etc/nginx/conf.d/ -name "*.conf" -exec grep -l "Content-Security-Policy" {} \;
Common Override Locations
/etc/nginx/conf.d/ha.TLD.com.d/redirect.conf/etc/nginx/conf.d/TLD.com.d/n8n.conf/etc/nginx/conf.d/TLD.com.d/freshrss.conf(intentional - leave alone)
Handling Overrides
If an app clears and sets its own CSP:
# Bad - clears global CSP without WebSocket support
more_clear_headers "Content-Security-Policy";
add_header Content-Security-Policy "default-src https: ..." always;
Either:
- Remove the override to use the fixed global CSP
- Fix the override to include
connect-src https: wss: data: 'self';
Related Issues
HTTP/2 Protocol Errors
Some apps (particularly Home Assistant) may also show:
Failed to load resource: net::ERR_HTTP2_PROTOCOL_ERROR
Solution: Ensure HTTP/2 is enabled in nginx:
listen 443 ssl http2;
listen [::]:443 ssl http2;
Manifest.json Errors
Service Worker issues may appear alongside WebSocket errors. These often resolve once CSP is fixed, as Service Workers also use connect-src.
Monitoring & Maintenance
Check Status
# View automated fix logs
sudo tail -f /var/log/yunohost-csp-fix.log
# View systemd service logs
sudo journalctl -u yunohost-csp-fix.service -f
# Check timer status
sudo systemctl list-timers | grep csp
# Check watcher status
sudo systemctl status yunohost-csp-watcher.path
Manual Run
# Test the fix script
sudo /usr/local/bin/yunohost-csp-fix.sh
# Force nginx config regeneration (then auto-fix runs)
sudo yunohost tools regen-conf nginx --force
Disable Automated Fix
If needed:
sudo systemctl stop yunohost-csp-fix.timer
sudo systemctl stop yunohost-csp-watcher.path
sudo systemctl disable yunohost-csp-fix.timer
sudo systemctl disable yunohost-csp-watcher.path
References
Content Security Policy Specification
WebSocket Protocol
- RFC 6455 - The WebSocket Protocol
- WebSocket URIs:
ws://(insecure) andwss://(secure)
YunoHost Configuration
- YunoHost nginx documentation
- Configuration regeneration:
yunohost tools regen-conf
Quick Reference Card
# CHECK CSP
curl -I https://TLD.com 2>&1 | grep -i content-security-policy
# MANUAL FIX
sudo nano /usr/share/yunohost/conf/nginx/security.conf.inc
sudo nano /etc/nginx/conf.d/security.conf.inc
# Add: connect-src https: wss: data: 'self';
sudo nginx -t && sudo systemctl reload nginx
# AUTO FIX
sudo /usr/local/bin/yunohost-csp-fix.sh
# CHECK LOGS
sudo tail -f /var/log/yunohost-csp-fix.log
# BROWSER TEST
# Clear cache, check DevTools Console for CSP errors, check Network tab for WS Status 101
yunohost-csp-fix.sh
#!/bin/bash
#
# YunoHost CSP Auto-Fix Script
# Ensures WebSocket support is enabled in nginx Content-Security-Policy
# Run at boot or after nginx regen-conf to restore WebSocket support
#
# Installation:
# sudo cp yunohost-csp-fix.sh /usr/local/bin/
# sudo chmod +x /usr/local/bin/yunohost-csp-fix.sh
# sudo systemctl enable yunohost-csp-fix.service
# sudo systemctl enable yunohost-csp-fix.timer
set -e
LOG_FILE="/var/log/yunohost-csp-fix.log"
TEMPLATE_FILE="/usr/share/yunohost/conf/nginx/security.conf.inc"
ACTIVE_FILE="/etc/nginx/conf.d/security.conf.inc"
# Correct CSP with WebSocket support
CORRECT_CSP='more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src https: data: blob:; connect-src https: wss: data: '\''self'\''; object-src https: data: '\''unsafe-inline'\''; style-src https: data: '\''unsafe-inline'\''; script-src https: data: '\''unsafe-inline'\'' '\''unsafe-eval'\''; worker-src '\''self'\'' blob:;";'
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
check_and_fix_file() {
local file="$1"
local file_name=$(basename "$file")
if [ ! -f "$file" ]; then
log "ERROR: File not found: $file"
return 1
fi
# Check if file contains the broken CSP (without connect-src or with spaces)
if grep -q 'more_set_headers "Content-Security-Policy' "$file"; then
# Check if it's already correct
if grep -q 'connect-src https: wss: data:' "$file"; then
log "✓ $file_name already has correct CSP with WebSocket support"
return 0
else
log "⚠ $file_name has broken CSP, fixing..."
# Backup the file
cp "$file" "$file.backup.$(date +%s)"
# Replace the CSP line
# This handles both the main CSP line and any duplicates
sed -i '/more_set_headers "Content-Security-Policy.*upgrade-insecure-requests.*default-src/c\'"$CORRECT_CSP" "$file"
log "✓ Fixed $file_name - added WebSocket support"
return 0
fi
else
log "⚠ No CSP header found in $file_name"
return 1
fi
}
notify_user() {
# Try to send notification via YunoHost if available
if command -v yunohost &> /dev/null; then
yunohost log display | tail -n 1 | grep -q "yunohost-csp-fix" || \
log "Note: Run 'tail -f /var/log/yunohost-csp-fix.log' to see this script's activity"
fi
}
main() {
log "=========================================="
log "Starting YunoHost CSP Fix Script"
log "=========================================="
# Fix template file (source for regen-conf)
check_and_fix_file "$TEMPLATE_FILE"
template_status=$?
# Fix active file (currently in use)
check_and_fix_file "$ACTIVE_FILE"
active_status=$?
# If any file was changed, reload nginx
if [ $template_status -eq 0 ] && [ $active_status -eq 0 ]; then
# Test nginx config
if nginx -t 2>&1 | tee -a "$LOG_FILE"; then
log "✓ Nginx config test passed"
# Reload nginx
if systemctl reload nginx; then
log "✓ Nginx reloaded successfully"
log "=========================================="
log "CSP fix completed successfully!"
log "WebSocket connections should now work."
log "=========================================="
else
log "✗ ERROR: Failed to reload nginx"
exit 1
fi
else
log "✗ ERROR: Nginx config test failed!"
log "Restoring backups..."
# Restore from backups if they exist
latest_template_backup=$(ls -t "$TEMPLATE_FILE.backup."* 2>/dev/null | head -1)
latest_active_backup=$(ls -t "$ACTIVE_FILE.backup."* 2>/dev/null | head -1)
[ -n "$latest_template_backup" ] && cp "$latest_template_backup" "$TEMPLATE_FILE"
[ -n "$latest_active_backup" ] && cp "$latest_active_backup" "$ACTIVE_FILE"
log "Backups restored. Please check manually."
exit 1
fi
else
log "✗ ERROR: Failed to fix one or more files"
exit 1
fi
notify_user
}
# Run main function
main

