# CyberPanel nginx for Plesk

**Version**: 1.0.0  
**Release date**: 2026-05-12  
**Extension ID**: `cyberpanel-nginx`  
**Public installer**: `https://cyberpanel.net/nginx-plesk-install.sh`

CyberPanel nginx for Plesk is a Plesk extension and nginx replacement layer that
lets a Plesk server run `nginx-cyberpanel` on public ports `80` and `443` while
still using Plesk's existing Apache configuration, vhosts, certificates, PHP
handlers, and WordPress cache signals.

The installer is intentionally safe by default: it installs the extension and
stages the correct nginx binary, but it does not enable takeover until the
admin saves a valid platform license. The License tab validates the key with
the platform and enables takeover immediately when the license is active.

## One-Line Install

Run as `root` on a Plesk node:

```bash
bash <(curl -sSL https://cyberpanel.net/nginx-plesk-install.sh)
```

Then open Plesk:

```text
Extensions > CyberPanel nginx > License
```

Buy or claim a license at `https://cyberpanel.net/cyberpanel-nginx-for-plesk/`,
paste the key, and click `Save`. A valid key is checked immediately and enables
takeover.

Uninstall and restore Plesk defaults:

```bash
bash <(curl -sSL https://cyberpanel.net/nginx-plesk-install.sh) --uninstall
```

## Supported Platforms

The installer supports `x86_64` Plesk nodes on:

| Platform | Binary URL |
|---|---|
| Ubuntu 22 / Ubuntu 24 / Debian 12 | `https://cyberpanel.net/nginx-cyberpanel-1.0.0-x86_64-ubuntu` |
| RHEL 9 / AlmaLinux 9 / Rocky 9 | `https://cyberpanel.net/nginx-cyberpanel-1.0.0-x86_64-rhel9` |
| RHEL 8 / AlmaLinux 8 / Rocky 8 | `https://cyberpanel.net/nginx-cyberpanel-1.0.0-x86_64-rhel8` |

Separate binaries are required because `nginx-cyberpanel` is dynamically linked
against OS libraries such as OpenSSL, PCRE2, brotli, libcrypt, zlib, and glibc.
This follows the same release model used by the CyberPanel OLS binaries.

## What Gets Installed

The one-line installer downloads and installs:

| Artifact | URL |
|---|---|
| Installer script | `https://cyberpanel.net/nginx-plesk-install.sh` |
| Plesk extension zip | `https://cyberpanel.net/cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip` |
| Platform binary | One of the OS-specific `nginx-cyberpanel-1.0.0-x86_64-*` files |

Canonical feature reference:

```text
https://cyberpanel.net/cyberpanel-nginx-feature-reference.md
```

Installed paths:

| Path | Purpose |
|---|---|
| `/usr/local/sbin/nginx-cyberpanel` | Main nginx replacement binary |
| `/usr/local/psa/admin/sbin/modules/cyberpanel-nginx/` | Plesk privileged helper scripts |
| `/usr/local/psa/admin/htdocs/modules/cyberpanel-nginx/` | Plesk extension UI |
| `/etc/nginx-cyberpanel/state` | Takeover state: `installed-not-active`, `active`, or `inactive` |
| `/etc/nginx-cyberpanel/extras.conf` | Generated gzip, brotli, HTTP/2, and LSCache settings |
| `/var/cache/nginx-cyberpanel/lscache/` | LSCache-compatible cache storage |
| `/var/log/cyberpanel-nginx-install.log` | Install and takeover log |
| `/var/backups/nginx-cyberpanel/` | Plesk/Apache/nginx backup data for rollback |

## Safety Model

Install and takeover are separate operations.

During install:

1. Detects OS and CPU architecture.
2. Installs runtime libraries where needed.
3. Downloads the Plesk extension zip.
4. Downloads the matching platform binary.
5. Installs the Plesk extension.
6. Copies the binary to `/usr/local/sbin/nginx-cyberpanel`.
7. Leaves Plesk default Apache plus nginx active.

After install, public traffic is still served by default Plesk nginx.

During activation and takeover:

