CSRF verification fails behind Apache2 proxy

I have installed Passbolt CE (Docker version) on a server that uses Apache2 as a reverse proxy. The installation went smoothly, and the health check shows no issues. I created an admin user, but when I try to log in with this user’s email address, I get the message:

Access to this service requires an invitation.

This email is not associated with any approved users on this domain. Please contact your administrator to request an invitation link.

This front-end message is rather misleading, because the Docker log reports the following error:

error: [Cake\Http\Exception\InvalidCsrfTokenException] Missing or incorrect CSRF cookie type. in /usr/share/php/passbolt/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php on line 392

When I check the submitted request header, I do see that X-CSRF-TOKEN is set correctly, but I suspect that it somehow gets dropped when passing through the reverse proxy. Previously I have set-up Passbolt using a Cloudflare tunnel and I never encountered this issue, reinforcing my suspicion.

I have tried to “manually” re-insert the X-CSRF-TOKEN from the csrfToken cookie, but this didn’t make any difference. Any ideas on how to debug/fix this?

Apache2 config:

<VirtualHost *:443>
  ServerName [redacted]
  ServerAdmin [redacted]

  RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
  RewriteRule .* https://127.0.0.1:4543%{REQUEST_URI} [P,QSA,L]
  # RequestHeader set X-Forwarded-Proto "https"
  
  # My attempt at fixing the request header
  # SetEnvIf Cookie "(^|;\ *)csrfToken=([^;\ ]+)" MyCookieValue=$2
  # RequestHeader set X-CSRF-Token "%{MyCookieValue}e"

  Timeout 28800
  KeepAlive On

  <Location /pass>
    ProxyPreserveHost On
    ProxyPass https://127.0.0.1:4543
    ProxyPassReverse https://127.0.0.1:4543
    ProxyPassReverseCookieDomain 127.0.0.1 [redacted]
  </Location>

  SSLEngine on
  SSLProxyEngine on

  SSLCertificateFile /etc/letsencrypt/live/[redacted]/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/[redacted]/privkey.pem
  Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>

