Desktop app & browser extension: "Could not verify the server key" — request never reaches backend, affects ALL users

Checklist

  • I have read the intro post: About the Installation Issues category

  • I have read tutorials/help and searched for similar issues

  • I provide relevant information about my server below

Environment

  • Passbolt version: Community Edition 5.14.3 (recently upgraded from 5.14.0 during this investigation)

  • Server OS: Ubuntu (Noble), kernel 6.8.0-137-generic

  • Passbolt server hostname: srv-pass, internal IP 192.168.55.184

  • Passbolt listens on HTTP/80 only (no local TLS)

  • Reverse proxy: separate Nginx server (192.168.55.188) managed by another administrator, terminates HTTPS, I do not have access to it

  • Public URL: https://it.sicc.gov.ma

  • Certificate: Sectigo (*.sicc.gov.ma), issued via “Sectigo Public Server Authentication CA DV R36”

Problem
The Passbolt desktop app and browser extension (Chrome, latest 5.14.3 extension, tested on multiple machines/users/browsers) suddenly stopped working about a week ago after months of normal operation. Error shown:

text

Something went wrong!
Could not verify the server key.
Server internal error. Check with your administrator.

This affects all users, across different browsers and machines — not isolated to one client.

Secondary Symptom: Blank Gray Page During Account Recovery
When attempting account recovery (/setup/recover/start/...), the page loads to a blank gray screen instead of the recovery UI. The extension’s background service worker console shows repeated errors during this flow:

text

Error: The user id cannot be empty
at J.__validate (index.js:2:57445)
at J._getLocal (index.js:2:58728)
at J.get (index.js:2:58333)
Error: The user is not set
at J._getLocal (index.js:2:58805)
quickaccessPagemod::attach legacy account cannot be retrieved, please contact your administrator.
TypeError: Cannot read properties of undefined (reading 'toDto')

This occurred even after a completely clean extension reinstall (removed extension, cleared site data/cookies for the domain, restarted browser, reinstalled from Chrome Web Store) on multiple machines — same errors persist. This suggests the extension either can’t complete initialization against the server at all, or is failing before it can establish a valid session, consistent with the “could not verify the server key” failure happening earlier in the same handshake.

What I’ve Already Ruled Out

  • GPG server key: healthcheck passes every GPG-related check (encrypt/decrypt/sign/verify, fingerprint match, keyring presence).

  • Database: healthy, schema up to date, connects fine.

  • Local Nginx/PHP-FPM on srv-pass: no blocking rules, syntax OK, PHP-FPM responds.

  • Firewall on srv-pass: ufw inactive, iptables all-ACCEPT/empty.

  • fail2ban: not installed on srv-pass.

  • Direct backend tests: curl http://127.0.0.1/auth/verify.json?api-version=v2 and curl http://192.168.55.184/auth/verify.json?api-version=v2 (from LAN) both return HTTP 200 with valid JSON containing the correct server public key and fingerprint.

  • Browser extension: fully removed, browser storage/cookies cleared, freshly reinstalled (5.14.3) on multiple machines — issue persists identically.

  • Passbolt CE upgraded 5.14.0 → 5.14.3 cleanly (migrations ran fine, cleanup shell reports “no issue found”) — did not resolve the problem.

Key Diagnostic Finding
While tailing /var/log/nginx/access.log on srv-pass in real time, a colleague triggered the “Could not verify the server key” error from the desktop app. Zero log entries appeared on srv-pass during the failure — meaning the request never reaches the Passbolt backend at all. Earlier, unrelated traffic from the reverse proxy IP (192.168.55.188) does appear in Passbolt’s error log for other requests (e.g., bare / route lookups), confirming the proxy can reach the backend in general — but the specific /auth/verify.json request during a live failure never arrives.

Also Found: Incomplete TLS Certificate Chain

text

openssl s_client -connect it.sicc.gov.ma:443 -servername it.sicc.gov.ma

Returns:

text

Verify error:num=20:unable to get local issuer certificate
Verify error:num=21:unable to verify the first certificate
Verify return code: 21 (unable to verify the first certificate)

curl -k (ignoring chain validation) succeeds and returns HTTP 200 with the correct JSON payload. This suggests the reverse proxy is serving only the leaf certificate, not the full chain with the Sectigo intermediate.

Also Found: NTP/Clock Sync Failure on Backend
A recent healthcheck run surfaced:

text

[FAIL] NTP service is active but the system clock is not synchronized.