1. The admin saves a platform license key in the Plesk extension License tab.
2. The extension validates the key with the platform API.
3. If the license is active, the extension runs the privileged takeover helper.
4. The helper writes and validates the synthesized nginx config before changing services.
5. Only after config validation passes, Apache and Plesk nginx are replaced with proxy/systemd shim units.
6. `nginx-cyberpanel` starts on public `:80` and `:443`.

If config validation fails, Apache and Plesk nginx are not changed.

## Main Benefits

- Uses existing Plesk Apache configuration without forcing manual vhost migration.
- Supports `.htaccess` behavior directly through the bundled htaccess module.
- Keeps Plesk as the control plane for domains, certificates, PHP handlers, and service management.
- Provides LiteSpeed Cache plugin compatibility for WordPress sites.
- Adds gzip, brotli, and HTTP/2 controls in the extension.
- Lets admins enable, revert, reload, purge cache, issue SSL, and diagnose from Plesk.
- Provides a clean rollback path to Plesk default Apache plus sw-nginx.

## Implemented Features

### Plesk Extension UI

The extension provides a self-contained Plesk UI with these tabs:

| Tab | Purpose |
|---|---|
| Overview | Shows current takeover status and provides enable/disable/reload controls |
| Cache | Purges LSCache-compatible cache globally or by URL |
| Config | Shows synthesized config and exposes key runtime toggles |
| SSL | Issues Let's Encrypt certificates for managed domains |
| Diagnose | Checks takeover, cache, gzip, brotli, HTTP/2, LSCache signal, PID shim, and Redis |
| License | Shows license/domain-count status and license input |

A platform license is required before takeover can serve sites. To activate a
free or paid license, buy or claim one at
`https://cyberpanel.net/cyberpanel-nginx-for-plesk/`, then open
`Extensions > CyberPanel nginx > License`, paste the key, and click `Save`.
The key is checked immediately; a valid key enables takeover from the License
tab.

### Platform License API

The extension and binary use platform product id `cyberpanel_nginx`.

The binary enforces license validity and `max_domains`. It does not currently
gate individual modules by feature flag, so all activated plans should expose
the same capability map; the tier and domain limit are the entitlement
differences.

Expected plan mapping:

| Plan ID | Tier | max_domains |
|---:|---|---:|
| 25 | free | 10 |
| 26 | pro | 50 |
| 27 | business | 100 |
| 28 | unlimited | 2147483647 |

Expected feature flags in platform responses:

```json
{
  "nginx_cyberpanel": true,
  "plesk_extension": true,
  "apache_config_translation": true,
  "htaccess": true,
  "ols_htaccess_parity": true,
  "lscache": true,
  "wordpress_lscache": true,
  "ssl_helper": true,
  "http2": true,
  "brotli": true,
  "gzip": true,
  "php_directives": true,
  "cache_purge": true,
  "diagnostics": true
}
```

The API should return the landing page as `upgrade_url`:

```text
https://cyberpanel.net/cyberpanel-nginx-for-plesk/
```

Live verification on 2026-05-14 confirmed plans 25-28 return the expected
`tier`, `max_domains`, `upgrade_url`, `manage_url`, and 14-key feature map.

### Takeover / Restore

The takeover flow installs systemd shims so Plesk still sees expected services
while `nginx-cyberpanel` serves public HTTP/HTTPS traffic.

Takeover active state:

| Port | Expected owner |
|---|---|
| `:80` | `nginx-cyberpanel` |
| `:443` | `nginx-cyberpanel` |
| `127.0.0.1:7080` | Apache backend |
| `127.0.0.1:7081` | Apache SSL backend |
| `:8443` | Plesk panel |

Restore returns the node to default Plesk:

| Port | Expected owner |
|---|---|
| `:80` | Plesk nginx |
| `:443` | Plesk nginx |
| `127.0.0.1:7080` | Apache |
| `127.0.0.1:7081` | Apache |

### LSCache Compatibility

`nginx-cyberpanel` implements LiteSpeed Cache plugin signals such as:

- `X-LiteSpeed-Cache-Control`
- `X-LiteSpeed-Tag`
- `X-LiteSpeed-Vary`
- PURGE support
- file-backed public cache storage

WordPress LSCache plugin responses can show:

```text
x-litespeed-cache: hit,public
```

