# 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](https://github.com/theta42/sso-manager-node)?
> [theta-env](https://github.com/theta42/theta-env) composes the two with one
> `setup.sh` — it generates the OIDC + LDAP wiring from a single `setup.env` so
> the proxy and the SSO find each other without manual config.

**Documentation:** [https://theta42.github.io/proxy/](https://theta42.github.io/proxy/)
([CHANGELOG.md](CHANGELOG.md) for what changed in each release) — also
readable from the running app itself at `/docs`, no internet access required.

## Screenshots

| Hosts | Authentication |
| --- | --- |
| [![Host list](docs/images/hosts.png)](docs/images/hosts.png) | [![Per-host SSO auth](docs/images/host-auth-sso.png)](docs/images/host-auth-sso.png) |

Basic auth and SSO are mutually exclusive per host, with per-user password
management once basic auth is enabled:

[![Per-host basic auth](docs/images/host-auth-basic.png)](docs/images/host-auth-basic.png)

Multiple backend targets per host, load balanced round-robin:

[![Load balancing](docs/images/load-balancing.png)](docs/images/load-balancing.png)

## 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](https://github.com/theta42/sso-manager-node) (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`](https://github.com/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/permission` and `/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](https://github.com/theta42/theta-env) composes this proxy with the
[theta42 SSO Manager](https://github.com/theta42/sso-manager-node) 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:

```bash
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](https://github.com/theta42/theta-env) 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):

```bash
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](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:

```bash
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.js` on first run (edit it, then re-run or `systemctl 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`.

```bash
# 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
```

## Manual Installation

For manual installation or other distributions, see the detailed steps below.

> **Recommended path:** `ops/install.sh` is 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
> to `ops/nginx_conf/` or `ops/proxy.service` — you'd need to re-copy them
> yourself after every update. Use the manual path only if `install.sh`
> doesn't fit your distribution.

### System Dependencies

**Ubuntu/Debian:**
```bash
apt install libpam0g-dev build-essential redis-server luarocks -y
```

**Node.js 22.x:**
```bash
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:**
```bash
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:**
```bash
luarocks install lua-resty-auto-ssl
luarocks install luasocket
```

### SSL Configuration

Create fallback SSL certificates:
```bash
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 configuration
- `autossl.conf` - Auto-SSL configuration for Let's Encrypt HTTP-01
- `proxy.conf` - Proxy server configuration with host lookup
- `targetinfo.lua` - Lua module for host lookup via Unix socket

Copy these files to `/etc/openresty/`:
```bash
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:
```bash
mkdir -p /opt/theta42
cd /opt/theta42
git clone https://github.com/theta42/proxy.git
cd proxy/nodejs
npm install
```

Configure secrets:
```bash
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:
```bash
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](https://www.duckdns.org) (e.g.
  `myhost` for `myhost.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:

1. **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

2. **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

3. **Redis** - Data store (using [model-redis](https://www.npmjs.com/package/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.com` matches only `example.com`
- **Single wildcard**: `*.example.com` matches `sub.example.com` but not `deep.sub.example.com`
- **Double wildcard**: `**.example.com` matches any depth (`sub.example.com`, `deep.sub.example.com`, etc.)
- **Mixed wildcards**: `api.*.example.com` matches `api.v1.example.com`, `api.v2.example.com`, etc.

Priority: Exact match > Single wildcard > Double wildcard

## Development

**Running locally:**
```bash
cd nodejs
npm install
npm run dev  # Runs with nodemon for auto-reload
```

**Running tests:**
```bash
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](nodejs/api.md) 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
```
