moonexpr/kimai-loopback-auth-plugin

Passwordless auto-login for same-machine (loopback) requests via the REMOTE_USER FastCGI param. No core files modified.

Maintainers

Package info

github.com/moonexpr/kimai-loopback-auth-plugin

Type:kimai-plugin

pkg:composer/moonexpr/kimai-loopback-auth-plugin

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.0 2026-06-04 04:17 UTC

This package is auto-updated.

Last update: 2026-07-04 18:12:52 UTC


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_USERprovided the request also originates from an allowlisted peer address. There are two independent gates:

  1. Web server — must be configured to set REMOTE_USER exclusively for trusted (default: loopback 127.0.0.1 / ::1) clients, and strip any client-supplied value on every other path.
  2. Plugin — independently checks the real REMOTE_ADDR against its trusted allowlist (default loopback; LOOPBACK_AUTH_TRUSTED_IPS to widen) and refuses any peer outside it, even if REMOTE_USER is set.

The second gate is defense in depth: a misconfigured fastcgi_param, a reverse proxy, or an untrusted upstream that populates REMOTE_USER for 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 run scripts/security-audit.sh to 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_USER FastCGI 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:

  1. Security/LoopbackAuthenticator is a self-validating AbstractAuthenticator. Its supports() returns true only when all of these hold: REMOTE_USER is 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 (returns false) otherwise, letting Kimai's normal form_login flow apply. The REMOTE_ADDR check is defense in depth — see Hardening for why it does not rely on the web server alone.
  2. When it does fire, authenticate() returns a SelfValidatingPassport built from a UserBadge for the REMOTE_USER identifier, 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.
  3. DependencyInjection/Compiler/RegisterLoopbackAuthenticatorPass appends the authenticator to the secured_area firewall's authenticator manager at compile time. This is the one Symfony-internal coupling in the plugin: it reads security.authenticator.manager.secured_area and 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_user defaults 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.). A geo block on $remote_addr is preferred over if blocks — it cannot be tricked by a header, accepts CIDR ranges, and avoids nginx's if-in-location pitfalls.
  • Behind a reverse proxy, $remote_addr is 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:

  1. Web serverexamples/nginx/loopback-auth-map.conf only sets REMOTE_USER for source addresses you list (default: loopback).
  2. Plugin — the authenticator independently re-checks the real REMOTE_ADDR against its own allowlist before honouring REMOTE_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/deny to 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's Request::getClientIp(), so a X-Forwarded-For cannot 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