feat(rcon): allow administrator-defined endpoints
This commit is contained in:
+1
-3
@@ -25,9 +25,7 @@ PROXYCHECK_API_KEY=
|
||||
IP_INTELLIGENCE_CACHE_HOURS=48
|
||||
BLOCK_HOSTING_IPS=false
|
||||
|
||||
# Internal RCON proxy. Endpoints must be exact host:port pairs.
|
||||
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.
|
||||
# Optional independent 32-byte base64 RCON keys. When omitted, domain-separated keys are derived from AUTH_SECRET.
|
||||
RCON_CREDENTIAL_KEY=
|
||||
RCON_AUDIT_KEY=
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const savedMessages: 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.",
|
||||
configuration: "RCON credential encryption is not configured.",
|
||||
"save-failed": "The RCON connection could not be saved.",
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sanitizeRconOutput, validateRconCommand, validateRconConnection } from "./rcon-validation";
|
||||
|
||||
const allowed = "season4.somc.svc.cluster.local:25575,creative.somc.svc.cluster.local:25576";
|
||||
|
||||
describe("RCON validation", () => {
|
||||
it("normalizes an allowlisted internal endpoint", () => {
|
||||
it("normalizes any valid DNS hostname and port without deployment configuration", () => {
|
||||
expect(validateRconConnection({
|
||||
name: " Season 4 ",
|
||||
host: "SEASON4.SOMC.SVC.CLUSTER.LOCAL",
|
||||
port: "25575",
|
||||
password: "correct horse battery staple",
|
||||
}, { allowedEndpoints: allowed, passwordRequired: true })).toEqual({
|
||||
}, { passwordRequired: true })).toEqual({
|
||||
name: "Season 4",
|
||||
host: "season4.somc.svc.cluster.local",
|
||||
port: 25575,
|
||||
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", () => {
|
||||
for (const [host, port] of [
|
||||
["postgres.somc.svc.cluster.local", "5432"],
|
||||
["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,
|
||||
it("rejects IP literals and malformed DNS hostnames", () => {
|
||||
for (const host of ["10.0.0.1", "2001:db8::1", "season4.", "-season4.example", "season4..example"]) {
|
||||
expect(validateRconConnection({ name: "Server", host, port: "25575", password: "secret" }, {
|
||||
passwordRequired: true,
|
||||
})).toBeNull();
|
||||
}
|
||||
@@ -34,11 +38,9 @@ describe("RCON validation", () => {
|
||||
|
||||
it("allows a blank replacement password only while editing", () => {
|
||||
expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, {
|
||||
allowedEndpoints: allowed,
|
||||
passwordRequired: false,
|
||||
})?.password).toBeNull();
|
||||
expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, {
|
||||
allowedEndpoints: allowed,
|
||||
passwordRequired: true,
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
@@ -12,25 +12,19 @@ export type ValidRconConnection = {
|
||||
password: string | null;
|
||||
};
|
||||
|
||||
function endpointSet(value: string) {
|
||||
return new Set(value.split(",").map((endpoint) => endpoint.trim().toLowerCase()).filter(Boolean));
|
||||
}
|
||||
|
||||
export function validateRconConnection(
|
||||
input: { name: unknown; host: unknown; port: unknown; password: unknown },
|
||||
options: { allowedEndpoints?: string; passwordRequired: boolean },
|
||||
options: { passwordRequired: boolean },
|
||||
): ValidRconConnection | null {
|
||||
const name = typeof input.name === "string" ? input.name.trim() : "";
|
||||
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 passwordText = typeof input.password === "string" ? input.password : "";
|
||||
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 (!host || host.endsWith(".") || isIP(host) !== 0 || !HOST_PATTERN.test(host)) 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 (options.passwordRequired && !passwordText) return null;
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
* **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.
|
||||
* **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.
|
||||
|
||||
## 2026-08-07
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ type: User Story
|
||||
title: Manage RCON server connections
|
||||
description: Administrators manage encrypted connection settings for internal Minecraft RCON endpoints.
|
||||
tags: [admin, rcon, minecraft, security, operations]
|
||||
timestamp: 2026-08-08T01:36:00Z
|
||||
timestamp: 2026-08-08T11:44:59Z
|
||||
story_id: US-021
|
||||
status: verified
|
||||
---
|
||||
@@ -18,7 +18,7 @@ As an administrator, I want to manage one or more Minecraft RCON connections, so
|
||||
- [x] Each connection has a unique display name, internal hostname, 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] 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] Deleting a connection requires explicit confirmation.
|
||||
- [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
|
||||
|
||||
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
|
||||
|
||||
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 DNS hostname and port pairs 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
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ type: User Story
|
||||
title: Operate servers through an RCON console
|
||||
description: Administrators execute bounded RCON commands through the server-side portal proxy.
|
||||
tags: [admin, rcon, minecraft, console, security]
|
||||
timestamp: 2026-08-08T01:36:00Z
|
||||
timestamp: 2026-08-08T11:44:59Z
|
||||
story_id: US-022
|
||||
status: in-progress
|
||||
status: verified
|
||||
---
|
||||
|
||||
# User Story
|
||||
@@ -22,15 +22,15 @@ As an administrator, I want an RCON console in the portal, so that I can operate
|
||||
- [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] The console is keyboard accessible and clearly identifies the selected server.
|
||||
- [ ] RCON remains internal to the cluster and is not exposed through public ingress or a load balancer.
|
||||
- [x] RCON remains internal to the cluster and is not exposed through public ingress or a load balancer.
|
||||
|
||||
# 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 client console 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
|
||||
|
||||
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. The SoMC GitOps deployment verifies Season 4 RCON through an authenticated internal ClusterIP Service backed by a Kubernetes Secret, with no public ingress or load balancer exposure.
|
||||
|
||||
# Related Stories
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ User authentication begins with an opaque, short-lived, single-use token created
|
||||
|
||||
### 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 endpoints 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 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. Production Minecraft RCON is exposed only through internal cluster services and never through public ingress.
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Database invariants
|
||||
|
||||
+2
-6
@@ -4,13 +4,9 @@ The administrator RCON console proxies commands through the Next.js server runti
|
||||
|
||||
## 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
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user