I’ve been using Tailscale to authenticate to some applications on my home lab for a bit. I use caddy with caddy-tailscale and proxy auth — essentially the same as laid out in Tailscale Authentication for NGINX. It’s terribly easy to misconfigure and break the security however it is also extremely convenient! Unfortunately it also rather niche and many apps don’t support it — either for that reason or for security. After a while of complaining and manual credentials I realized it would be possible to write an OpenID Connect server which took advantage of the same whois API to authorize tokens.
what could possibly go wrong with proxy authentication?Reverse proxy authentication varies in implementation but usually involves passing a username only in an HTTP header. The proxy in question would then populate that header and ensure incoming requests have it stripped. The problem with this scheme is fairly straightforward — any connections which bypass the proxy may authenticate as any user.
It’s worth noting that while OAuth2 is harder to mess up than proxy authenticate it does still share a flaw — all it guarantees is the connection was sourced from your device. In a sense password authentication on a server hosted over tailscale is two-factor authentication. You need credentials (“something you know”) and access to a device on the tailnet (“something you have”). Using Tailscale for authentication leaves only a single factor — any connection coming from my device is assumed to be me. That’s a security model I’m satisfied with but readers should consider their own before setting up authentication like this.
A few days of hacking and a proof of concept later… I learned that it already exists. My search engine skills are getting pretty weak clearly since it’s been around for years. At least I understand the oauth2 flow a bit better now and I don’t even have to production-ize the app to use it myself :D.
It’s pretty easy to configure. It uses tsnet to add it’s own named node to my tailnet and doesn’t require any configuration for client ID or secret. Configuration for Komga looks like this and it’ll be similar for any application which allows login using oauth2.
1spring:2 security:3 oauth2:4 client:5 provider:6 tsidp:7 issuer-uri: https://idp.tailnet.ts.net8 user-name-attribute: username9 registration:10 tsidp:11 authorization-grant-type: authorization_code12 client-id: unused13 client-name: Tailscale14 client-secret: unused15 provider: tsidp16 redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'17 scope: openid,email,profile
A basic NixOS module looks like this or alternatively there are docker containers.
1{2 options,3 config,4 lib,5 pkgs,6 ...7}:8
9with lib;10let11 cfg = config.tsheinen.services.tsidp;12in13{14 options.tsheinen.services.tsidp = with types; {15 enable = mkEnableOption "should tsidp be enabled";16 dataDir = mkOption {17 type = types.path;18 default = "/var/lib/tsidp";19 description = "tsidp state dir";20 };21 };22
23 config = mkIf cfg.enable {24 users.users.tsidp = {25 home = cfg.dataDir;26 createHome = true;27 group = "tailscale_key";28 isSystemUser = false;29 isNormalUser = true;30 collapsed lines
30 description = "Tailscale IdP";31 };32
33 users.groups.tailscale_key = {};34
35 systemd.services.tsidp = {36 enable = true;37 description = "Tailscale OpenID Connect service";38 wants = [ "network-online.target" ];39 after = [ "network-online.target" ];40 wantedBy = [ "multi-user.target" ];41
42 environment = {43 TS_HOSTNAME = "idp";44 TS_USERSPACE = "false";45 TAILSCALE_USE_WIP_CODE = "1";46 TS_STATE_DIR = "${cfg.dataDir}";47 };48
49 serviceConfig = {50 Type = "simple";51 RestartSec = 5;52 Restart = "always";53 User = "tsidp";54 WorkingDirectory = "${cfg.dataDir}";55 ExecStart = "${pkgs.tailscale}/bin/tsidp";56 };57 };58 };59}