diff --git a/docs/03_Configuration.md b/docs/03_Configuration.md index 39fb67d..4f6bb79 100644 --- a/docs/03_Configuration.md +++ b/docs/03_Configuration.md @@ -17,7 +17,9 @@ "port": 6379, "password": null, "database": 0, - "key_prefix": "network:" + "key_prefix": "network:", + "tls": false, + "verify_peer": true }, "network": { "auth_token": null @@ -52,6 +54,8 @@ | `password` | `null` | Redis AUTH password. Prefer the env var (below). | | `database` | `0` | Redis logical database (0-15 on stock Redis) | | `key_prefix` | `"network:"` | All keys NetworkCore writes start with this prefix. Server registry keys end up at `network:servers:`. Change this only if you're sharing a Redis with something unrelated. | +| `tls` | `false` | Enables TLS for the Redis connection (Lettuce `rediss://` equivalent). **Set to true whenever Redis is on a different host than the Hytale server**, since the connection crosses untrusted network. | +| `verify_peer` | `true` | When `tls` is on, verifies the Redis server's certificate against the JVM's default trust store. Set to false only in dev / against self-signed certs. Logs a warning when disabled. | ### network @@ -92,5 +96,6 @@ These take precedence over the config file at boot time. Use them for secrets so - `server.id` left null is fine if you're OK with the id changing on every boot. Set it to something stable like `lobby-prod-01` if you want logs and dashboards to keep continuity across restarts. - `server.role` set explicitly per server. This is what dashboards and routing use. - `redis.host` pointed at your shared Redis. `redis.password` not in the file; set `REDIS_PASSWORD` env var. +- `redis.tls` **on** whenever Redis is on a different host than the Hytale server. `verify_peer` stays true unless Redis uses a self-signed cert you haven't loaded into the JVM trust store. - `network.auth_token` not in the file; set `NETWORK_AUTH_TOKEN` env var to the same value on every server. - `metrics.bind` stays `127.0.0.1`. Run Prometheus on the same host (or use an SSH tunnel / reverse proxy with auth to expose it elsewhere). diff --git a/docs/09_Operations_And_Security.md b/docs/09_Operations_And_Security.md index d76eae6..275cb87 100644 --- a/docs/09_Operations_And_Security.md +++ b/docs/09_Operations_And_Security.md @@ -2,7 +2,7 @@ ## Threat model in one paragraph -NetworkCore assumes Redis is on a private network reachable only by trusted servers. The shared auth token guards against accidents (a misconfigured staging server talking to prod) and casual mischief (someone with bus access spoofing a server id). It does *not* protect against an attacker who already has Redis credentials - at that point they can read the token off any envelope and start signing their own. For hard isolation, rely on network controls: VPC peering, firewall rules, Redis bound to a private interface. +NetworkCore expects Redis to be reachable only by trusted servers. If Redis sits on a different host than the Hytale servers (the common case with hosted game servers), enable `redis.tls` so the connection is encrypted in transit. The shared auth token guards against accidents (a misconfigured staging server talking to prod) and casual mischief (someone with bus access spoofing a server id). It does *not* protect against an attacker who already has both Redis credentials and the token - at that point they can craft any envelope they want. The defenses in depth: TLS for the wire, the auth token for accidents, and network controls (firewall rules, private Redis interfaces, hosting-provider ACLs) for the actual attacker scenario. ## Auth token rollout @@ -16,10 +16,10 @@ When rotating: do a rolling restart. Mid-rotation, servers running the old token If you want zero rejection windows, you'd need a multi-token scheme where receivers accept either the old or new token during transition. That's not in v1. -## What is *not* encrypted or signed +## What is and isn't encrypted or signed -- **Redis traffic.** Plain TCP. Anyone sniffing the wire between Hytale servers and Redis sees every payload and every token. Mitigations: keep Redis on a private network, run it on the same host as the Hytale server, or use SSH tunnels / a VPC. TLS (rediss://) is a future addition. -- **Messages themselves.** No HMAC, no signing. A token in the envelope is checked for equality, not for authenticity. An attacker on the bus can craft any envelope. +- **Redis traffic.** Optional TLS (`redis.tls` in config). Turn it on whenever Redis runs on a different host than the Hytale server. Without TLS, anyone sniffing the network between the Hytale server and Redis sees every payload and the auth token. With TLS, the connection is encrypted end to end. Use `verify_peer: true` against any production Redis with a real cert. +- **Messages themselves.** No HMAC, no signing. The auth token in the envelope is checked for equality, not authenticity. An attacker with Redis credentials can craft any envelope and stamp it with the token (if they have it). TLS prevents the token from leaking on the wire, but doesn't prevent forgery by anyone who legitimately has Redis access. - **The metrics endpoint.** No auth at all. Defaults to `127.0.0.1` so only the local machine can hit it. If you bind to a non-loopback address, put a reverse proxy with auth in front. ## The metrics endpoint diff --git a/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java b/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java index 584cbc0..c808942 100644 --- a/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java +++ b/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java @@ -61,6 +61,13 @@ public final class RedisMessageBus implements MessageBus { if (redis.password != null && !redis.password.isEmpty()) { b.withPassword(redis.password.toCharArray()); } + if (redis.tls) { + b.withSsl(true); + b.withVerifyPeer(redis.verify_peer); + if (!redis.verify_peer) { + LOGGER.at(Level.WARNING).log("Redis TLS is on but peer verification is OFF. Only acceptable in dev."); + } + } RedisURI uri = b.build(); this.client = RedisClient.create(uri); @@ -91,8 +98,10 @@ public final class RedisMessageBus implements MessageBus { this.receivedCounter = (metrics == null) ? null : metrics.counter("messagebus_received_total", Map.of()); this.rejectedCounter = (metrics == null) ? null : metrics.counter("messagebus_rejected_total", Map.of()); - LOGGER.at(Level.INFO).log("Connected to Redis at %s:%d (db %d, auth=%s)", - redis.host, redis.port, redis.database, this.authToken != null ? "on" : "off"); + LOGGER.at(Level.INFO).log("Connected to Redis at %s:%d (db %d, tls=%s, auth=%s)", + redis.host, redis.port, redis.database, + redis.tls ? "on" : "off", + this.authToken != null ? "on" : "off"); } @Nonnull diff --git a/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java b/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java index bf70212..8919a86 100644 --- a/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java +++ b/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java @@ -23,6 +23,8 @@ public final class CoreConfig { public String password = null; public int database = 0; public String key_prefix = "network:"; + public boolean tls = false; + public boolean verify_peer = true; } public static final class NetworkSection {