Turning off JWT auth for my VPN in nginx
I run about thirty self-hosted services behind one nginx reverse proxy, and all of them sit behind a JWT auth gateway that logs you in with Google. For anything reachable from the internet that’s what I want. But most of the time I’m already on my own WireGuard VPN, and having to log in again just to open a dashboard got annoying.
So I wanted auth to stay on for everyone, except traffic coming from the VPN
subnet. nginx has a geo module for exactly this kind of thing. It sets a
variable based on the client’s address:
geo $jwt_enabled {
default on;
10.10.10.0/20 off; # VPN
}
The plan was to pass that to the JWT module with
auth_jwt_enabled $jwt_enabled;. When I tried it, nginx refused to start,
saying the value was invalid.
Why the variable didn’t work
In an nginx config it looks like variables work everywhere, but they only work
in directives that were written to accept them. Each directive decides in its C
code how its value gets read, and
auth_jwt_enabled was set up as an on/off flag:
NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
A flag like this gets read once, when nginx loads its config. It has to be
exactly on or off, and it gets stored as a 1 or a 0. $jwt_enabled isn’t
either of those, so nginx rejected it before any request came in.
Adding variable support
nginx already has a way for a directive to take a value that gets worked out
per request, called a complex value. So I changed auth_jwt_enabled to use
that instead of a flag. The config now stores the expression, and the module
evaluates it for each request:
ngx_int_t enabled = 1;
if (jwtcf->enabled != NULL) {
ngx_str_t cv;
if (ngx_http_complex_value(r, jwtcf->enabled, &cv) == NGX_OK && cv.len > 0) {
if (ngx_strncmp(cv.data, "off", 3) == 0) {
enabled = 0;
}
}
}
Auth only turns off if the value is exactly off. If it’s empty, misspelled,
or the geo block doesn’t match anything, auth stays on. Plain
auth_jwt_enabled on; still works the same as before, since on is just a
value that never changes.
Every site started asking for a token
After building my first version, every site on the box started asking for a JWT, including ones that had no JWT config at all.
With the old flag, “not set” had its own special value, and the module turned
that into off. That’s why it did nothing on sites that never mentioned it. With
my change the setting was a pointer, and “not set” was now just an empty
pointer. My code started from enabled = 1 and only turned it off for an
explicit off, so an empty pointer meant on.
I fixed it by adding a check at the start: if no JWT key is configured, the module steps aside. Without a key it can’t check tokens anyway.
Getting it merged
The maintainer liked the change and added some formatting fixes and tests. He also removed my key check, saying other checks further down already handled that case. I rebuilt with his changes and tried a config with no JWT settings, and nothing asked for a token, so I agreed to drop it.
For the tests, he couldn’t easily test a subnet in CI, so he set the variable from a request header instead:
map $http_test_auth_enabled $jwt_enabled {
default on;
on on;
off off;
}
A request with Test-Auth-Enabled: on should get a 401, and one with off
should get a 200.
It was released in v2.4.0 about a month after I opened the PR, and my reverse proxy runs it now. On the VPN, dashboards open straight away, and from anywhere else I get the Google login.