Compare commits

...
2 Commits
Author SHA1 Message Date
dmg 3564d24a45 feat(rcon): refine server console
CI / validate (push) Successful in 6m11s
Release / release (push) Successful in 7m59s
2026-08-08 08:06:11 -04:00
dmg 7f6d69e0a7 feat(rcon): allow administrator-defined endpoints
CI / validate (push) Successful in 6m16s
Release / release (push) Successful in 8m27s
2026-08-08 07:45:24 -04:00
13 changed files with 86 additions and 73 deletions
+1 -3
View File
@@ -25,9 +25,7 @@ PROXYCHECK_API_KEY=
IP_INTELLIGENCE_CACHE_HOURS=48 IP_INTELLIGENCE_CACHE_HOURS=48
BLOCK_HOSTING_IPS=false BLOCK_HOSTING_IPS=false
# Internal RCON proxy. Endpoints must be exact host:port pairs. # Optional independent 32-byte base64 RCON keys. When omitted, domain-separated keys are derived from AUTH_SECRET.
RCON_ALLOWED_ENDPOINTS=season4.somc.svc.cluster.local:25575
# Optional independent 32-byte base64 keys. When omitted, domain-separated keys are derived from AUTH_SECRET.
RCON_CREDENTIAL_KEY= RCON_CREDENTIAL_KEY=
RCON_AUDIT_KEY= RCON_AUDIT_KEY=
+1 -1
View File
@@ -76,7 +76,7 @@ The token is displayed once and stored only as a SHA-256 hash.
- PostgreSQL and Drizzle ORM - PostgreSQL and Drizzle ORM
- Keycloak OIDC for admin access with the `minecraft-account-manager-admin` role - Keycloak OIDC for admin access with the `minecraft-account-manager-admin` role
- Admin user search, account management, event exploration, DAU and confirmed-connection metrics, toggleable Natural Earth/OpenStreetMap user-location views, internal RCON connection management and command proxying, and automatic Discord nickname synchronization - Admin user search, account management, event exploration, DAU and confirmed-connection metrics, toggleable Natural Earth/OpenStreetMap user-location views, RCON server-address management and command proxying, and automatic Discord nickname synchronization
- Exclusive group admission: unassigned users fall back to protected `everyone`, and administrators manage effective membership, access, recurring UTC login windows, and VPN/proxy/Tor exceptions through confirmed group workflows - Exclusive group admission: unassigned users fall back to protected `everyone`, and administrators manage effective membership, access, recurring UTC login windows, and VPN/proxy/Tor exceptions through confirmed group workflows
- Deployment-managed Discord guild ID and invite URL - Deployment-managed Discord guild ID and invite URL
- discord.js bot with `/register` and `/account` - discord.js bot with `/register` and `/account`
@@ -22,7 +22,7 @@ const savedMessages: Record<string, string> = {
}; };
const errorMessages: Record<string, string> = { const errorMessages: Record<string, string> = {
"invalid-connection": "Enter a valid allowlisted hostname, port, name, and password.", "invalid-connection": "Enter a valid DNS hostname, port, name, and password.",
"duplicate-name": "Connection names must be unique.", "duplicate-name": "Connection names must be unique.",
configuration: "RCON credential encryption is not configured.", configuration: "RCON credential encryption is not configured.",
"save-failed": "The RCON connection could not be saved.", "save-failed": "The RCON connection could not be saved.",
@@ -60,7 +60,7 @@ export default async function RconPage({
<header className="border-b border-line pb-8"> <header className="border-b border-line pb-8">
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Server operations</p> <p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Server operations</p>
<h1 className="mt-4 font-display text-5xl font-black uppercase sm:text-7xl">RCON</h1> <h1 className="mt-4 font-display text-5xl font-black uppercase sm:text-7xl">RCON</h1>
<p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Run commands through the portal backend. RCON endpoints remain internal and credentials are never sent to the browser.</p> <p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Run commands through the portal backend to internal or external server addresses. Credentials are never sent to the browser.</p>
</header> </header>
{saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 font-mono text-xs font-bold uppercase tracking-wider" role="status">{savedMessages[saved] ?? "RCON settings saved."}</p>} {saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 font-mono text-xs font-bold uppercase tracking-wider" role="status">{savedMessages[saved] ?? "RCON settings saved."}</p>}
@@ -139,7 +139,7 @@ function ConnectionFields({
return ( return (
<> <>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-name`}>Name<input className={fieldClass} defaultValue={defaults?.name} id={`${prefix}-rcon-name`} maxLength={100} name="name" required /></label> <label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-name`}>Name<input className={fieldClass} defaultValue={defaults?.name} id={`${prefix}-rcon-name`} maxLength={100} name="name" required /></label>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-host`}>Internal hostname<input autoCapitalize="none" autoCorrect="off" className={fieldClass} defaultValue={defaults?.host} id={`${prefix}-rcon-host`} maxLength={253} name="host" placeholder="season4.somc.svc.cluster.local" required spellCheck={false} /></label> <label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-host`}>Server address<input autoCapitalize="none" autoCorrect="off" className={fieldClass} defaultValue={defaults?.host} id={`${prefix}-rcon-host`} maxLength={253} name="host" placeholder="minecraft.example.com" required spellCheck={false} /></label>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-port`}>Port<input className={fieldClass} defaultValue={defaults?.port ?? 25575} id={`${prefix}-rcon-port`} max={65535} min={1} name="port" required type="number" /></label> <label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-port`}>Port<input className={fieldClass} defaultValue={defaults?.port ?? 25575} id={`${prefix}-rcon-port`} max={65535} min={1} name="port" required type="number" /></label>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-password`}>{defaults ? "Replacement password" : "Password"}<input autoComplete="new-password" className={fieldClass} id={`${prefix}-rcon-password`} maxLength={512} name="password" required={!defaults} type="password" />{defaults && <span className="mt-2 block font-sans text-[10px] font-normal normal-case text-muted">Leave blank to preserve the current password.</span>}</label> <label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-password`}>{defaults ? "Replacement password" : "Password"}<input autoComplete="new-password" className={fieldClass} id={`${prefix}-rcon-password`} maxLength={512} name="password" required={!defaults} type="password" />{defaults && <span className="mt-2 block font-sans text-[10px] font-normal normal-case text-muted">Leave blank to preserve the current password.</span>}</label>
</> </>
@@ -8,12 +8,15 @@ vi.mock("@/app/admin/(console)/rcon/actions", () => ({
import { RconConsole } from "./rcon-console"; import { RconConsole } from "./rcon-console";
describe("RconConsole", () => { describe("RconConsole", () => {
it("renders labelled keyboard-operable controls without history", () => { it("renders a labelled keyboard-operable terminal without persisted history", () => {
const markup = renderToStaticMarkup(<RconConsole servers={[{ id: "one", name: "Season 4" }]} />); const markup = renderToStaticMarkup(<RconConsole servers={[{ id: "one", name: "Season 4" }]} />);
expect(markup).toContain('aria-label="RCON terminal"');
expect(markup).toContain('for="rcon-console-server"'); expect(markup).toContain('for="rcon-console-server"');
expect(markup).toContain('for="rcon-command"'); expect(markup).toContain('for="rcon-command"');
expect(markup).toContain("Season 4"); expect(markup).toContain("Season 4");
expect(markup).toContain("Run command"); expect(markup).toContain("server://");
expect(markup).toContain("Awaiting command");
expect(markup).toContain("Enter ↵");
expect(markup).not.toContain("Latest response"); expect(markup).not.toContain("Latest response");
}); });
+34 -17
View File
@@ -16,24 +16,41 @@ export function RconConsole({ servers }: { servers: ServerOption[] }) {
} }
return ( return (
<form action={action} className="mt-6 space-y-4"> <form action={action} aria-label="RCON terminal" className="mt-6 overflow-hidden border-2 border-ink bg-panel shadow-[6px_6px_0_var(--color-shadow)]">
<label className="block font-mono text-[10px] font-bold uppercase tracking-wider" htmlFor="rcon-console-server"> <div className="flex flex-wrap items-center justify-between gap-3 border-b border-line bg-canvas px-4 py-3">
Server <div className="flex items-center gap-2 font-mono text-[10px] font-bold uppercase tracking-wider text-muted">
<select className="mt-2 block w-full border border-line bg-canvas px-4 py-3 font-sans text-sm font-normal normal-case" defaultValue={state.serverId || servers[0]?.id} id="rcon-console-server" name="serverId" required> <span aria-hidden="true" className="size-2 rounded-full bg-signal shadow-[0_0_0_1px_var(--color-ink)]" />
{servers.map((server) => <option key={server.id} value={server.id}>{server.name}</option>)} <span>server://</span>
</select>
</label>
<label className="block font-mono text-[10px] font-bold uppercase tracking-wider" htmlFor="rcon-command">
Command
<input autoComplete="off" className="mt-2 block w-full border border-line bg-canvas px-4 py-3 font-mono text-sm outline-none focus:border-accent" id="rcon-command" maxLength={1024} name="command" placeholder="list" required spellCheck={false} />
</label>
<button className="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas disabled:cursor-wait disabled:opacity-60" disabled={pending} type="submit">{pending ? "Running…" : "Run command"}</button>
{state.status !== "idle" && (
<div aria-live="polite" className={`border-l-2 bg-canvas p-4 ${state.status === "error" ? "border-accent" : "border-signal"}`} role={state.status === "error" ? "alert" : "status"}>
<p className="font-mono text-[9px] font-bold uppercase tracking-wider text-muted">Latest response{responseServer ? `${responseServer.name}` : ""}</p>
<pre className="mt-2 max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-5">{state.message}</pre>
</div> </div>
)} <label className="flex items-center gap-2 font-mono text-[9px] font-bold uppercase tracking-wider" htmlFor="rcon-console-server">
Target
<select className="border border-line bg-panel px-3 py-2 font-mono text-xs font-bold normal-case outline-none focus:border-accent" defaultValue={state.serverId || servers[0]?.id} id="rcon-console-server" name="serverId" required>
{servers.map((server) => <option key={server.id} value={server.id}>{server.name}</option>)}
</select>
</label>
</div>
<div aria-live="polite" aria-relevant="additions text" className="min-h-52 max-h-80 overflow-auto p-5 font-mono text-xs leading-5" role={state.status === "error" ? "alert" : "status"}>
{pending ? (
<p className="text-muted"><span aria-hidden="true" className="mr-2 text-accent"></span>Executing command</p>
) : state.status === "idle" ? (
<p className="text-muted"><span aria-hidden="true" className="mr-2 text-accent">#</span>Awaiting command</p>
) : (
<>
<p className={`text-[9px] font-bold uppercase tracking-wider ${state.status === "error" ? "text-accent" : "text-muted"}`}>
{state.status === "error" ? "Error" : "Response"}{responseServer ? `${responseServer.name}` : ""}
</p>
<pre className="mt-3 whitespace-pre-wrap break-words font-mono text-xs leading-5">{state.message}</pre>
</>
)}
</div>
<div className="flex items-center gap-3 border-t-2 border-ink bg-canvas p-3">
<span aria-hidden="true" className="font-mono text-lg font-black text-accent">$</span>
<label className="sr-only" htmlFor="rcon-command">Command</label>
<input autoComplete="off" autoFocus className="min-w-0 flex-1 bg-transparent px-1 py-2 font-mono text-sm outline-none placeholder:text-muted focus-visible:outline-none" id="rcon-command" maxLength={1024} name="command" placeholder="list" required spellCheck={false} />
<button className="border border-ink bg-ink px-4 py-2 font-mono text-[9px] font-bold uppercase tracking-wider text-canvas disabled:cursor-wait disabled:opacity-60" disabled={pending} type="submit">{pending ? "Running…" : "Enter ↵"}</button>
</div>
</form> </form>
); );
} }
+17 -15
View File
@@ -1,32 +1,36 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { sanitizeRconOutput, validateRconCommand, validateRconConnection } from "./rcon-validation"; import { sanitizeRconOutput, validateRconCommand, validateRconConnection } from "./rcon-validation";
const allowed = "season4.somc.svc.cluster.local:25575,creative.somc.svc.cluster.local:25576";
describe("RCON validation", () => { describe("RCON validation", () => {
it("normalizes an allowlisted internal endpoint", () => { it("normalizes any valid DNS hostname and port without deployment configuration", () => {
expect(validateRconConnection({ expect(validateRconConnection({
name: " Season 4 ", name: " Season 4 ",
host: "SEASON4.SOMC.SVC.CLUSTER.LOCAL", host: "SEASON4.SOMC.SVC.CLUSTER.LOCAL",
port: "25575", port: "25575",
password: "correct horse battery staple", password: "correct horse battery staple",
}, { allowedEndpoints: allowed, passwordRequired: true })).toEqual({ }, { passwordRequired: true })).toEqual({
name: "Season 4", name: "Season 4",
host: "season4.somc.svc.cluster.local", host: "season4.somc.svc.cluster.local",
port: 25575, port: 25575,
password: "correct horse battery staple", password: "correct horse battery staple",
}); });
expect(validateRconConnection({
name: "Creative",
host: "creative.example.net",
port: "43210",
password: "secret",
}, { passwordRequired: true })).toEqual({
name: "Creative",
host: "creative.example.net",
port: 43210,
password: "secret",
});
}); });
it("rejects unlisted hosts, ports, IP literals, and suffix confusion", () => { it("rejects IP literals and malformed DNS hostnames", () => {
for (const [host, port] of [ for (const host of ["10.0.0.1", "2001:db8::1", "season4.", "-season4.example", "season4..example"]) {
["postgres.somc.svc.cluster.local", "5432"], expect(validateRconConnection({ name: "Server", host, port: "25575", password: "secret" }, {
["season4.somc.svc.cluster.local", "5432"],
["season4.somc.svc.cluster.local.attacker.example", "25575"],
["10.0.0.1", "25575"],
]) {
expect(validateRconConnection({ name: "Server", host, port, password: "secret" }, {
allowedEndpoints: allowed,
passwordRequired: true, passwordRequired: true,
})).toBeNull(); })).toBeNull();
} }
@@ -34,11 +38,9 @@ describe("RCON validation", () => {
it("allows a blank replacement password only while editing", () => { it("allows a blank replacement password only while editing", () => {
expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, { expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, {
allowedEndpoints: allowed,
passwordRequired: false, passwordRequired: false,
})?.password).toBeNull(); })?.password).toBeNull();
expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, { expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, {
allowedEndpoints: allowed,
passwordRequired: true, passwordRequired: true,
})).toBeNull(); })).toBeNull();
}); });
+1 -7
View File
@@ -12,25 +12,19 @@ export type ValidRconConnection = {
password: string | null; password: string | null;
}; };
function endpointSet(value: string) {
return new Set(value.split(",").map((endpoint) => endpoint.trim().toLowerCase()).filter(Boolean));
}
export function validateRconConnection( export function validateRconConnection(
input: { name: unknown; host: unknown; port: unknown; password: unknown }, input: { name: unknown; host: unknown; port: unknown; password: unknown },
options: { allowedEndpoints?: string; passwordRequired: boolean }, options: { passwordRequired: boolean },
): ValidRconConnection | null { ): ValidRconConnection | null {
const name = typeof input.name === "string" ? input.name.trim() : ""; const name = typeof input.name === "string" ? input.name.trim() : "";
const host = typeof input.host === "string" ? input.host.trim().toLowerCase() : ""; const host = typeof input.host === "string" ? input.host.trim().toLowerCase() : "";
const portText = typeof input.port === "string" || typeof input.port === "number" ? String(input.port).trim() : ""; const portText = typeof input.port === "string" || typeof input.port === "number" ? String(input.port).trim() : "";
const passwordText = typeof input.password === "string" ? input.password : ""; const passwordText = typeof input.password === "string" ? input.password : "";
const port = Number(portText); const port = Number(portText);
const allowed = endpointSet(options.allowedEndpoints ?? process.env.RCON_ALLOWED_ENDPOINTS ?? "");
if (!name || name.length > 100 || CONTROL_PATTERN.test(name)) return null; if (!name || name.length > 100 || CONTROL_PATTERN.test(name)) return null;
if (!host || host.endsWith(".") || isIP(host) !== 0 || !HOST_PATTERN.test(host)) return null; if (!host || host.endsWith(".") || isIP(host) !== 0 || !HOST_PATTERN.test(host)) return null;
if (!Number.isInteger(port) || port < 1 || port > 65_535) return null; if (!Number.isInteger(port) || port < 1 || port > 65_535) return null;
if (!allowed.has(`${host}:${port}`)) return null;
if (passwordText.length > 512 || CONTROL_PATTERN.test(passwordText)) return null; if (passwordText.length > 512 || CONTROL_PATTERN.test(passwordText)) return null;
if (options.passwordRequired && !passwordText) return null; if (options.passwordRequired && !passwordText) return null;
+1 -1
View File
@@ -34,7 +34,7 @@ This OKF bundle is the product record for implemented and proposed behavior. Sto
* [US-018 — Monitor community account activity](us-018-admin-dashboard.md) - Administrators review daily users, confirmed connections, locations, denials, and risky networks. * [US-018 — Monitor community account activity](us-018-admin-dashboard.md) - Administrators review daily users, confirmed connections, locations, denials, and risky networks.
* [US-019 — Manage groups efficiently](us-019-admin-group-management.md) - Administrators manage group identity, policies, membership, and creation through focused confirmed workflows. * [US-019 — Manage groups efficiently](us-019-admin-group-management.md) - Administrators manage group identity, policies, membership, and creation through focused confirmed workflows.
* [US-020 — Schedule group access in UTC](us-020-scheduled-group-access.md) - Enabled groups may be restricted to recurring weekly UTC windows with static denial-message templates. * [US-020 — Schedule group access in UTC](us-020-scheduled-group-access.md) - Enabled groups may be restricted to recurring weekly UTC windows with static denial-message templates.
* [US-021 — Manage RCON server connections](us-021-rcon-connections.md) - Administrators manage encrypted internal Minecraft RCON endpoints. * [US-021 — Manage RCON server connections](us-021-rcon-connections.md) - Administrators manage encrypted Minecraft RCON server addresses.
* [US-022 — Operate servers through an RCON console](us-022-rcon-console.md) - Administrators execute bounded commands through the server-side portal proxy. * [US-022 — Operate servers through an RCON console](us-022-rcon-console.md) - Administrators execute bounded commands through the server-side portal proxy.
# Tracking # Tracking
+3
View File
@@ -4,6 +4,9 @@
* **Verify**: Added encrypted, allowlisted administrator RCON connection management with credential-safe audits and a generated Drizzle migration. * **Verify**: Added encrypted, allowlisted administrator RCON connection management with credential-safe audits and a generated Drizzle migration.
* **Implement**: Added a bounded server-side RCON command console with safe output and error handling; internal-only deployment verification remains pending. * **Implement**: Added a bounded server-side RCON command console with safe output and error handling; internal-only deployment verification remains pending.
* **Refine**: Removed deployment-managed RCON endpoint allowlisting so administrators may configure any valid DNS hostname and port, while retaining IP-literal rejection and documenting the outbound-connectivity trust boundary.
* **Verify**: Confirmed the RCON console uses an authenticated internal ClusterIP deployment with secret-backed credentials and no public RCON exposure.
* **Refine**: Renamed RCON host configuration to server addresses, documented internal and external targets, and redesigned the console as a portal-colored terminal with a target bar, command prompt, and latest-response viewport.
## 2026-08-07 ## 2026-08-07
+6 -6
View File
@@ -1,9 +1,9 @@
--- ---
type: User Story type: User Story
title: Manage RCON server connections title: Manage RCON server connections
description: Administrators manage encrypted connection settings for internal Minecraft RCON endpoints. description: Administrators manage encrypted connection settings for Minecraft RCON server addresses.
tags: [admin, rcon, minecraft, security, operations] tags: [admin, rcon, minecraft, security, operations]
timestamp: 2026-08-08T01:36:00Z timestamp: 2026-08-08T12:05:37Z
story_id: US-021 story_id: US-021
status: verified status: verified
--- ---
@@ -15,10 +15,10 @@ As an administrator, I want to manage one or more Minecraft RCON connections, so
# Acceptance Criteria # Acceptance Criteria
- [x] Existing account-manager administrators can list, add, edit, test, enable or disable, and delete RCON server connections. - [x] Existing account-manager administrators can list, add, edit, test, enable or disable, and delete RCON server connections.
- [x] Each connection has a unique display name, internal hostname, port, enabled state, and write-only password. - [x] Each connection has a unique display name, server address, port, enabled state, and write-only password.
- [x] RCON passwords are encrypted with an authenticated cipher using a deployment-managed master key and are never returned to the browser, audit events, or application logs. - [x] RCON passwords are encrypted with an authenticated cipher using a deployment-managed master key and are never returned to the browser, audit events, or application logs.
- [x] Updating a connection preserves its password unless an administrator explicitly supplies a replacement. - [x] Updating a connection preserves its password unless an administrator explicitly supplies a replacement.
- [x] Connection host and port pairs must match a deployment-managed exact internal endpoint allowlist, and IP literals are rejected. - [x] Administrators can save any syntactically valid DNS hostname and port without deployment-managed endpoint configuration; IP literals remain rejected.
- [x] Testing a connection authenticates through the server-side RCON proxy and reports a safe success or failure result. - [x] Testing a connection authenticates through the server-side RCON proxy and reports a safe success or failure result.
- [x] Deleting a connection requires explicit confirmation. - [x] Deleting a connection requires explicit confirmation.
- [x] Connection mutations independently recheck administrator authorization and create credential-safe audit events. - [x] Connection mutations independently recheck administrator authorization and create credential-safe audit events.
@@ -26,11 +26,11 @@ As an administrator, I want to manage one or more Minecraft RCON connections, so
# Implementation # Implementation
The administrator RCON page and server actions manage allowlisted endpoints, preserve write-only passwords, encrypt credentials with connection-bound AES-256-GCM, and emit credential-safe audit events. The `rcon_servers` table is delivered through generated migration `0006_curious_lester.sql`. The administrator RCON page and server actions manage endpoints without deployment-managed endpoint configuration, preserve write-only passwords, encrypt credentials with connection-bound AES-256-GCM, and emit credential-safe audit events. The `rcon_servers` table is delivered through generated migration `0006_curious_lester.sql`.
# Validation # Validation
Verified with RCON validation, encryption, gateway, component, and server-action tests; full workspace tests and type checks; web lint; OKF validation; and a production Next.js build on 2026-08-08. Action tests confirm independent authorization, password preservation, enabled-state rechecks, safe failures, and command audit redaction. Verified with RCON validation, encryption, gateway, component, and server-action tests; full workspace tests and type checks; web lint; OKF validation; Semgrep; dependency audit; and a production Next.js build on 2026-08-08. Validation confirms arbitrary valid internal or external DNS server addresses and ports no longer require deployment configuration while IP literals and malformed hostnames remain rejected. Action tests confirm independent authorization, password preservation, enabled-state rechecks, safe failures, and command audit redaction.
# Related Stories # Related Stories
+8 -8
View File
@@ -3,34 +3,34 @@ type: User Story
title: Operate servers through an RCON console title: Operate servers through an RCON console
description: Administrators execute bounded RCON commands through the server-side portal proxy. description: Administrators execute bounded RCON commands through the server-side portal proxy.
tags: [admin, rcon, minecraft, console, security] tags: [admin, rcon, minecraft, console, security]
timestamp: 2026-08-08T01:36:00Z timestamp: 2026-08-08T12:05:37Z
story_id: US-022 story_id: US-022
status: in-progress status: verified
--- ---
# User Story # User Story
As an administrator, I want an RCON console in the portal, so that I can operate internal Minecraft servers without exposing RCON publicly. As an administrator, I want an RCON console in the portal, so that I can operate configured Minecraft servers without exposing credentials to the browser.
# Acceptance Criteria # Acceptance Criteria
- [x] Existing account-manager administrators can select an enabled connection and execute an RCON command from the admin UI. - [x] Existing account-manager administrators can select an enabled connection and execute an RCON command from the admin UI.
- [x] Browsers never connect to RCON directly; commands pass through the authenticated Next.js server runtime to an internal endpoint. - [x] Browsers never connect to RCON directly; commands pass through the authenticated Next.js server runtime to the configured endpoint.
- [x] Every command independently rechecks administrator authorization and the selected connection's enabled state. - [x] Every command independently rechecks administrator authorization and the selected connection's enabled state.
- [x] Commands are length-limited, reject control characters, execute with bounded concurrency and a timeout, and return bounded output. - [x] Commands are length-limited, reject control characters, execute with bounded concurrency and a timeout, and return bounded output.
- [x] Command responses are displayed safely and are not persisted in console history, audit data, or application logs. - [x] Command responses are displayed safely and are not persisted in console history, audit data, or application logs.
- [x] Audit events record the administrator, connection, command verb and digest, success, and duration without recording complete commands or responses. - [x] Audit events record the administrator, connection, command verb and digest, success, and duration without recording complete commands or responses.
- [x] Authentication, timeout, and connection failures return safe operator-facing messages without credentials or stack traces. - [x] Authentication, timeout, and connection failures return safe operator-facing messages without credentials or stack traces.
- [x] The console is keyboard accessible and clearly identifies the selected server. - [x] The console uses the portal color palette to present a terminal-style server header, single keyboard-accessible prompt, pending state, and scrollable latest-response viewport.
- [ ] RCON remains internal to the cluster and is not exposed through public ingress or a load balancer. - [x] Configured server addresses may be internal or external, and operators receive guidance that RCON network exposure and transport security remain their responsibility.
# Implementation # Implementation
The client console invokes an authenticated server action that revalidates the enabled allowlisted connection, decrypts its credential only in the server runtime, and executes one bounded command. The gateway limits each process to one operation per server and eight total operations, applies a five-second end-to-end deadline plus bounded cleanup, sanitizes and truncates output, and records keyed command lifecycle audits without command or response content. The portal-colored terminal interface identifies the selected server in its header, accepts one command through a keyboard-focused prompt, and displays only the latest bounded response in a scrollable viewport. The client invokes an authenticated server action that revalidates the enabled connection, decrypts its credential only in the server runtime, and executes one bounded command. The gateway limits each process to one operation per server and eight total operations, applies a five-second end-to-end deadline plus bounded cleanup, sanitizes and truncates output, and records keyed command lifecycle audits without command or response content.
# Validation # Validation
Application behavior is verified with gateway, validation, component, credential, and server-action tests; full workspace tests and type checks; web lint; OKF validation; and a production Next.js build on 2026-08-08. Deployment-level verification remains pending because this repository has no Minecraft Kubernetes Service, NetworkPolicy, ingress, or load-balancer manifests with which to prove that the RCON port is internal-only. Application behavior is verified with gateway, validation, component, credential, and server-action tests; full workspace tests and type checks; web lint; OKF validation; Semgrep; dependency audit; and a production Next.js build on 2026-08-08. Component validation confirms labelled server and command controls, terminal semantics, an idle output viewport, and an accessible no-server state. The SoMC GitOps deployment verifies Season 4 RCON through an authenticated internal ClusterIP Service backed by a Kubernetes Secret while product guidance also covers external server addresses.
# Related Stories # Related Stories
+2 -2
View File
@@ -10,7 +10,7 @@ User authentication begins with an opaque, short-lived, single-use token created
### RCON administration ### RCON administration
The administrator console stores one or more internal Minecraft RCON endpoints with write-only AES-GCM-encrypted passwords. Browser requests invoke authenticated server actions; only the Next.js runtime opens RCON TCP connections. Exact deployment-managed endpoint allowlisting prevents the connection registry from becoming an arbitrary internal network proxy. Commands and responses are bounded and ephemeral, while credential-safe audit events retain the operator, server, command verb, keyed digest, outcome, and duration. RCON is exposed only through internal cluster services and never through public ingress. The administrator console stores one or more RCON server addresses with write-only AES-GCM-encrypted passwords. Browser requests invoke authenticated server actions; only the Next.js runtime opens RCON TCP connections. Administrators may configure any syntactically valid internal or external DNS hostname and port without deployment-managed endpoint configuration; IP literals remain rejected. Commands and responses are bounded and ephemeral, while credential-safe audit events retain the operator, server, command verb, keyed digest, outcome, and duration. Operators remain responsible for endpoint exposure and transport security.
### Discord bot ### Discord bot
@@ -30,7 +30,7 @@ The admission decision is fail closed. Unknown players, disabled effective group
- Velocity requests use hashed per-server bearer credentials, timestamps, and database-unique request IDs for authentication and replay prevention. - Velocity requests use hashed per-server bearer credentials, timestamps, and database-unique request IDs for authentication and replay prevention.
- Session and one-time-code values are random and stored only as hashes. - Session and one-time-code values are random and stored only as hashes.
- Exact IP addresses are sensitive data and require an explicit retention policy before production deployment. - Exact IP addresses are sensitive data and require an explicit retention policy before production deployment.
- RCON hostnames and ports must match the deployment allowlist on save and use; passwords never cross the browser trust boundary. - RCON endpoints require syntactically valid DNS hostnames and ports; passwords never cross the browser trust boundary. Cluster egress policy and administrator authorization constrain the resulting outbound-connectivity trust boundary.
- RCON commands and responses are untrusted, bounded, rendered only as text, and excluded from persistent history and logs. - RCON commands and responses are untrusted, bounded, rendered only as text, and excluded from persistent history and logs.
## Database invariants ## Database invariants
+4 -8
View File
@@ -4,19 +4,15 @@ The administrator RCON console proxies commands through the Next.js server runti
## Application configuration ## Application configuration
Set `RCON_ALLOWED_ENDPOINTS` to a comma-separated allowlist of exact internal `host:port` pairs: Administrators may configure any syntactically valid DNS hostname and TCP port without deployment-managed endpoint configuration. IP literals, trailing-dot hostnames, and malformed DNS names are rejected whenever a connection is saved, tested, or used.
```text This flexibility means an authorized or compromised administrator can make RCON connection attempts to any DNS hostname and port reachable from the web runtime. Use cluster egress policy and administrator access controls to constrain that trust boundary where required.
RCON_ALLOWED_ENDPOINTS=season4.somc.svc.cluster.local:25575
```
IP literals, trailing-dot hostnames, malformed DNS names, and endpoints absent from the allowlist are rejected whenever a connection is saved, tested, or used.
Saved passwords are encrypted with AES-256-GCM and connection-bound authenticated data. By default, domain-separated credential and audit keys are derived from `AUTH_SECRET`. Deployments may instead provide independent 32-byte base64 values through `RCON_CREDENTIAL_KEY` and `RCON_AUDIT_KEY`. Rotating the credential key requires replacing saved RCON passwords. Saved passwords are encrypted with AES-256-GCM and connection-bound authenticated data. By default, domain-separated credential and audit keys are derived from `AUTH_SECRET`. Deployments may instead provide independent 32-byte base64 values through `RCON_CREDENTIAL_KEY` and `RCON_AUDIT_KEY`. Rotating the credential key requires replacing saved RCON passwords.
## Minecraft server configuration ## Minecraft server configuration
Enable RCON with a high-entropy password supplied through the deployment secret. Expose its port only on an internal `ClusterIP` service. Do not add RCON to an Ingress, NodePort, or public LoadBalancer. Enable RCON with a high-entropy password supplied through the deployment secret. Server addresses may resolve internally or externally. Prefer private networking, a VPN, or an encrypted tunnel; do not expose plaintext RCON directly to the public internet.
The password entered in the administrator connection form must match the server password. Existing passwords are write-only; leave the replacement field blank when editing unrelated connection settings. The password entered in the administrator connection form must match the server password. Existing passwords are write-only; leave the replacement field blank when editing unrelated connection settings.
@@ -30,7 +26,7 @@ The password entered in the administrator connection form must match the server
- Full commands and responses are not persisted or logged. Audit events contain the command verb and a domain-separated HMAC digest. - Full commands and responses are not persisted or logged. Audit events contain the command verb and a domain-separated HMAC digest.
- Connection passwords are never selected by page queries or returned to the browser. - Connection passwords are never selected by page queries or returned to the browser.
RCON is plaintext TCP. Keep it on the cluster network and use network policy or an encrypted tunnel when the network trust model requires stronger isolation. RCON is plaintext TCP. Internal deployments should use network policy; external connections should use private routing, a VPN, or an encrypted tunnel rather than direct public exposure.
## Migration ## Migration