# Web publishing The collected tilde.club configuration uses nginx, PHP-FPM, and fcgiwrap. Apache's `mod_userdir`, `.htaccess`, and `a2enmod` do not configure this stack. ## Production layout The main site's document root is `/usr/share/nginx/html`. Its server block serves HTTP and HTTPS. Several separate service vhosts redirect HTTP to HTTPS, but the main host's block serves both. The main nginx config includes `/etc/nginx/sites-enabled/*`; this is a local layout to reproduce explicitly, not an assumption about the distribution default. | Request | Backend or directory | | --- | --- | | Main site | `/usr/share/nginx/html` | | `/~USER/` and `/u/USER/` | `/home/USER/public_html/` | | User PHP | `/run/php-fpm/php-fpm-php-user.sock`, pool UID `php-user`, group `club` | | Main site PHP | `/run/php-fpm/www.sock`, pool UID/group `nginx` | | User CGI | `/run/fcgiwrap/fcgiwrap-cgi.sock`; service runs as `cgiwrap` | | `/~USER/git` | Local cgit wrapper through fcgiwrap | | `/~USER/myplant` | Central Botany PHP application | | WeeChat relay beneath a user page | A socket beneath the user's `.weechat` directory | | Webmail | SnappyMail in a separate vhost | The user-page location enables directory listings and SSI, and uses a per-user HTML 404 page. Account for those features when replacing a running config. The collected pool generator can create per-user FPM sockets, but the main user-page route uses the **shared** pool. `makeuser` does not call the pool generator. Do not describe this as per-user PHP execution. The CGI unit has UID/GID-changing capabilities. Those capabilities do not, by themselves, establish that each script is switched to its owner. That needs the wrapper implementation and a runtime identity test. Likewise, the collected CGI `SCRIPT_FILENAME` expression prepends a home path to `$request_filename` inside an alias location. Verify the expanded path before using it in a new config. The new-host examples below use explicit per-account paths instead. ## Start with static pages Use [tilde-static.conf](../examples/nginx/tilde-static.conf) inside nginx's `http` context and replace the example domain. It provides both user URL forms, blocks dotfiles and script-source downloads, and disables symlinks in public user content. The example leaves directory listings and SSI off; enable those features deliberately if your community wants them. A 0711 home can be traversed without being listed. Public directories need 0755 and public files 0644. Keep the user's other files private. Check every path component with `namei -l`, not just the page's final mode. For a new SELinux-enforcing host, use the installed policy's home publishing support. A common Fedora layout is: ```bash setsebool -P httpd_enable_homedirs on semanage fcontext -a -t httpd_user_content_t '/home/[^/]+/public_html(/.*)?' restorecon -RFv /home/member/public_html ``` If a matching custom rule already exists, review it and use `semanage fcontext -m` to modify it instead of adding a duplicate. Test access as the service and inspect AVC denials. Policy versions and service domains can require additional booleans or labels for writing or connecting to other services; enable only the access actually needed. The production snapshot with SELinux disabled cannot validate those policy rules for a new enforcing installation. Useful checks: ```bash nginx -t namei -l /home/member/public_html/index.html ls -Zd /home/member /home/member/public_html ausearch -m AVC,USER_AVC -ts recent journalctl -u nginx --since today ``` The production host sends nginx logs to syslog; do not assume every nginx error will be in `/var/log/nginx/error.log`. Check the effective `error_log` setting. ## Add HTTPS After obtaining a certificate, retain the HTTP challenge location and decide whether other HTTP requests should be served or redirected. For an HTTPS-only policy, redirect the non-challenge requests to a fixed configured hostname, rather than constructing a redirect from an unvalidated Host header. Copy the publishing locations into an HTTPS server block with these settings, using the certificate paths returned by Certbot: ```nginx listen 443 ssl; listen [::]:443 ssl; http2 on; server_name tilde.example.net; ssl_certificate /etc/letsencrypt/live/tilde.example.net/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/tilde.example.net/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ``` Keep shared locations in one root-owned include if both server blocks use them. Set response headers in the context where they are intended to apply. Test headers on error responses and nested locations; inheritance is version-specific. A blanket CSP, frame policy, or cross-origin isolation policy on every member's page can break the projects you are hosting. Keep the operator application and member publishing policies separate. Add HSTS only after deciding that all of its affected names will continue to support HTTPS. A Certbot deploy hook for nginx can be a root-owned executable file in `/etc/letsencrypt/renewal-hooks/deploy/`: ```bash #!/usr/bin/env bash set -euo pipefail /usr/sbin/nginx -t /usr/bin/systemctl reload nginx ``` Enable the packaged renewal timer and test renewal. The collected production service instead invokes `/usr/local/bin/certbot renew --standalone`, with hooks to stop and start nginx. Its timer checks at 06:09 and 18:09 in the host's local time, with `Persistent=true`. That produces a different availability profile from webroot renewal. Verify all certificate consumers after renewal, including Dovecot and Postfix if they share the certificate. ## Add PHP with a deliberate execution identity A new host can give an enabled member a dedicated pool. Install maintained packages, including the process extension if you use the identity test below: ```bash dnf install php-fpm php-cli php-process ``` Review [member.conf](../examples/php-fpm/member.conf), then install it as `/etc/php-fpm.d/member.conf`. The Unix account and group must already exist. Create the private session directory named in that file: ```bash sudo -u member -- install -d -m 0700 /home/member/.local/state/php/sessions php-fpm -t systemctl enable --now php-fpm ``` If PHP-FPM is already running, reload it after successful validation. Ensure the nginx worker can connect to the new socket. With SELinux enabled, directory labels, FPM's domain, and socket policy need to be checked on the target system. Insert [member-php.inc](../examples/nginx/member-php.inc) into the site's server block **after** the dotfile-denial location but **before** the generic script source-denial and static userdir locations. Keep both HTTP and HTTPS policy consistent. The example supports `.php` endpoints for this named member; it does not implement PATH_INFO routing for arbitrary frameworks. If you want PHP index files, add `index.php` to the userdir index list after enabling its handler. Validate and reload nginx. Use this temporary test in the member's public directory, then remove it: ```php