this post was submitted on 16 Aug 2026
52 points (100.0% liked)

Selfhosted

61513 readers
1447 users here now

A place to share alternatives to popular online services that can be self-hosted without giving up privacy or locking you into a service you don't control.

Rules:

Detailed Rules Post

  1. Be civil.

  2. No spam.

  3. Posts are to be related to self-hosting.

  4. Don't duplicate the full text of your blog or readme if you're providing a link.

  5. Submission headline should match the article title.

  6. No trolling.

  7. Promotion posts require active participation, with an account that is at least 30 days old. F/LOSS without a paywall has exceptions, with requirements. See the rules link for details. Tags [CBH] or [AIP] are required, see the links in Rule 8 for details.

  8. AI-related discussions and AI-involved promotional posts have additional requirements for tagging, as noted in Rule 7 and the AI & Promotional Post Expanded Rules post, and find example disclosures here.

Resources:

Any issues on the community? Report it using the report flag.

Questions? DM the mods!

founded 3 years ago
MODERATORS
 

There are some services that I expose to the internet (using Apache reverse proxy) that really should be accessed by only a small set of devices. Requiring client certificates seems like a great way to reduce the attack surface and prevent brute force attacks (since the attacker doesn't even get a chance to attempt a login).

I wonder about the difficulty on the client side as well as other practical implications. The clients are smartphones of various makes.

you are viewing a single comment's thread
view the rest of the comments
[–] lemmyvore@feddit.nl 4 points 2 days ago* (last edited 2 days ago) (1 children)

It's very easy to make a custom CA and issue certs. Here's a good tutorial.

Unfortunately in practice It depends greatly on what's on the other side (the client app). Some examples:

  • DAVx5 on Android works perfectly fine and uses the client cert from the system store. 10/10, this is how all apps should work.
  • Ntfy on Android works perfectly fine but wants the client cert file loaded in the app, it doesn't use the one loaded in the system store. This sucks because instead of loading a cert into the system store once and then deleting it you have to keep the cert file around for this kind of apps, in Android shared storage, which is accessible to all apps.
  • Same for Immich, wants the cert loaded in the app. Also, it will randomly lose it (on both iOS and Android). Yes, you heard that right. So it's basically useless and I had to resort instead to a key in a custom HTTP header; which isn't exactly the same as mTLS, but helps secure the service at reverse proxy level so it's better than nothing.
  • Firefox on Android will use the cert from the system store, and then it will crash. Again, useless.

Oh did I mention how you get a mTLS client cert to an app on an iOS device? You send it over email to an account that the device has access to through the Mail app, then share the attachment. Yep.

It's also not exactly straightforward to use mTLS with reverse proxies.

Let's take for example Caddy and say you want unconditional mTLS for all reverse proxy hosts. Easy enough:

tls /path/to/domain-cert/fullchain.pem /path/to/domain-cert/privkey.pem {
  client_auth {
    mode required
    trust_pool file /path/to/custom/ca.pem
  }
}

But suppose you don't want unconditional mTLS, you'd like to let clients in if they have mTLS or a custom header, or do different things depending if the client has valid mTLS or not. Does Caddy offer a built-in conditional to act on mTLS status? Nope!

As a workaround I'm setting the client_auth mode to verify_if_given and then using a DIY conditional that checks if the variable http.request.tls.client.certificate_der_base64 is empty or not. But it's undocumented so who knows if it may break at any point.

For reference, how you handle both custom headers and mTLS at once (after setting the mode as I've mentioned):

@immich host "whatever.example.com"
handle @immich {
  @not_authorized {
    not header X-Custom-Pass "LONGRANDOMKEY01" # jim
    not header X-Custom-Pass "LONGRANDOMKEY02" # bob
    vars_regexp {http.request.tls.client.certificate_der_base64} ^$
  }
  error @not_authorized 403
  reverse_proxy http://immich.lan:port
}

The nested "not not" is required because Caddy can only do logical AND in group conditionals, so to do logical OR you basically have to do NOT (NOT a AND NOT b).

[–] surewhynotlem@lemmy.world 1 points 2 days ago (1 children)

In caddy, isn't an OR is just two separate blocks?

@authorized { header X-Custom-Pass "LONGRANDOMKEY01" # jim } @authorized { header X-Custom-Pass "LONGRANDOMKEY02" # Bob }

I could be wrong. Caddy config is a dark art.

[–] lemmyvore@feddit.nl 3 points 2 days ago* (last edited 2 days ago)

Lol I don't know, I'll have to try. I would've never thought of it. If it works it's definitely not intuitive. Dark art indeed.