The Permissions page only offered Delete, so changing a role or scope meant removing the grant and re-adding it from memory. A permission's id is derived from (subjectType, subject, scope, domain), so changing any of those is a different record rather than an update. The new PUT creates the new grant and removes the superseded one in that order, so an edit can never leave the old grant behind still conferring access. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Proxy
A reverse proxy and HTTPS termination service built on OpenResty/nginx, with a management API and web GUI. It puts any of your apps behind single sign-on (OIDC) and can also look users up directly in LDAP — so the same people who log in to your SSO are the people allowed to reach your proxied apps.
It handles the parts that are tedious to do by hand: automatic HTTPS certificates from Let's Encrypt (including wildcards via DNS-01), routing by hostname with wildcard matching, and per-host access control tied to your identity provider. You manage hosts, DNS providers, and permissions from a web UI or a REST API; the proxy serves them over TLS with auto-renewing certs and no downtime on changes.
Running it together with the theta42 SSO Manager? theta-env composes the two with one
setup.sh— it generates the OIDC + LDAP wiring from a singlesetup.envso the proxy and the SSO find each other without manual config.
Documentation: https://theta42.github.io/proxy/
(CHANGELOG.md for what changed in each release) — also
readable from the running app itself at /docs, no internet access required.
Screenshots
| Hosts | Authentication |
|---|---|
![]() |
![]() |
Basic auth and SSO are mutually exclusive per host, with per-user password management once basic auth is enabled:
Multiple backend targets per host, load balanced round-robin:
Why this over the alternatives
Nginx Proxy Manager, Traefik, and Caddy are all good reverse proxies with auto-HTTPS. This one is built around identity: it is both an OIDC client of an SSO provider (for browser login) and a direct LDAP client (for user lookups and per-host access control), so access decisions come from your real user directory, not a static allow-list or a separate auth proxy bolted on top. The trade-off is that it expects an OIDC/LDAP identity source to point at — it is not a standalone auth server. Pair it with the theta42 SSO Manager (bundled OpenLDAP + OIDC) for a self-hosted SSO + proxy stack, or point it at any OIDC provider + LDAP directory you already run.
Features
- Automated HTTPS/SSL certificate management via Let's Encrypt
- Support for HTTP-01 (auto-ssl) and DNS-01 (wildcard) ACME challenges
- Multiple DNS provider integrations (Cloudflare, DigitalOcean, PorkBun, DuckDNS — DuckDNS is free)
- Wildcard SSL certificate support with automatic renewal
- Dynamic host routing with wildcard domain matching (*, **)
- Multi-target load balancing — configure multiple backend targets per host with built-in round-robin load balancing
- Web-based management interface
- RESTful API for automation
- OIDC login — the proxy is an OpenID Connect client of an external SSO
(e.g.
theta42/sso-manager-node) - Direct LDAP lookups, independent of the OIDC flow
- Role-based access control (RBAC) — global admins, local groups, and
per-domain permissions (viewer/manager) via
/api/permissionand/api/group - Self-service API tokens (PATs) for scripting/CI without a browser session
- Unix socket-based host lookup for high-performance routing
Requirements
- Node.js 18+ (tested with 18.x, 20.x, 22.x)
- OpenResty (nginx with Lua support)
- Redis
- Modern Linux distribution (tested on Ubuntu 20.04+, Debian 11+)
- Inbound internet access for Let's Encrypt validation
- Root access (required for user management features)
Quick start
Three ways to run it, in order of how much it sets up for you:
1. As part of the unified stack (recommended)
theta-env composes this proxy with the
theta42 SSO Manager and generates
all the wiring (OIDC endpoints, LDAP bind, hostnames) from a single setup.env.
You enter your domain once and the proxy is registered as an OIDC client of the
SSO automatically:
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
cp setup.env.example setup.env # set CFG_DOMAIN to your domain
./setup.sh # generates ./config/, builds + bootstraps + starts both
See the theta-env README for the full first-run flow, DNS/port requirements, and backups.
2. Standalone, in Docker
The all-in-one image bundles OpenResty, the Node management app, and Redis. The
OIDC/LDAP/auth config is read from a bind-mounted ./config/proxy-secrets.js
(the OAuth client id/secret are filled in by your SSO's bootstrap, or you set
them yourself):
git clone https://github.com/theta42/proxy.git
cd proxy
mkdir -p config && chmod 700 config
cp secrets.js.example config/proxy-secrets.js
$EDITOR config/proxy-secrets.js # point oidc/ldap at your SSO + directory
docker compose up -d --build
The management UI comes up at http://127.0.0.1:3000 (bound to localhost; the
OpenResty front proxies it under TLS on 443). See DEPLOYMENT.md
and secrets.js.example for the full config shape.
3. Bare metal (Debian/Ubuntu)
An automated installer installs Node.js, OpenResty, Redis, the Lua modules, and the app, then symlinks the nginx config and starts a systemd service:
wget -O - https://raw.githubusercontent.com/theta42/proxy/master/ops/install.sh | sudo bash
This installer will:
- Install Node.js 22.x
- Install OpenResty and required dependencies
- Install and configure Redis
- Set up SSL fallback certificates
- Install Lua dependencies (lua-resty-auto-ssl, luasocket)
- Clone/update the proxy application at
/opt/theta42/proxy - Seed
/etc/proxy/secrets.json first run (edit it, then re-run orsystemctl restart proxy) - Configure systemd service
- Start the proxy service
It's idempotent and safe to re-run — re-running it updates the app in place and
prints the version you're updating from and to (e.g. Updated v1.1.13 -> v1.1.14), or Already up to date if there's nothing new.
Logs (Docker)
The all-in-one image runs OpenResty in the foreground and the Node app in the
background, both writing to the container's stdout/stderr. The proxy's nginx
access/error logs go to files (/var/log/nginx, on the proxy-logs volume),
so they do not show up in docker logs.
# App + OpenResty (stdout/stderr)
docker compose logs -f proxy
# or, by container name:
docker logs -f proxy
# nginx access / error logs (in-container files, not in docker logs)
docker compose exec proxy tail -f /var/log/nginx/access.log
docker compose exec proxy tail -f /var/log/nginx/error.log
# Recent context
docker compose logs --tail=200 --since=10m proxy
Secrets
Secrets are loaded from OpenBao at boot via
@simpleworkjs/bao-conf, which
deep-merges secret/proxy/conf over the file-loaded config. The proxy's OIDC
clientSecret is captured at require time (inside createOidcClient during
require('../models')), so bin/www runs bao-conf.init() before
require('../app') (which transitively loads models). Fail-soft: if OpenBao is
unreachable, boot continues from CONF_SECRETS. The proxy authenticates to
OpenBao with the scoped VAULT_TOKEN (env, policy proxy — read only
secret/proxy/conf), never the root token.
The config/proxy-secrets.js file is an operator-edit seed artifact
(gitignored); the bootstrap writes the generated OAuth client creds into
OpenBao, which is authoritative. For the full architecture see theta-env's
Secrets docs.
Manual Installation
For manual installation or other distributions, see the detailed steps below.
Recommended path:
ops/install.shis idempotent and safe to re-run — it symlinks the OpenResty/systemd config from the repo checkout, so updates stay in sync automatically. The manual steps below copy those same files instead of symlinking them, so they will not auto-track future changes toops/nginx_conf/orops/proxy.service— you'd need to re-copy them yourself after every update. Use the manual path only ifinstall.shdoesn't fit your distribution.
System Dependencies
Ubuntu/Debian:
apt install libpam0g-dev build-essential redis-server luarocks -y
Node.js 22.x:
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=22
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
apt update && apt install nodejs -y
OpenResty:
wget -O - https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list
apt update && apt install openresty -y
Lua Dependencies:
luarocks install lua-resty-auto-ssl
luarocks install luasocket
SSL Configuration
Create fallback SSL certificates:
mkdir -p /etc/ssl/
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
-subj '/CN=sni-support-required-for-valid-ssl' \
-keyout /etc/ssl/resty-auto-ssl-fallback.key \
-out /etc/ssl/resty-auto-ssl-fallback.crt
OpenResty Configuration
Configuration files are provided in ops/nginx_conf/:
nginx.conf- Main nginx configurationautossl.conf- Auto-SSL configuration for Let's Encrypt HTTP-01proxy.conf- Proxy server configuration with host lookuptargetinfo.lua- Lua module for host lookup via Unix socket
Copy these files to /etc/openresty/:
mkdir -p /etc/openresty/sites-enabled/
cp ops/nginx_conf/nginx.conf /etc/openresty/nginx.conf
cp ops/nginx_conf/autossl.conf /etc/openresty/autossl.conf
cp ops/nginx_conf/proxy.conf /etc/openresty/sites-enabled/000-proxy
cp ops/nginx_conf/targetinfo.lua /usr/local/openresty/lualib/targetinfo.lua
Application Setup
Clone and install:
mkdir -p /opt/theta42
cd /opt/theta42
git clone https://github.com/theta42/proxy.git
cd proxy/nodejs
npm install
Configure secrets:
mkdir -p /etc/proxy
cp ../secrets.js.example /etc/proxy/secrets.js
chmod 600 /etc/proxy/secrets.js
$EDITOR /etc/proxy/secrets.js
Create systemd service:
cp ../ops/proxy.service /etc/systemd/system/proxy.service
systemctl daemon-reload
systemctl enable proxy.service
systemctl start proxy.service
DNS Provider Configuration
For wildcard SSL certificates, configure a DNS provider via the web UI or API:
Supported providers:
- Cloudflare - Requires API token
- DigitalOcean - Requires API token
- PorkBun - Requires API key and secret API key
- DuckDNS - Free. Requires your account token and the list of subdomains
you've registered at duckdns.org (e.g.
myhostformyhost.duckdns.org). Good option if you don't own a domain — DuckDNS gives you one for free. Note DuckDNS only supports a single A/AAAA record and a single TXT record per domain (no arbitrary subdomains), which is enough for both dynamic DNS and DNS-01 wildcard certs but not for hosting other DNS records.
Once configured, create a wildcard host (e.g., *.example.com) and the system will automatically request and manage the DNS-01 challenge certificate.
Architecture
The system consists of three main components:
-
OpenResty/Nginx - Frontend proxy with Lua-based routing
- Handles SSL termination via lua-resty-auto-ssl
- Queries Node.js backend via Unix socket for host routing
- Proxies requests to configured backend servers
-
Node.js API - Backend management and control plane
- RESTful API for host/user/DNS management
- Wildcard SSL certificate orchestration
- Host lookup tree with wildcard matching
- User authentication and authorization
-
Redis - Data store (using model-redis ORM)
- Host configurations
- User accounts and tokens
- SSL certificate storage
- Domain and DNS provider configurations
Host Lookup System
The proxy supports sophisticated domain matching:
- Exact match:
example.commatches onlyexample.com - Single wildcard:
*.example.commatchessub.example.combut notdeep.sub.example.com - Double wildcard:
**.example.commatches any depth (sub.example.com,deep.sub.example.com, etc.) - Mixed wildcards:
api.*.example.commatchesapi.v1.example.com,api.v2.example.com, etc.
Priority: Exact match > Single wildcard > Double wildcard
Development
Running locally:
cd nodejs
npm install
npm run dev # Runs with nodemon for auto-reload
Running tests:
npm test # Run all tests
npm run test:unit # Run unit tests only
npm run test:integration # Run integration tests only
npm run test:watch # Watch mode for development
Tests use Node.js built-in test runner (requires Node 18+).
API Documentation
See API Documentation for complete API reference.
Contributing
Pull requests are welcome. The project uses GitHub Actions for CI/CD:
- Tests run automatically on all PRs
- All tests must pass before merging to master
- Tests run on Node.js 18.x, 20.x, and 22.x
License
MIT - See LICENSE file for details.
Project Structure
proxy/
├── nodejs/ # Node.js backend application
│ ├── bin/ # Entry point (www)
│ ├── 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, User, DNS providers)
│ ├── routes/ # API routes
│ ├── services/ # Background services (host lookup, scheduler)
│ ├── middleware/ # Express middleware
│ ├── utils/ # Utility functions
│ ├── public/ # Static web assets
│ ├── views/ # EJS templates
│ └── test/ # Test suite
├── ops/ # Operations and deployment
│ ├── nginx_conf/ # OpenResty configuration files
│ ├── install.sh # Automated installer
│ └── proxy.service # Systemd service definition
└── .github/workflows/ # CI/CD workflows



