DEV Community

Tatsuo Takahashi
Tatsuo Takahashi

Posted on

Why Nextcloud Locked Me Out of My Own Server (and How trusted_proxies Fixed It)

TL;DR

I moved a client file-sharing server from ownCloud on a reserved EC2 instance to Nextcloud on a $10 Lightsail box. Two things drove the move: the cost no longer made sense (a 1.7 GB workload sitting on a 24/7 instance, made worse by a reserved-instance commitment and a weakening yen), and the old ownCloud/OS stack had reached end-of-life.

The migration itself was trivial. Two authentication problems behind the Caddy reverse proxy were not:

  • Nextcloud locked me out with "Too many requests." Behind the proxy, every request looked like it came from Caddy's container IP (172.18.0.2), so Nextcloud's brute-force protection saw one IP hammering the login and blocked it. Fixed with trusted_proxies.
  • Basic auth and Nextcloud fought over the Authorization header. My first fix (header_up -Authorization inside reverse_proxy) silently did nothing. Moving it to request_header -Authorization at the site-block level fixed it.

This post covers the cost reasoning, why I picked Nextcloud over object storage or a SaaS, and both fixes in detail.


The background

For a few years, my wife's design business has exchanged deliverables — banners, image assets — with a client through a self-hosted ownCloud instance running on EC2.

It worked, but the economics stopped making sense:

  • The actual data was 1.7 GB. That's it. Everything else on the disk was years-old junk (an 8-year-old VirtualBox installer, a Documents folder nobody had touched in 7 years).
  • To host 1.7 GB, a full EC2 instance ran 24/7.
  • To make that "cheaper," I'd bought a Reserved Instance — which is a commitment to keep paying for a server I shouldn't have needed at all.
  • Then the yen weakened. My AWS bill is denominated in USD, so even a "fixed" reservation quietly got more expensive in the currency I actually pay in.

So I was paying a premium, locked in, in a strengthening currency, to host 1.7 GB of files on an always-on server. Every layer of that sentence is a mistake.

On top of the cost, the ownCloud version and its OS had reached end-of-life. I spend a lot of my day job on security work, and knowingly exposing an EOL PHP stack to the public internet is exactly the kind of attack surface I'd flag for a client. I wanted it gone.

Why Nextcloud (and not object storage or a SaaS)

I looked at three options:

Option Cost Trade-off
Cloudflare R2 + presigned URLs Cheapest (~$0/mo at this scale) Presigned URLs expire in 7 days max; the client is used to a persistent folder they can revisit
SaaS (Dropbox / Google Workspace) ~$10–20/mo, zero ops A completely different UI; the client would have to relearn the workflow
Nextcloud on Lightsail $10/mo fixed I run the box, but the client sees almost no change

The deciding factor wasn't cost or ops. It was the client's experience.

ownCloud and Nextcloud share a lineage — Nextcloud is a fork of ownCloud — so their web UIs are nearly identical. Moving to Nextcloud meant my wife's client could keep opening the same kind of folder, see the same kind of layout, and never really notice the server underneath had changed. Object storage or a SaaS would have meant retraining a client on my schedule, for my cost problem. That's backwards.