This was passing in an earlier healthcheck run in this same investigation, so it may be a new/independent issue. Currently investigating whether this contributed to signature/timestamp validation failures, or is unrelated.

My Working Hypothesis
Since curl with -k succeeds while stricter clients (browser extension, desktop app) fail uniformly across all users and machines, I suspect either:

  1. The reverse proxy is blocking/dropping the specific /auth/verify.json request (WAF rule, location block, or similar) before forwarding it to the backend, or

  2. The incomplete certificate chain is causing clients that do full TLS chain validation to abort the connection before any HTTP request is even sent (explaining why nothing reaches the backend’s logs and why the recovery flow also fails to properly initialize).

Questions for the Community/Team

  1. Does the Passbolt desktop app or browser extension enforce stricter TLS certificate chain validation than a typical browser tab load? Could an incomplete intermediate chain cause exactly this “could not verify the server key” error while normal page navigation still partially works?

  2. Could a TLS/handshake failure during setup/recovery cause the extension to hang on a blank gray page with “user id cannot be empty” / “user is not set” errors, as opposed to a client-side storage corruption issue?

  3. Are there known cases where NTP/clock drift on the backend causes GPG signature or JWT timestamp validation to fail in a way that produces this specific error message?

  4. Any recommended way to get more verbose/debug logging out of the desktop app or extension to see exactly where in the verification handshake it’s failing?

Happy to provide more logs/output as needed.

G’day emad, and welcome to the forum.

I think two of your key findings might be pointing the wrong way. Worth re-testing both before you go after the proxy.

The curl test didn’t hit the failing step

/auth/verify.json does two different jobs depending on the method. A GET just hands back the server’s public key. The check the extension calls “verify the server key” is a POST to that same URL, and it lands in the login controller: the client encrypts a nonce to the public key, the server decrypts it with its private key and echoes it back in a response header.

So your 200 from curl http://127.0.0.1/auth/verify.json tells you the public key file is readable. It never goes near the private key or the handshake.

Two things follow from that, and they’re the interesting part. The POST returns 200 even when it fails, and the actual result travels in X-GPGAuth-* response headers rather than in the status code or body. So anything in the path that strips or rewrites response headers, or hands back an HTML error page instead of JSON, breaks login while every server-side check still looks fine.

The access log might be the wrong file

If that’s the Debian package, passbolt’s vhost writes its own:

/var/log/nginx/passbolt-access.log
/var/log/nginx/passbolt-error.log

A server-level access_log overrides the global one, so passbolt traffic never shows up in /var/log/nginx/access.log. That would explain zero entries whether or not the request arrived. Worth re-running your live test against passbolt-access.log.

What the wording is telling you

“Could not verify the server key.” is just a prefix the extension puts on whatever failed underneath. The “Server internal error. Check with your administrator.” part is the specific bit: once the handshake fails, the extension re-fetches the key with a GET to work out why, and it’s that second request that threw.

That happens if the GET fails at the network or TLS layer, returns a non-2xx, or comes back as something that isn’t JSON. A WAF or proxy error page would do it.

So your TLS theory fits the message. But you’ve only tested that endpoint from the LAN over plain HTTP, and through the public URL with -k. From an affected client machine, I’d try:

curl -i https://it.sicc.gov.ma/auth/verify.json
curl -s -D - -o /dev/null https://it.sicc.gov.ma/auth/verify.json | grep -i gpgauth
curl -s -D - -o /dev/null http://192.168.55.184/auth/verify.json | grep -i gpgauth

The first one without -k: if it fails where -k succeeds, there’s your chain. The other two compare the GPGAuth headers through the proxy against the backend. Every passbolt response carries a handful of them, so if they’re on the backend and gone through the public URL, that’s where I’d look.

Your four questions

1. Neither client does any pinning or chain validation of its own, so nothing in passbolt is stricter than a browser. The stacks do differ though. The Chrome extension uses plain fetch, same network stack and trust store as a tab, so if the site loads cleanly in a tab the extension should validate the same way. The desktop app goes through .NET’s HttpClient rather than the browser engine, so it’s Schannel and the Windows certificate store. Whether a missing intermediate gets recovered is down to those stacks, not to us, and they don’t necessarily behave alike.

That’s a handy way to split it. If Chrome loads the site with no warning but the extension still fails, the chain probably isn’t the extension’s problem and I’d look harder at the proxy. If the tab warns too, the chain likely explains both. The openssl commands for checking it are here: https://www.passbolt.com/docs/hosting/tls/troubleshooting/