For gzip-compressed cache objects, both plain clients and gzip-capable clients
should be cache hits:

```bash
curl -skI https://example.com/
curl -skI --compressed https://example.com/
```

### Diagnostics

The Diagnose tab checks:

| Check | Meaning |
|---|---|
| Takeover active | `/etc/nginx-cyberpanel/state` and service state |
| Cache directory | `/var/cache/nginx-cyberpanel/lscache` exists |
| gzip | gzip config is present |
| brotli | brotli config is present |
| HTTP/2 on SSL listeners | synthesized SSL listeners include HTTP/2 |
| LSCache plugin signal | synthesized config includes LSCache directives |
| Plesk PID shim | Apache PID path points to nginx-cyberpanel during takeover |
| Object cache (Redis) | Redis service is running |

Today the Diagnose parser was fixed to allow numeric key names such as
`http2_listeners`. Without this, the HTTP/2 check could be incorrectly dropped.

### Let's Encrypt Flow

The SSL tab can issue certificates for managed domains. The flow now:

- normalizes domains to lowercase
- strips trailing dots
- validates domain syntax
- validates email syntax where supplied
- rejects unmanaged domains before calling certbot/Plesk
- serves HTTP-01 challenges from the domain document root, including Plesk
  redirect-only HTTP vhosts whose `DocumentRoot` only exists on the SSL peer
- surfaces useful Let's Encrypt errors instead of only a generic failure

Example surfaced causes:

- DNS `NXDOMAIN`
- domain not pointing to the server
- Let's Encrypt rate limits
- ACME validation details

### Security Improvements

Implemented UI and helper hardening:

- CSRF token generated in PHP session.
- Every POST action validates the CSRF token.
- Flash messages from query parameters are sanitized.
- SSL issuance is restricted to valid Plesk-managed domains.
- Shell helper domain/email validation was added.
- Optional `www` SAN is passed as a shell array, not string-expanded arguments.
- sudoers installation now fails extension install if validation fails.

### Install Hook Behavior

The Plesk extension `post-install.php` runs the privileged install helper. If the
helper fails, the exception is rethrown so Plesk does not silently report success
with broken privileged helpers.

### Restore Improvements

Restore now:

- removes stale Apache PID symlink if it points to `/run/nginx-cyberpanel.pid`
- restarts the real Apache unit (`apache2` on Debian/Ubuntu, `httpd` on
  RHEL/AlmaLinux) and `nginx` after restoring vendor service files
- avoids leaving Plesk in a state where service files are restored but processes
  are not running

## Manual Verification Commands

Check default Plesk state after install but before takeover:

```bash
cat /etc/nginx-cyberpanel/state
APACHE_SERVICE=apache2; [ -d /etc/httpd/conf/plesk.conf.d ] && APACHE_SERVICE=httpd
systemctl is-active "$APACHE_SERVICE" nginx nginx-cyberpanel
ss -ltnp 'sport = :80 or sport = :443 or sport = :7080 or sport = :7081'
curl -skI https://example.com/ | sed -n '1,10p'
```

Expected before takeover:

```text
installed-not-active
apache2/httpd: active
nginx: active
nginx-cyberpanel: inactive
server: nginx
```

Check active takeover:

```bash
cat /etc/nginx-cyberpanel/state
APACHE_SERVICE=apache2; [ -d /etc/httpd/conf/plesk.conf.d ] && APACHE_SERVICE=httpd
systemctl is-active "$APACHE_SERVICE" nginx nginx-cyberpanel
ss -ltnp 'sport = :80 or sport = :443 or sport = :7080 or sport = :7081'
curl -skI https://example.com/ | sed -n '1,10p'
```

Expected during takeover:

```text
active
nginx-cyberpanel: active
server: nginx-cyberpanel/1.0.0
x-powered-by: CyberPanel-Nginx/1.0.0
```

Check Plesk default restore:

```bash
bash <(curl -sSL https://cyberpanel.net/nginx-plesk-install.sh) --uninstall
APACHE_SERVICE=apache2; [ -d /etc/httpd/conf/plesk.conf.d ] && APACHE_SERVICE=httpd
systemctl is-active "$APACHE_SERVICE" nginx nginx-cyberpanel
nginx -t
if command -v apache2ctl >/dev/null 2>&1; then apache2ctl -t; else httpd -t; fi
```

