Login-limbo image (LOOHP/Limbo + felis-limbo)
The always-on login auth gate. felis setup provisions it as a system
service (DesiredState=Running, reaper-exempt) when [velocity] login_image is
set in felis.toml. Every fresh connection lands here first; it is the only safe
fallback (a stopped/starting backend routes here, never past authentication).
Code-only. This image is not built by the Go CI. It compiles the
plugins/limboplugin (plus the sharedplugins/sharedlink core it srcDir-includes) and bundles it with a LOOHP/Limbo release.
What the plugin does
felis-limbo does two jobs — readiness and the in-game login flow.
Readiness
LOOHP/Limbo has no RCON, so Felis cannot use its usual RCON readiness probe
(spec §5). A bare TCP check would report "ready" the instant the socket binds.
The plugin instead serves an HTTP readiness endpoint that flips to 200 only
after the first server tick — i.e. once the limbo has genuinely started. The
login MinecraftServer sets spec.startup.healthHTTPPort: 8080, so the pod's
HTTP readinessProbe (and, with RCON disabled, the operator's Ready gate) follows
that true signal.
- Endpoint:
GET /healthzon:8080(override withFELIS_HEALTH_PORT). 503 startingbefore the first tick,200 okafter.- Fail-closed: if the endpoint cannot bind, readiness never turns green and the
operator keeps the gate in
Starting— it never advertises an unstarted gate.
In-game login (spec §B3)
A player reaching the limbo has been UUID-verified upstream (Velocity online-mode) but is not yet linked to a web account. On join, off the tick thread, the plugin:
- checks the username-collision blacklist and disconnects a barred squatter UUID (the genuine Mojang player — different UUID — passes);
- mints a one-time Bind Code for the verified UUID via the felis-api internal face;
- opens a book with a clickable link to
console.<root_domain>plus a chat line carrying the code, and tells the player to finish in their system browser — never the WeChat/QQ in-app browser, where passkey/WebAuthn does not work (the web entry additionally guards this; seeinternal/panel); - polls
link/status/{uuid}until the player redeems the code on the web console, then transfers them to the lobby via a BungeeCordConnectplugin message onbungeecord:main; - disconnects (fail-closed) on blacklist, on a mint/transport failure, or when the login window elapses — "rather refuse than admit unauthenticated".
Configuration (deployment inputs, never compiled in; env wins over a
felis-link.properties template written in the plugin data dir on first run):
| Env | Meaning | Default |
|---|---|---|
FELIS_API_BASE_URL |
felis-api internal face base URL | (required for login) |
FELIS_SERVICE_TOKEN |
internal service token (secret) | (required for login) |
FELIS_ROOT_DOMAIN |
deployment zone, builds https://console.<zone> |
(required for login) |
FELIS_LOBBY_SERVER |
Velocity server name to transfer to | lobby |
FELIS_LOGIN_TIMEOUT_SECONDS |
login window (clamped 30–3600) | 300 |
FELIS_HEALTH_PORT |
readiness port | 8080 |
If the API config or the root domain is absent the login flow stays OFF and
the plugin runs readiness-only (the same "load un-crippled" fail-safe the other
Felis plugins use), so a bare image still boots — production must supply the config
for the gate to authenticate. Transfer requires Velocity to accept the BungeeCord
plugin-message channel (bungee-plugin-message-channel on the proxy).
Build
LOOHP/Limbo has no official image and no release zip. Its CI
(ci.loohpjames.com/job/Limbo) publishes two loose artifacts per build —
target/Limbo-<ver>.jar and spawn.schem — so the image is assembled from those
two URLs (there is no bundled server.properties; Limbo writes a default on first
run):
docker build -f deploy/limbo/Dockerfile \
--build-arg LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/target/Limbo-<ver>.jar \
--build-arg LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/spawn.schem \
--build-arg LIMBO_VERSION=<maven-api-version> \
-t felis-limbo:demo .
LIMBO_JAR_URL(required) — the server jar; it is saved asLimbo.jar.LIMBO_SCHEM_URL(optional) — the default spawn schematic, saved asspawn.schemand loaded as the spawn world.LIMBO_VERSION— the Limbo maven API version the plugin compiles against (Gradle-PlimboVersion). This differs from the jar's CI build-qualified filename: e.g. the jarLimbo-2026.0.2-ALPHA-26.2.jarcorresponds to maven version2026.0.2-ALPHA(the-26.2CI qualifier is not published to the maven repo).
Import into k3s and point config at it:
docker save felis-limbo:demo | sudo k3s ctr images import -
# felis.toml → [velocity] login_image = "felis-limbo:demo"
sudo felis setup
Ports (handled for you)
The entrypoint (deploy/limbo/entrypoint.sh) pins Limbo's server-port to
FELIS_GAME_PORT (default 25565, the operator's GamePort) on every start —
LOOHP/Limbo would otherwise default to 30000, unreachable through the Velocity
NetworkPolicy / Service / probe the operator drives off that one const. It is
idempotent, so a persisted world volume keeps all its other server.properties
settings. Do not override FELIS_GAME_PORT except in lockstep with the operator.
Configure (deployer's responsibility)
One setting this image does not guess (it keeps the release's own default):
- Player forwarding — align Limbo's forwarding with the off-cluster Velocity
proxy so authenticated players hand off cleanly, and enable the BungeeCord
plugin-message channel on the proxy so the login gate's
Connecttransfer to the lobby lands.
The login flow's own inputs (FELIS_API_BASE_URL, FELIS_ROOT_DOMAIN,
FELIS_LOBBY_SERVER, FELIS_SERVICE_TOKEN; see
In-game login above) are wired in for you — you do not
set them by hand:
- The three non-secret vars are baked into the
loginMinecraftServer'sspec.envbyfelis setup(cmd/felisderives the internal API URL from the control namespace — the platform defaultfelis; a renamed control namespace must be reflected by hand — and the root domain fromfelis.toml). FELIS_SERVICE_TOKENis a secret, so it is never written into the CRD.felis setupreplicates thefelis-service-tokenSecret from the control namespace into the minecraft namespace, and the operator injects it into theloginpod (only) via asecretKeyRef, keyed off the reservedloginname. Until the token is present the plugin fail-safes to readiness-only, so the gate is never broken — it simply does not authenticate yet.- Service: the login pod dials
FELIS_API_BASE_URL, which resolves to the ClusterIP Servicefelis-api-internal(control namespace) that fronts the api pod's internal port 8081. That Service is deliberately separate from the external NodePortfelis-api(443) so the no-Zero-Trust internal face is never published on a node's external IP. - NetworkPolicy: none is required today — neither the minecraft-namespace egress nor the control-namespace ingress is policy-locked, so the login pod's call to the API internal port is reachable. If a future deployment adds a minecraft egress lock or a control-namespace ingress fence, it must also open the login-pod → felis-api-internal (8081) path.
The Velocity gate/lobby wiring is printed by felis setup and enforces the
invariant: fresh connections hit login first, and only an authenticated release
from that gate can enter the post-auth lobby or a remembered user backend.