2. I reckon those console errors are a red herring. “The user id cannot be empty” and “The user is not set” are what an extension with no account configured prints, and they turn up during normal browsing. The quickaccess line and the toDto error come from the popup asking for an account that doesn’t exist yet, which has nothing to do with recovery. After a clean reinstall all four are expected.

That said, the blank page may well share the same cause: the recover bootstrap calls that same GET /auth/verify.json, and when it fails the extension removes its own overlay instead of showing an error. Worth knowing the recover page runs in its own frame, so its console is separate from the service worker one you were reading.

3. Not for this one, as far as I can tell. The nonce carries no timestamp and the server-key step reads no clock at all. The login token does expire after five minutes, but the same server clock writes it and reads it, so steady drift cancels out. Only a clock jump mid-handshake would actually break it.

Still worth fixing. TOTP codes get a single 30-second window, and account setup rejects a key whose creation date looks like it’s in the future, with no margin at all, so a slow server clock breaks new setups. https://www.passbolt.com/docs/hosting/ntp/

4. There’s no verbose mode in either client. The extension’s debug flag is baked in at build time, so the service worker console is all you get, and the desktop app ships with devtools off (there are separate debug builds on its GitHub releases page if it comes to that).

The response headers on that failing POST are more useful anyway. X-GPGAuth-Progress is set at every stage, so it tells you how far the handshake actually got.

If the header comparison does show a difference, that’s the thing to take to whoever runs the proxy.

Let us know how you go.

Cheers
Gareth

Thanks for the earlier guidance. I continued troubleshooting and I think I’ve now found the actual failure point.

The browser extension is definitely installed and enabled (Passbolt extension 5.14.3, site access allowed on all sites), and I also tested after clean extension reinstalls. The recovery page and its resources all load successfully, including the recovery JSON, which returns HTTP 200 and the correct active user account.

The important breakthrough came from the extension’s own index.html console. During the failure it shows:

GET /auth/is-authenticated.json?api-version=v2
→ 401

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

PassboltBadResponseError:
An internal error occurred. The server response could not be parsed.

I then tested that endpoint directly from the affected Windows client.

Through the public URL:

https://it.sicc.gov.ma/auth/is-authenticated.json?api-version=v2

I get:

HTTP/1.1 401 Unauthorized
Content-Type: text/html

and the body is our custom HTML page:

“Accès indisponible en dehors des horaires autorisés”

with the configured access window of 07h00–21h00.

However, when I bypass the public HTTPS path and request the Passbolt backend directly:

http://192.168.55.184/auth/is-authenticated.json?api-version=v2

I get:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

with the normal Passbolt JSON response:

{
  "header": {
    "status": "error",
    "message": "Authentication is required to continue",
    "code": 401
  }
}

So the difference appears very clear now:

Public HTTPS
→ /auth/is-authenticated.json
→ custom access-hours layer
→ 401 + HTML
→ Passbolt extension tries to parse JSON
→ Unexpected token '<'
→ PassboltBadResponseError

while the direct backend returns the expected JSON response.

There is another interesting detail: the public /auth/verify.json GET currently works and returns HTTP 200 with the expected X-GPGAuth-* headers, so the incomplete TLS chain may not be the immediate cause of the current failure.

I also checked the recovery flow. The recovery JSON returns HTTP 200 and the correct active account, and the recovery page loads the HTML/JS/iframe/resources successfully. The blank page appears afterward during extension initialization.

The attached screenshots show the extension console errors, the recovery Network panel, and the desktop app behavior.

For the desktop app specifically: my user/account was already configured a long time ago and was working normally for months. I can open the desktop app, enter my existing passphrase, and then it fails with:

“Something went wrong, the sign in failed with the following error: An internal error occurred. The server response could not be parsed. Please contact your administrator.”

So this is not an initial setup problem.

At this point, I’m honestly not sure what to do next. I’ve reached the point where I can clearly see that the public path is returning the wrong response for the Passbolt API, but I don’t have access to the reverse-proxy server/configuration, which is managed by another administrator.

I’m not sure whether I should ask the reverse-proxy administrator to exclude the Passbolt API/authentication endpoints from the access-hours rule, whether there is another Passbolt-specific configuration I should check on the backend, or whether something else in the proxy is rewriting the response.

Any guidance on what I should investigate or ask the reverse-proxy administrator to check next would be greatly appreciated.