Living Document Notice
Published 2026-09-13. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.
Separating Public Renderers from Headless CMS Backends
Summary
Deploying headless content management systems like Directus alongside public web applications frequently leads to architectural misconfigurations. Exposing the Directus administrative login portal, GraphQL endpoints, and database connection pools directly to the public internet expands the attack surface, invites credential stuffing, and risks accidental exposure of internal collections and drafts.
Harbor enforces a strict operational separation between public-facing rendering tiers and private content stores. By deploying the Harbor edge renderer into a DMZ bridge network with scoped read-only API credentials and restricting Directus behind an internal management network accessible solely via local loopback and VPN tunnels, the public perimeter exposes only static-like HTML endpoints while keeping administrative surfaces invisible.
Threat Vectors in Unified CMS Deployments
When a headless CMS shares ingress routing with public web visitors, operators face distinct failure modes:
- Administrative Surface Exposure: Automated bot scanners continuously brute-force
/admin,/auth/login, and/usersroutes. - Credential Escalation: If the edge presentation layer holds broad administrative API tokens, any server-side request forgery (SSRF) or dependency exploit in the frontend runtime leaks full read/write database permissions.
- Denial of Service via Uncached Queries: Direct access to CMS REST or GraphQL query interfaces allows attackers to submit deeply nested or unindexed queries, exhausting database connections and starving the rendering pipeline.
| Security Layer | Exposed CMS Pattern | Harbor Isolated Boundary Pattern |
|---|---|---|
| Directus Admin UI | Publicly accessible at cms.domain.com | Bound to 127.0.0.1 / WireGuard VPN mesh only |
| Directus API Exposure | Public internet via edge reverse proxy | Internal Docker network (172.24.0.0/16) only |
| Edge Service Tokens | Admin or elevated role credentials | Scoped Read-Only role; limited collection visibility |
| Network Interface | Single flat bridge shared by all containers | Dual bridge: Public DMZ vs Private Control Plane |
| Public Ports Open | 80, 443, 8055 (CMS port) | 80, 443 (Harbor ingress only) |
Network Boundary and Docker Compose Topology
Harbor isolates the network stack into two separate bridge definitions. The edge container binds to both public_dmz and internal_api, while Directus and its backing PostgreSQL database attach exclusively to internal_api.
version: "3.8"
networks:
public_dmz:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
internal_api:
driver: bridge
internal: true # Disables external gateway routing
ipam:
config:
- subnet: 172.24.0.0/16
services:
harbor_edge:
image: harbor-renderer:latest
networks:
public_dmz:
ipv4_address: 172.20.0.10
internal_api:
ipv4_address: 172.24.0.10
ports:
- "127.0.0.1:3000:3000"
environment:
- DIRECTUS_INTERNAL_URL=http://172.24.0.20:8055
- DIRECTUS_READ_TOKEN=tok_read_only_edge_98a7cf
restart: unless-stopped
directus_cms:
image: directus/directus:10.10.0
networks:
internal_api:
ipv4_address: 172.24.0.20
environment:
- KEY=harbor-secret-key-128941
- SECRET=harbor-secret-salt-894712
- DB_CLIENT=pg
- DB_HOST=172.24.0.30
- DB_PORT=5432
- DB_DATABASE=directus
- DB_USER=directus_user
- DB_PASSWORD=directus_secure_pass
restart: unless-stopped
postgres_db:
image: postgres:16-alpine
networks:
internal_api:
ipv4_address: 172.24.0.30
environment:
- POSTGRES_DB=directus
- POSTGRES_USER=directus_user
- POSTGRES_PASSWORD=directus_secure_pass
restart: unless-stoppedDirectus Read-Only Role Definition
The service token utilized by Harbor belongs to a custom role (harbor_reader) configured via Directus role permissions. The role possesses zero permissions on system collections (directus_*), no mutation permissions (create, update, delete), and strict read filters on content collections:
{
"collection": "articles",
"action": "read",
"permissions": {
"status": {
"_eq": "published"
}
},
"fields": [
"id",
"title",
"slug",
"content_html",
"published_at",
"tags"
]
}Even in the event of an application exploit within the edge renderer, the exposed token cannot query draft records, private notes, system credentials, or administrative logs.
Boundary Auditing and Port Verification
Verify that Directus ports are unreachable from external network adapters:
# Verify port 8055 is not listening on public interfaces
ss -tulpn | grep 8055
# Probe Directus from within the Harbor edge container (should succeed)
docker exec -it harbor_edge curl -s -o /dev/null -w "%{http_code}\n" http://172.24.0.20:8055/server/ping
# Probe Directus from the host public interface (must fail or time out)
curl -m 3 http://$(curl -s ifconfig.me):8055/server/ping || echo "Boundary verified: CMS unreachable from WAN"
# Verify role token cannot mutate content
curl -X POST http://172.24.0.20:8055/items/articles \
-H "Authorization: Bearer tok_read_only_edge_98a7cf" \
-H "Content-Type: application/json" \
-d '{"title": "Unauthorized Write"}'- Directus Target: harbor
- Garden Source Reference: [HAR-1004 - Separating Public Renderers from Headless CMS Backends](HAR-1004 - Separating Public Renderers from Headless CMS Backends), MOC - Harbor Ecosystem, MOC - Local-First Systems and Synchronization, MOC - Bosun PKM Tools