Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/webhood-io/webhood/llms.txt

Use this file to discover all available pages before exploring further.

Webhood separates its containers across two Docker bridge networks so that the scanner — the component most exposed to untrusted content — cannot directly reach the user-facing UI, and so that the backend database is never accessible from the public internet. Understanding this topology helps you verify that a deployment is correctly isolated and reason about the blast radius if a scan target were to compromise the scanner process.

Docker Network Topology

The compose file defines two networks, frontend and rest, and assigns each container to only the networks it needs:
networks:
  frontend:
  rest:

frontend network

The frontend network connects Kong, the core UI (webhood-core), and the backend (webhood-backend).
Internet → Kong (ports 8000/8443) → core:3000
                                  → backend:8090
The backend has no published ports in the compose configuration. It is reachable only from within the frontend network, meaning it is inaccessible from the host or from any container not on that network.

rest network

The rest network connects Kong, the scanner (webhood-scanner), and the core UI (webhood-core).
scanner → Kong:8000 → backend:8090
The scanner reaches the backend exclusively through Kong. The backend is not on the rest network — it is only on the frontend network. Kong is the sole bridge between the two networks, and its routing table forwards /api/ paths from the rest side through to the backend on the frontend side. The scanner’s authenticated API calls therefore pass through the same proxy as all other traffic.

Scanner isolation

The scanner is assigned only to the rest network:
scanner:
  networks:
    - rest
It is not on the frontend network and therefore cannot reach the backend directly. The backend is only accessible through Kong, which enforces route-level authentication. If the scanner process were compromised by a malicious scan target, it would be unable to reach the backend except through Kong’s defined, authenticated routes.
The backend healthcheck is configured as curl -f http://localhost:8090/_/. This means /_/ is a live path on the backend container’s loopback interface. Ensure that no Kong route, reverse proxy, or firewall rule ever forwards external traffic to /_/ on the backend, as this would expose the PocketBase admin panel to the internet.

Chrome Seccomp Profile

The scanner container runs headless Chromium to render scan targets. Chromium’s renderer process is itself sandboxed, but that sandbox requires a set of Linux syscalls that are not available by default in a Docker container. Webhood ships a dedicated seccomp profile to grant exactly those syscalls while restricting everything else:
# docker-compose.yml
scanner:
  security_opt:
    - seccomp=./files/chrome.json
The files/chrome.json profile is a syscall allowlist tuned for Chrome. Its effects are:
  • Reduced kernel attack surface — a malicious page that exploits a Chrome vulnerability and reaches the renderer process still cannot make arbitrary kernel calls from the container.
  • Sandbox without root — Chrome’s built-in sandboxing (--sandbox) uses clone(2) and related calls that Docker’s default seccomp profile blocks. The custom profile re-enables them, so the container does not need to run as root or with --privileged.

Private IP Blocking

The SCANNER_NO_PRIVATE_IPS environment variable controls whether the scanner will follow URLs that resolve to private or loopback addresses:
# .env
SCANNER_NO_PRIVATE_IPS=true
When set to true, the scanner resolves the hostname of each submitted URL using a DNS lookup before visiting it. If the resolved IP address is in a private range, the scan is rejected:
// src/scanner/src/utils/dnsUtils.ts
export function resolvesPublicIp(scanurl: string): Promise<string> {
  const hostname = new url.URL(scanurl).hostname;
  return new Promise((resolve, reject) => {
    dns.lookup(hostname, (err, address) => {
      if (err) {
        reject(err);
      } else {
        if (!ip.isPrivate(address)) {
          resolve(address);
        } else {
          reject(new Error("Not a public IP"));
        }
      }
    });
  });
}
The ip.isPrivate() check covers the following ranges:
RangeDescription
10.0.0.0/8RFC 1918 class A private
172.16.0.0/12RFC 1918 class B private
192.168.0.0/16RFC 1918 class C private
127.0.0.0/8Loopback
169.254.0.0/16Link-local (APIPA)
Without this protection, an attacker who can submit scan URLs could craft a URL whose hostname resolves to an internal service — the backend API, a metadata endpoint, or any other service reachable from the scanner container. This class of attack is known as server-side request forgery (SSRF).
Enable SCANNER_NO_PRIVATE_IPS=true for every deployment where scan URL submissions are not fully trusted, including any instance accessible by more than one user. The default value is false for local development convenience, but production deployments should always set it to true.

Host-Level Hardening

The Docker network isolation described above operates at the container layer. For defence in depth, apply a host firewall to restrict which source IPs can reach the Kong proxy ports:
In production, add a firewall rule that limits inbound access to ports 8000 and 8443 (or the values of WEBHOOD_HTTP_PORT and WEBHOOD_HTTPS_PORT) to your trusted IP ranges. This prevents unauthenticated users from reaching the login endpoint and eliminates the brute-force surface against PocketBase’s authentication API.
A minimal ufw example:
# Allow only your office or VPN CIDR
ufw allow from 203.0.113.0/24 to any port 8000
ufw allow from 203.0.113.0/24 to any port 8443
ufw deny 8000
ufw deny 8443

Build docs developers (and LLMs) love