Files
proxy/docs/architecture.md
wmantly 426fa111ec Add plain-language concept docs; fix docs viewer rendering; link API tokens
- New docs/concepts-{hosts,dns,access,api-tokens}.md -- plain-language
  guides aimed at less technical readers, each linking onward to the
  existing system-design-level doc for anyone who wants that detail.
  Card help links (Proxy List, Add/Edit host, DNS Provider cards,
  Users/Permissions/Groups cards) now point here instead of straight at
  Installation/Architecture.
- The "New API Token" card had no help link at all -- added, pointing to
  the new API Tokens doc.
- Fixed the in-app docs viewer rendering every docs/*.md page with a
  garbled heading + stray <hr> at the top: Jekyll front matter (meant
  only for the GitHub Pages build) was never stripped before being
  handed to the markdown renderer.
- Fixed cross-doc links never resolving in-app, since this viewer serves
  docs at /docs/<slug> with no .html suffix: rewritten to the correct
  in-app URL, first by registered slug, falling back to the doc's real
  filename (the correct, working link form on the Jekyll/GitHub Pages
  build) -- same idea as the existing image-path fix, and lets one link
  written in a doc work on both targets.

Bumps to v1.1.13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 22:09:53 -04:00

12 KiB

layout, title, description
layout title description
default Architecture How the proxy's OIDC client, LDAP client, and OpenResty routing fit together.

Architecture

← Back to Home

Looking for a plainer explanation of hosts, HTTPS, or the local permission model instead of internals? See Hosts & HTTPS and Users, Groups & Permissions.

System Overview

The proxy system consists of three main components working together to provide high-performance reverse proxying with automated SSL management.

┌──────────────────────────────────────────────────────────────┐
│                         Internet                              │
└─────────────────────────┬────────────────────────────────────┘
                          │ HTTPS/HTTP
                          ▼
┌──────────────────────────────────────────────────────────────┐
│                    OpenResty/Nginx                            │
│  ┌────────────────┐  ┌──────────────┐  ┌─────────────────┐  │
│  │ SSL Termination│  │ Host Routing │  │ Request Proxying│  │
│  │ (lua-resty-    │  │ (targetinfo. │  │                 │  │
│  │  auto-ssl)     │  │  lua)        │  │                 │  │
│  └────────────────┘  └──────┬───────┘  └─────────────────┘  │
└────────────┬──────────────────┼───────────────────────────┬──┘
             │                  │                           │
     Let's Encrypt       1. Check Redis FIRST          Backend
       HTTP-01           2. Unix Socket (fallback)    Services
             │                  │                           │
             ▼                  ▼                           ▼
┌──────────────────────┐  ┌──────────────────────────────────┐
│       Redis          │  │      Node.js Application         │
│  (Primary Cache)     │  │  ┌──────────────┐  ┌─────────┐  │
│  - Host configs ◄────┼──┼──┤   Services   │  │ Routes  │  │
│  - User accounts     │  │  │ - host_lookup│  │ - /api/*│  │
│  - SSL certs         │  │  │ - scheduler  │  │         │  │
│  - Auth tokens       │  │  └──────────────┘  └─────────┘  │
└──────────────────────┘  └─────────┬────────────────────────┘
                                    │
                                    ▼
                          ┌──────────────────────┐
                          │    DNS Providers     │
                          │  - Cloudflare        │
                          │  - DigitalOcean      │
                          │  - PorkBun           │
                          │  - DuckDNS (free)    │
                          │  (DNS-01 challenges) │
                          └──────────────────────┘

Component Details

OpenResty/Nginx (Frontend)

Responsibilities:

  • Accept incoming HTTP/HTTPS requests
  • SSL termination using lua-resty-auto-ssl
  • Host-based routing decisions (Redis-first lookup)
  • Proxy requests to backend services

Key Features:

  • HTTP-01 ACME challenge handling for automatic SSL
  • Redis-first host lookup with Node.js fallback via Unix socket
  • High-performance event-driven architecture
  • Support for WebSocket connections
  • Continues serving cached hosts even if Node.js is down

Configuration Files:

  • /etc/openresty/nginx.conf - Main configuration
  • /etc/openresty/autossl.conf - Let's Encrypt integration
  • /etc/openresty/sites-enabled/000-proxy - Proxy configuration
  • /usr/local/openresty/lualib/targetinfo.lua - Host lookup module

Node.js Application (Backend)

Responsibilities:

  • API for host/user/DNS management
  • Wildcard SSL certificate orchestration
  • Host lookup tree maintenance
  • User authentication and authorization

Directory Structure:

nodejs/
├── bin/www              # Application entry point
├── conf/                # Configuration (base.js, environment overlays, secrets.js)
├── controller/          # App-level wiring (pubsub, startup)
├── migrations/          # One-off Redis data migration scripts
├── models/              # Data models
│   ├── host.js          # Host configuration and lookup
│   ├── auth.js          # Authentication logic
│   ├── user.js          # User management
│   └── dns_provider/    # DNS provider implementations
├── routes/              # API endpoints
│   ├── host.js          # Host CRUD operations
│   ├── dns.js           # DNS provider management
│   ├── user.js          # User management
│   ├── auth.js          # Authentication (login + OIDC)
│   ├── permission.js    # RBAC permission management
│   ├── group.js         # Local group management
│   └── api_token.js     # Self-service API (PAT) tokens
├── services/            # Background services
│   ├── host_lookup.js   # Unix socket server
│   └── host_scheduler.js # Cert renewal scheduler
├── middleware/          # Express middleware
│   └── auth.js          # Authentication middleware
└── utils/               # Utility modules
    └── unix_socket_json.js # Unix socket server

Redis (Data Store)

ORM: model-redis - A lightweight Redis ORM for Node.js with schema validation, relationships, and automatic key management.

Stored Data:

  • Host configurations (domain, IP, port, SSL settings)
  • User accounts and hashed passwords
  • Authentication tokens
  • SSL certificates (for wildcard domains)
  • DNS provider credentials
  • Domain-to-provider mappings

Key Prefixes:

proxy_Host_<hostname>           # Host configuration
proxy_User_<username>           # User account
proxy_AuthToken_<token>         # Auth tokens
proxy_DnsProvider_<id>          # DNS provider
proxy_Domain_<domain>           # Domain info
<hostname>:latest               # SSL certificate cache

Request Flow

Standard HTTP/HTTPS Request

  1. Client sends HTTPS request to app.example.com
  2. OpenResty receives request, terminates SSL
  3. Lua script (targetinfo.lua) queries Redis first for host config
  4. If found in Redis, jump to step 7 (Node.js not involved)
  5. If not in Redis, Lua queries Node.js via Unix socket as fallback
  6. Node.js performs host lookup (supports wildcards), caches result in Redis
  7. OpenResty proxies request to backend service using target IP and port
  8. Response proxied back to client

Resilience: If Node.js goes down, all hosts already cached in Redis continue to work. Only new/uncached hosts will fail until Node.js recovers.

Wildcard SSL Certificate Request

  1. User creates wildcard host (*.example.com) via API
  2. Node.js validates domain has DNS provider configured
  3. Let's Encrypt DNS-01 challenge initiated
  4. DNS provider API creates TXT record (_acme-challenge.example.com)
  5. Let's Encrypt validates TXT record
  6. Certificate generated and stored in Redis
  7. DNS provider cleans up TXT record
  8. Background scheduler monitors expiration, renews 30 days before expiry

Host Lookup Algorithm

The lookup tree enables sophisticated domain matching:

Input: "api.v1.example.com"

Tree Structure:
{
  "com": {
    "example": {
      "*": {          // Matches api.example.com
        "#record": {...}
      },
      "v1": {
        "api": {      // Matches api.v1.example.com (exact)
          "#record": {...}
        }
      }
    }
  }
}

Priority: Exact > Single wildcard (*) > Double wildcard (**)

Wildcard Types:

  • example.com - Exact match only
  • *.example.com - Matches sub.example.com (single level)
  • **.example.com - Matches any depth (sub.deep.example.com)
  • api.*.example.com - Matches api.v1.example.com, api.v2.example.com

Security Architecture

Authentication Flow

  1. User sends credentials to /api/auth/login
  2. Credentials validated against stored hash (bcrypt)
  3. Token generated and stored in Redis with TTL
  4. Token returned to client
  5. Subsequent requests include token in auth-token header
  6. Middleware validates token before processing request

SSL Certificate Security

  • Private keys stored only in Redis (memory/disk based on config)
  • Fallback certificates used when SNI unavailable
  • Let's Encrypt rate limiting respected
  • DNS provider credentials marked as isPrivate (not returned in API)

Unix Socket Communication

  • Socket file: /var/run/proxy_lookup.socket
  • Permissions: 777 (container-safe, single-use deployment)
  • Protocol: JSON over Unix stream socket
  • Buffer handling: Accumulates partial messages until complete JSON

Performance Optimizations

Caching Strategy

The system uses a multi-tier caching approach:

  1. Redis (L1 Cache) - OpenResty checks Redis FIRST for every request

    • Primary host configuration storage
    • Survives Node.js restarts/failures
    • Shared across all OpenResty workers
  2. Node.js Lookup Tree (L2 Cache) - In-memory host lookup with wildcard matching

    • Only queried when Redis has no entry
    • Rebuilt automatically when hosts change
    • Supports complex wildcard resolution
  3. Wildcard Parent Caching - Resolved wildcard matches stored back to Redis

    • Subsequent requests to api.example.com hit Redis directly
    • No repeated wildcard resolution needed

Unix Socket vs HTTP API

Unix socket chosen over HTTP for host lookups:

  • Lower latency - No TCP overhead
  • Higher throughput - No HTTP parsing
  • Simpler - Direct JSON communication
  • Secure - Filesystem permissions, no network exposure

Scalability Considerations

Current Architecture

  • Single instance - OpenResty + Node.js + Redis on one server
  • Vertical scaling - Add CPU/RAM as needed
  • Limitations - Unix socket ties OpenResty to Node.js on same host

Future Scaling Options

  • Redis cluster - Distribute data storage
  • Multiple OpenResty instances - Load balance incoming requests
  • Stateless Node.js - Run multiple API instances
  • Replace Unix socket - Use TCP/HTTP for cross-host communication
  • Separate cert management - Dedicated service for wildcard SSL

Monitoring and Observability

Logs

  • OpenResty: /var/log/nginx/access.log, /var/log/nginx/error.log
  • Node.js: journalctl -u proxy.service
  • Redis: redis-cli MONITOR

Health Checks

  • Node.js API: curl http://localhost:3000/api/host
  • Redis: redis-cli PING
  • OpenResty: systemctl status openresty
  • Unix socket: ls -la /var/run/proxy_lookup.socket

Metrics to Monitor

  • Request rate and response times
  • SSL certificate expiration dates
  • Redis memory usage
  • Host lookup cache hit rate
  • Background service execution times

← Back to Home