Websites

Sites, PHP versions and layered nginx/PHP configuration with validate, atomic swap, health check and rollback.

In preview web version 0.1.0 Part of Websites and PHP

Actions

40 actions, callable from the panel, the command palette and the API as POST /api/v1/a/<id>. Internal actions used between modules are not listed.

ActionWhat it doesRiskPreview
web.server.ensure Prepare the web server on a node high No dry run
web.php.versions PHP versions offered on a node (read) low No dry run
web.sites.create Create a site for a domain medium
web.sites.list List sites (read) low No dry run
web.sites.get Get a site (read) low No dry run
web.sites.update Change a site medium
web.sites.suspend Suspend a site medium No dry run
web.sites.unsuspend Resume a site medium No dry run
web.sites.delete Delete a site high
web.sites.reconcile Re-apply every site of a node medium No dry run
web.layers.get Read a configuration layer (read) low No dry run
web.layers.set Replace a configuration layer medium
web.layers.effective Effective configuration of a site (read) low No dry run
web.config.history Configuration history of a node (read) low No dry run
web.config.status Hand-edited web files (read) low No dry run
web.config.adopt Keep a hand edit on top of the panel configuration medium
web.config.reapply Render the panel version again medium
web.config.reset Reset a file to defaults medium
web.config.unmanage Hand a file off to the administrator high
web.config.rollback Roll a node's web configuration back to an earlier generation high
web.sites.cert.install Install a certificate on a site medium No dry run
web.sites.cert.remove Remove the certificate of a site medium No dry run
web.sites.set_nodes Serve a site from several nodes high
web.panel_host.ensure Serve the panel on a hostname medium
web.panel_host.remove Stop serving the panel on a hostname medium
web.panel_host.list List panel hostnames (read) low No dry run
web.static.publish Publish an error, suspension or parked page medium
web.static.remove Stop serving a customer page on a hostname medium No dry run
web.static.list List hostnames that serve customer pages (read) low No dry run
web.config.reset_all Reset the whole service to defaults high
web.config.snapshot.list Snapshots before resets (read) low No dry run
web.config.snapshot.restore Undo a reset (restore a snapshot) high
web.config.history.list Earlier versions of a file (read) low No dry run
web.config.history.diff Compare an earlier version (read) low No dry run
web.config.history.restore Restore an earlier version of a file high
web.unmanaged.scan Scan now for objects made outside the panel low No dry run
web.unmanaged.list Objects made outside the panel (read) low No dry run
web.unmanaged.ignore Ignore or un-ignore an unmanaged object low
web.unmanaged.adopt Adopt an object made outside the panel medium
web.unmanaged.remove Remove an object made outside the panel high

Permissions and limits

Permissions

  • web.view View sites
  • web.manage Create, change and delete sites
  • web.config.edit Edit configuration layers and snippets of own sites
  • web.cert.install Install a certificate on a site
  • web.admin Prepare web servers, edit global/server/plan layers, roll back configuration

Plan limits

  • web.sites Websites

Engineering notes

Generated from modules/web/docs.md at build d90e9e2. These are the notes the engineers keep next to the code: precise, technical, and honest about what is not done yet.

nginx (Ubuntu/Debian package) + one PHP-FPM master per account and PHP version. Spec: docs/specs/web.md. Runtimes beyond PHP, presets, deploys, WordPress toolkit are the next card.

How a change is applied (AC-web-12/13/16)

The agent keeps immutable numbered generations under /var/lib/respirecloud/web/gen/<N>/ and a symlink /etc/respirecloud/web/current -> gen/<N>. nginx and every PHP-FPM unit read their config through that symlink (nginx -c /etc/respirecloud/web/current/nginx/nginx.conf, nginx uses only relative includes). web.config.apply = load generation N, apply the change, render N+1 into a scratch dir, run nginx -t -c and php-fpm -t -y on the exact bytes that would go live, diff, create docroot/tmp/log dirs (symlink-safe), swap the symlink (atomic rename), start/reload units, health-check every changed site through the local nginx (refused connection or HTTP 500; 502/504 for PHP sites) and on any failure flip back to N and reload again. A rejected snippet never touches the live config (hash unchanged). History = the generation list (web.config.history, who/when/why also in m_web.history); web.config.rollback activates an older generation with the same validate/health/auto-revert flow. 30 generations are kept. dry_run on create/update/delete/layers.set/ rollback returns the per-file unified diffs as a module.Plan.

Layers (AC-web-14/15)

