YunoHost WebSocket CSP Issues and Fix

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-src is not explicitly set, it falls back to default-src
  • default-src https: data: blob: does not include wss: (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:// or ws:// 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

  1. Always edit BOTH files:

    • Template: /usr/share/yunohost/conf/nginx/security.conf.inc
    • Active: /etc/nginx/conf.d/security.conf.inc
  2. Use the automated fix:

    • Runs on boot (2 minutes after)
    • Runs hourly (catches manual regen-conf)
    • Watches for file changes (catches automatic regen-conf)
  3. Create a reminder:

    • After running yunohost tools regen-conf nginx --force
    • After installing new apps
    • After YunoHost updates

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:

  1. Remove the override to use the fixed global CSP
  2. 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

YunoHost Configuration


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

Hello @hazzamyman

In the web admin panel (Tools>Yunohost Settings), there is a security setting that is critical for websocket connections:

  • when experimental security features are enabled :

= 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: dat>

  • when disabled :

= more_set_headers "Content-Security-Policy : upgrade-insecure-requests";