Skip to content

Using a Reverse Proxy with Paperless ngx

Martin Richtarsky edited this page Jun 12, 2024 · 9 revisions

If you want to expose paperless to the internet, you should hide it behind a reverse proxy with SSL enabled. The officially supported method is Nginx.

Nginx

In addition to the usual configuration for SSL, the following configuration is required for paperless to operate:

http {

    # Adjust as required. This is the maximum size for file uploads.
    # The default value 1M might be a little too small.
    client_max_body_size 10M;

    server {

        location / {

            # Adjust host and port as required.
            proxy_pass http://localhost:8000/;

            # These configuration options are required for WebSockets to work.
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Host $server_name;
            add_header Referrer-Policy "strict-origin-when-cross-origin";
        }
    }
}

The PAPERLESS_URL configuration variable is also required when using a reverse proxy; consider setting USE_X_FORWARD_HOST=true and USE_X_FORWARD_PORT=true. Please refer to the hosting and security docs.

When using a domain subpath (e.g. /paperless), you need to set PAPERLESS_FORCE_SCRIPT_NAME=/paperless and adjust proxy_pass as well:

        ...
        location /paperless {

            # Adjust host and port as required.
            proxy_pass http://localhost:8000/paperless;
        ...

Also read this, towards the end of the section.

Some have found adding the P3P header (add_header P3P 'CP=""'; see #817) works; only IE and Edge support it.

Apache

Below is an example of an apache2 conf file that you may customize to fit your environment and needs.

    DEFINE local_url 127.0.0.1
    DEFINE local_port 8000
    DEFINE url_prefix paperless
    DEFINE public_url ${url_prefix}.my.domain
    DEFINE email ${url_prefix}@my.domain
    ServerTokens Prod
    SSLStaplingCache "shmcb:${APACHE_LOG_DIR}/stapling-cache(150000)"
    SSLSessionCache "shmcb:${APACHE_LOG_DIR}/ssl_scache(512000)"
    SSLSessionCacheTimeout 300
### If you have Google's Mod PageSpeed, disable it ###
#    ModPagespeed Off
<VirtualHost *:80>
    ServerName ${public_url}
    DocumentRoot /var/www/html
    ServerAdmin ${email}
    ErrorLog ${APACHE_LOG_DIR}/${url_prefix}.error.log
    CustomLog ${APACHE_LOG_DIR}/${url_prefix}.access.log combined
    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/\.well\-known/acme\-challenge/
    RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
    ServerName ${public_url}
    DocumentRoot /var/www/html
    ServerAdmin ${email}
    ErrorLog ${APACHE_LOG_DIR}/${url_prefix}.error.log
    CustomLog ${APACHE_LOG_DIR}/${url_prefix}.access.log combined
    SSLEngine On
    SSLCertificateFile /etc/letsencrypt/live/my.domain/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/my.domain/privkey.pem
### Forbid the http1.0 protocol ###
    Protocols h2 http/1.1
    Timeout 360
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyTimeout 600
    ProxyReceiveBufferSize 4096
    SSLProxyEngine On
    RequestHeader set Front-End-Https "On"
    ServerSignature Off
    SSLCompression Off
    SSLUseStapling On
    SSLStaplingResponderTimeout 5
    SSLStaplingReturnResponderErrors Off
    SSLSessionTickets Off
    RequestHeader set X-Forwarded-Proto 'https' env=HTTPS
    Header always set Strict-Transport-Security "max-age=15552000; preload"
    Header always set X-Content-Type-Options nosniff
    Header always set X-Robots-Tag none
    Header always set X-XSS-Protection "1; mode=block"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    RequestHeader set X-Real-IP %{REMOTE_ADDR}s
### Lax CSP and will not score the best on Mozilla Observatory or other platforms alike, but won't need to be updated with version changes ###
    Header always set Content-Security-Policy "default-src 'none'; base-uri 'self'; font-src 'self' data: ${public_url}; media-src 'self' blob: data: https: ${public_url}; script-src 'self' 'unsafe-inline' ${public_url}; style-src 'self' 'unsafe-inline' ${public_url}; img-src 'self' data: blob: https: ${public_url}; worker-src * blob:; frame-src 'self' https://${public_url}; connect-src 'self' wss: https: ${public_url}; form-action 'self'; frame-ancestors 'self' https://${public_url} https://my.domain https://*.my.domain; manifest-src 'self'; object-src 'self' https://${public_url}"
    Header always set Permissions-Policy 'geolocation=(self "https://${public_url}"), midi=(self "https://${public_url}"), sync-xhr=(self "https://${public_url}"), microphone=(self "https://${public_url}"), camera=(self "https://${public_url}"), magnetometer=(self "https://${public_url}"), gyroscope=(self "https://${public_url}"), fullscreen=(self "https://${public_url}"), payment=(self "https://${public_url}")'
    SSLHonorCipherOrder Off
### Use next two for very secure connections ###
    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
    SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
### Use next two for secure connections and support more endpoints ###
    #SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4
    #SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
### Actually proxy the traffic and really the only important part ###
    AllowEncodedSlashes On
    RewriteEngine On
    SetEnvIf Cookie "(^|;\ *)csrftoken=([^;\ ]+)" csrftoken=$2
    RequestHeader set  X-CSRFToken "%{csrftoken}e"
### Proxy Websockets Section 1 (works for me) ###
    RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC]
    RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC]
    RewriteRule ^/?(.*) "ws://${local_url}:${local_port}/$1" [P,L]
### Proxy Websockets Section 2 (untested) ###
    #RewriteCond %{HTTP:UPGRADE} =websocket [NC]
    #RewriteRule ^/ws/(.*) ws://${local_url}:${local_port}/ws/$1 [P,L]
### Proxy everything else ###
    ProxyPass / http://${local_url}:${local_port}/ connectiontimeout=6 timeout=60
    ProxyPassReverse / http://${local_url}:${local_port}/
### If Docker and/or Paperless-NGX server is down but webserver is up, show error page ###
    ErrorDocument 503 '<!DOCTYPE html>\n<html xml:lang="en" lang="en" dir="ltr" prefix="og: http://ogp.me/ns#">\n<meta http-equiv="refresh" content="15" />\n<head id="head">\n<meta http-equiv="X-UA-Compatible" content="IE=edge"/>\n<title>Offline</title>\n<style>html{width:100%}body{background-color:#a6a6a6;text-align:center;font-family:Helvetica,Tahoma}</style>\n</head>\n<body>\n<h1>${public_url}</h1>\n<p>Appears to be offline... will try again every 15 seconds.<br><br>Nothing happening? Contact the <a href="mailto:${email}" target="_blank">admin</a>.</p>\n</body>\n</html>'
</VirtualHost>

Caddy

Below is an example Caddy configuration

:80 {
    reverse_proxy http://localhost:8000 {
        header_down Referrer-Policy "strict-origin-when-cross-origin"
    }
}