moonexpr / kimai-loopback-auth-plugin
Passwordless auto-login for same-machine (loopback) requests via the REMOTE_USER FastCGI param. No core files modified.
Package info
github.com/moonexpr/kimai-loopback-auth-plugin
Type:kimai-plugin
pkg:composer/moonexpr/kimai-loopback-auth-plugin
Requires
- php: ^8.1
README
A Kimai plugin that auto-logs-in a user when the request comes from the same machine (loopback). It modifies no Kimai core file, so it survives upgrades.
The plugin holds no password logic. It reads one signal — the REMOTE_USER
server variable, which your web server sets only for trusted (by default,
loopback) clients — and, as a second independent check, confirms the real peer
address is in its own trusted allowlist before logging that user in. Auto-login
therefore requires two gates to agree: the web-server config and the
plugin's REMOTE_ADDR allowlist (see Hardening). A mistake in
either one alone does not grant access.
⚠️ Security model — read before installing
This plugin grants a session, without a password, to whichever username appears in
REMOTE_USER— provided the request also originates from an allowlisted peer address. There are two independent gates:
- Web server — must be configured to set
REMOTE_USERexclusively for trusted (default: loopback127.0.0.1/::1) clients, and strip any client-supplied value on every other path.- Plugin — independently checks the real
REMOTE_ADDRagainst its trusted allowlist (default loopback;LOOPBACK_AUTH_TRUSTED_IPSto widen) and refuses any peer outside it, even ifREMOTE_USERis set.The second gate is defense in depth: a misconfigured
fastcgi_param, a reverse proxy, or an untrusted upstream that populatesREMOTE_USERfor a non-loopback request is not by itself enough to authenticate. It is still a deliberate password bypass, though — treat both gates as part of the plugin, keep their allowlists in agreement, and runscripts/security-audit.shto check your deployment.Intended use is a single-operator, local-only (or authenticated-tunnel) Kimai instance, where typing a password on every visit to your own machine is pure friction.
Requirements
- Kimai 2.0 or newer (
extra.kimai.require: 20000) - PHP 8.1+
- A web server that can set the
REMOTE_USERFastCGI parameter conditionally (the examples below use nginx + PHP-FPM)
How it works
Kimai is a Symfony application. Symfony's security layer forbids a plugin from declaring firewall configuration in a second file, so this plugin reaches the firewall at the dependency-injection container level instead:
Security/LoopbackAuthenticatoris a self-validatingAbstractAuthenticator. Itssupports()returnstrueonly when all of these hold:REMOTE_USERis a non-empty string; the real peer address (REMOTE_ADDR) is inside the trusted allowlist (default loopback — see Hardening); and the current session is not already authenticated as that same user. It stands aside (returnsfalse) otherwise, letting Kimai's normalform_loginflow apply. TheREMOTE_ADDRcheck is defense in depth — see Hardening for why it does not rely on the web server alone.- When it does fire,
authenticate()returns aSelfValidatingPassportbuilt from aUserBadgefor theREMOTE_USERidentifier, resolved against Kimai's internal user provider (security.user.provider.concrete.kimai_internal). There is no credential to check — the trust boundary is the web server — so the passport self-validates. DependencyInjection/Compiler/RegisterLoopbackAuthenticatorPassappends the authenticator to thesecured_areafirewall's authenticator manager at compile time. This is the one Symfony-internal coupling in the plugin: it readssecurity.authenticator.manager.secured_areaand adds a reference to the authenticator. It fails safe — if that service definition is not found (e.g. a future Symfony release renames it), the pass no-ops and the firewall simply lacks loopback auth; normal password login keeps working.
onAuthenticationSuccess and onAuthenticationFailure both return null:
success lets the request continue to its intended controller without a redirect,
and failure falls through to the other authenticators and the normal login entry
point.
Installation
Kimai plugins are loaded from the var/plugins/ directory; they are not
installed into vendor/ like ordinary Composer libraries. Use the Git method
(the one Kimai documents); the Composer method is offered as a convenience for
installs that carry the kimai/kimai2-composer installer.
1. Install the plugin
Git (recommended): clone the bundle so its path is exactly
var/plugins/LoopbackAuthBundle/ — the directory name must match the bundle
class for Kimai's autoloader to find it:
cd /path/to/kimai
git clone https://github.com/moonexpr/kimai-loopback-auth-plugin.git var/plugins/LoopbackAuthBundle
Composer (alternative): on a Kimai install whose root composer.json
includes the kimai/kimai2-composer installer (Kimai's kimai-plugin package
type routes the package to var/plugins/ rather than vendor/):
composer require moonexpr/kimai-loopback-auth-plugin
2. Configure the web server
The plugin does nothing until your web server sets REMOTE_USER for loopback
clients. This is the security-critical half — see
Web server configuration
below and the ready-made files in examples/nginx/.
3. Rebuild the Kimai cache
So the compiler pass runs and the authenticator is wired into the firewall:
bin/console kimai:reload --env=prod
# or, equivalently:
bin/console cache:clear --env=prod
4. Confirm Kimai sees the plugin
bin/console kimai:bundles
LoopbackAuth should appear in the list.
The user named in REMOTE_USER must already exist as a Kimai user — the plugin
authenticates an existing account, it does not create one.
Web server configuration (the required other half)
The plugin is inert until your web server sets REMOTE_USER, and it is only
safe if your web server sets REMOTE_USER only for loopback clients. Two
ready-made files in examples/nginx/ do this for nginx +
PHP-FPM; adapt them to your deployment rather than copying blindly.
Step 1 — define the loopback→user map
examples/nginx/loopback-auth-map.conf
maps the real peer address to a username, defaulting to the empty string for
everyone else. Drop it into nginx's http context:
cp examples/nginx/loopback-auth-map.conf /etc/nginx/conf.d/kimai-loopback-auth.conf
# then edit it: set the username you want auto-logged-in for 127.0.0.1 / ::1
map $remote_addr $kimai_loopback_user { default ""; 127.0.0.1 "admin"; ::1 "admin"; }
Step 2 — forward it to PHP as REMOTE_USER
The Kimai vhost routes PHP through location ~ ^/index\.php(/|$). One line
inside that block forwards the mapped value.
examples/nginx/enable-remote-user.patch
adds it for you, against Kimai's documented vhost:
patch -p1 --fuzz=3 /etc/nginx/sites-available/kimai.conf < examples/nginx/enable-remote-user.patch
If the hunk fails because your config differs, add the single line by hand
inside the index.php location:
fastcgi_param REMOTE_USER $kimai_loopback_user;
Then validate and reload:
nginx -t && systemctl reload nginx
Why it is built this way
- Default empty, then map.
$kimai_loopback_userdefaults to"", so no request path can inherit a stale or client-supplied value; only a loopback peer is given an identity. - Key off the real peer address (
$remote_addr), never off a client- controlled request header (X-Forwarded-For,X-Remote-User, etc.). Ageoblock on$remote_addris preferred overifblocks — it cannot be tricked by a header, accepts CIDR ranges, and avoids nginx'sif-in-location pitfalls. - Behind a reverse proxy,
$remote_addris the proxy, not the browser, so the map must run on the edge that terminates the real client connection — otherwise every proxied request looks like loopback.
Behaviour summary
| Request origin | In REMOTE_ADDR allowlist? |
REMOTE_USER |
Result |
|---|---|---|---|
| Loopback, first request | yes | set to a valid user | Auto-logged-in, no password |
| Loopback, already logged in as that user | yes | set | Plugin stands aside; existing session used |
| Loopback, user does not exist in Kimai | yes | set | Auth fails, falls through to normal login |
| Allowlisted peer (e.g. tailnet, if configured) | yes | set | Auto-logged-in, no password |
| Non-allowlisted peer (LAN/WAN) | no | set (e.g. via a misconfigured map) | Plugin refuses — normal Kimai login form |
| Any non-loopback client | — | empty | Plugin stands aside; normal Kimai login form |
The fifth row is the defense-in-depth guarantee: even if the web server wrongly
sets REMOTE_USER for a non-allowlisted peer, the plugin still refuses it.
Hardening
This plugin is a deliberate password bypass; the rest of this section is about shrinking what can go wrong.
Two independent allowlists
Auto-login requires the request to pass both gates, so a mistake in one does not by itself grant access:
- Web server —
examples/nginx/loopback-auth-map.confonly setsREMOTE_USERfor source addresses you list (default: loopback). - Plugin — the authenticator independently re-checks the real
REMOTE_ADDRagainst its own allowlist before honouringREMOTE_USER.
Configure the plugin allowlist with an environment variable (comma-separated IPs/CIDRs, IPv4 and IPv6). Unset or empty falls back to loopback only:
# .env.local — default behaviour, no need to set it: # LOOPBACK_AUTH_TRUSTED_IPS="127.0.0.1,::1" # To also auto-login authenticated tailnet devices, widen BOTH allowlists in # step — the plugin var and the nginx geo block: LOOPBACK_AUTH_TRUSTED_IPS="127.0.0.1,::1,100.64.0.0/10"
Keep the two allowlists in agreement: the plugin will refuse any peer the nginx side lets through but the plugin's own list does not include.
Run the audit
scripts/security-audit.sh inspects (it changes
nothing) what the HTTP port is bound to, which of the host's networks can
actually reach it (classified tailnet / LAN / other), the effective plugin
allowlist, and nginx config smells. Exit code 0/1/2 = clean/warnings/
failures, so it drops into CI or a cron check.
scripts/security-audit.sh --port 80 --nginx-conf /etc/nginx/sites-enabled/kimai.conf
Other levers
- Bind narrowly. If only same-machine access is intended, bind the vhost to
listen 127.0.0.1:80;rather than all interfaces — then no network can reach it regardless of the allowlists. - Restrict reachability at the proxy. If you want trusted nets to reach
Kimai but untrusted ones blocked entirely, add
allow/denyto the server block (e.g.allow 127.0.0.1; allow 100.64.0.0/10; deny all;). This is about who can reach Kimai, separate from who can auto-login. - Mind trusted proxies. The plugin reads the raw
REMOTE_ADDR, not Symfony'sRequest::getClientIp(), so aX-Forwarded-Forcannot fake a trusted peer. If a real reverse proxy sits in front, the loopback/allowlist check must run on the edge that sees the true client address. - Least-privilege account. The auto-logged-in user has whatever role it has in Kimai. For a shared trusted network, consider pointing the allowlist at a non-admin Kimai account so a stray device cannot land in the admin UI.
Uninstall
rm -rf var/plugins/LoopbackAuthBundle bin/console cache:clear --env=prod
Removing the bundle removes the authenticator from the firewall on the next
cache build. You should also remove the REMOTE_USER rule from your web-server
configuration.
License
MIT © John Chandara