The Isolation & Privacy Tier: KASM, SearXNG, and Hardened Docker
April 12, 2026
Two problems this tier solves: I need a browser I can throw away after touching something sketchy, and I need search that doesn't profile me. Both run as self-hosted containers in the DMZ, behind a reverse proxy, on a Docker setup hardened enough that a container breakout doesn't become a network breach. This is the build.
Why browser isolation matters in a home lab
Half of security work is looking at things you'd rather not run on your daily driver: a suspicious URL from a phishing sample, an attachment, a sketchy download. Doing that in your main browser is how a lab compromises its owner. KASM Workspaces gives me a full browser running inside a container, streamed to my screen over the browser itself. When I'm done, I destroy the container and every trace goes with it.
The threat model is explicit: assume the isolated browser will get compromised, and make sure that's a non-event.
Architecture
┌─────────────────────────────────────┐
client ──HTTPS──▶ reverse proxy (DMZ) ──▶ │ KASM (disposable browsers) │
(TLS, auth) │ SearXNG (private metasearch) │
│ Uptime Kuma (status) │
└──────────────┬──────────────────────┘
│ DMZ VLAN 40
│ egress-filtered via pfSense
▼
internet (KASM only, scoped)Everything sits in the DMZ VLAN, which pfSense firewalls so these containers cannot initiate connections back into the internal network. The reverse proxy is the only ingress; pfSense egress rules are the only way out.
KASM: disposable browsers
KASM runs each session as an ephemeral container from a base image. My configuration choices:
- Non-persistent profiles. Sessions are destroyed on logout: no cookies, no history, no downloaded payload survives. That's the entire point.
- Isolated network. The KASM session containers are on a Docker network with only the egress pfSense permits, so a compromised browser can reach the internet (to complete the analysis) but not the lab.
- Resource caps per session so a malicious page that tries to peg CPU/RAM can't starve the host.
# docker-compose override (excerpt): session isolation + limits
services:
kasm_agent:
networks: [kasm_isolated]
deploy:
resources:
limits: { cpus: "2.0", memory: 4G }
networks:
kasm_isolated:
driver: bridge
internal: false # egress allowed, but pfSense scopes itGotcha #1: KASM wants its own ports and a real cert. It runs its own proxy on 443 by default, which collides with the DMZ reverse proxy. I moved KASM to an internal port and let the reverse proxy terminate TLS in front of it. Running two things that both want 443 on one host is a classic self-inflicted outage.
SearXNG: search without the surveillance
SearXNG is a metasearch engine: it queries other engines on your behalf and returns aggregated results with no tracking, no profile, no ad personalization. Self-hosting it means the queries never leave my infrastructure attached to my identity.
# settings.yml (excerpt)
server:
secret_key: "<generated>"
limiter: true # rate-limit to prevent abuse if exposed
search:
safe_search: 0
autocomplete: "" # no autocomplete calls leaking keystrokes
engines:
- name: google
disabled: false
- name: bing
disabled: falseGotcha #2: set the `secret_key` and the limiter, or regret it. The default config ships with a placeholder secret and no rate limiting. If a SearXNG instance is reachable and unlimited, bots will find it and use it as a free search proxy until the upstream engines rate-limit you. Generate a real secret and enable the limiter before it's ever exposed.
Docker hardening: the part people skip
These are internet-adjacent services running untrusted content. The container runtime is the boundary, so it gets hardened:
- No `--privileged`, ever. Drop all capabilities and add back only what's needed.
- Read-only root filesystems where the app allows, with explicit writable volumes.
- User namespaces / non-root containers so container-root isn't host-root.
- No Docker socket mounted into any container: that's a full host-takeover primitive and it's astonishing how many compose files hand it out casually.
# hardening defaults applied per service
security_opt:
- no-new-privileges:true
cap_drop: [ALL]
read_only: true
tmpfs:
- /tmpGotcha #3: the Docker socket is a loaded gun. Any container with /var/run/docker.sock mounted can create a privileged container and own the host. Monitoring stacks love to ask for it. I refuse it and use a socket-proxy with a read-only, filtered API when a container genuinely needs Docker visibility.The reverse proxy and access control
A single reverse proxy in the DMZ terminates TLS, enforces authentication in front of the internal-only services, and is the only thing published outward. Uptime Kuma watches all of it and drives the public status page. Nothing behind the proxy is directly reachable; nothing in this tier can call inward.
Why this tier is designed this way
- Assume breach at the container. Every choice (ephemeral sessions, egress filtering, dropped capabilities, no Docker socket) exists so that a compromised browser or search container is contained to a dead-end DMZ, not a foothold.
- Privacy is a control, not a preference. Self-hosting search and isolating browsing removes whole categories of data exposure rather than trusting a third party to behave.
- Disposable beats clean. It's easier to destroy and rebuild than to sanitize. Ephemeral-by-default means I never have to wonder whether something lingered.
What's next
- gVisor or Kata runtime under the KASM session containers for kernel-level isolation on top of the namespace boundary.
- Pipe SearXNG and reverse-proxy access logs into Wazuh for anomaly detection on the DMZ tier.
- Automated nightly rebuild of the KASM base image so it's always freshly patched.