Unverified Commit 16b7ad27 authored by Minseong Choi's avatar Minseong Choi 💬
Browse files

feat(runtime): add authenticated system backends

parent fd062882
Loading
Loading
Loading
Loading
+11 −5
Changes for deploy/limbo/Dockerfile: 11 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -70,17 +70,23 @@ RUN set -eu; \
    mkdir -p /limbo/plugins
# Drop the login+readiness plugin in beside Limbo.jar.
COPY --from=plugin /felis-limbo.jar /limbo/plugins/felis-limbo.jar
# The entrypoint pins the game port to the operator's GamePort before launching Limbo.
# The entrypoint pins the game port AND enables Velocity modern forwarding — refusing to
# start without the secret, because a login gate that derives offline UUIDs would let
# anyone claim any Minecraft identity (the Owner's included).
COPY deploy/limbo/entrypoint.sh /usr/local/bin/felis-entrypoint.sh

# The operator mounts the world PVC at /data. Runtime state lives there; /limbo
# remains the immutable image seed copied into the volume by the entrypoint.
WORKDIR /data

ENV FELIS_HEALTH_PORT=8080
# FELIS_GAME_PORT is the port the entrypoint pins Limbo to; it MUST equal the operator's
# GamePort (internal/operator/builders.go). Default 25565 — override only in lockstep
# with the operator.
ENV FELIS_GAME_PORT=25565
EXPOSE 25565 8080
# felis-entrypoint.sh pins server-port then execs `java -jar Limbo.jar --nogui` (headless:
# the pod has no console). Limbo writes the rest of server.properties on first run and
# loads ./spawn.schem as the spawn world. Invoked via `sh` so no +x bit is needed from the
# (Windows) build host.
# felis-entrypoint.sh pins server-port + velocity-modern/forwarding-secrets, then execs
# `java -jar Limbo.jar --nogui` from /data (headless: the pod has no console). Limbo writes
# the rest of server.properties on the persistent volume and loads ./spawn.schem as the
# spawn world. Invoked via `sh` so no +x bit is needed from the (Windows) build host.
ENTRYPOINT ["/bin/sh", "/usr/local/bin/felis-entrypoint.sh"]
+3 −3
Changes for deploy/limbo/README.md: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -142,6 +142,6 @@ set them by hand:
  or a control-namespace ingress fence, it must also open the login-pod →
  felis-api-internal (8081) path.

The Velocity default-landing and waiting-park wiring is printed by `felis setup`
and enforces the invariant: fresh connections hit `login` first; nothing falls
back to the lobby.
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.
+74 −18
Changes for deploy/limbo/entrypoint.sh: 74 added lines, 18 removed lines.
Original line number Diff line number Diff line
#!/bin/sh
# Felis login-limbo entrypoint.
#
# Pin Limbo's game port to the pod-facing port the operator contract uses. LOOHP/Limbo
# defaults server-port to 30000, but the Felis operator drives everything — the Service
# Port/TargetPort, the TCP/HTTP readiness probe, the container port and the Velocity
# NetworkPolicy — off a single GamePort const (25565). A backend that bound 30000 would
# be unreachable through that fence. Limbo writes a full server.properties on first run
# and merges any partial we leave in place, so seeding/patching just server-port here is
# enough; the spawn schematic still loads from ./spawn.schem.
# Two things must be true before Limbo accepts a connection, and both are settings
# Limbo writes into server.properties on first run:
#
# Idempotent by design: it runs on every start and rewrites only the server-port line,
# so a persisted world volume that already carries a server.properties keeps all its
# other settings.
#   server-port      — pinned to the pod-facing port the operator contract uses. LOOHP/Limbo
#                      defaults it to 30000, but the Felis operator drives everything (the
#                      Service Port/TargetPort, the readiness probe, the container port and
#                      the Velocity NetworkPolicy) off a single GamePort const (25565). A
#                      backend that bound 30000 would be unreachable through that fence.
#
#   velocity-modern  — Velocity modern player-info forwarding. This is the ONLY reason the
#   forwarding-secrets  login gate can be trusted to know WHO joined. With it on, Limbo
#                      verifies the proxy's HMAC over the login payload and takes the
#                      player's UUID from that signed payload (ClientConnection.java:
#                      validateVelocityModernResponse → getVelocityDataFrom → new Player(...,
#                      data.getUuid())). With it off, Limbo derives an OFFLINE UUID from the
#                      username — and the felis-limbo plugin would then mint a /link code
#                      bound to the WRONG Minecraft identity. The Owner IS a Minecraft
#                      account, claimed by joining this gate, so that is account takeover,
#                      not a cosmetic bug.
#
# Hence: NO SECRET, NO START. Refusing to boot is the safe failure — the operator marks the
# server Failed, `felis setup` surfaces the reason and stops before asking for a link code.
# A limbo that came up in offline mode would look perfectly healthy while handing out
# forgeable identities. forwarding-secrets is Limbo's ';'-separated list; Felis writes one.
#
# Idempotent by design: it runs on every start and rewrites only the keys below, so a
# persisted volume that already carries a server.properties keeps its other settings (and
# the spawn schematic still loads from ./spawn.schem).
set -eu