## Public Release Checksums

SHA256:

```text
329812abe027010ac653c800128ba5f88c88c519876d748296fd47520328862b  nginx-plesk-install.sh
4feae065639f115d65bcc35ef6745c63745b831b86141a51ef7db7d7c5f48bb6  cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip
dc4a7ccd496d0f8265e3e5619e2fceb95a3839982d056ee963fdb9f0f1fc73b3  nginx-cyberpanel-1.0.0-x86_64-ubuntu
26e279714e82876ad36f651ddae4b3b444551df1023fa7438002ac74b9534052  nginx-cyberpanel-1.0.0-x86_64-rhel9
8eb463908e0d0468a3e62098e997611ac5304c4035a51a61dcef13f2c71ab086  nginx-cyberpanel-1.0.0-x86_64-rhel8
```

Verify downloads:

```bash
curl -fLsS https://cyberpanel.net/nginx-plesk-install.sh -o /tmp/nginx-plesk-install.sh
sha256sum /tmp/nginx-plesk-install.sh

curl -fLsS https://cyberpanel.net/cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip -o /tmp/cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip
sha256sum /tmp/cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip
```

## Developer Notes

Repo layout:

```text
cyberpanel_nginx/
├── build.sh
├── nginx-plesk-install.sh
├── installer/
│   ├── plesk-takeover.sh
│   ├── plesk-restore.sh
│   ├── nginx-cyberpanel.service
│   ├── apache2-proxy.service
│   └── nginx-proxy.service
├── plesk-extension/
│   ├── build-extension.sh
│   └── cyberpanel-nginx/
│       ├── htdocs/index.php
│       ├── plib/
│       └── sbin/
├── modules/
│   ├── ngx_apacheconf_module/
│   ├── ngx_htaccess_module/
│   ├── ngx_lscache_module/
│   └── ngx_cyberpanel_license_module/
├── apacheconf_parser/
└── test/run-tests.sh
```

Build the extension zip:

```bash
NGX_BIN=/tmp/nginx-cyberpanel-no-bundled-binary \
  bash /usr/local/CyberCP/cyberpanel_nginx/plesk-extension/build-extension.sh
```

The public release zip should not bundle a platform-specific binary. The public
installer downloads the correct binary separately based on `/etc/os-release`.

Build the nginx binary on each platform:

```bash
cd /usr/local/CyberCP/cyberpanel_nginx
bash build.sh
strip /usr/local/CyberCP/nginx_src/objs/nginx
```

Release binaries are built on real target OS families, not cross-built from one
machine. Use the oldest supported OS in each family so the dynamic libc/OpenSSL
dependencies remain compatible with newer minor releases.

| Platform artifact | Build host | OS baseline | Output |
|---|---|---|---|
| Ubuntu / Debian | `95.216.252.29` | Ubuntu 22.04 | `nginx-cyberpanel-1.0.0-x86_64-ubuntu` |
| RHEL 9 / AlmaLinux 9 / Rocky 9 | `70.36.114.207` | AlmaLinux 9 | `nginx-cyberpanel-1.0.0-x86_64-rhel9` |
| RHEL 8 / AlmaLinux 8 / Rocky 8 | `70.36.114.199` | AlmaLinux 8.10 | `nginx-cyberpanel-1.0.0-x86_64-rhel8` |

Install build dependencies:

```bash
# Ubuntu / Debian build host
apt-get update -y
apt-get install -y git gcc make libpcre2-dev libssl-dev zlib1g-dev \
  libbrotli-dev curl ca-certificates

# RHEL / AlmaLinux / Rocky build host
dnf install -y git gcc make pcre2-devel openssl-devel zlib-devel \
  brotli-devel curl ca-certificates
```

Sync the current source tree to the build host:

```bash
rsync -az /usr/local/CyberCP/cyberpanel_nginx/ \
  root@BUILD_HOST:/usr/local/CyberCP/cyberpanel_nginx/
```

Prepare the shared nginx/brotli source tree on each build host:

