Home Assistant + HAProxy: Fixing Add-on Access Blocked by X-Frame-Options

Article written following real debugging on production infrastructure. Configuration tested with HAProxy 2.8.15 and Home Assistant 2024.12 since I had this issue for some times.

4 min read
Home Assistant + HAProxy: Fixing Add-on Access Blocked by X-Frame-Options

The Problem

Since recent versions of Home Assistant, accessing add-ons (ESPHome, File Editor, Studio Code Server, etc.) through an HTTPS reverse proxy can generate these errors in the browser console:

Refused to display 'https://homeassistant.domain.com/api/hassio_ingress/...' 
in a frame because it set 'X-Frame-Options' to 'DENY'.

Sandbox access violation: Blocked a frame at "https://homeassistant.domain.com" 
from accessing a frame at "https://homeassistant.domain.com". 
The frame being accessed is sandboxed and lacks the "allow-same-origin" flag.

Add-ons refuse to display, leaving a blank page in the Home Assistant interface.

Architecture and Context

A typical infrastructure setup:

  • HAProxy 2.8 as the main reverse proxy
  • Home Assistant exposed via homeassistant.domain.com
  • Strict security policy with hardened headers (HSTS, X-Frame-Options, CSP, etc.)

Typical HAProxy configuration with global security headers:

frontend https_frontend
    bind *:443 ssl crt /etc/haproxy/certs/
    
    # Security headers applied to all backends
    http-response set-header X-Frame-Options DENY
    http-response set-header X-Content-Type-Options nosniff
    http-response set-header Strict-Transport-Security "max-age=63072000"

Diagnosis: Why Does It Block?

Understanding Home Assistant Ingress

Home Assistant uses an ingress mechanism to display add-ons directly in its web interface. Technically, add-ons are loaded in <iframe> elements via URLs like:

https://homeassistant.domain.com/api/hassio_ingress/{token}/

The X-Frame-Options Conflict

The X-Frame-Options: DENY header prevents any site from loading the page in an iframe, even from the same domain. This is protection against clickjacking attacks.

However, Home Assistant needs to load its own add-ons in iframes to function correctly.

Why SAMEORIGIN Isn't Always Enough

Even when using X-Frame-Options: SAMEORIGIN at the Home Assistant backend level, the frontend HAProxy header is applied last and overwrites the backend header.

The Complete Solution

Step 1: Conditionally Set X-Frame-Options in the Frontend

We need to prevent the frontend from applying X-Frame-Options: DENY for Home Assistant while maintaining it for other services.

The trap: you can't directly use an ACL based on hdr(host) in an http-response rule because request headers are no longer available during the response phase.

Solution: Use a Transaction Variable

frontend https_frontend
    bind *:443 ssl crt /etc/haproxy/certs/
    
    # Mark Home Assistant during the request phase
    acl is_homeassistant hdr(host) -i homeassistant.domain.com
    http-request set-var(txn.is_ha) int(1) if is_homeassistant
    
    # Apply DENY only if NOT Home Assistant
    http-response set-header X-Frame-Options DENY if !{ var(txn.is_ha) -m int 1 }
    
    # Other security headers
    http-response set-header Strict-Transport-Security "max-age=63072000"
    http-response set-header X-Content-Type-Options nosniff
    
    # Reverse proxy headers
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Forwarded-Host %[req.hdr(Host)]
    
    # Routing
    use_backend homeassistant_backend if is_homeassistant

Step 2: Force SAMEORIGIN in the Home Assistant Backend

Some ingress routes may send their own X-Frame-Options header. To ensure consistency, we remove then force SAMEORIGIN:

backend homeassistant_backend
    description Home Assistant IoT Platform
    balance roundrobin
    
    # CRITICAL: Remove then force SAMEORIGIN
    http-response del-header X-Frame-Options
    http-response set-header X-Frame-Options SAMEORIGIN
    
    # WebSocket support for add-ons
    option http-server-close
    option forwardfor
    
    # Extended timeouts for long sessions
    timeout server 600s
    timeout tunnel 3600s
    
    server has 192.168.1.100:8123 check

Step 3: Home Assistant Configuration

In configuration.yaml, declare HAProxy as a trusted proxy:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 192.168.1.10  # HAProxy server IP
    - 127.0.0.1
  ip_ban_enabled: true
  login_attempts_threshold: 5

Restart Home Assistant: SettingsSystemRestart

Validation

Command-Line Tests

# Home Assistant should return SAMEORIGIN (or no header)
curl -I https://homeassistant.domain.com/api/hassio_ingress/test | grep -i x-frame
# Expected result: x-frame-options: SAMEORIGIN

# Other services should return DENY
curl -I https://grafana.domain.com/ | grep -i x-frame
# Expected result: x-frame-options: DENY

Browser Testing

  1. Clear all browser cache (Ctrl+Shift+Del → Clear everything)
  2. Or test in private/incognito mode
  3. Access Home Assistant → SettingsAdd-ons
  4. Open an add-on (ESPHome, File Editor, etc.)

Add-ons should now load correctly without frame errors.

Pitfalls to Avoid

1. Browser Cache

Security headers are heavily cached by browsers. Even after fixing the server-side configuration, the browser may continue to display errors for several minutes.

Solution: Always test in incognito/private mode during debugging, or clear the complete cache.

2. HAProxy Rule Order

HAProxy applies headers in order: frontend first, then backend. If the frontend forces DENY, the backend cannot override it.

Solution: Conditionally set headers at the frontend level using transaction variables.

3. Forgetting trusted_proxies

Without the trusted_proxies directive in Home Assistant, X-Forwarded-* headers are ignored and HA thinks it's running in direct HTTP mode, which can cause redirect issues.

4. Multiple is_homeassistant ACLs

I initially defined the is_homeassistant ACL in multiple frontends (stats, https). This duplication can cause warnings.

Solution: Define ACLs where they're used and avoid unnecessary definitions.

Performance and Monitoring

This configuration has zero impact on performance. Transaction variables are stored in memory during request processing and freed immediately after.

To monitor proper header application:

# From HAProxy stats
echo "show info" | socat stdio /var/run/haproxy/admin.sock

# Real-time logs
tail -f /var/log/haproxy.log | grep homeassistant

Conclusion

The conflict between strict security policies (X-Frame-Options: DENY) and Home Assistant's ingress mechanisms is a common problem in reverse proxy architectures.

The solution relies on three pillars:

  1. Intelligent conditioning of headers at the frontend level via transaction variables
  2. Explicit forcing of SAMEORIGIN at the backend level
  3. Proper configuration of Home Assistant to accept proxied connections

This approach maintains maximum security for all other services while allowing Home Assistant to function correctly with its add-ons.

Resources