How to serve the Incus web UI via reverse proxy (caddy)

This is a quick tutorial for serving the Incus web UI via caddy.

I have the web UI exposed on my local network so I can access it via local IP/domain. I also want to be able to access it outside my LAN just like I access many of my other self-hosted services.

By default, caddy provisions certs and handles HTTPS for you, so when you try to access the web UI via caddy, you’re not using your Incus cert. As such, you won’t be authenticated and Incus will ask you to generate a new cert.

The idea here is to get caddy to use the cert generated and trusted by Incus to authenticate.


WARNING: If your caddy instance is exposed to the public internet, you will be exposing your Incus host as well. Anyone who can connect to caddy will have full access to Incus.

It is assumed that your caddy instance is behind some form of authentication such as caddy-security, Authelia, etc.


Prerequisites

Unless you haven’t set up the web UI at all yet, you probably already generated a cert and told Incus to trust it. We’ll remove this cert so Incus forces us to generate a new one.

Get your cert’s fingerprint so you can remove it:

  • incus config trust list
  • incus config trust remove <fingerprint>

Generate a new cert

Go the web UI where you’ll be asked to generate a cert. Set a password for the cert (you’ll need it later).

Copy this cert to Incus and trust it:

  • incus config trust add-certificate incus-ui.crt

Go through the rest of the setup, including the download of the .pfx file, and make sure you can fully access Incus.

Set up your client auth cert and key

Extract the cert and key from the .pfx file (you should be prompted for your cert password):

  • openssl pkcs12 -in incus-ui.pfx -clcerts -nokeys -out incus-client.crt
  • openssl pkcs12 -in incus-ui.pfx -nocerts -nodes -out incus-client.key

Copy incus-client.crt and incus-client.key to wherever you’re running caddy.

  • If you’re using caddy in docker or something like that, you will of course need to adjust as needed.

Set permissions/owner so caddy can read them:

  • sudo chmod 644 incus-client.crt incus-client.key
  • sudo chown caddy:caddy incus-client.crt incus-client.key

The files also need to be in a directory caddy can read (this is the caddy user’s home dir):

  • sudo mv incus-client.crt incus-client.key /var/lib/caddy/

Caddyfile

Because we’re proxying HTTPS → HTTPS, we have to set tls_insecure_skip_verify. Setting tls_insecure_skip_verify has security implications. Use at your own risk.

By using tls_client_auth, we can tell caddy to use our cert to authenticate with Incus, just like we do with our browser. This means all requests made to Incus via caddy will be authenticated by default. This effectively turns off authentication for Incus when accessed via caddy (see the warning at the beginning of this post).

incus.example.com {
  reverse_proxy https://incus.lan:8000 {
    transport http {
      tls_insecure_skip_verify
      tls_client_auth /var/lib/caddy/incus-client.crt /var/lib/caddy/incus-client.key
    }
  }
}

Replace incus.example.com and https://incus.lan:8000 as appropriate.

Restart caddy and you’re done. You should be able to access Incus on your local network and through caddy.

4 Likes

Thanks for this, I moved it to the Tutorials category.

2 Likes

This should probably come with a huge warning that this grants full incus access to whomever can reach that caddy instance. And it’s probably also possible to use this to get root on the incus host itself as you’ve basically got authenticated api access with admin privileges.

Nonetheless it’s interesting to see that is possible and i don’t want to sound to harsh but this seems like really bad practice and really should be avoided IMHO.
Proxying incus via caddy in itself seems fine (altough even there i would carefully consider which clients/hosts should have access since if there ever is an security issue with the API authorizaiton it helps mitigating the imidiate risk if not everyone on the internet can reach it). But then Client Authorization really should happen on the client itself via one of the currently two ways to authenticate (TLS client certificates or OpenID Connect) and not via the proxy: Remote API authentication - Incus documentation

That’s a good point, I’ll add a warning.

Does this really give full auth though? Someone would be able to see the web UI and be shown the authentication/cert page, but they would have no way of getting Incus to trust that cert, right? For this to be an issue they would have to have no authentication on their SSH server or something.

The way i understand this (haven’t tried it) is that you are using caddy to inject the TLS Client certificate during the proxying and the individual client therefore doesn’t need to authenticate with incus.

So i don’t think a client would see the authentication/cert page because they are always already authenticated?

And regarding full root access to the incus host: I haven’t experimented with this but I would suspect you could do stuff via directory storage pools and mounting that inside a VM. That would be at least what i would try first if i would want to do something in that regard. But there are other vectors you could attempt.
It’s mentioned mutliple times in the docs that incus admin privileges should only be given to people you would also trust with root access to the host:
https://linuxcontainers.org/incus/docs/main/explanation/security/#local-access-to-the-incus-daemon
https://linuxcontainers.org/incus/docs/main/authorization/#openfga-model

1 Like

Ah yeah, you’re completely right. So this effectively turns off authentication to the web UI when accessed via caddy. Gonna update the warning to make that clear.

2 Likes

There was a post recently on this forum that was asking how to implement such a feature (access to the Web UI without supplying the certificate to the browser).

Such a feature (ability to access the Web UI without authentication) is useful in some scenarios, those when the URL is not accessible from a public URL, and when an additional static password has been set to access the local URL (HTTP Auth).