← lemonlink.eu All guides
Homelab · Public exposure

Safely expose client-side, zero-server-state tools

How to serve CyberChef, IT-Tools, and Draw.io publicly with Caddy, without storing visitor data on your server. Remove Authelia from specific subdomains, add hardening headers, verify the result, and keep the rest of your homelab behind authentication.

Roberth · July 2026 · ~15 min read · Linux + Docker + Caddy + a domain

Most homelab guides stop at "put it behind Authelia." That's correct for apps with server-side state: Home Assistant, Nextcloud, Portainer. But a class of tools is fully client-side and stateless — the browser does all the work, and the server is just a dumb byte pump. For those, authentication is theater. Every login wall adds friction, log-overhead, and a false sense of security while the actual attack surface barely changes.

This guide strips that theater for CyberChef, IT-Tools, and Draw.io. The result: public endpoints that refuse to log, remember, or persist anything, with the rest of your homelab staying exactly where you left it — behind Authelia.

1.What "zero-server-state" really means

A tool is zero-server-state when ALL of these are true:

Quick sanity check

CyberChef, IT-Tools, and Draw.io all hit this bar. They're single-page apps that execute logic client-side; the backend only serves static assets. If you're extending this list, read the app's own docs before declaring it stateless.

2.Why this is safe — and where it isn't

Because there's no server-side state, the main risks aren't data leaks — they're supply-chain and metadata exposure:

Not safe for these

If the tool has a database, uploads, logins, or secrets in URL paths, this pattern does not apply. Nextcloud, Vaultwarden, and Home Assistant belong behind Authelia or VPN. The test above is non-negotiable.

3.The architecture

The goal is: three subdomains bypass Authelia. everything else keeps it.

chef.example.com      # CyberChef — no Authelia, hardened headers
it.example.com         # IT-Tools — no Authelia, hardened headers
draw.example.com       # Draw.io — no Authelia, hardened headers
home.example.com       # Home Assistant — Authelia still active
cloud.example.com      # Nextcloud — Authelia still active
Quick-start — if you already have Caddy + the three tools running
# 1. Add the rules in §4 to your Caddyfile
# 2. Reload Caddy
caddy reload

# 3. Verify headers
curl -I https://chef.example.com | grep -E 'strict-transport|csp|permissions|frame|x-frame|x-content'

# 4. Confirm no cookies/session are set
curl -I https://it.example.com | egrep -i 'set-cookie|www-authenticate' || echo 'clean'

4.Caddyfile edits

4a. Disable forward_auth for the three subdomains

If your Caddyfile currently pushes all traffic through Authelia, add a bypass list. There are two clean ways; pick one.

Option A — inline in each site block (simplest):

chef.example.com {
    reverse_proxy 10.0.0.10:8000
    # No forward_auth = Authelia bypassed for this subdomain only
    import static_security_headers
}

it.example.com {
    reverse_proxy 10.0.0.20:8080
    import static_security_headers
}

draw.example.com {
    reverse_proxy 10.0.0.30:8080
    import static_security_headers
}

Option B — centralized skip list (cleaner at scale):

(authelia_basics) {
    forward_auth authelia:9091 {
        uri /api/authz/forward-auth
        copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
    }
}

(static_security_headers) {
    header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src * data:; connect-src 'self' ws: wss:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
    header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), accelerometer=(), gyroscope=()"
    header X-Frame-Options "SAMEORIGIN"
    header X-Content-Type-Options "nosniff"
    header Referrer-Policy "no-referrer"
    header Cache-Control "public, max-age=3600, must-revalidate"
}

(public_docker_sites) {
    @public_sites host chef.example.com it.example.com draw.example.com
    handle @public_sites {
        reverse_proxy {http.reverse_proxy}
        import static_security_headers
    }
    handle {
        import authelia_basics
        reverse_proxy {http.reverse_proxy}
    }
}

* {
    import public_docker_sites
}
Tip

If you use Docker Compose labels instead of a Caddyfile, the equivalent is an HTTP_AUTH_BYPASS=true environment label or removing the authelia matcher on the Caddy proxy for those specific services.

5.Hardening headers — why each one matters

The snippet above has a full block. Here's the rationale so you can tighten or loosen it later:

