WebDAV breaks when behind two nginx reverse proxies
My Nextcloud instance is technically behind two nginx reverse proxies. The first one passes HTTP traffic to the local php-fpm socket on the server. The second one sits on the edge of my network, routing incoming HTTP/HTTPS traffic to different upstream servers on my network, depending on the hostname that is requested.
The Nextcloud web UI works perfectly with this architecture, but WebDAV was broken when accessed through the external reverse proxy. When I tried to access my account through a client, such as the Android Nextcloud app, it could authenticate, but it couldn’t list any files. The error logs on the external nginx server stateed:
2022/06/08 01:14:00 [crit] 3666#3666: *310 SSL_read() failed (SSL: error:0A000126:SSL routines::unexpected eof while reading) while reading response header from upstream, client: xxx.xxx.x.x, server: nextcloud.example.net, request: "PROPFIND /remote.php/dav/files/sean// HTTP/1.1", upstream: "https://nextcloudvm.example.net:443/remote.php/dav/files/sean//", host: "nextcloud.example.net"
If I used a client in the internal network to connect directly to the Nextcloud server (i.e. without the second reverse proxy), WebDAV worked correctly.
It turns out that SSL: error:0A000126:SSL routines::unexpected eof while reading
iccurs when using the nginx packages provided by the Debian or Ubuntu projects. Replacing the distrobution nginx
package with the stable nginx
package provided by the NGINX Project resolved the issue.
After replacing the nginx
package, a couple small configuration changes need to be made so with works with existing Debian or Ubuntu configurations.
- In
/etc/nginx/nginx.conf
, change theuser
setting towww-data
- Under the
include
option, addinclude /etc/nginx/sites-enabled/*;
Add the IP address of your reverse proxy to your Nextcloud config/config.php
file.
'trusted_proxies' =>
array (
0 => '192.168.1.42',
) 'forwarded_for_headers' =>
array (
0 => 'X-Forwarded-For',
),
Then restart NGINX
sudo service restart nginx
NGINX site configurations
Nextcloud server
upstream php-handler {
server unix:/var/run/php/php7.4-fpm.sock;
}
server {
listen 443 ssl http2;
server_name nextcloudvm.example.net;
ssl_certificate /etc/nginx/ssl/nextcloud.crt;
ssl_certificate_key /etc/nginx/ssl/nextcloud.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
ssl_dhparam /etc/ssl/dhparam.pem;
root /var/www/nextcloud;
index index.php index.html index.htm;
access_log off;
# Add headers to serve security related headers
# Before enabling Strict-Transport-Security headers please read into this
# topic first.
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always;
#
# WARNING: Only add the preload option once you read about
# the consequences in https://hstspreload.org/. This option
# will add the domain to a hardcoded list that is shipped
# in all major browsers and getting removed from this list
# could take several months.
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always;
# Remove X-Powered-By, which is an information leak
fastcgi_hide_header X-Powered-By;
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# Make a regex exception for `/.well-known` so that clients can still
# access it despite the existence of the regex rule
# `location ~ /(\.|autotest|...)` which would otherwise handle requests
# for `/.well-known`.
location ^~ /.well-known {
# The rules in this block are an adaptation of the rules
# in `.htaccess` that concern `/.well-known`.
location = /.well-known/carddav { return 301 /remote.php/dav/; }
location = /.well-known/caldav { return 301 /remote.php/dav/; }
location /.well-known/acme-challenge { try_files $uri $uri/ =404; }
location /.well-known/pki-validation { 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.
return 301 /index.php$request_uri;
}
# set max upload size
client_max_body_size 512M;
fastcgi_buffers 64 4K;
# Set timeouts
fastcgi_read_timeout 900000;
proxy_read_timeout 900000;
# 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 application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject 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/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
# Uncomment if your server is build with the ngx_pagespeed module
# This module is currently not supported.
#pagespeed off;
location / {
rewrite ^ /index.php;
}
location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ {
deny all;
}
location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) {
deny all;
}
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) {
fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
fastcgi_param HTTPS on;
# Avoid sending the security headers twice
fastcgi_param modHeadersAvailable true;
# Enable pretty urls
fastcgi_param front_controller_active true;
fastcgi_pass php-handler;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
}
location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
try_files $uri/ =404;
index index.php;
}
# Adding the cache control header for js, css and map files
# Make sure it is BELOW the PHP block
location ~ \.(?:css|js|woff2?|svg|gif|map)$ {
try_files $uri /index.php$request_uri;
add_header Cache-Control "public, max-age=15778463";
# Add headers to serve security related headers (It is intended to
# have those duplicated to the ones above)
# Before enabling Strict-Transport-Security headers please read into
# this topic first.
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always;
#
# WARNING: Only add the preload option once you read about
# the consequences in https://hstspreload.org/. This option
# will add the domain to a hardcoded list that is shipped
# in all major browsers and getting removed from this list
# could take several months.
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always;
# Optional: Don't log access to assets
access_log off;
}
location ~ \.(?:png|html|ttf|ico|jpg|jpeg|bcmap)$ {
try_files $uri /index.php$request_uri;
# Optional: Don't log access to other assets
access_log off;
}
}
server {
listen 80;
server_name nextcloud.example.net www.nextcloud.example.net;
root /var/www/nextcloud/;
location /.well-known {
try_files $uri $uri/ =404;
}
location / {
return 301 https://$host$request_uri;
}
}
Network edge reverse proxy
server {
server_name nextcloud.example.net;
location / {
proxy_pass https://nextcloudvm.example.net;
proxy_ssl_trusted_certificate /etc/nginx/ssl/nextcloud.example.net.crt;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/nextcloud.example.net/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/nextcloud.example.net/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
access_log off;
# Add headers to serve security related headers
# Before enabling Strict-Transport-Security headers please read into this
# topic first.
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always;
# set max upload size
client_max_body_size 512M;
fastcgi_buffers 64 4K;
# Set timeouts
fastcgi_read_timeout 900000;
proxy_read_timeout 900000;
}
server {
if ($host = nextcloud.example.net) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name nextcloud.example.net;
return 404; # managed by Certbot
}