DEV Community

Cover image for 'Doglabbing' ngrok: Eyes-only photo sharing with immich and OIDC
Joel Hans for ngrok

Posted on

'Doglabbing' ngrok: Eyes-only photo sharing with immich and OIDC

The series continues!

... despite the overall lack of traction around the term I made up for when your engineers don't just dogfood your product in production, but also use it to front their all-important and very-personal homelab setups.

This time, a slightly different angle—instead of hearing from yet another infrastructure engineer, one of our beloved backend engineers is here to share how he secures his homelab setup without, you know, becoming an infrastructure engineer himself.

--

Ryan (Senior Software Engineer): Eyes-only photo sharing with immich and OIDC

For the past few years, I've slowly been de-Googling my life. One of the last Google services in my life was Google Photos, which remained for one reason: sharing. After going on a trip, I want to give my companions access to all the pictures I took, allowing them to pick their favorites and download them. I also want an easy, convenient place for them to share their pictures with me.

After doing some research, I came across the perfect solution: immich!

Well, almost perfect. The main downside is that it would run on my home server, so I'd have to be careful about how I expose it. I came up with a few requirements:

  • Only trusted individuals should be able to make any contact with my home network. There have been cases of attackers using insecure, publicly facing software to infiltrate home networks. I don't personally want to take that risk.
  • Trusted individuals should be able to access immich from a normal browser. While I’m comfortable setting up Wireguard, others are not.
  • I want to quickly/easily be able to update who is and isn't allowed to use the service, without logging into a machine.

How I solved the problem using ngrok

The first thing I need to do is pick a login method. While it would be super simple to just pick Google OAuth, it kind of defeats the purpose. Instead, I'll use SimpleLogin. It's a little more setup, but it's free and lets me use my Proton address. ngrok has an OIDC Traffic Policy action, meaning I can set up OIDC with a few lines of YAML!

From there, I check that the email of the logged-in user exists in a list of all allowed users, created using the set-vars action. If not, they are rejected, ensuring unauthorized users never contact my home network. From there, ngrok routes traffic to an internal endpoint, depending on the host (service) they are trying to reach. Each service also has its own access list.

The internal endpoints are initiated by agents that sit in my home network. They do nothing more than forward the traffic to the appropriate local IP/port, meaning I only ever have to interact with the local machine to occasionally update the system/ngrok client.

The coolest thing about this setup is that immich can be configured with OAuth as well! By configuring immich with the same OAuth settings as our OIDC action and turning on Auto Start, they will be transparently logged into immich after authenticating at my ngrok-powered gateway.

Even better, by turning on Auto Register, an account will be automatically created for them upon first login! This means I can manage my users solely through the immich_users variable in my traffic policy. When I want to add a new user, I add their protonmail address to that list, they sign in, and they can immediately start updating and sharing!

Limitations

The biggest limitation of my current approach is that the immich mobile app does not work with this setup. It wants to connect to the server (unauthenticated) to obtain OIDC information. While I could update my policy to allow those particular routes to be accessed without authentication, I would rather err on the side of security. As someone who does not use a phone much, I can live with the tradeoff.

Gateway config with a Cloud Endpoint:

on_http_request:
  - name: "basic firewall"
    actions:
      - type: "custom-response"
        config:
          content: "Denied! Bye Bye!"
          status_code: 401
    expressions:
      - conn.client_ip.geo.location.country_code != 'US' || conn.client_ip.is_on_blocklist || conn.client_ip.is_tor_node
  - name: "set up user access groups"
    actions:
      - type: "set-vars"
        config:
          vars:
            - main_user: "mainuser@protonmail.com"
            - immich_users: ['${vars.main_user}','friend1@protonmail.com','friend2@protonmail.com']
            - proxmox_users: ['${vars.main_user}', 'roommate1@protonmail.com', 'roommate2@protonmail.com']
  - name: "OIDC with simplelogin"
    actions:
    - type: "openid-connect"
      config:
        issuer_url: "<https://app.simplelogin.io>"
        client_id: "sample-client-id"
        client_secret: "${secrets.get('prod-vault', 'simplelogin-client-secret')}"
  - name: "immich forward"
    actions:
    - type: "forward-internal"
      config:
          url: "<https://example-immich.internal>"
    expressions:
       - "(actions.ngrok.oidc.identity.email in vars.immich_users)"
       - "endpoint.host == 'immich.example.com'"
  - name: "rss reader forward"
    actions:
    - type: "forward-internal"
      config:
          url: "<https://example-freshrss.internal>"
    expressions:
       - "(actions.ngrok.oidc.identity.email == vars.main_user)"
       - "endpoint.host == 'freshrss.example.com'"
  - name: "proxmox forward"
    actions:
    - type: "forward-internal"
      config:
          url: "<https://example-proxmox.internal>"
    expressions:
       - "(actions.ngrok.oidc.identity.email in vars.proxmox_users)"
       - "endpoint.host == 'proxmox.example.com'"
  - name: "default route (DENY)"
    actions:
    - type: "custom-response"
      config:
        content: >
          example.com is currently unused.
        status_code: 200
Enter fullscreen mode Exit fullscreen mode

What's happening here? On every HTTP request, this policy does a lot:

  1. Filters and denies traffic from outside the US, on any blocklist, and coming from Tor exit nodes.
  2. Sets up user access groups using variables.
  3. Enforces an OIDC-based authentication process using simplelogin and a secret.
  4. Forwards authenticated traffic to one of a few possible internal endpoints for immich, freshrss, and proxmox.
  5. Sends a catch-all response to any request to ryans-domain.com.

Agent configs:

version: 3
agent:
  authtoken: supersecrettoken
endpoints:
  - name: immich
    url: <https://example-immich.internal>
    upstream:
      url: 192.168.1.29:8080
  - name: freshrss
    url: <https://example-freshrss.internal>
    upstream:
      url: 192.168.1.29:6342
  - name: proxmox
    url: <https://example-proxmox.internal>
    upstream:
      url: 192.168.1.111:80
Enter fullscreen mode Exit fullscreen mode

--

Sadly, tomorrow is the last day of our doglabbing explorations... for now. But as before, a collection of all the ngrok docs you'd need to replicate Ryan's setup:

Of course, if you've never heard of ngrok before, or just want to try out a slightly simpler version of this, there's always our quickstart.

Top comments (0)