```bash
mkdir -p /usr/local/CyberCP
test -d /usr/local/CyberCP/nginx_src/.git || \
  git clone https://github.com/nginx/nginx.git /usr/local/CyberCP/nginx_src
test -d /root/ngx_brotli/.git || \
  git clone --recursive https://github.com/google/ngx_brotli.git /root/ngx_brotli
```

Build and stage the release binary:

```bash
PLATFORM=rhel8   # ubuntu | rhel9 | rhel8

cd /usr/local/CyberCP/nginx_src
git fetch --quiet origin
git checkout --quiet 5eaf45f11e85459b52c18f876e69320df420ae29

cd /usr/local/CyberCP/cyberpanel_nginx
bash build.sh >/tmp/cpnginx-${PLATFORM}-build.log 2>&1
cp /usr/local/CyberCP/nginx_src/objs/nginx \
  /tmp/nginx-cyberpanel-1.0.0-x86_64-${PLATFORM}
strip /tmp/nginx-cyberpanel-1.0.0-x86_64-${PLATFORM}
sha256sum /tmp/nginx-cyberpanel-1.0.0-x86_64-${PLATFORM}
```

Run at least the full regression suite on any rebuilt OS-family artifact:

```bash
cd /usr/local/CyberCP
bash cyberpanel_nginx/test/run-tests.sh
```

Run tests:

```bash
bash /usr/local/CyberCP/cyberpanel_nginx/test/run-tests.sh
bash /usr/local/CyberCP/cyberpanel_nginx/test/run-ols-docs-parity.sh
```

Current release test status:

```text
Ubuntu 22.04 build host: 151 PASS, 0 FAIL
AlmaLinux 9 build host: 151 PASS, 0 FAIL
AlmaLinux 8.10 build host: 151 PASS, 0 FAIL
OLS docs parity: 55 PASS, 0 GAP, 0 FAIL
```

## cyberpanel.net Deployment

Public web root:

```text
/home/cyberpanel.net/public_html
```

Upload/update artifacts:

```bash
scp nginx-plesk-install.sh root@136.243.211.211:/home/cyberpanel.net/public_html/
scp /tmp/cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip root@136.243.211.211:/home/cyberpanel.net/public_html/
scp /tmp/nginx-cyberpanel-1.0.0-x86_64-* root@136.243.211.211:/home/cyberpanel.net/public_html/
```

Set ownership and permissions:

```bash
ssh root@136.243.211.211
cd /home/cyberpanel.net/public_html
chown cyber6636:cyber6636 nginx-plesk-install.sh cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip nginx-cyberpanel-1.0.0-x86_64-*
chmod 0644 nginx-plesk-install.sh cyberpanel-nginx-1.0.0-htaccess-phpvalue-20260514.zip nginx-cyberpanel-1.0.0-x86_64-*
```

Cloudflare note:

- Versioned URLs are new and usually do not need a purge.
- `nginx-plesk-install.sh` is a stable URL. Purge this URL if replacing it and
  immediate propagation is needed.

## Current Live Validation

The one-line installer was tested on a Plesk Obsidian 18.0.77.2 Ubuntu 24.04
node.

Result after install:

```text
installer exit code: 0
/etc/nginx-cyberpanel/state: installed-not-active
apache2/httpd: active
nginx: active
nginx-cyberpanel: inactive
```

Public HTTPS remained served by default Plesk nginx until takeover was enabled.

Takeover and restore were also tested on the same node:

- takeover served public `:80` and `:443` with `nginx-cyberpanel`
- Diagnose showed 8/8 pass
- LSCache returned `x-litespeed-cache: hit,public` for browser-like requests
- restore returned public `:80` and `:443` to Plesk nginx
- Apache remained on `127.0.0.1:7080` and `127.0.0.1:7081`

## Operational Guidance

Recommended production rollout:

1. Run the one-line installer.
2. Confirm Plesk is still serving sites with default nginx.
3. Open the extension and review Diagnose.
4. During a maintenance window, save a valid license key in the License tab.
5. Verify public site headers and application behavior.
6. Test LSCache with `curl --compressed`.
7. If needed, click disable/revert or run the uninstall one-liner.

The install command is safe to run before the maintenance window because it does
not switch traffic. The traffic switch happens only when the admin enables
takeover.
