@cytario/web 2.2.5 → 2.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/app/.server/db/redis.ts +16 -32
- package/app/.server/db/redisOptions.ts +72 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,6 +230,21 @@ cp .env.template .env # Pre-configured for the Podman cluster
|
|
|
230
230
|
npm run dev
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
### Session Cache (Redis/Valkey)
|
|
234
|
+
|
|
235
|
+
Sessions hold OAuth access/refresh/ID tokens and short-lived STS credentials. **TLS is required in production.** The app refuses to boot when `NODE_ENV !== "development"` unless one of the following is true:
|
|
236
|
+
|
|
237
|
+
| Env var | Value | Meaning |
|
|
238
|
+
| -------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| `REDIS_TLS` | `"true"` | Wrap the ioredis connection in TLS (recommended). |
|
|
240
|
+
| `REDIS_CA_CERT` | PEM | Optional CA bundle for self-signed deployments. Multi-line PEM string. |
|
|
241
|
+
| `REDIS_TLS_SERVER_NAME` | hostname | Optional SNI / certificate hostname override. |
|
|
242
|
+
| `REDIS_INSECURE_ALLOW_PLAINTEXT` | `"true"` | Explicit opt-out for trusted private networks. Logs a warning. Not for use on shared infrastructure. |
|
|
243
|
+
|
|
244
|
+
The local Podman cluster runs Valkey without TLS, which is allowed because `NODE_ENV=development`. Managed Valkey deployments (helm chart, AWS ElastiCache, etc.) should set `REDIS_TLS=true`. Valkey reuses the standard `6379` port for TLS when `tls.enabled` is set — it does not move the listener to `6380` and refuses plaintext on the same port — so leave `REDIS_PORT` at `6379` unless your provider explicitly publishes a separate TLS endpoint.
|
|
245
|
+
|
|
246
|
+
In the production cluster (see `cytario-infrastructure`, C-212) the Valkey leaf cert is signed by a cluster-internal CA managed by cert-manager. The CA's public cert is distributed to every namespace as a `cytario-internal-ca` ConfigMap by trust-manager, and the `cytario-web` helm chart's `redis.caCertConfigMap.{name,key}` wires it into the pod as `REDIS_CA_CERT` via `valueFrom.configMapKeyRef`. The app sees the PEM through the normal env var path — no code-side knowledge of the trust source is required.
|
|
247
|
+
|
|
233
248
|
### Database
|
|
234
249
|
|
|
235
250
|
PostgreSQL with [Prisma ORM](https://www.prisma.io/). Connection configured via `DATABASE_URL` in `.env`.
|
package/app/.server/db/redis.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Redis from "ioredis";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { buildRedisOptions } from "./redisOptions";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Redis/Valkey client instance
|
|
@@ -14,55 +14,39 @@ import { cytarioConfig } from "~/config";
|
|
|
14
14
|
* - REDIS_PORT: Server port (default: 6379)
|
|
15
15
|
* - REDIS_USERNAME: Optional username for authenticated connections (Redis 6+ / Valkey)
|
|
16
16
|
* - REDIS_PASSWORD: Optional password for authenticated connections
|
|
17
|
+
* - REDIS_TLS: Set to "true" to wrap the connection in TLS (required in production)
|
|
18
|
+
* - REDIS_CA_CERT: Optional PEM-encoded CA certificate (string) for self-signed deployments
|
|
19
|
+
* - REDIS_TLS_SERVER_NAME: Optional SNI / certificate hostname override
|
|
20
|
+
* - REDIS_INSECURE_ALLOW_PLAINTEXT: Set to "true" to opt out of the production TLS requirement
|
|
17
21
|
*
|
|
18
22
|
* @example
|
|
19
|
-
* // Use with
|
|
20
|
-
* REDIS_HOST=redis.example.com
|
|
21
|
-
* REDIS_PORT=6379
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* // Use with Valkey with authentication
|
|
23
|
+
* // Use with managed Valkey over TLS — Valkey reuses 6379 for TLS when tls.enabled is set
|
|
25
24
|
* REDIS_HOST=valkey.example.com
|
|
26
25
|
* REDIS_PORT=6379
|
|
27
26
|
* REDIS_USERNAME=myuser
|
|
28
27
|
* REDIS_PASSWORD=mypassword
|
|
28
|
+
* REDIS_TLS=true
|
|
29
29
|
*
|
|
30
30
|
* @example
|
|
31
|
-
* //
|
|
32
|
-
* REDIS_HOST=
|
|
31
|
+
* // Local development without TLS (only allowed when NODE_ENV=development)
|
|
32
|
+
* REDIS_HOST=localhost
|
|
33
33
|
* REDIS_PORT=6379
|
|
34
|
-
* REDIS_PASSWORD=mypassword
|
|
35
34
|
*/
|
|
36
35
|
|
|
37
|
-
const
|
|
38
|
-
redis: { host, port, username, password },
|
|
39
|
-
} = cytarioConfig;
|
|
36
|
+
const options = buildRedisOptions(process.env);
|
|
40
37
|
|
|
41
|
-
export const redis = new Redis(
|
|
42
|
-
host,
|
|
43
|
-
port,
|
|
44
|
-
// Only include username if provided (Redis 6+ ACL support)
|
|
45
|
-
...(username && { username }),
|
|
46
|
-
// Only include password if provided (backwards compatible)
|
|
47
|
-
...(password && { password }),
|
|
48
|
-
maxRetriesPerRequest: 3,
|
|
49
|
-
retryStrategy(times) {
|
|
50
|
-
const delay = Math.min(times * 50, 2000);
|
|
51
|
-
return delay;
|
|
52
|
-
},
|
|
53
|
-
lazyConnect: false,
|
|
54
|
-
});
|
|
38
|
+
export const redis = new Redis(options);
|
|
55
39
|
|
|
56
|
-
// Log connection errors
|
|
57
40
|
redis.on("error", (err) => {
|
|
58
41
|
console.error("Redis/Valkey connection error:", err);
|
|
59
42
|
});
|
|
60
43
|
|
|
61
44
|
redis.on("connect", () => {
|
|
62
|
-
const authInfo = username
|
|
63
|
-
? ` (authenticated as ${username})`
|
|
64
|
-
: password
|
|
45
|
+
const authInfo = options.username
|
|
46
|
+
? ` (authenticated as ${options.username})`
|
|
47
|
+
: options.password
|
|
65
48
|
? " (authenticated)"
|
|
66
49
|
: "";
|
|
67
|
-
|
|
50
|
+
const tlsInfo = options.tls ? " over TLS" : "";
|
|
51
|
+
console.log(`Connected to Redis/Valkey at ${options.host}:${options.port}${authInfo}${tlsInfo}`);
|
|
68
52
|
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { RedisOptions } from "ioredis";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build ioredis connection options from environment variables.
|
|
5
|
+
*
|
|
6
|
+
* Recognised env vars:
|
|
7
|
+
* - `REDIS_HOST` (default: `localhost`)
|
|
8
|
+
* - `REDIS_PORT` (default: `6379`)
|
|
9
|
+
* - `REDIS_USERNAME` — optional ACL username
|
|
10
|
+
* - `REDIS_PASSWORD` — optional password
|
|
11
|
+
* - `REDIS_TLS` — `"true"` to wrap the connection in TLS
|
|
12
|
+
* - `REDIS_CA_CERT` — PEM-encoded CA certificate (string) used to verify
|
|
13
|
+
* the server when the cert chain is not in the system trust store
|
|
14
|
+
* - `REDIS_TLS_SERVER_NAME` — SNI / certificate hostname override
|
|
15
|
+
* - `REDIS_INSECURE_ALLOW_PLAINTEXT` — `"true"` to opt out of the
|
|
16
|
+
* production TLS requirement (escape hatch for trusted in-cluster
|
|
17
|
+
* networks; logs a warning)
|
|
18
|
+
*
|
|
19
|
+
* Fails fast outside development if TLS is off and the opt-out flag is
|
|
20
|
+
* not set — session blobs contain OAuth tokens and STS credentials and
|
|
21
|
+
* must not traverse plaintext links in production. See C-204.
|
|
22
|
+
*/
|
|
23
|
+
export function buildRedisOptions(env: Record<string, string | undefined>): RedisOptions {
|
|
24
|
+
const host = env.REDIS_HOST || "localhost";
|
|
25
|
+
const port = Number(env.REDIS_PORT) || 6379;
|
|
26
|
+
const username = env.REDIS_USERNAME;
|
|
27
|
+
const password = env.REDIS_PASSWORD;
|
|
28
|
+
const tlsEnabled = env.REDIS_TLS === "true";
|
|
29
|
+
const caCert = env.REDIS_CA_CERT;
|
|
30
|
+
const tlsServerName = env.REDIS_TLS_SERVER_NAME;
|
|
31
|
+
const allowPlaintext = env.REDIS_INSECURE_ALLOW_PLAINTEXT === "true";
|
|
32
|
+
const nodeEnv = env.NODE_ENV;
|
|
33
|
+
|
|
34
|
+
const isLocalEnv = nodeEnv === "development" || nodeEnv === "test";
|
|
35
|
+
|
|
36
|
+
if (!tlsEnabled && !isLocalEnv && !allowPlaintext) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Refusing to start: Redis/Valkey TLS is disabled. Set REDIS_TLS=true " +
|
|
39
|
+
"(recommended) or REDIS_INSECURE_ALLOW_PLAINTEXT=true to opt out. " +
|
|
40
|
+
"See C-204 / OWASP A02:2021.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!tlsEnabled && allowPlaintext && !isLocalEnv) {
|
|
45
|
+
console.warn(
|
|
46
|
+
"REDIS_INSECURE_ALLOW_PLAINTEXT=true — session tokens will traverse " +
|
|
47
|
+
"an unencrypted connection. Only safe on a trusted private network.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const options: RedisOptions = {
|
|
52
|
+
host,
|
|
53
|
+
port,
|
|
54
|
+
...(username && { username }),
|
|
55
|
+
...(password && { password }),
|
|
56
|
+
maxRetriesPerRequest: 3,
|
|
57
|
+
retryStrategy(times) {
|
|
58
|
+
const delay = Math.min(times * 50, 2000);
|
|
59
|
+
return delay;
|
|
60
|
+
},
|
|
61
|
+
lazyConnect: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (tlsEnabled) {
|
|
65
|
+
options.tls = {
|
|
66
|
+
...(caCert && { ca: caCert }),
|
|
67
|
+
...(tlsServerName && { servername: tlsServerName }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return options;
|
|
72
|
+
}
|
package/package.json
CHANGED