Health check result:

     ____                  __          ____  
    / __ \____  _____ ____/ /_  ____  / / /_ 
   / /_/ / __ `/ ___/ ___/ __ \/ __ \/ / __/ 
  / ____/ /_/ (__  |__  ) /_/ / /_/ / / /    
 /_/    \__,_/____/____/_.___/\____/_/\__/   

 Open source password manager for teams
-------------------------------------------------------------------------------
 Healthcheck shell...............................................................                                                             
-------------------------------------------------------------------------------

 Environment

  [INFO]  Linux ee4eb03b429f 5.4.0-204-generic #224-Ubuntu SMP Thu Dec 5 13:38:28 UTC 2024 x86_64 GNU/Linux
  [PASS]  PHP version 8.2.26.
  [PASS]  PHP version is 8.1 or above.
  [PASS]  64-bit architecture system detected.
  [INFO]  gpg (GnuPG) 2.2.40 / libgcrypt 1.10.1
  [PASS]  PCRE compiled with unicode support.
  [PASS]  Mbstring extension is installed.
  [PASS]  Intl extension is installed.
  [PASS]  GD or Imagick extension is installed.
  [PASS]  The temporary directory and its content are writable and not executable.
  [PASS]  The logs directory and its content are writable.
  [WARN]  System clock and NTP service information cannot be found.
  [HELP]  See `timedatectl | grep -i -A 1 clock`. More information: https://www.passbolt.com/docs/hosting/configure/ntp/

 Config files

  [PASS]  The application config file is present
  [WARN]  The passbolt config file is missing in /etc/passbolt/
  [HELP]  Copy /etc/passbolt/passbolt.default.php to /etc/passbolt/passbolt.php
  [HELP]  The passbolt config file is not required if passbolt is configured with environment variables

 Core config

  [PASS]  Cache is working.
  [PASS]  Debug mode is off.
  [PASS]  Unique value set for security.salt
  [PASS]  Full base url is set to https://[redacted]
  [PASS]  App.fullBaseUrl validation OK.
  [PASS]  /healthcheck/status is reachable.

 SSL Certificate

  [PASS]  SSL peer certificate validates.
  [PASS]  Hostname is matching in SSL certificate.
  [PASS]  Not using a self-signed certificate.

 SMTP settings

  [PASS]  The SMTP Settings plugin is enabled.
  [PASS]  SMTP Settings coherent. You may send a test email to validate them.
  [WARN]  The SMTP Settings source is: env variables.
  [HELP]  It is recommended to set the SMTP Settings in the database through the administration section.
  [WARN]  The SMTP Settings plugin endpoints are enabled.
  [HELP]  It is recommended to disable the plugin endpoints.
  [HELP]  Set the PASSBOLT_SECURITY_SMTP_SETTINGS_ENDPOINTS_DISABLED environment variable to true.
  [HELP]  Or set passbolt.security.smtpSettings.endpointsDisabled to true in /etc/passbolt/passbolt.php.
  [PASS]  No custom SSL configuration for SMTP server.

 JWT Authentication

  [PASS]  The JWT Authentication plugin is enabled.
  [PASS]  The /etc/passbolt/jwt/ directory is not writable.
  [PASS]  A valid JWT key pair was found.

 GPG Configuration

  [PASS]  PHP GPG Module is installed and loaded.
  [PASS]  The environment variable GNUPGHOME is set to /var/lib/passbolt/.gnupg.
  [PASS]  The directory /var/lib/passbolt/.gnupg containing the keyring is writable by the webserver user.
  [PASS]  The server OpenPGP key is not the default one.
  [PASS]  The public key file is defined in /etc/passbolt/passbolt.php and readable.
  [PASS]  The private key file is defined in /etc/passbolt/passbolt.php and readable.
  [PASS]  The server key fingerprint matches the one defined in /etc/passbolt/passbolt.php.
  [PASS]  The server public key defined in the /etc/passbolt/passbolt.php (or environment variables) is in the keyring.
  [PASS]  There is a valid email id defined for the server key.
  [PASS]  The public key can be used to encrypt a message.
  [PASS]  The private key can be used to sign a message.
  [PASS]  The public and private keys can be used to encrypt and sign a message.
  [PASS]  The private key can be used to decrypt a message.
  [PASS]  The private key can be used to decrypt and verify a message.
  [PASS]  The public key can be used to verify a signature.
  [PASS]  The server public key format is Gopengpg compatible.
  [PASS]  The server private key format is Gopengpg compatible.

 Application configuration

  [PASS]  Using latest passbolt version (4.11.0).
  [PASS]  Passbolt is configured to force SSL use.
  [PASS]  App.fullBaseUrl is set to HTTPS.
  [PASS]  Selenium API endpoints are disabled.
  [PASS]  Search engine robots are told not to index content.
  [INFO]  The Self Registration plugin is enabled.
  [INFO]  Registration is closed, only administrators can add users.
  [PASS]  The deprecated self registration public setting was not found in /etc/passbolt/passbolt.php.
  [WARN]  Host availability checking is disabled.e
  [HELP]  Make sure this instance is not publicly available on the internet.
  [HELP]  Or set the PASSBOLT_EMAIL_VALIDATE_MX environment variable to true.
  [HELP]  Or set passbolt.email.validate.mx to true in /etc/passbolt/passbolt.php.
  [PASS]  Serving the compiled version of the javascript app.
  [WARN]  Some email notifications are disabled by the administrator.e
  [PASS]  The database schema is up to date.

 Database

  [PASS]  The application is able to connect to the database
  [PASS]  34 tables found.
  [PASS]  Some default content is present.

  [PASS]  No error found. Nice one, sparky!

Checklist
I have read intro post: https://community.passbolt.com/t/about-the-installation-issues-category/12
I have read the tutorials, help and searched for similar issues
I provide relevant information about my server (component names and versions, etc.)
I provide a copy of my logs and healthcheck
I describe the steps I have taken to trouble shoot the problem
I describe the steps on how to reproduce the issue

A new bit of information: previously the Passbolt instance was configured to be found at https://my.domain.com/pass, which caused problems with the CSRF token. But I managed to get it to work with an alternative location https://pass.domain.com (without a subdirectory). I suspect that I misconfigured Apache2 to properly account for the subdirectory /pass, but I still don’t understand why the configuration given in the original post doesn’t work (or why it does work without a subdirectory, for that matter).

For now I’ll stick with a root domain, but I hope that someone smarter than me can explain how to make it work for a subdirectory.