PORT="${FELIS_GAME_PORT:-25565}"
SECRET="${FELIS_FORWARDING_SECRET:-}"
RUNTIME_DIR="/limbo"
DATA_DIR="/data"
PROPS="server.properties"

if [ -f "$PROPS" ]; then
  if grep -q '^server-port=' "$PROPS"; then
    sed -i "s/^server-port=.*/server-port=${PORT}/" "$PROPS"
  else
    printf 'server-port=%s\n' "$PORT" >> "$PROPS"
if [ -z "$SECRET" ]; then
  echo "felis-limbo: FATAL — FELIS_FORWARDING_SECRET is empty." >&2
  echo "  The login gate authenticates the Owner, so it must not run without Velocity modern" >&2
  echo "  forwarding: an unverified UUID would let anyone claim any Minecraft identity." >&2
  echo "  Provision the secret with deploy/bootstrap.sh, then re-run 'sudo felis setup'." >&2
  exit 1
fi

# Keep mutable server state on the operator-mounted PVC. Refresh code artifacts on
# every boot so an image upgrade takes effect without replacing worlds or config.
mkdir -p "$DATA_DIR/plugins"
cp -f "$RUNTIME_DIR/Limbo.jar" "$DATA_DIR/Limbo.jar"
cp -f "$RUNTIME_DIR/plugins/felis-limbo.jar" "$DATA_DIR/plugins/felis-limbo.jar"
if [ -f "$RUNTIME_DIR/spawn.schem" ] && [ ! -f "$DATA_DIR/spawn.schem" ]; then
  cp "$RUNTIME_DIR/spawn.schem" "$DATA_DIR/spawn.schem"
fi
cd "$DATA_DIR"

# set_prop KEY VALUE — replace the key's line, or append it if absent.
set_prop() {
  if [ -f "$PROPS" ] && grep -q "^$1=" "$PROPS"; then
    # The secret is base64/hex-ish, but a '/' or '&' would still break a bare sed s///.
    # '|' as the delimiter plus escaping it is enough for every value we write.
    esc=$(printf '%s' "$2" | sed 's/[|\\&]/\\&/g')
    sed -i "s|^$1=.*|$1=${esc}|" "$PROPS"
  else
  printf 'server-port=%s\n' "$PORT" > "$PROPS"
    printf '%s=%s\n' "$1" "$2" >> "$PROPS"
  fi
}

echo "felis-limbo: pinned server-port=${PORT} (operator GamePort)"
exec java -jar Limbo.jar --nogui "$@"
set_prop server-port "$PORT"
# velocity-modern is mutually exclusive with the two legacy schemes in Limbo's own
# check — pin them off so a stale persisted properties file cannot silently downgrade
# the gate to a forwarding mode that carries no signature at all.
set_prop bungeecord false
set_prop bungee-guard false
set_prop velocity-modern true
set_prop forwarding-secrets "$SECRET"

echo "felis-limbo: server-port=${PORT}, velocity-modern=true (forwarding secret loaded, UUIDs are Mojang-verified)"
JAVA_MEMORY_ARG=""
if [ -n "${JAVA_MEMORY:-}" ]; then
  JAVA_MEMORY_ARG="-Xmx${JAVA_MEMORY}"
