Setting up NextCloud behind Caddy

I’m in the process of setting up Nextcloud using this guide.
I have it partially working.

my internal domain is something like “example.house” and my external domain is “example.com

I had already had my caddyfile set up for internal certs using Hetzner DNS challenges which works great. Now I’m adding a section at the top to handle the above situation (not docker, just a debian 11 VM running on the network).

Caddy file on internet accessible machine looks like this:

caddy.example.house {
  acme_server
  tls internal
  }

nextcloud.example.com {
  reverse_proxy https://nextcloud.example.house {
    header_up Host {http.reverse_proxy.upstream.hostport}
    header_up X-Forwarded-Host {host}
  }
}

#.... (more configured sites) ...

server.example.house {
#  acme_server
  reverse_proxy localhost
  tls {
    issuer acme {
    dns hetzner sEcReTcHaRaCtErS
    resolvers 8.8.8.8 8.8.4.4
    }
  }
}

Caddyfile on the nextcloud installation looks like this:

{
#       debug
        acme_ca https://caddy.example.house/acme/local/directory
        acme_ca_root /etc/ssl/certs/root.crt
}

nextcloud.example.house {
        #        tls {
        #                ca https://caddy.example.house/acme/local/directory
        #                ca_root /etc/ssl/certs/root.crt
        #        }

        root * /var/www/nextcloud
        file_server
        log {
                output file /var/log/caddy/nextcloud.log
                format single_field common_log
        }

        php_fastcgi 127.0.0.1:9000 {
                env PATH /bin
        }

        header {
                # enable HSTS
                Strict-Transport-Security max-age=31536000;
        }

        redir /.well-known/carddav /remote.php/dav 301
        redir /.well-known/caldav /remote.php/dav 301

        # .htaccess / data / config / ... shouldn't be accessible from outside
        @forbidden {
                path /.htaccess
                path /data/*
                path /config/*
                path /db_structure
                path /.xml
                path /README
                path /3rdparty/*
                path /lib/*
                path /templates/*
                path /occ
                path /console.php
        }

        respond @forbidden 404
}

And it works for the nextcloud.example.house domain (not HTTPS though)
but it does NOT work for the nextcloud.example.com case. The location bar changes to the local IP address (accessed internally) and I get a sadface page error that

this site can’t provide a secure connection.

Caddy log on the backend (nextcloud machine) shows:

{"level":"info","ts":1642718060.4591007,"msg":"serving initial configuration"}

What am I missing?

Make sure to enable the debug global option to make Caddy show more details in the logs.

Use curl -v to make test requests, and show us what happens.

I think this is probably a misconfiguration in NextCloud, you probably need to configure it with the domain you’re actually using in front.

1 Like

I found the section in nextcloud’s config.php to set the domains so now both domains work, but both are non-https. So thanks for that tip.

I had the debug on for a while but it spits out SO MUCH INFORMATION that my eyes go blurry. I will try it again. In the meantime, I did the curl -v. See below. Is it possible the DNS cert from Hetzner is conflicting with the other domain on that machine? they are different FQDNs and they’re using different challenges to different places.

External nextcloud address:

user@machine:~ $ curl -v https://nextcloud.example.com
* Expire in 0 ms for 6 (transfer 0xaaa8b0)
* Expire in 1 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 1 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 0 ms for 1 (transfer 0xaaa8b0)
* Expire in 2 ms for 1 (transfer 0xaaa8b0)
* Expire in 1 ms for 1 (transfer 0xaaa8b0)
* Expire in 1 ms for 1 (transfer 0xaaa8b0)
* Expire in 1 ms for 1 (transfer 0xaaa8b0)
*   Trying 123.456.789.100...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0xaaa8b0)
* Connected to nextcloud.example.com (123.456.789.100) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: none
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_CHACHA20_POLY1305_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=nextcloud.example.com
*  start date: Jan 18 16:31:09 2022 GMT
*  expire date: Apr 18 16:31:08 2022 GMT
*  subjectAltName: host "nextcloud.example.com" matched cert's "nextcloud.example.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0xaaa8b0)
> GET / HTTP/2
> Host: nextcloud.example.com
> User-Agent: curl/7.64.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 502
< server: Caddy
< content-length: 0
< date: Fri, 21 Jan 2022 02:08:43 GMT
<
* Connection #0 to host nextcloud.example.com left intact

internal nextcloud address:

user@machine:~ $ curl -v https://nextcloud.example.home
* Expire in 0 ms for 6 (transfer 0x1e5f8b0)
* Expire in 1 ms for 1 (transfer 0x1e5f8b0)
* Expire in 0 ms for 1 (transfer 0x1e5f8b0)
* Expire in 2 ms for 1 (transfer 0x1e5f8b0)
* Expire in 0 ms for 1 (transfer 0x1e5f8b0)
* Expire in 0 ms for 1 (transfer 0x1e5f8b0)
* Expire in 0 ms for 1 (transfer 0x1e5f8b0)
*   Trying 192.168.1.42...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x1e5f8b0)
* Connected to nextcloud.example.home (192.168.1.42) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: none
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection 0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

I’ll report back later after looking at debug output.

Really all you need to look at is the last like 10 or so lines at most, immediately after making an HTTP request. The rest is not so relevant.

This one looks okay, except that it responds with a 502 so that makes me think there’s a problem with the front Caddy connecting to the backend Caddy. If it was a problem with the backend Caddy connecting to PHP-FPM then you’d see two Server: Caddy lines (since they would both add their own Server header).

The error for this one means that curl doesn’t trust the TLS certificate that it was served by Caddy. If you used tls internal for that domain, then that’s to be expected unless you install the root CA cert that Caddy generated onto that machine.

I have broken something. When I try to use my external IP, caddy says:

an 21 13:39:36 nextcloud caddy[804]: {"level":"debug","ts":1642790376.593259,"logger":"http.stdlib","msg":"http: TLS handshake error from 192.168.1.15:37768: no certificate available for '192.168.1.42'"}
Jan 21 13:39:36 nextcloud caddy[804]: {"level":"debug","ts":1642790376.5972867,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"192.168.1.42"}
Jan 21 13:39:36 nextcloud caddy[804]: {"level":"debug","ts":1642790376.5973434,"logger":"tls.handshake","msg":"no certificate matching TLS ClientHello","server_name":"","remote":"192.168.1.15:37770","identifier":"192.168.1.42","cipher_suites":[43690,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"cert_cache_fill":0.0001,"load_if_necessary":true,"obtain_if_necessary":true,"on_demand":false}
Jan 21 13:39:36 nextcloud caddy[804]: {"level":"debug","ts":1642790376.5974622,"logger":"http.stdlib","msg":"http: TLS handshake error from 192.168.1.15:37770: no certificate available for '192.168.1.42'"}

I did try to play around with using hetzner to address the second curl example (instead of tls internal) and then switched back.

Ideally I’d like nice little lock icons in the URL bar like I have with my other URLs in my downstream server (the one identified as caddy.example.house). It’s running a few services on various ports under a few subdomains and it all works so well.

That’s normal, you can’t do HTTPS without the hostname in the request. The hostname is required to match the certificate being used for the TLS handshake.

Right, then you’ll need to add Caddy’s root CA cert to your system’s trust store, since it’s not signed by a publicly trusted CA. Browsers will throw up errors if the certificate can’t be verified to be signed by a trusted source.

You can find Caddy’s internal issuer’s root CA cert in Caddy’s storage location (depends how you’re running Caddy, you didn’t quite say because the post was originally a reply to the wiki topic and we moved it out).

1 Like

What puts the hostname in the request? caddy is running on the host / backend. caddy is running on the downstream as the acme_server. when a request comes in to my network on 443, it goes to the downstream machine and is reverse_proxy to the local internal address of the backend.

It’s set up just like the wiki example.

I’m running caddy on the downstream machine as a service with systemd and using a custom hetzner dns build binary. On the backend, I’m doing the same but with a vanilla build (no hetzner dns).

Incidentally I originally set up the root.crt the way it’s shown in the wiki entry. I pulled it from /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt on the downstream host and copied it to the backend host at /etc/ssl/certs/root.crt. I have verified multiple times that they match.

New update:
I think something is cached incorrectly somewhere. I just replaced “nextcloud.example.com” with “otherdomain.example.com” that happened to also be pointed at my network and it worked properly.

It’s still showing “Not secure | …”

It also stil redirects to the internal domain from the external one–kinda defeating the point of external domains.

To clarify, Francis is actually referring to the Server Name Indicator (SNI) of the TLS handshake. If that is missing, Caddy can only use the IP address to try to find a matching certificate that the client will accept. And if there is no certificate for that IP, then you’ll get “no certificate available.”

That makes sense that you can’t generate a cert for a missing name.

I’m still unclear on how that name is provided. Where in the chain is it added?

  1. when I type “nextcloud.example.com” into a browser
  2. when my Dynamic DNS service reroutes it to my WAN IP
  3. when my router forwards it to the matching machine for port 443
  4. when caddy on the downstream server recognizes it and reverse proxies it to “nextcloud.example.house”
  5. when nextcloud.example.house loads asks the acme_server (caddy.example.house) for a cert
  6. other?

And what would make X not provide it?

And remember, this was working yesterday. Until I goofed around that is. It does work with identical setups using other hostnames for some reason. That’s why I was wondering if caddy is caching or storing something that has bad/outdated information.

It’s passed during the TLS handshake:

OK it looks like it’s a local computer thing. For some reason the machine I remote into doesn’t resolve it properly. It does work on other machines on the lan.

So let’s start there:
I get local domain and public domain access.
I do not get https.
public domain redirects to local domain (I’m guessing this is a nextcloud config issue but I’m not sure)

How can I get https working on the second caddy server using DNS challenge or otherwise?

FYI since this is all running within your local network, you don’t really need mTLS to connect your front Caddy to your backend Caddy. You can just proxy over HTTP, which will make it simpler to manage. Your concern would be if some bad actor got into your local network to intercept/man-in-the-middle the requests between the Caddy instances. That’s generally pretty unlikely, and they can probably do worse things if they got into your network anyways (probably already game-over).

Just so we get the terminology right, do you really mean redirect or do you mean proxy? Those are distinct things. A redirect is an HTTP response with the Location header that tells the browser “try again but at this URL instead please”, whereas proxying means sending the request to another server and send back that response the client.

The guide in the wiki topic you came from has the right instructions for mTLS, so you could give that another run or read-through.

In this comment, what was the request you tried? Were the logs from your front Caddy, or from the backend?

Try making the request with curl -v and show what that does as well, I guess.

1 Like

Francis, first off thank you for all the help you’re giving me with this.

Second, I do want nextcloud.example.com available on the internet outside my local network.

(I also like to have internal https because it just looks and feels good–weird but caddy really makes it easy with the Hetzner dns challenge and Hetzner is free).

If I am inaccurate in describing the functions of all the servers, it’s because I’m somewhat less than an amateur at all this. It’s a home lab and very low budget.

when I say “public doman redirects to local domain” I mean this: I open a browser and type in nextcloud.example.com and the URL is replaced with nextcloud.example.house and nextcloud loads. If this were all internal, it’d be fine. But since I want outside access, it’s not gonna work out to have it pointing to a publically inaccessible domain.

This sounds like it’s still a misconfiguration of nextcloud. You probably want it configured with your external domain.

But if you make the request with curl -v we should be able to see what server is serving you the redirect.

Well what do you know.

Nextcloud generates its own config file when you first log in and add the database. It defaulted to the local address but doesn’t add a config entry explicitly for this. It does add a URL rewrite entry (which gave me a clue) but I had to manually add:

'overwritehost' => 'nextcloud.example.com',

once I did, the URL remained as the external one when entered AND https worked as expected.

other config entries that may have contributed or at least helped (in case someone else sees this):
putting my external domain as the first entry (0) in the “trusted domains” list
updating the overwrite.cli.url to the external domain

together those entries are:

  array (
    0 => 'nextcloud.example.com',
    1 => 'nextcloud.example.house'
  ),
  'overwritehost' => 'nextcloud.example.com',
  'overwrite.cli.url' => 'https://nextcloud.example.com',

So this did turn out to be a nextcloud mystery more than a caddy mystery, but I really do appreciate you sticking with me to find the problem and at least rule out what it wasn’t so I could figure out what it really was.

1 Like

Nice.

Glad you figured it out!

:relaxed:

1 Like

Sorry to hear about your struggle and sorry I’m too late. That part was actually in the guide.

Adding for reference:
Caddy reverse proxy + Nextcloud + Collabora + Bitwarden_rs with local HTTPS

1 Like

Hey look at that.

I was scrolling up and down a lot and must have missed it. There was a lot of information I was skipping around over the installs I wasn’t using.

1 Like

This topic was automatically closed after 29 days. New replies are no longer allowed.