-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathindex.js
More file actions
269 lines (253 loc) · 9.24 KB
/
Copy pathindex.js
File metadata and controls
269 lines (253 loc) · 9.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
const fs = require('node:fs');
const process = require('node:process');
const util = require('node:util');
const dayjs = require('dayjs-with-plugins');
const isSANB = require('is-string-and-not-blank');
const semver = require('semver');
const { boolean } = require('boolean');
const RATE_LIMIT_EXCEEDED = 'Rate limit exceeded, retry in %s.';
const TIMEOUT_MESSAGE =
'Your request has timed out and we have been alerted of this issue. Please try again or contact us.';
//
// Hardened TLS cipher suites (only AEAD ciphers with forward secrecy).
//
// Ordered by security level per NCSC/internet.nl guidelines:
// "Good" = ECDHE + AEAD, "Sufficient" = DHE + AEAD
//
// Excludes:
// - RSA key exchange (no forward secrecy)
// - CBC mode ciphers
// - ARIA ciphers
// - CCM/CCM8 ciphers
// - DSS authentication
//
// TLS 1.3 ciphers are always enabled and not affected by this setting.
//
const TLS_CIPHERS = [
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-CHACHA20-POLY1305',
'DHE-RSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'DHE-RSA-CHACHA20-POLY1305'
].join(':');
//
// Compat cipher list: Adds CBC ciphers with forward secrecy for
// backward compatibility with older clients (e.g. TLS 1.0/1.1).
// Still excludes RSA key exchange (no forward secrecy).
//
// Modeled after WildDuck and Haraka cipher lists, but
// with RSA key exchange removed (no forward secrecy).
//
// Cipher ordering:
// 1. AEAD ciphers with ECDHE (best)
// 2. AEAD ciphers with DHE (good)
// 3. CBC ciphers with ECDHE + SHA-256/SHA-384 (TLS 1.2 CBC)
// 4. CBC ciphers with DHE + SHA-256 (TLS 1.2 CBC)
// 5. CBC ciphers with ECDHE + SHA-1 (TLS 1.0/1.1 compat)
// 6. CBC ciphers with DHE + SHA-1 (TLS 1.0/1.1 compat)
//
// NOTE: RSA key exchange is still excluded (no forward secrecy).
// NOTE: 3DES is excluded (too weak, CVE-2016-2183 Sweet32).
// NOTE: ARIA is excluded (internet.nl marks as "phase out").
//
const TLS_COMPAT_CIPHERS = [
// === AEAD ciphers with forward secrecy (preferred) ===
// ECDHE + AES-GCM
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
// ECDHE + CHACHA20-POLY1305
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-CHACHA20-POLY1305',
// DHE + AES-GCM
'DHE-RSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
// DHE + CHACHA20-POLY1305
'DHE-RSA-CHACHA20-POLY1305',
// === CBC ciphers with forward secrecy + SHA-256/SHA-384 MACs ===
// (TLS 1.2 CBC - for clients that support TLS 1.2 but not AEAD)
'ECDHE-ECDSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-ECDSA-AES128-SHA256',
'ECDHE-RSA-AES128-SHA256',
'DHE-RSA-AES256-SHA256',
'DHE-RSA-AES128-SHA256',
// === CBC ciphers with forward secrecy + SHA-1 MACs ===
// (TLS 1.0/1.1 compat - for very old SMTP clients/servers)
// NOTE: SHA-1 in HMAC-SHA1 for record MAC is NOT the same as SHA-1
// in signatures; HMAC-SHA1 is still considered secure for MAC usage.
'ECDHE-ECDSA-AES256-SHA',
'ECDHE-RSA-AES256-SHA',
'ECDHE-ECDSA-AES128-SHA',
'ECDHE-RSA-AES128-SHA',
'DHE-RSA-AES256-SHA',
'DHE-RSA-AES128-SHA'
].join(':');
//
// Signature algorithms for TLS 1.2 key exchange.
// Excludes SHA-1 and SHA-224 (outdated per internet.nl/NCSC guidelines).
//
const TLS_SIGALGS = [
'ecdsa_secp256r1_sha256',
'ecdsa_secp384r1_sha384',
'ecdsa_secp521r1_sha512',
'rsa_pss_rsae_sha256',
'rsa_pss_rsae_sha384',
'rsa_pss_rsae_sha512',
'rsa_pkcs1_sha256',
'rsa_pkcs1_sha384',
'rsa_pkcs1_sha512'
].join(':');
// eslint-disable-next-line complexity
function sharedConfig(prefix, env = process.env.NODE_ENV || 'development') {
prefix = prefix.toUpperCase();
let ssl = false;
const keys = ['KEY', 'CERT', 'CA'];
const validKeys = keys.filter((key) =>
isSANB(process.env[`${prefix}_SSL_${key}_PATH`])
);
if (validKeys.length > 0) {
ssl = { allowHTTP1: true };
if (semver.gte(process.version, 'v18.16.0')) ssl.ecdhCurve = 'auto';
//
// Hardened TLS options:
// - Enforce server cipher suite preference order
// - Only allow AEAD ciphers with forward secrecy
// - Exclude weak signature algorithms (SHA-1, SHA-224)
//
ssl.honorCipherOrder = true;
ssl.ciphers = TLS_CIPHERS;
ssl.sigalgs = TLS_SIGALGS;
ssl.minVersion = 'TLSv1.2';
for (const key of validKeys) {
ssl[key.toLowerCase()] = fs.readFileSync(
process.env[`${prefix}_SSL_${key}_PATH`]
);
}
}
const port = process.env[`${prefix}_PORT`] || 0;
const serverHost = process.env[`${prefix}_SERVER_HOST`] || '::';
const protocol = process.env[`${prefix}_PROTOCOL`] || 'http';
const config = {
// this is used as defaults for `app.listen(port, serverHost)`
port,
// by listening on '0.0.0.0' by default we avoid IPv6 issues
// <https://stackoverflow.com/questions/29411551/express-js-req-ip-is-returning-ffff127-0-0-1>
// <https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
// <https://github.com/koajs/koa/issues/599>
// <https://stackoverflow.com/a/33957043>
serverHost,
protocol,
...(ssl ? { ssl } : {}),
routes: false,
passport: false,
i18n: {},
rateLimit:
env === 'development'
? false
: {
errorMessage(exp, ctx) {
const fn =
typeof ctx.request.t === 'function'
? ctx.request.t
: util.format;
return fn(
RATE_LIMIT_EXCEEDED,
dayjs()
.add(exp, 'ms')
.locale(ctx.state.locale || 'en')
.fromNow(true)
);
},
duration: process.env[`${prefix}_RATELIMIT_DURATION`]
? Number.parseInt(process.env[`${prefix}_RATELIMIT_DURATION`], 10)
: 60_000,
max: process.env[`${prefix}_RATELIMIT_MAX`]
? Number.parseInt(process.env[`${prefix}_RATELIMIT_MAX`], 10)
: 1000,
id: (ctx) => ctx.ip,
prefix:
process.env[`${prefix}_RATELIMIT_PREFIX`] ||
`${prefix}_limit_${env}`.toLowerCase(),
// whitelist/blacklist parsing inspired by `dotenv-parse-variables`
allowlist: process.env[`${prefix}_RATELIMIT_WHITELIST`]
? process.env[`${prefix}_RATELIMIT_WHITELIST`]
.split(',')
.filter((string) => string !== '')
: [],
blocklist: process.env[`${prefix}_RATELIMIT_BLACKLIST`]
? process.env[`${prefix}_RATELIMIT_BLACKLIST`]
.split(',')
.filter((string) => string !== '')
: []
},
// <https://github.com/ladjs/timeout>
timeout: {
ms: process.env[`${prefix}_TIMEOUT_MS`]
? Number.parseInt(process.env[`${prefix}_TIMEOUT_MS`], 10)
: 30_000,
message: (ctx) =>
typeof ctx.request.t === 'function'
? ctx.request.t(TIMEOUT_MESSAGE)
: TIMEOUT_MESSAGE
},
auth: false,
// these are hooks that can get run before/after configuration
// and must be functions that accept one argument `app`
hookBeforeSetup: false,
hookBeforeRoutes: false,
// <https://github.com/ladjs/store-ip-address>
storeIPAddress: {},
// ioredis configuration object
// <https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options>
redis: {
username: process.env[`${prefix}_REDIS_USERNAME`] || null,
port: process.env[`${prefix}_REDIS_PORT`]
? Number.parseInt(process.env[`${prefix}_REDIS_PORT`], 10)
: 6379,
host: process.env[`${prefix}_REDIS_HOST`] || 'localhost',
password: process.env[`${prefix}_REDIS_PASSWORD`] || null,
showFriendlyErrorStack: boolean(
process.env[`${prefix}_REDIS_SHOW_FRIENDLY_ERROR_STACK`]
),
//
// NOTE: we override default values in ioredis for `maxRetriesPerRequest`
// and also `maxLoadingRetryTime`, because otherwise the default values
// of 20 * 10000 would cause 200s retry time, which is more than the
// HTTP default timeout per request of 30s as per above, and we use
// sensible default values of 3 * 3000 = 9s max redis retry time
//
// default in ioredis is 20
maxRetriesPerRequest: process.env[
`${prefix}_REDIS_MAX_RETRIES_PER_REQUEST`
]
? Number.parseInt(
process.env[`${prefix}_REDIS_MAX_RETRIES_PER_REQUEST`],
10
)
: 3,
// default in ioredis is 10000 (10s)
maxLoadingRetryTime: process.env[`${prefix}_REDIS_MAX_LOADING_RETRY_TIME`]
? Number.parseInt(
process.env[`${prefix}_REDIS_MAX_LOADING_RETRY_TIME`],
10
)
: 3000
},
redisMonitor: boolean(process.env[`${prefix}_REDIS_MONITOR`]),
// <https://github.com/koajs/cors#corsoptions>
cors: {}
};
// <https://stackoverflow.com/a/66810848>
if (boolean(process.env[`${prefix}_REDIS_TLS`])) config.redis.tls = {};
return config;
}
module.exports = sharedConfig;
module.exports.TLS_CIPHERS = TLS_CIPHERS;
module.exports.TLS_COMPAT_CIPHERS = TLS_COMPAT_CIPHERS;
module.exports.TLS_SIGALGS = TLS_SIGALGS;