fi
set -f
# JAVA_FLAGS is emitted by the operator as a whitespace-separated JVM argument list.
# shellcheck disable=SC2086
exec java $JAVA_MEMORY_ARG ${JAVA_FLAGS:-} -jar Limbo.jar --nogui "$@"
+31 −9
Changes for deploy/lobby/Dockerfile: 31 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -5,16 +5,22 @@
# the POST-auth /menu hub: it is reached only when the login gate transfers an
# authenticated player onward, and it must never be a fallback target.
#
# Build:
# Build (deploy/bootstrap.sh does this for you; the PAPER_JAR_URL comes from PaperMC's
# Fill v3 API — api.papermc.io v2 has returned HTTP 410 since 2026-07-01):
#   docker build -f deploy/lobby/Dockerfile \
#     --build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \
#     --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-26.2-<build>.jar \
#     -t felis-lobby:demo .
#   docker save felis-lobby:demo | sudo k3s ctr images import -
#   # felis.toml → [velocity] lobby_image = "felis-lobby:demo"
#
# Contract: the game server listens on 25565 (the CRD GamePort). The lobby speaks
# only the felis:control plugin-message channel (spec §12) — it holds no felis-api
# token.
# The Paper version must match the LOGIN gate's: LOOHP/Limbo speaks exactly ONE protocol
# per build (its SERVER_IMPLEMENTATION_VERSION), and a client has to satisfy both hops.
#
# Contract: the game server listens on 25565 (the CRD GamePort). The lobby speaks only the
# felis:control plugin-message channel (spec §12) — it holds no felis-api token. It DOES
# receive FELIS_FORWARDING_SECRET (operator-injected from the felis-forwarding-secret
# Secret) and refuses to start without it: a lobby that cannot verify the proxy's signed
# handshake would trust an offline, forgeable UUID.

