How to configure nginx for two reverse proxies

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 the user setting to www-data
  • Under the include option, add include /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


}

Have you tried asking the Nginx community for their ideas and suggestions?

This issue is so specific to Nginx that this forum might not be the place to find a true solution, but maybe.