global -> server (node) -> plan -> reseller -> user (account) -> site. Typed settings (http.*, php.*, pool.*, proxy.allow_internal, site.auto_create) are merged lowest-authority-first; a layer may lock keys so lower layers cannot change them (checked on save and on resolve). Admin-only keys cannot be set from reseller/user/site layers. web.layers.effective shows value + source layer. Snippet hooks: http (admin layers only; users may only declare limit_req_zone/limit_conn_zone prefixed with their account name), server, location, php_ini, php_pool. Plan/reseller ids of an account come from core.principal.get; when the caller may not read them the last value seen (account_ctx) is used so everybody resolves the same layers.

Snippet allow-list (non-admin layers)

Parsed with a real tokenizer; every statement is checked, errors carry the line number. Allowed: timeouts, gzip, add_header, expires, index, try_files, return, rewrite, error_page, allow/deny, auth_basic(+file inside home), root/alias inside the account home only (no variables, no ..), set (not $rc_*), proxy_pass to non-internal http(s) hosts, proxy_set_header, limit_req/limit_conn (own zones), access_log off, location and a restricted if. Rejected with a reason: load_module, lua/perl/js, include (all), fastcgi_pass/uwsgi/scgi, server_name, listen, ssl_*, error_log, fastcgi_param, disable_symlinks, resolver, real_ip*, anything unknown. php.ini keys are allow-listed with value checks; open_basedir must stay inside home; pool keys are bounded (pm.max_children 1..200). The agent re-validates everything (control plane validation is only for early, friendly errors).

Isolation (AC-web-18)

  • PHP workers run as the account user, in a per-account master unit rc-fpm-<ver>-<user>.service with Slice=rc-acct-<user>.slice (limits from system.slice.apply apply). Socket /run/rc-php/<user>-<ver>.sock is user:www-data 0660: other accounts cannot connect; fastcgi_pass cannot be written by users.
  • Pool-level php_admin_value[open_basedir] = own home (+tmp, system php dirs); upload/session/tmp dirs in ~/tmp.
  • nginx runs as www-data, which is added to each account's group to read docroots; every server block has disable_symlinks if_not_owner from=$document_root, so a symlink to another account's files is not served.
  • Docroot/tmp creation by root uses openat(O_NOFOLLOW) per component: a symlink planted in the home cannot redirect it.
  • Logs live in root-owned /var/log/rc-web/<account>/ (a user cannot symlink a log file onto a root-opened path).
  • Unknown Host headers get a neutral 404 page (00-default.conf); the https default server rejects the handshake.
  • Proxy sites to loopback/private hosts are refused unless the admin sets proxy.allow_internal=true (SSRF to the panel).

Actions

web.server.ensure (prepare node: nginx, PHP versions from the distro or the Ondrej Sury repo with pinned key fingerprint, drop-in for nginx.service, generation 1), web.php.versions, web.sites.create|list|get|update|suspend| unsuspend|delete|reconcile, web.layers.get|set|effective, web.config.history|rollback, web.sites.cert.install|remove, and event handlers web.on_domain_created (auto site per plan's site.auto_create; alias/parked join the parent's site), web.on_domain_deleted, web.on_account_deleted. Site types: static, php, proxy. Limit key web.sites. Resource kind site for sub-user grants.

Contract for the ssl module

  • HTTP-01: every plain-HTTP server block (and the default vhost) serves /.well-known/acme-challenge/ from /var/lib/rc-acme (agentop.WebAcmeDir), before any redirect rule. The ssl agent writes the token file to /var/lib/rc-acme/.well-known/acme-challenge/<token> (dir exists, root 0755). Works for any domain of any site, and unknown hosts.
  • Install a certificate: call action web.sites.cert.install {site_id, fullchain_pem, privkey_pem}. The agent validates the pair (tls.X509KeyPair, not expired), stores them in /etc/respirecloud/web/certs/<site id>/ (fullchain.pem, privkey.pem 0600), re-renders the site with a listen 443 ssl block (HTTP/2, HSTS when the http.hsts_max_age setting is > 0) through the normal validate/swap/health/rollback path, and restores the previous certificate if that fails. web.sites.update {force_https:true} turns plain HTTP into a 301 to https (ACME path stays on HTTP). web.sites.cert.remove reverses it. Certificates live outside the generations, so a rollback never loses them. All names of the site (primary, serve and redirect aliases) share the one certificate; ssl should request a SAN cert for web.sites.get -> domains[].name.
  • Events: web.site.created|updated|deleted carry site id and account for ssl to react (issue after create).

Verified

See docs/tasks/w2/w2-02-web.handover.md for the lab run (curl proofs, failed-config rollback, isolation).

Not done / next

PHP version install/remove by admin after first ensure (only additive via web.server.ensure), per-site pool tuning beyond the first site of a pool, extension manager, error/slow log viewers, permission fixer, site manifest export, UI bundle, basic-auth/IP/hotlink consumption from security, FrankenPHP, runtimes.