# ---- build the felis-paper plugin jar (Paper API is Java 21) ----
# gradle:8.14-jdk21 — an official Gradle image on JDK 21 (this tree vendors no Gradle
@@ -29,7 +35,10 @@ RUN cd plugins/paper \
    && cp build/libs/*.jar /felis-paper.jar

# ---- assemble the runtime ----
FROM eclipse-temurin:21-jre
# 25-jre, not 21: Paper 26.2 declares `java.version.minimum = 25` (PaperMC Fill v3,
# GET /v3/projects/paper/versions/26.2) and refuses to boot on anything older. A 25 JRE
# also runs the plugin's Java-21 bytecode, so only the runtime moves.
FROM eclipse-temurin:25-jre
ARG PAPER_JAR_URL
WORKDIR /paper
RUN set -eu; \
@@ -42,8 +51,21 @@ RUN set -eu; \
    mkdir -p /paper/plugins; \
    echo "eula=true" > /paper/eula.txt
COPY --from=plugin /felis-paper.jar /paper/plugins/felis-paper.jar
# The entrypoint writes the Velocity modern-forwarding config (and REFUSES to start
# without the secret — an offline-mode lobby would trust forged identities) before
# launching Paper.
COPY deploy/lobby/entrypoint.sh /usr/local/bin/felis-entrypoint.sh

# The operator mounts the world PVC at /data. Runtime state lives there; /paper
# remains the immutable image seed copied into the volume by the entrypoint.
WORKDIR /data

# FELIS_GAME_PORT is the port the entrypoint pins Paper to; it MUST equal the operator's
# GamePort (internal/operator/builders.go). Default 25565 — override only in lockstep
# with the operator.
ENV FELIS_GAME_PORT=25565
EXPOSE 25565
# nogui headless; the first boot generates server.properties (align online-mode /
# forwarding with the Velocity proxy afterwards — see README).
ENTRYPOINT ["java", "-jar", "paper.jar", "--nogui"]
# felis-entrypoint.sh writes config/paper-global.yml + server.properties, then execs
# `java -jar paper.jar --nogui` from /data (headless: the pod has no console). Invoked via
# `sh` so no +x bit is needed from the (Windows) build host.
ENTRYPOINT ["/bin/sh", "/usr/local/bin/felis-entrypoint.sh"]
+85 −0
Changes for deploy/lobby/entrypoint.sh: 85 added lines, 0 removed lines.
Original line number Diff line number Diff line
#!/bin/sh
# Felis lobby (Paper) entrypoint.
#
# The lobby sits BEHIND the login gate: a player only reaches it once the limbo has
# authenticated them and Velocity transferred them onward. For that transfer to arrive
# with a real identity, Paper has to be told to verify the proxy's signed handshake —
# otherwise it derives an offline UUID from the username and every /menu action would be
# attributed to whoever typed the name. So, exactly as in deploy/limbo/entrypoint.sh:
# NO SECRET, NO START. Refusing to boot is the safe failure; a lobby that came up in
# offline mode would look healthy while trusting forged identities.
#
# Two files carry the settings:
#
#   config/paper-global.yml  proxies.velocity.{enabled,online-mode,secret} — enable modern
#                            forwarding and share the proxy's HMAC key. online-mode mirrors
#                            the proxy's own online-mode (true: Velocity did the Mojang
#                            auth), which is what makes the forwarded UUID trustworthy.
#
#   server.properties        online-mode=false — the PROXY authenticated the player, so the
#                            backend must not try to reach Mojang itself (Paper refuses to
#                            start with velocity forwarding on and online-mode=true). This
#                            is not a downgrade: the trust comes from the signed handshake.
#                            server-port is pinned to the operator's GamePort (25565), the
#                            single const the Service, probes and NetworkPolicy all key off.
set -eu

PORT="${FELIS_GAME_PORT:-25565}"
SECRET="${FELIS_FORWARDING_SECRET:-}"
RUNTIME_DIR="/paper"
DATA_DIR="/data"
PROPS="server.properties"

if [ -z "$SECRET" ]; then
  echo "felis-lobby: FATAL — FELIS_FORWARDING_SECRET is empty." >&2
  echo "  Without Velocity modern forwarding Paper cannot verify who a joining player is," >&2
  echo "  and would trust an offline UUID derived from the username alone." >&2
  echo "  Provision the secret with deploy/bootstrap.sh, then re-run 'sudo felis setup'." >&2
  exit 1
fi

# Keep worlds, generated config, and plugin data on the operator-mounted PVC while
# refreshing executable artifacts from the immutable image on every boot.
mkdir -p "$DATA_DIR/plugins"
cp -f "$RUNTIME_DIR/paper.jar" "$DATA_DIR/paper.jar"
cp -f "$RUNTIME_DIR/plugins/felis-paper.jar" "$DATA_DIR/plugins/felis-paper.jar"
printf 'eula=true\n' > "$DATA_DIR/eula.txt"
cd "$DATA_DIR"

# set_prop KEY VALUE — replace the key's line in server.properties, or append it if absent.
set_prop() {
  if [ -f "$PROPS" ] && grep -q "^$1=" "$PROPS"; then
    sed -i "s|^$1=.*|$1=$2|" "$PROPS"
  else
    printf '%s=%s\n' "$1" "$2" >> "$PROPS"
  fi
}

set_prop server-port "$PORT"
set_prop online-mode false

# ponytail: rewritten whole, not merged. Paper loads this file and fills every key it does
# not find with the default, then writes the full tree back — so a proxies-only file is a
# complete, stable input, and the lobby's other globals are simply always the defaults.
# That is true of a system server Felis owns end to end; if admins are ever allowed to tune
# the lobby's globals, this has to become a real YAML merge (yq) instead.
mkdir -p config
cat > config/paper-global.yml <<YAML
# Written by felis-lobby's entrypoint on every boot. Do not hand-edit: the forwarding
# secret is injected from the felis-forwarding-secret Secret and must match the proxy.
proxies:
  velocity:
    enabled: true
    online-mode: true
    secret: "${SECRET}"
YAML

echo "felis-lobby: server-port=${PORT}, velocity modern forwarding on (UUIDs are Mojang-verified)"
JAVA_MEMORY_ARG=""
if [ -n "${JAVA_MEMORY:-}" ]; then
  JAVA_MEMORY_ARG="-Xmx${JAVA_MEMORY}"
fi
set -f
# JAVA_FLAGS is emitted by the operator as a whitespace-separated JVM argument list.
# shellcheck disable=SC2086
exec java $JAVA_MEMORY_ARG ${JAVA_FLAGS:-} -jar paper.jar --nogui "$@"
Loading