(There's a small irony here: years ago I chose ownCloud over Nextcloud specifically to avoid Nextcloud's faster release cadence and upgrade churn. That decision is what left me on an EOL stack. The migration target turned out to be the very thing I'd avoided.)

The migration itself was the easy part

With only 1.7 GB of real data, moving it took minutes. ownCloud speaks WebDAV, so rclone pulls it straight across:

rclone copy owncloud:ClientFolder r2:deliverables --progress
Enter fullscreen mode Exit fullscreen mode

I left the dead Documents/Photos/.exe files behind and moved only the live folder. The new Nextcloud runs in Docker Compose behind Caddy, which handles TLS automatically.

Then the two real problems started.

Gotcha 1: "Too many requests" — Nextcloud locked me out

Right after the migration, I tried to log in and got hit with:

Too many requests
There were too many requests from your network. Retry later or contact your administrator if this is an error.
Enter fullscreen mode Exit fullscreen mode

My first reaction was the correct one: wait, why me? I just set this up.

The cause is a classic reverse-proxy trap. Nextcloud has brute-force protection that counts failed login attempts per IP address. But behind Caddy, Nextcloud doesn't see the real client IPs — every single request arrives from Caddy's container address on the Docker bridge network, 172.18.0.2.

So from Nextcloud's point of view, one IP (172.18.0.2) was responsible for every login attempt from everyone. It didn't take much before that single "IP" tripped the threshold, and Nextcloud dutifully blocked it — which is to say, it blocked everybody, including me.

The fix is to tell Nextcloud which upstream addresses are trusted proxies, so it looks at the forwarded client IP instead of the proxy's own. In Nextcloud that's the trusted_proxies config:

docker compose exec --user www-data app \
  php occ config:system:set trusted_proxies 0 --value="172.18.0.0/16"
Enter fullscreen mode Exit fullscreen mode

Once Nextcloud trusts the proxy network, it reads the real client IP from the forwarded headers, and brute-force counting works per actual client again — not per proxy. The lock-out stopped immediately.

If you clear an existing lockout while testing, you can also reset the brute-force attempts, but fixing trusted_proxies is the part that stops it recurring.

Gotcha 2: Basic auth vs. Nextcloud's Authorization header

The access model I wanted matched the old setup, so the client's experience wouldn't change:

  • Office IPs (mine and the client's) → straight to the Nextcloud login, no extra step.
  • Everywhere else (e.g. the client's manager working remotely) → a Basic auth prompt first, then the Nextcloud login.

Basic auth at the proxy is exactly what the old ownCloud setup did at its front Apache, so I expected this to be routine. In Caddy:

files.example.com {
    # Remote (non-office) requests must pass Basic auth first
    @remote not remote_ip 203.0.113.10/32 203.0.113.20/32
    basic_auth @remote {
        remote_user <BCRYPT_HASH>
    }

    reverse_proxy app:80
}
Enter fullscreen mode Exit fullscreen mode

This authenticated correctly, but then Nextcloud threw a 401. The reason: newer Nextcloud interprets an incoming Authorization: Basic ... header as an attempt to log into its own API. It takes the Basic credentials I meant for the proxy, tries to match them against a Nextcloud user, finds none, and rejects the request.

In other words, the same Basic auth header that got the visitor through the proxy then confused Nextcloud. This isn't Caddy-specific — put any front (Apache, nginx, Caddy) in front of this Nextcloud and the app will still try to consume that header. The fix is to strip the Authorization header after the proxy consumes it, before it reaches Nextcloud.

My first attempt did this inside the reverse_proxy block:

reverse_proxy app:80 {
    header_up -Authorization   # ← silently did nothing
}
Enter fullscreen mode Exit fullscreen mode

It didn't work. basic_auth leaves the header on the request, and header_up runs at a point in the pipeline where the removal didn't take effect against it — the credentials still reached Nextcloud.

What did work was removing the header at the site-block level, before the request is proxied at all, using request_header instead of header_up:

files.example.com {
    @remote not remote_ip 203.0.113.10/32 203.0.113.20/32
    basic_auth @remote {
        remote_user <BCRYPT_HASH>
    }

    # Strip the Basic credentials BEFORE proxying,
    # so Nextcloud never tries to consume them.
    request_header -Authorization

    reverse_proxy app:80
}
Enter fullscreen mode Exit fullscreen mode

The order matters: request_header -Authorization sits at the site level and rewrites the request before the proxy stage, so by the time Nextcloud sees the request, the header is gone. Remote visitors now clear Basic auth and then get a clean Nextcloud login — no 401, and nothing new to learn.

Why I didn't just switch to 2FA

The "cleaner" fix would have been to drop Basic auth and turn on Nextcloud's two-factor auth. I deliberately didn't. The client had always logged in with just an ID and password; making them set up an authenticator app — to solve my migration problem — would be pushing my cost decision onto their workflow. Keeping the experience identical was the whole point of choosing Nextcloud in the first place.

The result

  • Cost: an always-on Reserved EC2 instance (in a rising currency) → a flat $10/mo Lightsail box.
  • Security: EOL stack retired, TLS auto-managed by Caddy, brute-force protection working correctly per real client IP.
  • Client experience: unchanged. Office logs in directly; remote passes one Basic auth prompt first — exactly like before.

The lesson I keep coming back to: a Reserved Instance felt like a cost optimization, but it was really a commitment to a design I should have questioned. The cheapest server is often the one you stop needing to run 24/7 at all.


If you run Nextcloud behind a reverse proxy and see "Too many requests," check trusted_proxies first — it's almost always the proxy IP getting blamed for everyone.

Top comments (0)