diff --git a/.dockerignore b/.dockerignore index 1a25abd..194639e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,13 +7,12 @@ nodejs/node_modules/ nodejs/test/ nodejs/npm-debug.log* -# Bare-metal installer + chef/vagrant ops not needed in the image. +# Bare-metal installer + chef ops not needed in the image. ops/install.sh ops/cert.sh ops/cookbooks/ ops/roles/ ops/proxy.service -Vagrantfile # Docs site (served via GitHub Pages, not from the image). docs/ diff --git a/.gitignore b/.gitignore index 3e06dbc..bd1404d 100755 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,6 @@ typings/ # dotenv environment variables file .env -.vagrant *~ *# .#* diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 2fd3033..69c5ddf 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -39,8 +39,12 @@ are `JSON.parse`-coerced when possible and kept as raw strings otherwise. | `app_ldap__tlsOptions__rejectUnauthorized` | `conf.ldap.tlsOptions.rejectUnauthorized` | `false` for self-signed LDAPS | | `app_ldap__tlsOptions__ca` | `conf.ldap.tlsOptions.ca` | path to a CA cert for strict trust | | `app_auth__adminUsers` | `conf.auth.adminUsers` | local anti-lockout admin (uid) | +| `app_auth__adminGroups` | `conf.auth.adminGroups` | SSO/LDAP groups that are global admin (JSON array) | | `app_redis__prefix` | `conf.redis.prefix` | default `proxy_` | +See [`docs/docker.md`](docs/docker.md) for a shorter, container-focused version +of this reference. + > **Requires `@simpleworkjs/conf` >= 1.1.0.** The Docker image will not honor > `app_*` env vars on 1.0.0. The lock is already on `^1.1.0`; if you regenerate it: > ```bash diff --git a/README.md b/README.md index 8a763ba..0f4d8f8 100755 --- a/README.md +++ b/README.md @@ -8,12 +8,17 @@ A reverse proxy and HTTPS termination service using OpenResty/nginx with a manag - 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) +- Multiple DNS provider integrations (Cloudflare, DigitalOcean, PorkBun) - Wildcard SSL certificate support with automatic renewal - Dynamic host routing with wildcard domain matching (*, **) - Web-based management interface - RESTful API for automation -- User authentication and management +- **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 @@ -34,7 +39,7 @@ wget -O - https://raw.githubusercontent.com/theta42/proxy/master/ops/install.sh ``` This installer will: -- Install Node.js 20.x +- Install Node.js 22.x - Install OpenResty and required dependencies - Install and configure Redis - Set up SSL fallback certificates @@ -68,6 +73,14 @@ docker compose logs --tail=200 --since=10m proxy 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:** @@ -75,10 +88,10 @@ For manual installation or other distributions, see the detailed steps below. apt install libpam0g-dev build-essential redis-server luarocks -y ``` -**Node.js 20.x:** +**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=20 +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 ``` @@ -147,7 +160,7 @@ systemctl start proxy.service For wildcard SSL certificates, configure a DNS provider via the web UI or API: **Supported providers:** -- **CloudFlare** - Requires API token +- **Cloudflare** - Requires API token - **DigitalOcean** - Requires API token - **PorkBun** - Requires API key and secret API key @@ -195,9 +208,10 @@ 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:watch # Watch mode for development +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+). @@ -223,6 +237,9 @@ MIT - See LICENSE file for details. 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) diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 0830cd8..0000000 --- a/Vagrantfile +++ /dev/null @@ -1,142 +0,0 @@ -require 'json' -begin - secrets = JSON.parse(File.read('secrets.json')) - puts 'Loading secrets file' -rescue - secrets = {} - puts 'Secrets file not found' -end - -class ::Hash - def deep_merge(second) - second.each do |key, value| - if value.class == Hash and self[key.to_sym] - self[key.to_sym].deep_merge(value) - else - self[key.to_sym] = value - end - end - return self - end -end - -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# All Vagrant configuration is done below. The "2" in Vagrant.configure -# configures the configuration version (we support older styles for -# backwards compatibility). Please don't change it unless you know what -# you're doing. -Vagrant.configure("2") do |config| - # The most common configuration options are documented and commented below. - # For a complete reference, please see the online documentation at - # https://docs.vagrantup.com. - - # Every Vagrant development environment requires a box. You can search for - # boxes at https://vagrantcloud.com/search. - config.vm.box = "ubuntu/jammy64" - config.vm.synced_folder '.', '/vagrant' # The vagrant dir just stopped automounting - - # Create a forwarded port mapping which allows access to a specific port - # within the machine from a port on the host machine. In the example below, - # accessing "localhost:8080" will access port 80 on the guest machine. - # NOTE: This will enable public access to the opened port - config.vm.network "forwarded_port", guest: 80, host: 8080 - config.vm.network "forwarded_port", guest: 443, host: 8443 - config.vm.network "forwarded_port", guest: 3000, host: 8300 - - - # Provider-specific configuration so you can fine-tune various - # backing providers for Vagrant. These expose provider-specific options. - # Example for VirtualBox: - # - config.vm.provider 'virtualbox' do |vb| - # Customize the amount of memory on the VM: - vb.memory = '1024' - vb.cpus = "2" - # vb.default_nic_type = "virtio" - vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] - end - # - # View the documentation for the provider you are using for more - # information on available options. - - # Enable provisioning with a shell script. Additional provisioners such as - # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the - # documentation for more information about their specific syntax and use. - config.vm.provision "shell", inline: <<~SHELL - apt-get update - if ! apt list ruby-dev | grep installed; then - # apt-add-repository ppa:brightbox/ruby-ng -y - sudo apt-get install -y build-essential resolvconf ruby-full gem - gem install chef -v 17.10.0 - - fi - - if ! which berks >/dev/null; then - gem install ruby-shadow berkshelf --no-document - ln -s /opt/chef/embedded/bin/berks /usr/local/bin/berks - fi - - cd /vagrant - - cd /vagrant/ops/cookbooks - rm -rf vendor - rm -rf $HOME/.berksfile - if [ -f ".Berksfile.lock" ]; then - berks update - else - berks install - fi - berks vendor vendor - SHELL - - config.vm.provision 'chef_solo' do |chef| - chef.arguments = "--chef-license accept" - # chef.version = '15.7.31' # version 14.12.9 fails to run - chef.cookbooks_path = [ - 'ops/cookbooks/', - 'ops/cookbooks/vendor/' - ] - chef.roles_path ='ops/roles' - chef.add_role('common') - chef.json = { - 'working-dir': '/vagrant', - 'app': { - 'name': 't42-proxy', - 'run_user': 'root', - 'domain': 'proxy.local', - }, - 'python': { - # 'working-dir': 'django', - 'version': '2.7' - }, - 'nodejs': { - 'working-dir': 'nodejs', - 'port': '3000', - 'install_version': 18, - 'exec_file': 'bin/www', - 'service': true, - }, - 'redis':{ - 'unix': { - 'perm': '777' - } - }, - 'web':{ - 'admin_email': 'admin2342@example.com', - 'do_ssl': true, - 't42-proxy': true - }, - }.deep_merge(secrets); - end - - config.vm.provision "shell", inline: <<~SHELL - cd /vagrant/openresty - shopt -s extglob - rm -frv !("README.md") - cp -a "/usr/local/openresty/nginx/conf/." /vagrant/openresty - rm -rf /usr/local/openresty/nginx/conf/ - ln -s /vagrant/openresty/ /usr/local/openresty/nginx/conf - SHELL -end diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 7e45811..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -proxy.projects.theta42.com \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index e23feb5..8203639 100644 --- a/docs/README.md +++ b/docs/README.md @@ -37,8 +37,3 @@ The site uses the Cayman theme (`jekyll-theme-cayman`). Configuration is in `_co 2. Commit and push to master branch 3. GitHub Pages automatically rebuilds (may take 1-2 minutes) 4. Changes visible at https://theta42.github.io/proxy/ - -## Legacy Documentation - -- `dev_setup.md` - Old development setup notes (kept for reference) -- `Update 4.11.md` - Old update notes (kept for reference) diff --git a/docs/Update 4.11.md b/docs/Update 4.11.md deleted file mode 100644 index 6824566..0000000 --- a/docs/Update 4.11.md +++ /dev/null @@ -1,46 +0,0 @@ -# Update 4.11 - -## Changes - -The API routes have changed a bit and I will update the documentation soon. - -All the endpoints now have key validations and return standardized errors. -Any 500 class error should be reported as an issue. - -End points for working with user have been added. - -A GUI as need added and can be accessed from root of port 3000, -`http://server.ip:3000/` - -## How to get it - -* Navigate to where the project is installed. This should be done as root. - -* Pull the newest version from master - -```bash -git pull origin master -``` - -* Move to the NodeJS folder - -```bash -cd nodejs -``` - -* Update and install the NPM packages - -```bash -npm update -npm install -``` - -* Run the migration script for the redis Hosts. - -```bash -node migrations/host_1.js -``` - -* Restart the proxy service or container. - -* Enoy the GUI! diff --git a/docs/api.md b/docs/api.md index 9ce0536..093b4d1 100755 --- a/docs/api.md +++ b/docs/api.md @@ -7,7 +7,26 @@ title: API Reference [← Back to Home](index.html) -All API endpoints require authentication via the `auth-token` header unless otherwise noted. +All API endpoints require authentication unless otherwise noted. Three +authentication methods are supported: + +- **`auth-token` header** — a browser-session token from `POST /api/auth/login` + or the OIDC flow (below). +- **`Authorization: Bearer ` header** — a self-service API token (PAT, + see [API Tokens](#api-tokens)), for scripts/CI without a browser session. +- **OIDC (browser)** — if the proxy is configured as an OIDC client of an SSO + (`app_oidc__*` / `conf.oidc`, see [DEPLOYMENT.md](https://github.com/theta42/proxy/blob/master/DEPLOYMENT.md)), + users can log in via `GET /api/auth/oidc/start` instead of posting a + username/password. + +The proxy can also be configured as a **direct LDAP client** (`app_ldap__*` / +`conf.ldap`) for looking up/validating users, independent of the OIDC flow — +see DEPLOYMENT.md for the full configuration reference. + +Authenticated requests also carry **RBAC** (role-based access control): +global admins can manage everything; other users are scoped to `viewer` or +`manager` rights on specific domains via [Permissions](#permissions) and +[Groups](#groups). Base URL: `https://your-proxy-host.com/api` @@ -47,17 +66,167 @@ curl -H "auth-token: your-token-here" \ **Responses:** - `200` `{"message": "Bye"}` +### OIDC Login (start) + +**GET** `/api/auth/oidc/start` + +Begin the OIDC authorization-code flow: creates a PKCE + state challenge and +redirects the browser to the configured SSO's authorize endpoint. Only +available when `conf.oidc.enabled` is true. + +**Query Parameters:** +- `redirect` - Internal path to return to after login (optional; sanitized to same-origin) + +```bash +curl -i "https://proxy-host.com/api/auth/oidc/start?redirect=/hosts" +``` + +**Responses:** +- `302` Redirect to the SSO's authorization endpoint +- `404` `{"name": "OidcDisabled", "message": "OIDC login is not enabled."}` + +### OIDC Callback + +**GET** `/api/auth/oidc/callback` + +Redirect target for the SSO after login. Validates the one-time `state`, +exchanges the authorization `code` for tokens, reads identity from the +userinfo endpoint, establishes a session, and redirects the browser back to +the login page with the app's own `auth-token` in a URL fragment. + +**Query Parameters:** +- `code` (required) - Authorization code from the SSO +- `state` (required) - State value from the `start` step + +```bash +# Not called directly — the SSO redirects the browser here after login. +``` + +**Responses:** +- `302` Redirect to `/login#token=...&redirect=...` +- `400` `{"name": "OidcCallbackInvalid", "message": "Missing code or state."}` or expired/unknown state + +--- + +## API Tokens + +Self-service personal access tokens (PATs) for scripting/CI without a browser +session. Every endpoint is owner-scoped: a user only sees/manages tokens they +created. Mounted at `/api/api-token`. + +### List API Tokens + +**GET** `/api/api-token` + +List the current user's API tokens. + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/api-token +``` + +**Responses:** +- `200` `{"results": [{"id": "...", "name": "ci", ...}, ...]}` + +### Create API Token + +**POST** `/api/api-token` + +Create a new API token. The raw token string is only returned once, at +creation. + +**Parameters:** +- `name` (required) - Display name +- `description` (optional) +- `expires_in_days` (optional) - `0` or omitted means no expiry + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"name": "ci", "expires_in_days": 90}' \ + https://proxy-host.com/api/api-token +``` + +**Responses:** +- `200` `{"results": {...}, "token": "prx__", "message": "API token 'ci' created. Save it now — it will not be shown again."}` + +### Get API Token + +**GET** `/api/api-token/:id` + +Get a token's metadata (not the raw secret, which is never stored/returned again). + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/api-token/ +``` + +**Responses:** +- `200` `{"results": {...}}` +- `403` Not your token + +### Update API Token + +**PUT** `/api/api-token/:id` + +Update a token's name/description/expiry. + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X PUT \ + -d '{"name": "ci-updated"}' \ + https://proxy-host.com/api/api-token/ +``` + +**Responses:** +- `200` `{"results": {...}, "message": "API token 'ci-updated' updated."}` + +### Delete (Revoke) API Token + +**DELETE** `/api/api-token/:id` + +Revoke a token immediately. + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/api-token/ +``` + +**Responses:** +- `200` `{"id": "", "message": "API token 'ci' revoked."}` + +### Rotate API Token + +**POST** `/api/api-token/:id/rotate` + +Issue a new secret for an existing token (same id, new raw value shown once). + +```bash +curl -H "auth-token: your-token-here" \ + -X POST \ + https://proxy-host.com/api/api-token//rotate +``` + +**Responses:** +- `200` `{"token": "prx__", "message": "API token 'ci' rotated. Save it — it will not be shown again."}` + --- ## Users -All user endpoints require authentication. +All user endpoints require authentication. `GET /me` and `PUT /password` +(self-service) work for any authenticated user; everything else (listing, +creating, deleting users, resetting another user's password) requires global +admin. ### List Users **GET** `/api/user` -Get list of all users. +Get list of all users. Admin only. ```bash curl -H "auth-token: your-token-here" \ @@ -70,12 +239,14 @@ curl -H "auth-token: your-token-here" \ **Responses:** - `200` `{"results": ["user1", "user2"]}` - `200` `{"results": [{"username": "user1", ...}, ...]}` (with `?detail=true`) +- `403` Not an admin ### Get Current User **GET** `/api/user/me` -Get information about the currently authenticated user. +Get the currently authenticated user's identity and effective RBAC rights +(drives the web UI's nav/button gating). ```bash curl -H "auth-token: your-token-here" \ @@ -83,13 +254,13 @@ curl -H "auth-token: your-token-here" \ ``` **Responses:** -- `200` `{"username": "myuser"}` +- `200` `{"username": "myuser", "groups": [...], "localGroups": [...], "externalGroups": [...], "isAdmin": false, "global": null, "domains": {...}}` ### Create User **POST** `/api/user` -Create a new user. +Create a new local user. Admin only. ```bash curl -H "Content-Type: application/json" \ @@ -101,14 +272,15 @@ curl -H "Content-Type: application/json" \ **Responses:** - `200` User created successfully +- `403` Not an admin - `409` Username already exists -- `422` `{"name": "ObjectValidateError", "message": ...}` Validation error +- `422` `{"name": "ObjectValidateError", "message": ...}` Validation error (also returned for weak passwords) ### Delete User **DELETE** `/api/user/:username` -Delete a user account. +Delete a user account. Admin only. ```bash curl -H "auth-token: your-token-here" \ @@ -118,6 +290,7 @@ curl -H "auth-token: your-token-here" \ **Responses:** - `200` `{"username": "olduser", "results": ...}` +- `403` Not an admin - `404` User not found ### Change Password (Self) @@ -136,12 +309,13 @@ curl -H "Content-Type: application/json" \ **Responses:** - `200` `{"results": ...}` Password changed successfully +- `422` Weak password rejected by the password policy ### Change Password (Other User) **PUT** `/api/user/password/:username` -Change the password for another user (admin function). +Change the password for another user. Admin only. ```bash curl -H "Content-Type: application/json" \ @@ -153,40 +327,163 @@ curl -H "Content-Type: application/json" \ **Responses:** - `200` `{"results": ...}` Password changed successfully +- `403` Not an admin - `404` User not found -### Create Invite Token +--- -**POST** `/api/user/invite` +## Permissions -Create an invitation token for new user registration. +RBAC: grants a `viewer` or `manager` role to a user or group, either globally +or scoped to one domain. Global-admin-only. Mounted at `/api/permission`. + +### List Permissions + +**GET** `/api/permission` ```bash curl -H "auth-token: your-token-here" \ - -X POST \ - https://proxy-host.com/api/user/invite + https://proxy-host.com/api/permission ``` **Responses:** -- `200` `{"token": "5caf94d2-2c91-4010-8df7-968d10802b9d"}` +- `200` `{"results": [{"id": "...", "subjectType": "user", "subject": "alice", "role": "manager", "scope": "domain", "domain": "example.com", ...}, ...]}` -### Add SSH Key +### List Permission Subjects -**POST** `/api/user/key` +**GET** `/api/permission/subjects` -Add an SSH public key to the current user's account. +Autocomplete source for the "Subject" field: known usernames plus known group +names (local groups, groups already used in permissions, and groups from +`conf.auth.adminGroups` / `conf.auth.groupRoleMap`). + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/permission/subjects +``` + +**Responses:** +- `200` `{"users": ["alice", "bob"], "groups": ["ops", "sre"]}` + +### Create Permission + +**POST** `/api/permission` + +Grant a role to a subject. + +**Parameters:** +- `subjectType` (required) - `user` or `group` +- `subject` (required) - username or group name +- `role` (required) - `viewer` or `manager` +- `scope` (required) - `global` or `domain` +- `domain` (required if `scope` is `domain`) ```bash curl -H "Content-Type: application/json" \ -H "auth-token: your-token-here" \ -X POST \ - -d '{"key": "ssh-rsa AAAAB3..."}' \ - https://proxy-host.com/api/user/key + -d '{"subjectType": "user", "subject": "alice", "role": "manager", "scope": "domain", "domain": "example.com"}' \ + https://proxy-host.com/api/permission ``` **Responses:** -- `200` `{"message": true}` Key added successfully -- `400` `{"message": "Bad SSH key"}` Invalid key format +- `200` `{"message": "Granted manager to user \"alice\" on example.com.", ...}` +- `422` Validation error + +### Delete Permission + +**DELETE** `/api/permission/:id` + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/permission/ +``` + +**Responses:** +- `200` `{"message": "Permission removed."}` + +--- + +## Groups + +Local groups (independent of any SSO/LDAP groups) used as subjects for +permission grants. Global-admin-only. Mounted at `/api/group`. + +### List Groups + +**GET** `/api/group` + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/group +``` + +**Responses:** +- `200` `{"results": [{"name": "ops", "members": ["alice", "bob"], ...}, ...]}` + +### Create Group + +**POST** `/api/group` + +**Parameters:** +- `name` (required) +- `members` (optional) - array of usernames + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"name": "ops", "members": ["alice"]}' \ + https://proxy-host.com/api/group +``` + +**Responses:** +- `200` `{"message": "Group \"ops\" created.", ...}` + +### Delete Group + +**DELETE** `/api/group/:name` + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/group/ops +``` + +**Responses:** +- `200` `{"message": "Group \"ops\" removed."}` + +### Add Group Member + +**POST** `/api/group/:name/members` + +**Parameters:** +- `username` (required) + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"username": "bob"}' \ + https://proxy-host.com/api/group/ops/members +``` + +**Responses:** +- `200` `{"message": "Added \"bob\" to \"ops\".", ...}` + +### Remove Group Member + +**DELETE** `/api/group/:name/members/:username` + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/group/ops/members/bob +``` + +**Responses:** +- `200` `{"message": "Removed \"bob\" from \"ops\".", ...}` --- @@ -320,6 +617,24 @@ curl -H "auth-token: your-token-here" \ - `200` `{"message": "example.com deleted", ...}` - `404` `{"name": "HostNotFound", "message": "Host does not exists"}` +### Clear Host Cache + +**DELETE** `/api/host/cache` + +Remove all cached wildcard-subdomain host lookups. Cache entries are created on +demand when a wildcard host serves a subdomain; clearing them forces the next +request for each subdomain to be resolved fresh through the lookup tree. +Admin only. + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/host/cache +``` + +**Responses:** +- `200` `{"message": "Cleared 3 cached hosts.", "count": 3}` + ### Renew Wildcard Certificate **PUT** `/api/host/:host/renew` @@ -372,7 +687,7 @@ curl -H "auth-token: your-token-here" \ ``` **Responses:** -- `200` `{"results": [{"name": "CloudFlare", "fields": {...}}, {"name": "DigitalOcean", ...}, ...]}` +- `200` `{"results": [{"name": "Cloudflare", "fields": {...}}, {"name": "DigitalOcean", ...}, ...]}` ### Create DNS Provider @@ -380,12 +695,12 @@ curl -H "auth-token: your-token-here" \ Configure a new DNS provider. -**CloudFlare:** +**Cloudflare:** ```bash curl -H "Content-Type: application/json" \ -H "auth-token: your-token-here" \ -X POST \ - -d '{"name": "My CloudFlare", "dnsProvider": "Cloudflare", "token": "your-api-token"}' \ + -d '{"name": "My Cloudflare", "dnsProvider": "Cloudflare", "token": "your-api-token"}' \ https://proxy-host.com/api/dns ``` @@ -508,6 +823,97 @@ curl -H "auth-token: your-token-here" \ - `200` `{"results": ...}` Updated domain list - `404` Provider not found +### Dynamic DNS + +A-records kept automatically pointed at this box's public (WAN) IP. All +`/api/dns/dynamic*` routes are viewer/manager scoped to the record's domain +(via [Permissions](#permissions)), not admin-only like the rest of `/api/dns`. + +#### Get Current Public IP + +**GET** `/api/dns/dynamic/ip` + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/dns/dynamic/ip +``` + +**Responses:** +- `200` `{"ip": "203.0.113.5"}` + +#### List Dynamic Records + +**GET** `/api/dns/dynamic` + +Lists records the caller may view (their own/granted domains, or all for admins). + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/dns/dynamic +``` + +**Responses:** +- `200` `{"results": [{"id": "...", "domain": "example.com", "name": "home", "last_status": "ok", ...}, ...]}` + +#### Create Dynamic Record + +**POST** `/api/dns/dynamic` + +Requires `manager` rights on the target domain. Applies the record immediately +against the current public IP (best-effort — failures are recorded in +`last_status` and retried by the scheduler). + +**Parameters:** +- `domain` (required) +- `name` (required) - sub-label, or `@` for the apex + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"domain": "example.com", "name": "home"}' \ + https://proxy-host.com/api/dns/dynamic +``` + +**Responses:** +- `200` `{"message": "\"home.example.com\" added.", ...}` +- `403` Missing `manager` rights on the domain +- `422` Validation error + +#### Refresh Dynamic Record + +**POST** `/api/dns/dynamic/:id/refresh` + +Force an immediate refresh of one record against the current public IP. +Requires `manager` rights on the record's domain. + +```bash +curl -H "auth-token: your-token-here" \ + -X POST \ + https://proxy-host.com/api/dns/dynamic//refresh +``` + +**Responses:** +- `200` `{"message": "Refreshed \"home.example.com\".", "result": {...}}` +- `403` Missing `manager` rights on the domain + +#### Delete Dynamic Record + +**DELETE** `/api/dns/dynamic/:id` + +Stop managing a record. Requires `manager` rights on the record's domain. +Leaves the provider's A record in place at its last value. + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/dns/dynamic/ +``` + +**Responses:** +- `200` `{"message": "home.example.com removed.", ...}` +- `403` Missing `manager` rights on the domain + --- ## Certificates @@ -543,7 +949,8 @@ All endpoints may return the following error responses: ## Notes - All timestamps are in milliseconds since epoch -- The `auth-token` header is required for all authenticated endpoints +- Authenticated endpoints accept either the `auth-token` header (browser + session / OIDC login) or an `Authorization: Bearer ` API token - Host names support wildcards: `*` (single level) and `**` (multi-level) - DNS providers are validated on creation - invalid API credentials will be rejected - Wildcard certificates are automatically renewed 30 days before expiration diff --git a/docs/architecture.md b/docs/architecture.md index b9f7d15..b848aac 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -42,7 +42,7 @@ The proxy system consists of three main components working together to provide h ▼ ┌──────────────────────┐ │ DNS Providers │ - │ - CloudFlare │ + │ - Cloudflare │ │ - DigitalOcean │ │ - PorkBun │ │ (DNS-01 challenges) │ @@ -84,6 +84,9 @@ The proxy system consists of three main components working together to provide h ``` 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 @@ -93,7 +96,10 @@ nodejs/ │ ├── host.js # Host CRUD operations │ ├── dns.js # DNS provider management │ ├── user.js # User management -│ └── auth.js # Authentication +│ ├── 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 diff --git a/docs/contributing.md b/docs/contributing.md index dad2b1c..6cc13c2 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -71,9 +71,19 @@ npm run test:watch ``` test/ ├── unit/ # Unit tests for isolated components +│ ├── basicauth.test.js │ ├── callback_queue.test.js +│ ├── dynamic_record.test.js +│ ├── host_features.test.js │ ├── host_lookup.test.js -│ └── unix_socket.test.js +│ ├── hostname_validate.test.js +│ ├── host_sso.test.js +│ ├── oidc.test.js +│ ├── password_policy.test.js +│ ├── roles.test.js +│ ├── safe_redirect.test.js +│ ├── unix_socket.test.js +│ └── wildcard_matchany.test.js ├── integration/ # Integration tests │ └── dns_provider.test.js └── helpers/ # Test utilities @@ -171,6 +181,9 @@ Understanding the codebase: ``` nodejs/ +├── 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 route handlers ├── services/ # Background services (lookup, scheduler) @@ -232,12 +245,12 @@ nodejs/ ### PR Requirements -- ✅ All tests must pass (CI/CD runs automatically) -- ✅ Tests run on Node.js 18.x, 20.x, and 22.x -- ✅ No merge conflicts with `master` -- ✅ Code follows project conventions -- ✅ New features include tests -- ✅ Documentation updated if needed +- All tests must pass (CI/CD runs automatically) +- Tests run on Node.js 18.x, 20.x, and 22.x +- No merge conflicts with `master` +- Code follows project conventions +- New features include tests +- Documentation updated if needed ### CI/CD Process @@ -305,7 +318,7 @@ class Host extends Table { ### Adding API Endpoints 1. **Add route** in appropriate file (`routes/`) -2. **Update API documentation** (`nodejs/api.md`) +2. **Update API documentation** (`nodejs/api.md` and `docs/api.md` — keep them in sync) 3. **Test the endpoint** manually and add integration tests if needed ## Getting Help diff --git a/docs/dev_setup.md b/docs/dev_setup.md deleted file mode 100644 index db05281..0000000 --- a/docs/dev_setup.md +++ /dev/null @@ -1,100 +0,0 @@ -# Development environment setup - -This project used vagrant for as standard development environment. This should -easy setting things up and reduce environment related errors. - -## What you need - -There are 3 things you will need to get before your local environment is up and -running. - -### Git - -This should already be installed on your system. If you are using Windows, -install [git bash for windows](https://git-scm.com/download/win) - -### VirtualBox - -Virtual box will be used to create a manged Linux VM on your computer. Please -install version 5.9 as version 6 is not supported. - -### Vagrant - -Vagrant is used to managed the local virtual environment and provision the VM. -**MAKE SURE GIT VIRTUAL BOX ARE INSTALLED FIRST!!!** At install time, vagrant -will integrate with them. - -## Usage - -Once you have everything installed and the projected cloned on your local -computer, open a terminal( Git Bash for windows users ) and move to the root of -the project. - -### Chef secrets - -This project like many other used secret API tokens that we do want tracked in -the git repo. in the root of the project, create a file called 'secrets.json' -and populate like so: - -```json -{ - "django": { - "github": { - "id": "", - "secret": "", - "token": "" - } - } -} - -``` - -### Basic vagrant usage and commands - -We will interact with the project using vagrant. The work flow is `vagrant up` -creates a VM for your project and runs the provisioner to set everything up. -This command should be ran after the project is cloned or when you sit down to -start development. This command may take some time to complete depending on how -complex the project is. Once the VM is set up, you may now interact with it. -Vagrant will forward ports from the project to the user localhost address. For -example with this project, port 80 from the container will be mapped to -localhost:8000 and you will be able to access there. If you make a change to any -provisioning chef recipes, `vagrant provision` will need to ran. This will -run the chef provisioner on the VM making any changes needed. - -Because we use the `secrets.json` file to store untracked configuration, all -vagrant command need to be ran where in the root of the project, where -`secrets.json` lives. - -### `vagrant up` - -Will start the local VM, creating it if needed. This command should always be -ran when a development session is started. - -### `vagrant provision` - -Will run the chef-solo provisioner. This will need to be ran anytime the chef -recipes or roles are changed. - -### `vagrant status` - -Will show you the status of local manged VM - -### `vagrant halt` - -Will shutdown the local VM. This should be done when you are finished working on -the project so you dont have a VM running in the background eating CPU/RAM and -battery. `vagrant up` can be used later to turn the VM back on. - -### `vagrant destroy` - -This will shutdown the VM and delete it. This command is useful if you have -messed up the VM and want to start from scratch. Or if you are done with the -project and want to free space from the computer. - -### `vagrant ssh` - -This will bring you into the local VM as the vagrant user. The vagrant user has -sudo. Use this only for debugging! **DO NOT INSTALL OR CHANGE THE STATE OF -PROJECT OR VM FROM HERE!!!!** that will break the concept of provisioning make -chef useless. Also make installation and configuration changes with chef. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 77cfa78..b2cab3a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ A reverse proxy and HTTPS termination service using OpenResty/nginx with a manag - **Automated HTTPS/SSL** - Let's Encrypt integration with HTTP-01 and DNS-01 challenges - **Wildcard SSL Certificates** - Support for wildcard domains with automatic renewal -- **Multiple DNS Providers** - CloudFlare, DigitalOcean, PorkBun integrations +- **Multiple DNS Providers** - Cloudflare, DigitalOcean, PorkBun integrations - **Advanced Routing** - Sophisticated wildcard domain matching (*, **) - **RESTful API** - Full programmatic control - **Web Interface** - User-friendly management GUI diff --git a/docs/installation.md b/docs/installation.md index 754bd62..0f4435c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -16,7 +16,7 @@ wget -O - https://raw.githubusercontent.com/theta42/proxy/master/ops/install.sh ``` This automated installer will: -- Install Node.js 20.x +- Install Node.js 22.x - Install OpenResty and required dependencies - Install and configure Redis - Set up SSL fallback certificates @@ -27,6 +27,15 @@ This automated installer will: ## Manual Installation +> **Recommended path:** `ops/install.sh` (above) is idempotent and safe to +> re-run — it symlinks the OpenResty/systemd config from the repo checkout, +> so future updates stay in sync automatically (`git pull` + re-run). The +> manual steps below *copy* those same files instead of symlinking them, so +> they will **not** auto-track later changes to `ops/nginx_conf/` or +> `ops/proxy.service` — you'd need to re-copy them by hand after every +> update. Prefer the manual path only if `install.sh` doesn't fit your +> distribution. + ### System Requirements - Modern Linux distribution (Ubuntu 20.04+, Debian 11+, or equivalent) @@ -41,13 +50,13 @@ This automated installer will: apt install libpam0g-dev build-essential redis-server luarocks -y ``` -### Step 2: Install Node.js 20.x +### Step 2: Install 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=20 +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 @@ -56,7 +65,7 @@ apt update && apt install nodejs -y Verify installation: ```bash -node --version # Should show v20.x.x +node --version # Should show v22.x.x npm --version ``` diff --git a/nodejs/api.md b/nodejs/api.md index faa69e7..113826d 100755 --- a/nodejs/api.md +++ b/nodejs/api.md @@ -1,6 +1,25 @@ # API Documentation -All API endpoints require authentication via the `auth-token` header unless otherwise noted. +All API endpoints require authentication unless otherwise noted. Three +authentication methods are supported: + +- **`auth-token` header** — a browser-session token from `POST /api/auth/login` + or the OIDC flow (below). +- **`Authorization: Bearer ` header** — a self-service API token (PAT, + see [API Tokens](#api-tokens)), for scripts/CI without a browser session. +- **OIDC (browser)** — if the proxy is configured as an OIDC client of an SSO + (`app_oidc__*` / `conf.oidc`, see [DEPLOYMENT.md](../DEPLOYMENT.md)), + users can log in via `GET /api/auth/oidc/start` instead of posting a + username/password. + +The proxy can also be configured as a **direct LDAP client** (`app_ldap__*` / +`conf.ldap`) for looking up/validating users, independent of the OIDC flow — +see DEPLOYMENT.md for the full configuration reference. + +Authenticated requests also carry **RBAC** (role-based access control): +global admins can manage everything; other users are scoped to `viewer` or +`manager` rights on specific domains via [Permissions](#permissions) and +[Groups](#groups). Base URL: `https://your-proxy-host.com/api` @@ -40,17 +59,167 @@ curl -H "auth-token: your-token-here" \ **Responses:** - `200` `{"message": "Bye"}` +### OIDC Login (start) + +**GET** `/api/auth/oidc/start` + +Begin the OIDC authorization-code flow: creates a PKCE + state challenge and +redirects the browser to the configured SSO's authorize endpoint. Only +available when `conf.oidc.enabled` is true. + +**Query Parameters:** +- `redirect` - Internal path to return to after login (optional; sanitized to same-origin) + +```bash +curl -i "https://proxy-host.com/api/auth/oidc/start?redirect=/hosts" +``` + +**Responses:** +- `302` Redirect to the SSO's authorization endpoint +- `404` `{"name": "OidcDisabled", "message": "OIDC login is not enabled."}` + +### OIDC Callback + +**GET** `/api/auth/oidc/callback` + +Redirect target for the SSO after login. Validates the one-time `state`, +exchanges the authorization `code` for tokens, reads identity from the +userinfo endpoint, establishes a session, and redirects the browser back to +the login page with the app's own `auth-token` in a URL fragment. + +**Query Parameters:** +- `code` (required) - Authorization code from the SSO +- `state` (required) - State value from the `start` step + +```bash +# Not called directly — the SSO redirects the browser here after login. +``` + +**Responses:** +- `302` Redirect to `/login#token=...&redirect=...` +- `400` `{"name": "OidcCallbackInvalid", "message": "Missing code or state."}` or expired/unknown state + +--- + +## API Tokens + +Self-service personal access tokens (PATs) for scripting/CI without a browser +session. Every endpoint is owner-scoped: a user only sees/manages tokens they +created. Mounted at `/api/api-token`. + +### List API Tokens + +**GET** `/api/api-token` + +List the current user's API tokens. + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/api-token +``` + +**Responses:** +- `200` `{"results": [{"id": "...", "name": "ci", ...}, ...]}` + +### Create API Token + +**POST** `/api/api-token` + +Create a new API token. The raw token string is only returned once, at +creation. + +**Parameters:** +- `name` (required) - Display name +- `description` (optional) +- `expires_in_days` (optional) - `0` or omitted means no expiry + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"name": "ci", "expires_in_days": 90}' \ + https://proxy-host.com/api/api-token +``` + +**Responses:** +- `200` `{"results": {...}, "token": "prx__", "message": "API token 'ci' created. Save it now — it will not be shown again."}` + +### Get API Token + +**GET** `/api/api-token/:id` + +Get a token's metadata (not the raw secret, which is never stored/returned again). + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/api-token/ +``` + +**Responses:** +- `200` `{"results": {...}}` +- `403` Not your token + +### Update API Token + +**PUT** `/api/api-token/:id` + +Update a token's name/description/expiry. + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X PUT \ + -d '{"name": "ci-updated"}' \ + https://proxy-host.com/api/api-token/ +``` + +**Responses:** +- `200` `{"results": {...}, "message": "API token 'ci-updated' updated."}` + +### Delete (Revoke) API Token + +**DELETE** `/api/api-token/:id` + +Revoke a token immediately. + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/api-token/ +``` + +**Responses:** +- `200` `{"id": "", "message": "API token 'ci' revoked."}` + +### Rotate API Token + +**POST** `/api/api-token/:id/rotate` + +Issue a new secret for an existing token (same id, new raw value shown once). + +```bash +curl -H "auth-token: your-token-here" \ + -X POST \ + https://proxy-host.com/api/api-token//rotate +``` + +**Responses:** +- `200` `{"token": "prx__", "message": "API token 'ci' rotated. Save it — it will not be shown again."}` + --- ## Users -All user endpoints require authentication. +All user endpoints require authentication. `GET /me` and `PUT /password` +(self-service) work for any authenticated user; everything else (listing, +creating, deleting users, resetting another user's password) requires global +admin. ### List Users **GET** `/api/user` -Get list of all users. +Get list of all users. Admin only. ```bash curl -H "auth-token: your-token-here" \ @@ -63,12 +232,14 @@ curl -H "auth-token: your-token-here" \ **Responses:** - `200` `{"results": ["user1", "user2"]}` - `200` `{"results": [{"username": "user1", ...}, ...]}` (with `?detail=true`) +- `403` Not an admin ### Get Current User **GET** `/api/user/me` -Get information about the currently authenticated user. +Get the currently authenticated user's identity and effective RBAC rights +(drives the web UI's nav/button gating). ```bash curl -H "auth-token: your-token-here" \ @@ -76,13 +247,13 @@ curl -H "auth-token: your-token-here" \ ``` **Responses:** -- `200` `{"username": "myuser"}` +- `200` `{"username": "myuser", "groups": [...], "localGroups": [...], "externalGroups": [...], "isAdmin": false, "global": null, "domains": {...}}` ### Create User **POST** `/api/user` -Create a new user. +Create a new local user. Admin only. ```bash curl -H "Content-Type: application/json" \ @@ -94,14 +265,15 @@ curl -H "Content-Type: application/json" \ **Responses:** - `200` User created successfully +- `403` Not an admin - `409` Username already exists -- `422` `{"name": "ObjectValidateError", "message": ...}` Validation error +- `422` `{"name": "ObjectValidateError", "message": ...}` Validation error (also returned for weak passwords) ### Delete User **DELETE** `/api/user/:username` -Delete a user account. +Delete a user account. Admin only. ```bash curl -H "auth-token: your-token-here" \ @@ -111,6 +283,7 @@ curl -H "auth-token: your-token-here" \ **Responses:** - `200` `{"username": "olduser", "results": ...}` +- `403` Not an admin - `404` User not found ### Change Password (Self) @@ -129,12 +302,13 @@ curl -H "Content-Type: application/json" \ **Responses:** - `200` `{"results": ...}` Password changed successfully +- `422` Weak password rejected by the password policy ### Change Password (Other User) **PUT** `/api/user/password/:username` -Change the password for another user (admin function). +Change the password for another user. Admin only. ```bash curl -H "Content-Type: application/json" \ @@ -146,40 +320,163 @@ curl -H "Content-Type: application/json" \ **Responses:** - `200` `{"results": ...}` Password changed successfully +- `403` Not an admin - `404` User not found -### Create Invite Token +--- -**POST** `/api/user/invite` +## Permissions -Create an invitation token for new user registration. +RBAC: grants a `viewer` or `manager` role to a user or group, either globally +or scoped to one domain. Global-admin-only. Mounted at `/api/permission`. + +### List Permissions + +**GET** `/api/permission` ```bash curl -H "auth-token: your-token-here" \ - -X POST \ - https://proxy-host.com/api/user/invite + https://proxy-host.com/api/permission ``` **Responses:** -- `200` `{"token": "5caf94d2-2c91-4010-8df7-968d10802b9d"}` +- `200` `{"results": [{"id": "...", "subjectType": "user", "subject": "alice", "role": "manager", "scope": "domain", "domain": "example.com", ...}, ...]}` -### Add SSH Key +### List Permission Subjects -**POST** `/api/user/key` +**GET** `/api/permission/subjects` -Add an SSH public key to the current user's account. +Autocomplete source for the "Subject" field: known usernames plus known group +names (local groups, groups already used in permissions, and groups from +`conf.auth.adminGroups` / `conf.auth.groupRoleMap`). + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/permission/subjects +``` + +**Responses:** +- `200` `{"users": ["alice", "bob"], "groups": ["ops", "sre"]}` + +### Create Permission + +**POST** `/api/permission` + +Grant a role to a subject. + +**Parameters:** +- `subjectType` (required) - `user` or `group` +- `subject` (required) - username or group name +- `role` (required) - `viewer` or `manager` +- `scope` (required) - `global` or `domain` +- `domain` (required if `scope` is `domain`) ```bash curl -H "Content-Type: application/json" \ -H "auth-token: your-token-here" \ -X POST \ - -d '{"key": "ssh-rsa AAAAB3..."}' \ - https://proxy-host.com/api/user/key + -d '{"subjectType": "user", "subject": "alice", "role": "manager", "scope": "domain", "domain": "example.com"}' \ + https://proxy-host.com/api/permission ``` **Responses:** -- `200` `{"message": true}` Key added successfully -- `400` `{"message": "Bad SSH key"}` Invalid key format +- `200` `{"message": "Granted manager to user \"alice\" on example.com.", ...}` +- `422` Validation error + +### Delete Permission + +**DELETE** `/api/permission/:id` + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/permission/ +``` + +**Responses:** +- `200` `{"message": "Permission removed."}` + +--- + +## Groups + +Local groups (independent of any SSO/LDAP groups) used as subjects for +permission grants. Global-admin-only. Mounted at `/api/group`. + +### List Groups + +**GET** `/api/group` + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/group +``` + +**Responses:** +- `200` `{"results": [{"name": "ops", "members": ["alice", "bob"], ...}, ...]}` + +### Create Group + +**POST** `/api/group` + +**Parameters:** +- `name` (required) +- `members` (optional) - array of usernames + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"name": "ops", "members": ["alice"]}' \ + https://proxy-host.com/api/group +``` + +**Responses:** +- `200` `{"message": "Group \"ops\" created.", ...}` + +### Delete Group + +**DELETE** `/api/group/:name` + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/group/ops +``` + +**Responses:** +- `200` `{"message": "Group \"ops\" removed."}` + +### Add Group Member + +**POST** `/api/group/:name/members` + +**Parameters:** +- `username` (required) + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"username": "bob"}' \ + https://proxy-host.com/api/group/ops/members +``` + +**Responses:** +- `200` `{"message": "Added \"bob\" to \"ops\".", ...}` + +### Remove Group Member + +**DELETE** `/api/group/:name/members/:username` + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/group/ops/members/bob +``` + +**Responses:** +- `200` `{"message": "Removed \"bob\" from \"ops\".", ...}` --- @@ -320,6 +617,7 @@ curl -H "auth-token: your-token-here" \ Remove all cached wildcard-subdomain host lookups. Cache entries are created on demand when a wildcard host serves a subdomain; clearing them forces the next request for each subdomain to be resolved fresh through the lookup tree. +Admin only. ```bash curl -H "auth-token: your-token-here" \ @@ -382,7 +680,7 @@ curl -H "auth-token: your-token-here" \ ``` **Responses:** -- `200` `{"results": [{"name": "CloudFlare", "fields": {...}}, {"name": "DigitalOcean", ...}, ...]}` +- `200` `{"results": [{"name": "Cloudflare", "fields": {...}}, {"name": "DigitalOcean", ...}, ...]}` ### Create DNS Provider @@ -390,12 +688,12 @@ curl -H "auth-token: your-token-here" \ Configure a new DNS provider. -**CloudFlare:** +**Cloudflare:** ```bash curl -H "Content-Type: application/json" \ -H "auth-token: your-token-here" \ -X POST \ - -d '{"name": "My CloudFlare", "dnsProvider": "Cloudflare", "token": "your-api-token"}' \ + -d '{"name": "My Cloudflare", "dnsProvider": "Cloudflare", "token": "your-api-token"}' \ https://proxy-host.com/api/dns ``` @@ -518,6 +816,97 @@ curl -H "auth-token: your-token-here" \ - `200` `{"results": ...}` Updated domain list - `404` Provider not found +### Dynamic DNS + +A-records kept automatically pointed at this box's public (WAN) IP. All +`/api/dns/dynamic*` routes are viewer/manager scoped to the record's domain +(via [Permissions](#permissions)), not admin-only like the rest of `/api/dns`. + +#### Get Current Public IP + +**GET** `/api/dns/dynamic/ip` + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/dns/dynamic/ip +``` + +**Responses:** +- `200` `{"ip": "203.0.113.5"}` + +#### List Dynamic Records + +**GET** `/api/dns/dynamic` + +Lists records the caller may view (their own/granted domains, or all for admins). + +```bash +curl -H "auth-token: your-token-here" \ + https://proxy-host.com/api/dns/dynamic +``` + +**Responses:** +- `200` `{"results": [{"id": "...", "domain": "example.com", "name": "home", "last_status": "ok", ...}, ...]}` + +#### Create Dynamic Record + +**POST** `/api/dns/dynamic` + +Requires `manager` rights on the target domain. Applies the record immediately +against the current public IP (best-effort — failures are recorded in +`last_status` and retried by the scheduler). + +**Parameters:** +- `domain` (required) +- `name` (required) - sub-label, or `@` for the apex + +```bash +curl -H "Content-Type: application/json" \ + -H "auth-token: your-token-here" \ + -X POST \ + -d '{"domain": "example.com", "name": "home"}' \ + https://proxy-host.com/api/dns/dynamic +``` + +**Responses:** +- `200` `{"message": "\"home.example.com\" added.", ...}` +- `403` Missing `manager` rights on the domain +- `422` Validation error + +#### Refresh Dynamic Record + +**POST** `/api/dns/dynamic/:id/refresh` + +Force an immediate refresh of one record against the current public IP. +Requires `manager` rights on the record's domain. + +```bash +curl -H "auth-token: your-token-here" \ + -X POST \ + https://proxy-host.com/api/dns/dynamic//refresh +``` + +**Responses:** +- `200` `{"message": "Refreshed \"home.example.com\".", "result": {...}}` +- `403` Missing `manager` rights on the domain + +#### Delete Dynamic Record + +**DELETE** `/api/dns/dynamic/:id` + +Stop managing a record. Requires `manager` rights on the record's domain. +Leaves the provider's A record in place at its last value. + +```bash +curl -H "auth-token: your-token-here" \ + -X DELETE \ + https://proxy-host.com/api/dns/dynamic/ +``` + +**Responses:** +- `200` `{"message": "home.example.com removed.", ...}` +- `403` Missing `manager` rights on the domain + --- ## Certificates @@ -553,7 +942,8 @@ All endpoints may return the following error responses: ## Notes - All timestamps are in milliseconds since epoch -- The `auth-token` header is required for all authenticated endpoints +- Authenticated endpoints accept either the `auth-token` header (browser + session / OIDC login) or an `Authorization: Bearer ` API token - Host names support wildcards: `*` (single level) and `**` (multi-level) - DNS providers are validated on creation - invalid API credentials will be rejected - Wildcard certificates are automatically renewed 30 days before expiration diff --git a/nodejs/test/README.md b/nodejs/test/README.md index 90aa81c..5f379b5 100644 --- a/nodejs/test/README.md +++ b/nodejs/test/README.md @@ -23,9 +23,19 @@ npm run test:watch ``` test/ ├── unit/ # Unit tests for isolated components +│ ├── basicauth.test.js │ ├── callback_queue.test.js +│ ├── dynamic_record.test.js +│ ├── host_features.test.js │ ├── host_lookup.test.js -│ └── unix_socket.test.js +│ ├── hostname_validate.test.js +│ ├── host_sso.test.js +│ ├── oidc.test.js +│ ├── password_policy.test.js +│ ├── roles.test.js +│ ├── safe_redirect.test.js +│ ├── unix_socket.test.js +│ └── wildcard_matchany.test.js ├── integration/ # Integration tests for complex interactions │ └── dns_provider.test.js └── helpers/ # Test utilities and contracts @@ -58,7 +68,7 @@ test/ **dns_provider.test.js** - DNS provider contract compliance -- All existing providers (CloudFlare, DigitalOcean, PorkBun) +- All existing providers (Cloudflare, DigitalOcean, PorkBun) - Method signatures - Key mapping - Type validation diff --git a/openresty/README.md b/openresty/README.md index a052576..69882d9 100644 --- a/openresty/README.md +++ b/openresty/README.md @@ -1,9 +1,10 @@ -The folder will hold a link to the openresty conf file. Nothing in here will be -tracked, and edits are only valid on you local VM. The contents of this folder -is also reset during each provision run. Be mind full of that when making -changes here. +This directory is currently unused. -To apply any changes made in this folder, run the following command from outside -the VM: +The real OpenResty/nginx configuration for this project lives in +[`ops/nginx_conf/`](../ops/nginx_conf/) (`nginx.conf`, `autossl.conf`, +`proxy.conf`, `targetinfo.lua`, `hostfeatures.lua`). `ops/install.sh` symlinks +those files into place on a bare-metal host, and the Docker image copies them +in at build time — see [`DEPLOYMENT.md`](../DEPLOYMENT.md) for details. -`vagrant ssh -c "sudo service openresty restart"` +If you're looking for how to change the nginx/Lua configuration, edit the +files under `ops/nginx_conf/` instead of this directory.