HeaderValueWhy
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadBrowsers will never downgrade to HTTP. Submit to the preload list once you're confident.
Content-Security-PolicySee snippetLocks down where scripts, styles, images, and connections can come from. 'unsafe-inline' and 'unsafe-eval' are included because CyberChef/Draw.io need them — expect a JS console warning about bundle safety; document what it blocks and accept it.
Permissions-PolicyAll deniedRemoves access to geolocation, camera, microphone, USB, and payment APIs. The page has no legitimate need for these.
X-Frame-OptionsSAMEORIGINPrevents clickjacking via embedding. Necessary because Draw.io can be framed by a malicious parent.
X-Content-Type-OptionsnosniffBrowsers trust your Content-Type. Without it, an attacker uploads a crafted HTML file disguised as JS.
Referrer-Policyno-referrerNo referer string leaves the browser. Protects downstream resource URLs.
Cache-Controlpublic, max-age=3600Static assets cache aggressively; HTML stays fresh. Tune to your taste.
CSP caveat

The 'unsafe-inline' and 'unsafe-eval' relaxations exist because WebContainer-based tools like CyberChef and Draw.io use dynamic code generation. A stricter CSP breaks them. The added frame-ancestors 'self' and X-Frame-Options compensate — you're not opening an embedding hole just to make the app work.

6.Docker Compose — minimal, pinned images

Never run :latest on public-facing containers. Pin by digest so supply-chain surprises don't become zero-days.

docker-compose.yml — CyberChef + IT-Tools + Draw.io on a private Docker overlay network
name: public-tools
networks:
  public:
  internal:

services:
  cyberchef:
    image: ghcr.io/gchq/cyberchef:10.19.3
    container_name: cyberchef
    restart: unless-stopped
    networks: [public]
    ports: ["127.0.0.1:8000:80"]
    read_only: true
    tmpfs:
      - /tmp

  it-tools:
    image: corentinth/it-tools:2025.2.14
    container_name: it-tools
    restart: unless-stopped
    networks: [public]
    ports: ["127.0.0.1:8080:80"]
    read_only: true

  drawio:
    image: jgraph/drawio:24.7.19
    container_name: drawio
    restart: unless-stopped
    networks: [public]
    ports: ["127.0.0.1:8081:80"]
    read_only: true
    tmpfs:
      - /tmp

Key choices:

7.Keep the homelab behind Authelia

The whole point of this guide is surgical removal. Verify Authelia is still protecting every other subdomain.

# Should challenge with a 401 or redirect to /auth
curl -I https://home.example.com    | head -20
curl -I https://cloud.example.com   | head -20

# Should NOT challenge
curl -I https://chef.example.com    | head -20
curl -I https://it.example.com      | head -20
curl -I https://draw.example.com    | head -20

If Authelia still prompts on the public tools, your host matcher is still in the global block — pull it into the handle branch instead.

Or let Hermes do it for you

"Edit /opt/stacks/caddy/Caddyfile: for chef, it-*, and draw* subdomains replace forward_auth with the shared static_security_headers snippet from /snippets/security-headers.conf. Reload and verify with curl. Revert if any non-public subdomain loses auth."

8.Runbook for a weekend test

  1. Stand up the three tools on a non-routable Docker overlay network.
  2. Add the Caddyfile rules; reload.
  3. Run the verification commands in §7 from outside the LAN.
  4. Confirm no Set-Cookie, no WWW-Authenticate, headers present.
  5. Leave them for 48 hours. Check access logs for anything unusual.
  6. Automate image digests updates or accept the 52-week CVE turnover window.

9.What to extend next

10.More guides

These cover the Hermes-based setup that manages the stack above.

Getting started with Hermes

Install, model selection, gateway, memory, skills — the zero-to-useful path.

Optimizing Hermes

Cost routing, memory hygiene, skill curation, profiles, config knobs.

Letting an AI agent run Home Assistant

Telegram-controlled automations, entities, climate, media — through Hermes.

Hermes + free MCP servers

Filesystem, memory, web, fetch — zero-cost MCP without hosting.

Also see the blog: Practical Homelab AI: Hermes, Docker, Caddy & the Overhaul

Next guide

Hermes + free MCP servers →

Filesystem, memory, web, fetch — zero-cost MCP without hosting anything.

Related: Practical Homelab AI blog post · All guides: lemonlink.eu/guides