8 May 2024

Tailscale's nginx-auth doesn't provide an email

Initial setup

I’m currently running OpenWebUI behind a Nginx reverse proxy and inside a Tailscale network. I desperately don’t want to manage OpenWebUI users and ask them to manage an extra set of credentials. As of May 2024 OpenWebUI doesn’t support SSO, OAuth, SAML, or OIDC. Fortunately, it does support headers from an authenticating proxy.

To spice things up I’m running it with NixOS on arm, but it’s a minor part of this story.

Rough idea

Tailscale provides an nginx-auth that can send Tailscale user information as headers to Nginx upstream.

OpenWebUI expects a trusted authentication email header. If it matches an existing user, they are authenticated seamlessly; otherwise, a new user is created pending admin approval. If a request comes from outside the Tailscale network, regular login procedures apply.

The journey

Tailscale doesn’t provide an email header, sort of. Let’s take look at available headers from nginx-auth README.md file:

HeaderExample Value
Tailscale-User[email protected]
Tailscale-Loginazurediamond
Tailscale-NameAzure Diamond
Tailscale-Profile-Picturehttps://i.kym-cdn.com/photos/images/newsfeed/001/065/963/ae0.png
Tailscale-Tailnethunter2.net

Tailscale-User may look like a properly formatted email. It is an email for my account with custom OIDC. Same for Google accounts. For GitHub, however, it is not. There might be other cases when user isn’t an actual email, but I haven’t seen them yet.

GitHub’s accounts have ${GITHUB_USERNAME}@github as a Tailscale-User. OpenWebUI doesn’t like it as an email and fails to register a user.

email format you entered is invalid

Since there is no email, let’s come up with one. Nginx can map a variable to a new one with some additional logic.

By this moment Nginx configuration looks like this:

. . .
http {
  . . .
  server {
    . . .
    location / {
      . . .
      auth_request /auth;
      auth_request_set $auth_user $upstream_http_tailscale_user;
      auth_request_set $auth_name $upstream_http_tailscale_name;
      auth_request_set $auth_login $upstream_http_tailscale_login;
      auth_request_set $auth_tailnet $upstream_http_tailscale_tailnet;
      auth_request_set $auth_profile_picture $upstream_http_tailscale_profile_picture;
      proxy_set_header X-Webauth-User "$auth_user";
      proxy_set_header X-Webauth-Name "$auth_name";
      proxy_set_header X-Webauth-Login "$auth_login";
      proxy_set_header X-Webauth-Tailnet "$auth_tailnet";
      proxy_set_header X-Webauth-Profile-Picture "$auth_profile_picture";
    };
    location /auth {
      . . .
      internal;
      proxy_pass http://unix:/run/tailscale-nginx-auth/tailscale-nginx-auth.sock;
      proxy_pass_request_body off;
      # Upstream uses $http_host here, but we are using gixy to check nginx configurations
      # gixy wants us to use $host: https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md
      proxy_set_header Host $host;
      proxy_set_header Remote-Addr $remote_addr;
      proxy_set_header Remote-Port $remote_port;
      proxy_set_header Original-URI $request_uri;
      proxy_set_header X-Scheme                $scheme;
      proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
    }
  }
}

In my case it is set by NixOS option services.nginx.tailscaleAuth.virtualHosts.

I took $auth_user and mapped it to $auth_email. GitHub’s users have @github updated to @github-users.my.domain. Others are left as is.

. . .
http {
  . . .
  map $auth_user $auth_email {
    ~^(?<user>[^@]+)@github$ [email protected];
    default $auth_user;
  }
  . . .
  server {
    . . .
    location / {
      . . .
      # Should be places after $auth_user definition
      proxy_set_header X-Webauth-Email "$auth_email";
      . . .
    }
  }
}

The only thing left is to update OpenWebUI environment variable WEBUI_AUTH_TRUSTED_EMAIL_HEADER to be X-Webauth-Email or the lower case version of that. After that GitHub users should bed greeted this way:

account activation pending

It’s not an actual email of a GitHub user, but it allows them to get into the system and me to understand weather it’s a highly unlikely GitHub employee or this email “hack”.

Additional detours

lower case header name

OpenWebUI and nginx-auth both provide upper case examples for the trusted authentication email header. My configurations of Nginx & nginx-auth pass lower case headers to OpenWebUI. OpenWebUI is case-sensitive.

NixOS specific

I’ve tried to contain all NixOS details in a separate section.

Resulting Nginx config

To review the active Nginx configuration you need to figure out its path.

systemctl cat nginx.service 

Look for ExecStart and -c flag. It should look like /nix/store/somehashsum...-nginx.conf

Nginx’s configuration order and placement in NixOS

To place map into http section of Nginx config I’ve used services.nginx.commonHttpConfig.

I’ve tried services.nginx.virtualHosts..locations..extraConfig to set just an email header. Didn’t work. It places the header definition before configuration provided by services.nginx.tailscaleAuth.virtualHosts which contains $auth_user. This way a variable to map from is not defined yet.

I’ve ended up ditching services.nginx.tailscaleAuth.virtualHosts and defining all header and /auth myself.

Packages & options