From 36f7bbb00d43ccbfaa50ae2d2efedfbb3761cc91 Mon Sep 17 00:00:00 2001 From: Timo Wilken Date: Wed, 29 Nov 2023 00:40:38 +0100 Subject: Migrate Nextcloud from Apache to nginx --- tw/services.scm | 6 -- tw/services/matrix.scm | 25 ++---- tw/services/nextcloud.scm | 191 ++++++++++++++++++++++++++++++++-------------- tw/services/web.scm | 63 +++++++++++++++ tw/system/lud.scm | 36 --------- 5 files changed, 202 insertions(+), 119 deletions(-) delete mode 100644 tw/services.scm create mode 100644 tw/services/web.scm (limited to 'tw') diff --git a/tw/services.scm b/tw/services.scm deleted file mode 100644 index 2ef11298..00000000 --- a/tw/services.scm +++ /dev/null @@ -1,6 +0,0 @@ -(define-module (tw services) - #:use-module (guix gexp)) - -(define-public %httpd-cert-deploy-hook - (program-file "httpd-cert-deploy-hook" - #~(kill (call-with-input-file "/var/run/httpd" read) SIGHUP))) diff --git a/tw/services/matrix.scm b/tw/services/matrix.scm index db21f172..6b184f49 100644 --- a/tw/services/matrix.scm +++ b/tw/services/matrix.scm @@ -1,27 +1,14 @@ (define-module (tw services matrix) #:use-module (gnu services) - #:use-module (gnu services certbot) - #:use-module (gnu services web) - #:use-module (tw services)) + #:use-module (tw services web)) (define-public %matrix-services - (list (simple-service 'synapse-certificates certbot-service-type - (list (certificate-configuration + (list (simple-service 'synapse-reverse-proxy https-reverse-proxy-service-type + ;; Synapse can't access certbot certs, but nginx can, so proxy HTTPS + ;; access through. Also, it's good to have Synapse available on :443. + (list (https-reverse-proxy-configuration (domains '("matrix.twilken.net")) - (deploy-hook %httpd-cert-deploy-hook)))) - - (simple-service 'synapse-https-proxy httpd-service-type - ;; Synapse can't access certbot certs, but Apache/httpd - ;; can, so proxy HTTPS access through. It's good to have - ;; Synapse available on port 443 anyway. - (list (httpd-virtualhost "*:443" (list "\ -# Redirect to Synapse, to avoid having to specify its port number in Matrix clients. -ServerName matrix.twilken.net -SSLEngine on -SSLCertificateFile \"/etc/letsencrypt/live/matrix.twilken.net/fullchain.pem\" -SSLCertificateKeyFile \"/etc/letsencrypt/live/matrix.twilken.net/privkey.pem\" -ProxyPass \"/\" \"https://127.0.0.1:48448/\" -")))) + (destination-port 48448)))) ;; TODO: Postgres for Synapse ;; (service postgresql-service-type diff --git a/tw/services/nextcloud.scm b/tw/services/nextcloud.scm index 7275cabf..eed2f4fd 100644 --- a/tw/services/nextcloud.scm +++ b/tw/services/nextcloud.scm @@ -1,15 +1,18 @@ (define-module (tw services nextcloud) + #:use-module (ice-9 match) #:use-module (gnu) #:use-module (gnu packages backup) #:use-module (gnu packages certs) #:use-module (gnu packages databases) #:use-module (gnu packages linux) #:use-module (gnu packages php) + #:use-module (gnu packages web) #:use-module (gnu services certbot) #:use-module (gnu services mcron) #:use-module (gnu services web) #:use-module (guix gexp) - #:use-module (tw services) + #:use-module ((guix packages) #:select (package-version)) + #:use-module ((guix utils) #:select (version-major)) #:use-module (tw services restic)) (define-public %nextcloud-php.ini @@ -24,7 +27,8 @@ (with-output-to-file #$output ;; Guix's PHP comes with the following extensions built-in, ;; so no extension= line necessary: - ;; pdo_mysql, bcmath, bz2, exif, gd, iconv, intl + ;; pdo_mysql, pdo_pgsql, pgsql, bcmath, bz2, exif, gd, iconv, intl + ;; The default pgsql.* settings are fine. (lambda () (display (string-append "\ memory_limit=512M extension_dir=/run/current-system/profile/lib/php/extensions/" (basename php-extdir) " @@ -43,6 +47,8 @@ opcache.save_comments=1 opcache.revalidate_freq=120 ")))))))) +(define nextcloud-domain "cloud.wilkenfamily.de") + (define nextcloud-backup-repository "/var/backups/nextcloud") (define nextcloud-backup-password-file "/etc/restic/lud-nextcloud") @@ -68,8 +74,8 @@ opcache.revalidate_freq=120 (let ((child-pid (primitive-fork))) (if (zero? child-pid) (begin ; this is the child - (setgid (group:gid (getgr "httpd"))) ; while still root - (setuid (passwd:uid (getpw "httpd"))) + (setgid (group:gid (getgr "php-fpm"))) ; while still root + (setuid (passwd:uid (getpw "php-fpm"))) (execl #$(file-append php "/bin/php") "php" "-c" #$%nextcloud-php.ini (string-append nextcloud-dir "/occ") @@ -148,66 +154,135 @@ opcache.revalidate_freq=120 (cleanup)))) (define-public %nextcloud-services - (list (simple-service 'nextcloud-https-server httpd-service-type - ;; The certbot service redirects everything on port 80 to - ;; port 443 by default, modulo its own /.well-known paths. - (list (httpd-virtualhost "*:443" (list "\ -# For Nextcloud. -ServerName cloud.wilkenfamily.de -DocumentRoot /var/www/nextcloud -SSLEngine on -SSLCertificateFile \"/etc/letsencrypt/live/cloud.wilkenfamily.de/fullchain.pem\" -SSLCertificateKeyFile \"/etc/letsencrypt/live/cloud.wilkenfamily.de/privkey.pem\" -Header always set Strict-Transport-Security \"max-age=15552000\" - -# Don't check for .htaccess files above DocumentRoot. - - AllowOverride None - - - - Options +FollowSymlinks - AllowOverride All - - Dav off - - SetEnv HOME /var/www/nextcloud - SetEnv HTTP_HOME /var/www/nextcloud - - -# Redirect to local php-fpm if mod_php is not available - - - # Enable http authorization headers - - SetEnvIfNoCase ^Authorization$ \"(.+)\" HTTP_AUTHORIZATION=$1 - - - - SetHandler \"proxy:unix:/var/run/php-fpm.sock|fcgi://localhost/\" - - - # Deny access to raw PHP sources and files without filename (e.g. '.php') - - Require all denied - - - -")))) + (list (simple-service 'nextcloud-https-server nginx-service-type + ;; https://docs.nextcloud.com/server/latest/admin_manual/installation/nginx.html + (list (nginx-server-configuration + ;; The certbot service redirects everything on port 80 to + ;; port 443 by default, modulo its own /.well-known paths. + (listen '("443 ssl http2")) + (server-name (list nextcloud-domain)) + (root "/var/www/nextcloud") + (index '("index.php" "index.html" "/index.php$request_uri")) + (try-files '("$uri" "$uri/" "/index.php$request_uri")) + (ssl-certificate (string-append "/etc/letsencrypt/live/" nextcloud-domain "/fullchain.pem")) + (ssl-certificate-key (string-append "/etc/letsencrypt/live/" nextcloud-domain "/privkey.pem")) + (server-tokens? #f) + (raw-content + `(;; Set max upload size and increase upload timeout + "client_max_body_size 512M;" + "client_body_timeout 300s;" + "fastcgi_buffers 64 4K;" + + ;; Enable gzip but do not remove ETag headers + "gzip on;" + "gzip_vary on;" + "gzip_comp_level 4;" + "gzip_min_length 256;" + "gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;" + ("gzip_types application/atom+xml text/javascript application/javascript " + "application/json application/ld+json application/manifest+json " + "application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject " + "application/wasm application/x-font-ttf application/x-web-app-manifest+json " + "application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml " + "image/x-icon text/cache-manifest text/css text/plain text/vcard text/vtt " + "text/vnd.rim.location.xloc text/x-component text/x-cross-domain-policy;") + + ;; HTTP response headers borrowed from Nextcloud `.htaccess` + ,@(map (match-lambda + ((hdr . value) + `("add_header " ,hdr " \"" ,value "\" always;"))) + '(("Referrer-Policy" . "no-referrer") + ("X-Content-Type-Options" . "nosniff") + ("X-Frame-Options" . "SAMEORIGIN") + ("X-Permitted-Cross-Domain-Policies" . "none") + ("X-Robots-Tag" . "noindex, nofollow") + ("X-XSS-Protection" . "1; mode=block"))) + + ;; Remove X-Powered-By, which is an information leak + "fastcgi_hide_header X-Powered-By;" + + ;; Add .mjs as a file extension for JavaScript + ("include " ,nginx "/share/nginx/conf/mime.types;") + "types { text/javascript js mjs; }")) + + (locations + (list + (nginx-location-configuration ; Handle Microsoft DAV clients + (uri "= /") + (body '("if ( $http_user_agent ~ ^DavClnt ) { return 302 /remote.php/webdav/$is_args$args; }"))) + (nginx-location-configuration + (uri "= /robots.txt") + (body '("allow all;" "log_not_found off;" "access_log off;"))) + + ;; The rules in these blocks are an adaptation of the rules + ;; in `.htaccess` that concern `/.well-known`. + (nginx-location-configuration + (uri "= /.well-known/carddav") + (body '("return 301 /remote.php/dav/;"))) + (nginx-location-configuration + (uri "= /.well-known/caldav") + (body '("return 301 /remote.php/dav/;"))) + (nginx-location-configuration + (uri "= /.well-known/acme-challenge") + (body '("try_files $uri $uri/ =404;"))) + (nginx-location-configuration + (uri "= /.well-known/pki-validation") + (body '("try_files $uri $uri/ =404;"))) + ;; Let Nextcloud's API for `/.well-known` URIs handle all other + ;; requests by passing them to the front-end controller. + (nginx-location-configuration + (uri "^~ /.well-known") + (body '("return 301 /index.php$request_uri;"))) + + ;; Rules borrowed from `.htaccess` to hide certain paths from clients + (nginx-location-configuration + (uri "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)") + (body '("return 404;"))) + (nginx-location-configuration + ;; Exclude .well-known to avoid overriding rules above (regexes take precedence). + (uri "~ ^/(?:\\.(?!well-known)|autotest|occ|issue|indie|db_|console)") + (body '("return 404;"))) + + ;; Ensure this block, which passes PHP files to the PHP process, is above the blocks + ;; which handle static assets (as seen below). If this block is not declared first, + ;; then Nginx will encounter an infinite rewriting loop when it prepends `/index.php` + ;; to the URI, resulting in a HTTP 500 error response. + (nginx-location-configuration + (uri "~ \\.php(?:$|/)") + (body + (let ((phpver (version-major (package-version php)))) + `(("rewrite ^/(?!index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|" ; Legacy support + "updater\\/.+|ocs-provider\\/.+|.+\\/richdocumentscode\\/proxy) /index.php$request_uri;") + "fastcgi_request_buffering off;" + "fastcgi_split_path_info ^(.+?\\.php)(/.*)$;" + "try_files $fastcgi_script_name =404;" + ("include " ,nginx "/share/nginx/conf/fastcgi.conf;") + "fastcgi_param HTTP_PROXY \"\";" ; Mitigate https://httpoxy.org/ + "fastcgi_param modHeadersAvailable true;" ; Avoid sending the security headers twice + "fastcgi_param front_controller_active true;" ; Enable pretty urls + ("fastcgi_pass unix:/var/run/php" ,phpver "-fpm.sock;"))))) ; Match php-fpm-configuration + + (nginx-location-configuration + (uri "~ \\.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$") + (body '("try_files $uri /index.php$request_uri;" + "add_header Cache-Control \"public, max-age=15778463, immutable\";"))) + (nginx-location-configuration + (uri "~ \\.woff2?$") + (body '("try_files $uri /index.php$request_uri;" "expires 7d;"))) + (nginx-location-configuration + (uri "/remote") + (body '("return 301 /remote.php$request_uri;")))))))) (service (@ (tw services php-fpm) php-fpm-service-type) (php-fpm-configuration - (user "httpd") - (group "httpd") - (socket "/var/run/php-fpm.sock") - (socket-user "httpd") - (socket-group "httpd") (php-ini-file %nextcloud-php.ini))) (simple-service 'nextcloud-certificates certbot-service-type (list (certificate-configuration - (domains '("cloud.wilkenfamily.de")) - (deploy-hook %httpd-cert-deploy-hook)))) + (domains (list nextcloud-domain)) + (deploy-hook + (program-file "nginx-cert-deploy-hook" + #~(kill (call-with-input-file "/var/run/nginx/pid" read) SIGHUP)))))) (simple-service 'nextcloud-backup-cleanup restic-cleanup-service-type (list (restic-scheduled-cleanup @@ -229,7 +304,7 @@ Header always set Strict-Transport-Security \"max-age=15552000\" (setenv "SSL_CERT_DIR" #$(file-append nss-certs "/etc/ssl/certs")) (execl #$(file-append php "/bin/php") "php" "-c" #$%nextcloud-php.ini "/var/www/nextcloud/cron.php"))) - #:user "httpd") + #:user "php-fpm") ;; TODO: try `with-mail-out' from `(mcron redirect)'? #~(job "0 6 * * *" #$nextcloud-backup-program))))) diff --git a/tw/services/web.scm b/tw/services/web.scm new file mode 100644 index 00000000..12851a72 --- /dev/null +++ b/tw/services/web.scm @@ -0,0 +1,63 @@ +(define-module (tw services web) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services certbot) + #:use-module (gnu services web) + #:use-module (guix gexp) + #:use-module (guix records) + #:use-module ((srfi srfi-1) #:select (concatenate)) + #:export (https-reverse-proxy-service-type + https-reverse-proxy-configuration)) + +(define-configuration/no-serialization https-reverse-proxy-configuration + (domains list-of-strings "List of domain names that nginx should proxy +requests for.") + (destination-port integer "The port number of the service that should be +proxied to.") + (destination-ip (string "127.0.0.1") "The IP address of the server that +should be proxied to. Usually, this should be localhost.") + (destination-protocol (string "http") "The protocol that the proxied service +speaks. Set to @code{\"https\"} if you want to proxy HTTPS-to-HTTPS.") + (nginx-pid-file (string "/var/run/nginx/pid") "The file containing nginx's +process ID. This may differ from the default if nginx's @code{run-directory} +differs from its default.")) + +(define (reverse-proxy-certificate config) + (match-record config (domains nginx-pid-file) + (certificate-configuration + (domains domains) + (deploy-hook + (program-file "nginx-cert-deploy-hook" + #~(kill (call-with-input-file #$nginx-pid-file read) SIGHUP)))))) + +(define (reverse-proxy-nginx-server config) + (match-record config + (domains destination-port destination-ip) + (nginx-server-configuration + (listen '("443 ssl http2")) + (server-name domains) + (ssl-certificate (string-append "/etc/letsencrypt/live/" (car domains) "/fullchain.pem")) + (ssl-certificate-key (string-append "/etc/letsencrypt/live/" (car domains) "/privkey.pem")) + (server-tokens? #f) + (locations + (list (nginx-location-configuration + (uri "/") + (body `(("proxy_pass http://" ,destination-ip ":" + ,(number->string destination-port)))))))))) + +(define (reverse-proxy-certificates configs) + (map reverse-proxy-certificate configs)) + +(define (reverse-proxy-nginx-servers configs) + (map reverse-proxy-nginx-server configs)) + +(define https-reverse-proxy-service-type + (service-type + (name 'reverse-proxy) + (extensions (list (service-extension nginx-service-type reverse-proxy-nginx-servers) + (service-extension certbot-service-type reverse-proxy-certificates))) + (default-value '()) + (compose concatenate) + (extend append) + (description "Configure nginx as a reverse proxy proxying external HTTPS +requests to another host or a local port over plain HTTP."))) diff --git a/tw/system/lud.scm b/tw/system/lud.scm index 82a3e43f..9a07fb0d 100644 --- a/tw/system/lud.scm +++ b/tw/system/lud.scm @@ -26,21 +26,6 @@ (define data-partition ; /dev/sdc1 (uuid "4715ae0e-5cef-48f2-a59e-025321153888" 'btrfs)) -(define httpd-intermediate-ssl-config "\ -# SSL configuration. -# https://ssl-config.mozilla.org/#server=apache&version=2.4.53&config=intermediate&openssl=1.1.1n&ocsp=false&guideline=5.6 -SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 -SSLHonorCipherOrder Off -SSLSessionTickets Off -SSLUseStapling On -SSLStaplingCache \"shmcb:logs/ssl_stapling(32768)\" -SSLSessionCache \"shmcb:logs/ssl_scache(65535)\" -# 20 minutes -- default is 5 minutes, which is not long, and the cache -# size is limited anyway above. -SSLSessionCacheTimeout 1200 -") - (define-public %lud-system (operating-system (host-name "lud.twilken.net") @@ -145,27 +130,6 @@ SSLSessionCacheTimeout 1200 (certbot-configuration (email "letsencrypt@twilken.net"))) - (service httpd-service-type - (httpd-configuration - (config - (httpd-config-file - (listen '("443")) ; leave port 80 free for certbot/nginx - (modules - (cons* (httpd-module (name "ssl_module") (file "modules/mod_ssl.so")) - (httpd-module (name "proxy_module") (file "modules/mod_proxy.so")) - (httpd-module (name "rewrite_module") (file "modules/mod_rewrite.so")) - (httpd-module (name "alias_module") (file "modules/mod_alias.so")) - (httpd-module (name "socache_shmcb_module") ; for SSLStaplingCache - (file "modules/mod_socache_shmcb.so")) - (httpd-module (name "proxy_fcgi_module") ; for PHP/FastCGI - (file "modules/mod_proxy_fcgi.so")) - %default-httpd-modules)) - ;; Preserve default value for `extra-config'. - (extra-config - (list "TypesConfig etc/httpd/mime.types\n" - "ServerAdmin webmaster@twilken.net\n" - httpd-intermediate-ssl-config)))))) - ;; For Nextcloud (and Streama) (service mysql-service-type (mysql-configuration -- cgit v1.2.3