@fjall/components-infrastructure 0.102.0 → 2.1.1

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.
Files changed (72) hide show
  1. package/dist/lib/lambda-assets/cert-generator/asset/index.js +17948 -0
  2. package/dist/lib/lambda-assets/cert-generator/asset/package.json +4 -0
  3. package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +45 -2
  4. package/dist/lib/patterns/aws/clickhouseDatabase.js +156 -32
  5. package/dist/lib/patterns/aws/clickhouseTls/index.d.ts +1 -0
  6. package/dist/lib/patterns/aws/clickhouseTls/index.js +1 -0
  7. package/dist/lib/patterns/aws/clickhouseTls/types.d.ts +48 -0
  8. package/dist/lib/patterns/aws/interfaces/database.d.ts +7 -5
  9. package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +21 -0
  10. package/dist/lib/resources/aws/database/clickhouseConstants.js +21 -0
  11. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.d.ts +2 -0
  12. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.js +2 -0
  13. package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +21 -0
  14. package/dist/lib/resources/aws/database/clickhouseUserData.js +68 -3
  15. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.d.ts +1 -1
  16. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +1 -1
  17. package/dist/lib/resources/aws/secrets/index.d.ts +2 -0
  18. package/dist/lib/resources/aws/secrets/index.js +2 -0
  19. package/dist/lib/resources/aws/secrets/tlsCaSecret.d.ts +13 -0
  20. package/dist/lib/resources/aws/secrets/tlsCaSecret.js +15 -0
  21. package/dist/lib/resources/aws/secrets/tlsServerSecret.d.ts +15 -0
  22. package/dist/lib/resources/aws/secrets/tlsServerSecret.js +17 -0
  23. package/dist/lib/resources/aws/utilities/index.d.ts +1 -0
  24. package/dist/lib/resources/aws/utilities/index.js +1 -0
  25. package/dist/lib/resources/aws/utilities/tlsCertGenerator.d.ts +33 -0
  26. package/dist/lib/resources/aws/utilities/tlsCertGenerator.js +67 -0
  27. package/dist/lib/utils/manifestWriter.js +6 -13
  28. package/package.json +8 -6
  29. package/dist/lib/config/aws/__t17fixture.js +0 -3
  30. package/dist/lib/config/aws/__t17fixtureType.d.ts +0 -2
  31. package/dist/lib/config/aws/__t17fixtureType.js +0 -1
  32. package/dist/lib/config/aws/eventBus.d.ts +0 -7
  33. package/dist/lib/config/aws/eventBus.js +0 -21
  34. package/dist/lib/config/aws/identityCenterGroupMembership.d.ts +0 -10
  35. package/dist/lib/config/aws/identityCenterGroupMembership.js +0 -102
  36. package/dist/lib/config/aws/securityBaseline.d.ts +0 -15
  37. package/dist/lib/config/aws/securityBaseline.js +0 -27
  38. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.d.ts +0 -1
  39. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.js +0 -4
  40. package/dist/lib/patterns/aws/managedIdentityCenter.d.ts +0 -4
  41. package/dist/lib/patterns/aws/managedIdentityCenter.js +0 -19
  42. package/dist/lib/patterns/aws/subdomainHostedZone.d.ts +0 -9
  43. package/dist/lib/patterns/aws/subdomainHostedZone.js +0 -34
  44. package/dist/lib/resources/aws/analytics/clickhouse.d.ts +0 -15
  45. package/dist/lib/resources/aws/analytics/clickhouse.js +0 -310
  46. package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +0 -49
  47. package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +0 -140
  48. package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +0 -73
  49. package/dist/lib/resources/aws/analytics/clickhouseConstants.js +0 -89
  50. package/dist/lib/resources/aws/analytics/clickhouseSecurityGroup.d.ts +0 -13
  51. package/dist/lib/resources/aws/analytics/clickhouseSecurityGroup.js +0 -28
  52. package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +0 -59
  53. package/dist/lib/resources/aws/analytics/clickhouseTypes.js +0 -1
  54. package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +0 -6
  55. package/dist/lib/resources/aws/analytics/clickhouseUserData.js +0 -299
  56. package/dist/lib/resources/aws/analytics/index.d.ts +0 -4
  57. package/dist/lib/resources/aws/analytics/index.js +0 -2
  58. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.d.ts +0 -2
  59. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.js +0 -11
  60. package/dist/lib/resources/aws/messaging/defaultEventBus.d.ts +0 -7
  61. package/dist/lib/resources/aws/messaging/defaultEventBus.js +0 -21
  62. package/dist/lib/resources/aws/networking/domain.d.ts +0 -13
  63. package/dist/lib/resources/aws/networking/domain.js +0 -100
  64. package/dist/lib/synth_dump.d.ts +0 -1
  65. package/dist/lib/synth_dump.js +0 -42
  66. package/dist/lib/utils/bastionFactory.d.ts +0 -10
  67. package/dist/lib/utils/bastionFactory.js +0 -29
  68. package/dist/lib/utils/constructMap.d.ts +0 -33
  69. package/dist/lib/utils/constructMap.js +0 -154
  70. package/dist/lib/utils/dnsRecords.d.ts +0 -4
  71. package/dist/lib/utils/dnsRecords.js +0 -104
  72. /package/dist/lib/{config/aws/__t17fixture.d.ts → patterns/aws/clickhouseTls/types.js} +0 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "commonjs",
3
+ "private": true
4
+ }
@@ -2,8 +2,10 @@ import { Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
2
2
  import { type TaskDefinition } from "aws-cdk-lib/aws-ecs";
3
3
  import { type IBucket } from "aws-cdk-lib/aws-s3";
4
4
  import { Construct } from "constructs";
5
- import { Secret } from "../../resources/aws/secrets/secret.js";
5
+ import { Secret, type SecretImport } from "../../resources/aws/secrets/secret.js";
6
6
  import { type ClickHouseSchemaAdmin, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
7
+ import { type ISecret } from "aws-cdk-lib/aws-secretsmanager";
8
+ import type { ClickHouseTlsOptions } from "./clickhouseTls/index.js";
7
9
  import { type ClickHouseMigrationsConfig, type IClickHouseDatabase } from "./interfaces/database.js";
8
10
  import { type ISecurityGroupConnector } from "./interfaces/connector.js";
9
11
  import { type MigrationContributions } from "./interfaces/migrationContributor.js";
@@ -104,6 +106,15 @@ export interface ClickHouseDatabaseProps {
104
106
  * records in `_schema_migrations.ch_version`.
105
107
  */
106
108
  migrations?: ClickHouseMigrationsConfig;
109
+ /**
110
+ * TLS configuration. Default: `{ mode: "self-signed" }` — mints an ECDSA
111
+ * P-256 CA + leaf cert at deploy via a Lambda custom resource, stores both
112
+ * PEMs in Secrets Manager, and the EC2 user-data materialises them at boot
113
+ * for the CH server to read. Set `mode: "imported"` to bring your own CA;
114
+ * `mode: "none"` (acknowledgement literal required) opts out of TLS.
115
+ * See ./clickhouseTls/types.ts.
116
+ */
117
+ tls?: ClickHouseTlsOptions;
107
118
  }
108
119
  /**
109
120
  * ClickHouse analytics database wrapper implementing IClickHouseDatabase.
@@ -121,6 +132,25 @@ export declare class ClickHouseDatabase extends Construct implements IClickHouse
121
132
  readonly additionalTcpPorts: number[];
122
133
  readonly id: string;
123
134
  readonly connections: Connections;
135
+ /**
136
+ * CA cert secret. Self-signed: minted by `TlsCertGenerator`. Imported: the
137
+ * user-supplied ISecret. `undefined` when `tls.mode === "none"`.
138
+ * SecretString shape: raw PEM (single cert, no JSON wrapping).
139
+ */
140
+ readonly tlsCaSecret: ISecret | undefined;
141
+ /**
142
+ * Server cert + key secret. Self-signed: minted by `TlsCertGenerator`.
143
+ * Imported: the user-supplied ISecret. `undefined` when `tls.mode === "none"`.
144
+ * SecretString shape: JSON `{ "cert": "<leaf-pem>", "key": "<key-pem>" }`.
145
+ */
146
+ readonly tlsServerSecret: ISecret | undefined;
147
+ /**
148
+ * SHA-256 of the CA cert PEM. CFN token in self-signed mode (from the cert
149
+ * generator custom resource Data attribute). Plain string in imported mode
150
+ * when supplied. `undefined` when `tls.mode === "none"` or when imported
151
+ * without a digest. Surface on consumers as a task-replacement trigger.
152
+ */
153
+ readonly caCertSha256: string | undefined;
124
154
  constructor(scope: Construct, id: string, props: ClickHouseDatabaseProps);
125
155
  /**
126
156
  * Returns the Fjall `Secret` wrapper for the named user. Throws with a
@@ -138,7 +168,20 @@ export declare class ClickHouseDatabase extends Construct implements IClickHouse
138
168
  getHttpPort(): number;
139
169
  getNativePort(): number;
140
170
  getHostEndpoint(): string;
141
- getHttpUrl(): string;
171
+ /**
172
+ * Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
173
+ * active (default), `http` only when `tls: { mode: "none", … }` is set.
174
+ * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise.
175
+ */
176
+ getUrl(): string;
177
+ /**
178
+ * SecretImport for the CA certificate secret, or `undefined` when TLS is
179
+ * disabled (`tls: { mode: "none", … }`). Consumers wire this into their
180
+ * `secretsImport:` block under `CLICKHOUSE_CA_CERT` so `@fjall/clickhouse`
181
+ * `createFjallClickHouseClient()` picks it up from `process.env`. The secret
182
+ * holds raw PEM (no JSON field) so `field` is `undefined`.
183
+ */
184
+ getTlsCaCertImport(): SecretImport | undefined;
142
185
  getNativeUrl(): string;
143
186
  getDatabaseName(): string;
144
187
  getBackupBucket(): IBucket;
@@ -20,7 +20,8 @@ import { buildClickHouseEntrypointWrapper, buildClickHouseUserData, generateUser
20
20
  import { toPascalCase } from "../../utils/capitaliseString.js";
21
21
  import { ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, ClickHouseDefaultProfiles, PROFILE_NAME_PATTERN } from "../../resources/aws/database/clickhouseSchemas.js";
22
22
  import { inferAmiHardwareType } from "../../resources/aws/compute/ecsConstants.js";
23
- import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, CLICKHOUSE_TASK_MEMORY_MIB, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_SERVER_ROLE_TAG, clickHouseUserSecretName, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_STOP_TIMEOUT_SECONDS, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, userPasswordEnvName, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, CLICKHOUSE_SERVER_CONTAINER_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "../../resources/aws/database/clickhouseConstants.js";
23
+ import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, CLICKHOUSE_TASK_MEMORY_MIB, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_SERVER_ROLE_TAG, clickHouseUserSecretName, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_STOP_TIMEOUT_SECONDS, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, userPasswordEnvName, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, CLICKHOUSE_SERVER_CONTAINER_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "../../resources/aws/database/clickhouseConstants.js";
24
+ import { TlsCertGenerator } from "../../resources/aws/utilities/tlsCertGenerator.js";
24
25
  import { EcsCompute } from "./computeEcs.js";
25
26
  /**
26
27
  * Resolve the ECS desired task count for the ClickHouse service.
@@ -81,9 +82,30 @@ function createClickHouseUserSecret(scope, name) {
81
82
  export class ClickHouseDatabase extends Construct {
82
83
  databaseType = "ClickHouse";
83
84
  connectorType = "relational";
84
- additionalTcpPorts = [CLICKHOUSE_NATIVE_PORT];
85
+ additionalTcpPorts;
85
86
  id;
86
87
  connections;
88
+ /**
89
+ * CA cert secret. Self-signed: minted by `TlsCertGenerator`. Imported: the
90
+ * user-supplied ISecret. `undefined` when `tls.mode === "none"`.
91
+ * SecretString shape: raw PEM (single cert, no JSON wrapping).
92
+ */
93
+ tlsCaSecret;
94
+ /**
95
+ * Server cert + key secret. Self-signed: minted by `TlsCertGenerator`.
96
+ * Imported: the user-supplied ISecret. `undefined` when `tls.mode === "none"`.
97
+ * SecretString shape: JSON `{ "cert": "<leaf-pem>", "key": "<key-pem>" }`.
98
+ */
99
+ tlsServerSecret;
100
+ /**
101
+ * SHA-256 of the CA cert PEM. CFN token in self-signed mode (from the cert
102
+ * generator custom resource Data attribute). Plain string in imported mode
103
+ * when supplied. `undefined` when `tls.mode === "none"` or when imported
104
+ * without a digest. Surface on consumers as a task-replacement trigger.
105
+ */
106
+ caCertSha256;
107
+ #httpPort;
108
+ #nativePort;
87
109
  #users;
88
110
  #schemaAdmin;
89
111
  #managedPasswordNames;
@@ -157,7 +179,46 @@ export class ClickHouseDatabase extends Construct {
157
179
  (props.backupRetentionDays < 1 || props.backupRetentionDays > 3650)) {
158
180
  throw new Error(`ClickHouseDatabase: backupRetentionDays must be between 1 and 3650; got ${props.backupRetentionDays}.`);
159
181
  }
160
- const securityGroup = createClickHouseSecurityGroup(this, vpc, CLICKHOUSE_NATIVE_PORT);
182
+ const tlsOptions = props.tls ?? {
183
+ mode: "self-signed"
184
+ };
185
+ const tlsActive = tlsOptions.mode !== "none";
186
+ const httpPort = tlsActive ? CLICKHOUSE_HTTPS_PORT : CLICKHOUSE_HTTP_PORT;
187
+ const nativePort = tlsActive
188
+ ? CLICKHOUSE_TCP_SECURE_PORT
189
+ : CLICKHOUSE_NATIVE_PORT;
190
+ let tlsCaSecret;
191
+ let tlsServerSecret;
192
+ let caCertSha256;
193
+ if (tlsOptions.mode === "self-signed") {
194
+ const namespaceName = App.getInstance().getNamespace().namespaceName;
195
+ const hostname = `${CLICKHOUSE_CLOUDMAP_SERVICE_NAME}.${namespaceName}`;
196
+ const certGen = new TlsCertGenerator(this, "TlsCertGenerator", {
197
+ appName: App.getInstance().getName(),
198
+ hostname,
199
+ ...(tlsOptions.caValidityYears !== undefined && {
200
+ caValidityYears: tlsOptions.caValidityYears
201
+ }),
202
+ ...(tlsOptions.leafValidityYears !== undefined && {
203
+ leafValidityYears: tlsOptions.leafValidityYears
204
+ })
205
+ });
206
+ tlsCaSecret = certGen.caSecret.secret;
207
+ tlsServerSecret = certGen.serverSecret.secret;
208
+ caCertSha256 = certGen.caCertSha256;
209
+ }
210
+ else if (tlsOptions.mode === "imported") {
211
+ tlsCaSecret = tlsOptions.caCertSecret;
212
+ tlsServerSecret = tlsOptions.serverCertSecret;
213
+ caCertSha256 = tlsOptions.caCertSha256;
214
+ }
215
+ this.tlsCaSecret = tlsCaSecret;
216
+ this.tlsServerSecret = tlsServerSecret;
217
+ this.caCertSha256 = caCertSha256;
218
+ this.additionalTcpPorts = [nativePort];
219
+ this.#httpPort = httpPort;
220
+ this.#nativePort = nativePort;
221
+ const securityGroup = createClickHouseSecurityGroup(this, vpc, nativePort);
161
222
  const userSecrets = new Map();
162
223
  for (const name of allUserNames) {
163
224
  userSecrets.set(name, createClickHouseUserSecret(this, name));
@@ -200,6 +261,13 @@ export class ClickHouseDatabase extends Construct {
200
261
  bucketName: coldTierBucket.bucketName,
201
262
  region: Stack.of(this).region
202
263
  }
264
+ }),
265
+ tlsActive,
266
+ ...(tlsActive &&
267
+ tlsCaSecret !== undefined &&
268
+ tlsServerSecret !== undefined && {
269
+ caSecretArn: tlsCaSecret.secretArn,
270
+ serverSecretArn: tlsServerSecret.secretArn
203
271
  })
204
272
  }));
205
273
  // Source.data wraps the content in a zip; the default `extract: true`
@@ -226,6 +294,15 @@ export class ClickHouseDatabase extends Construct {
226
294
  ...OPTIMISE_MV_TABLES.map((table) => `OPTIMIZE TABLE ${CLICKHOUSE_DATABASE_NAME}.${table}`)
227
295
  ].join("; ");
228
296
  const adminSecret = expectDefined(userSecrets.get(schemaAdmin.name), `schemaAdmin '${schemaAdmin.name}' secret not minted.`);
297
+ const sidecarTlsPreamble = tlsActive
298
+ ? `set -eu && printf '%s\\n' "$CLICKHOUSE_CA_CERT" > /tmp/ca.crt && printf '%s' '<?xml version="1.0"?><config><openSSL><client><caConfig>/tmp/ca.crt</caConfig><verificationMode>strict</verificationMode><loadDefaultCAFile>false</loadDefaultCAFile><invalidCertificateHandler><name>RejectCertificateHandler</name></invalidCertificateHandler></client></openSSL></config>' > /tmp/clickhouse-client.xml && `
299
+ : "";
300
+ const sidecarTlsClientArgs = tlsActive
301
+ ? " --config-file=/tmp/clickhouse-client.xml --secure"
302
+ : "";
303
+ const sidecarTlsSecrets = tlsActive && tlsCaSecret !== undefined
304
+ ? { CLICKHOUSE_CA_CERT: EcsSecret.fromSecretsManager(tlsCaSecret) }
305
+ : {};
229
306
  const scheduledTasks = [];
230
307
  if (optimiseEnabled) {
231
308
  scheduledTasks.push({
@@ -235,18 +312,13 @@ export class ClickHouseDatabase extends Construct {
235
312
  cpu: OPTIMISE_TASK_CPU_UNITS,
236
313
  memoryLimitMiB: OPTIMISE_TASK_MEMORY_MIB,
237
314
  command: [
238
- "clickhouse-client",
239
- "--host",
240
- clickHouseHost,
241
- "--port",
242
- String(CLICKHOUSE_NATIVE_PORT),
243
- "--user",
244
- schemaAdmin.name,
245
- "--query",
246
- `${optimiseQuery};`
315
+ "sh",
316
+ "-c",
317
+ `${sidecarTlsPreamble}clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${sidecarTlsClientArgs} --query "${optimiseQuery};"`
247
318
  ],
248
319
  secrets: {
249
- CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
320
+ CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password"),
321
+ ...sidecarTlsSecrets
250
322
  },
251
323
  logRetention: RetentionDays.ONE_WEEK,
252
324
  securityGroups: [securityGroup]
@@ -263,10 +335,11 @@ export class ClickHouseDatabase extends Construct {
263
335
  "sh",
264
336
  "-c",
265
337
  // Password via CLICKHOUSE_PASSWORD env, not --password on argv (argv → /proc/<pid>/cmdline).
266
- `STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${CLICKHOUSE_NATIVE_PORT} --user ${schemaAdmin.name} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
338
+ `${sidecarTlsPreamble}STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${sidecarTlsClientArgs} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
267
339
  ],
268
340
  secrets: {
269
- CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
341
+ CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password"),
342
+ ...sidecarTlsSecrets
270
343
  },
271
344
  logGroup: backupTaskLogGroup,
272
345
  securityGroups: [securityGroup]
@@ -331,12 +404,12 @@ export class ClickHouseDatabase extends Construct {
331
404
  secretsImport: clickHouseContainerSecrets,
332
405
  portMappings: [
333
406
  {
334
- containerPort: CLICKHOUSE_HTTP_PORT,
335
- hostPort: CLICKHOUSE_HTTP_PORT
407
+ containerPort: httpPort,
408
+ hostPort: httpPort
336
409
  },
337
410
  {
338
- containerPort: CLICKHOUSE_NATIVE_PORT,
339
- hostPort: CLICKHOUSE_NATIVE_PORT
411
+ containerPort: nativePort,
412
+ hostPort: nativePort
340
413
  },
341
414
  {
342
415
  containerPort: CLICKHOUSE_PROMETHEUS_PORT,
@@ -360,12 +433,24 @@ export class ClickHouseDatabase extends Construct {
360
433
  hostSourcePath: `${CLICKHOUSE_DATA_MOUNT_PATH}/${CLICKHOUSE_USERS_SUBDIR}`,
361
434
  mountPath: "/etc/clickhouse-server/users.d",
362
435
  readOnly: true
363
- }
436
+ },
437
+ ...(tlsActive
438
+ ? [
439
+ {
440
+ name: "clickhouse-certs",
441
+ hostSourcePath: `${CLICKHOUSE_DATA_MOUNT_PATH}/server-certs`,
442
+ mountPath: CLICKHOUSE_TLS_CERT_MOUNT_PATH,
443
+ readOnly: true
444
+ }
445
+ ]
446
+ : [])
364
447
  ],
365
448
  healthCheck: {
366
449
  command: [
367
450
  "CMD-SHELL",
368
- `wget -q -O /dev/null http://127.0.0.1:${CLICKHOUSE_HTTP_PORT}/ping || exit 1`
451
+ tlsActive
452
+ ? `wget -q --ca-certificate=${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/ca.crt -O /dev/null https://127.0.0.1:${httpPort}/ping || exit 1`
453
+ : `wget -q -O /dev/null http://127.0.0.1:${httpPort}/ping || exit 1`
369
454
  ],
370
455
  interval: CLICKHOUSE_HEALTH_CHECK.INTERVAL_SECONDS,
371
456
  timeout: CLICKHOUSE_HEALTH_CHECK.TIMEOUT_SECONDS,
@@ -390,6 +475,10 @@ export class ClickHouseDatabase extends Construct {
390
475
  instanceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));
391
476
  backupBucket.grantRead(instanceRole, "config/*");
392
477
  adminSecret.secret.grantRead(instanceRole);
478
+ if (tlsCaSecret !== undefined)
479
+ tlsCaSecret.grantRead(instanceRole);
480
+ if (tlsServerSecret !== undefined)
481
+ tlsServerSecret.grantRead(instanceRole);
393
482
  const adminSecretName = clickHouseUserSecretName(schemaAdmin.name);
394
483
  // Password via CLICKHOUSE_CLIENT_PASSWORD env, not --password on argv
395
484
  // (argv → /proc/<pid>/cmdline). `jq -r .password` on an empty pipeline
@@ -404,7 +493,7 @@ export class ClickHouseDatabase extends Construct {
404
493
  `ADMIN_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "${adminSecretName}" --query SecretString --output text | jq -r .password)`,
405
494
  `if [ -z "$ADMIN_PASSWORD" ] || [ "$ADMIN_PASSWORD" = "null" ]; then echo "fjall:reload:status=failed reason=password-fetch" >&2; exit 1; fi`,
406
495
  `mv /var/lib/clickhouse/users.d/fjall.xml.new /var/lib/clickhouse/users.d/fjall.xml`,
407
- `docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client --port 9000 --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
496
+ `docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client${tlsActive ? ` --config-file=${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/client.xml --secure --host localhost` : ""} --port ${nativePort} --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
408
497
  ].join("\n");
409
498
  const region = Stack.of(this).region;
410
499
  const account = Stack.of(this).account;
@@ -456,15 +545,21 @@ export class ClickHouseDatabase extends Construct {
456
545
  reloadCustomResource.node.addDependency(usersConfigDeployment);
457
546
  this.connections = new Connections({
458
547
  securityGroups: [securityGroup],
459
- defaultPort: Port.tcp(CLICKHOUSE_HTTP_PORT)
548
+ defaultPort: Port.tcp(httpPort)
460
549
  });
461
550
  const declareOutput = (name, value) => {
462
551
  const output = new CfnOutput(this, name, { value });
463
552
  output.overrideLogicalId(name);
464
553
  };
465
554
  declareOutput("Endpoint", clickHouseHost);
466
- declareOutput("HttpPort", String(CLICKHOUSE_HTTP_PORT));
467
- declareOutput("NativePort", String(CLICKHOUSE_NATIVE_PORT));
555
+ declareOutput("HttpPort", String(httpPort));
556
+ declareOutput("NativePort", String(nativePort));
557
+ if (tlsActive) {
558
+ declareOutput("TlsActive", "true");
559
+ }
560
+ if (caCertSha256 !== undefined) {
561
+ declareOutput("CaCertSha256", caCertSha256);
562
+ }
468
563
  declareOutput("DatabaseName", CLICKHOUSE_DATABASE_NAME);
469
564
  for (const [name, secret] of userSecrets) {
470
565
  declareOutput(`${toPascalCase(name)}SecretArn`, secret.secret.secretArn);
@@ -497,19 +592,41 @@ export class ClickHouseDatabase extends Construct {
497
592
  return this.#users.get(name);
498
593
  }
499
594
  getHttpPort() {
500
- return CLICKHOUSE_HTTP_PORT;
595
+ return this.#httpPort;
501
596
  }
502
597
  getNativePort() {
503
- return CLICKHOUSE_NATIVE_PORT;
598
+ return this.#nativePort;
504
599
  }
505
600
  getHostEndpoint() {
506
601
  return `${CLICKHOUSE_CLOUDMAP_SERVICE_NAME}.${App.getInstance().getNamespace().namespaceName}`;
507
602
  }
508
- getHttpUrl() {
509
- return `http://${this.getHostEndpoint()}:${this.getHttpPort()}`;
603
+ /**
604
+ * Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
605
+ * active (default), `http` only when `tls: { mode: "none", … }` is set.
606
+ * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise.
607
+ */
608
+ getUrl() {
609
+ const scheme = this.tlsCaSecret !== undefined ? "https" : "http";
610
+ return `${scheme}://${this.getHostEndpoint()}:${this.#httpPort}`;
611
+ }
612
+ /**
613
+ * SecretImport for the CA certificate secret, or `undefined` when TLS is
614
+ * disabled (`tls: { mode: "none", … }`). Consumers wire this into their
615
+ * `secretsImport:` block under `CLICKHOUSE_CA_CERT` so `@fjall/clickhouse`
616
+ * `createFjallClickHouseClient()` picks it up from `process.env`. The secret
617
+ * holds raw PEM (no JSON field) so `field` is `undefined`.
618
+ */
619
+ getTlsCaCertImport() {
620
+ if (this.tlsCaSecret === undefined)
621
+ return undefined;
622
+ return {
623
+ id: `${this.node.id}TlsCaCertImport`,
624
+ name: this.tlsCaSecret.secretName,
625
+ field: undefined
626
+ };
510
627
  }
511
628
  getNativeUrl() {
512
- return `tcp://${this.getHostEndpoint()}:${this.getNativePort()}`;
629
+ return `tcp://${this.getHostEndpoint()}:${this.#nativePort}`;
513
630
  }
514
631
  getDatabaseName() {
515
632
  return CLICKHOUSE_DATABASE_NAME;
@@ -561,12 +678,19 @@ export class ClickHouseDatabase extends Construct {
561
678
  */
562
679
  getMigrationContributions() {
563
680
  const environment = {
564
- CLICKHOUSE_URL: this.getHttpUrl(),
681
+ CLICKHOUSE_URL: this.getUrl(),
565
682
  CLICKHOUSE_DATABASE: this.getDatabaseName()
566
683
  };
567
684
  const secretsImport = {};
568
685
  const adminSecret = this.getUser(this.#schemaAdmin.name);
569
686
  secretsImport.SCHEMA_ADMIN_PASSWORD = adminSecret.getImport("password");
687
+ const caCertImport = this.getTlsCaCertImport();
688
+ if (caCertImport !== undefined) {
689
+ secretsImport.CLICKHOUSE_CA_CERT = caCertImport;
690
+ }
691
+ if (this.caCertSha256 !== undefined) {
692
+ environment.CLICKHOUSE_CA_CERT_SHA256 = this.caCertSha256;
693
+ }
570
694
  for (const name of this.#managedPasswordNames) {
571
695
  const userSecret = this.getUser(name);
572
696
  secretsImport[userPasswordEnvName(name)] =
@@ -606,7 +730,7 @@ export class ClickHouseDatabase extends Construct {
606
730
  if (container === undefined) {
607
731
  throw new Error("ClickHouseDatabase.connectFromTask: task has no default container");
608
732
  }
609
- container.addEnvironment("CLICKHOUSE_URL", this.getHttpUrl());
733
+ container.addEnvironment("CLICKHOUSE_URL", this.getUrl());
610
734
  container.addEnvironment("CLICKHOUSE_DATABASE", CLICKHOUSE_DATABASE_NAME);
611
735
  if (opts?.user !== undefined) {
612
736
  const userSecret = this.getUser(opts.user);
@@ -0,0 +1 @@
1
+ export * from "./types.js";
@@ -0,0 +1 @@
1
+ export * from "./types.js";
@@ -0,0 +1,48 @@
1
+ import type { ISecret } from "aws-cdk-lib/aws-secretsmanager";
2
+ /**
3
+ * Self-signed mode (default). The construct mints an ECDSA P-256 CA + leaf
4
+ * cert at stack deploy via a custom-resource Lambda. Bumping
5
+ * `caValidityYears` / `leafValidityYears` triggers re-issuance on the next
6
+ * deploy.
7
+ */
8
+ export interface ClickHouseSelfSignedTls {
9
+ readonly mode: "self-signed";
10
+ readonly caValidityYears?: number;
11
+ readonly leafValidityYears?: number;
12
+ }
13
+ /**
14
+ * Imported mode. Caller owns the CA + server-cert secrets in Secrets
15
+ * Manager. `serverCertSecret`'s SecretString MUST be JSON of the shape
16
+ * `{ "cert": "<pem>", "key": "<pem>" }`. `caCertSha256` is optional — when
17
+ * supplied the construct surfaces it as a CFN env-var token so consumers
18
+ * can wire it into containers for rotation-triggers-task-replacement
19
+ * parity with self-signed mode. Omit it and rotation requires manual task
20
+ * restart (documented in `aiDocs/patterns/clickhouse-tls-pattern.md § "Imported mode"`).
21
+ */
22
+ export interface ClickHouseImportedTls {
23
+ readonly mode: "imported";
24
+ readonly caCertSecret: ISecret;
25
+ readonly serverCertSecret: ISecret;
26
+ readonly caCertSha256?: string;
27
+ }
28
+ /**
29
+ * Plaintext escape hatch. Disables TLS entirely. The acknowledgement is a
30
+ * literal-string type so misuse fails at compile time — passing any other
31
+ * string fails `tsc --noEmit`.
32
+ */
33
+ export interface ClickHouseNoTls {
34
+ readonly mode: "none";
35
+ readonly acknowledgement: "I understand plaintext-only ClickHouse fails SOC2 DP5";
36
+ }
37
+ export type ClickHouseTlsOptions = ClickHouseSelfSignedTls | ClickHouseImportedTls | ClickHouseNoTls;
38
+ /**
39
+ * The resolved TLS surface a `ClickHouseDatabase` carries after dispatch.
40
+ * Every field is `undefined` in `mode: "none"`. `certGeneratorFunction`
41
+ * is `undefined` in `mode: "imported"`. `caCertSha256` is `undefined` in
42
+ * `mode: "none"` AND in `mode: "imported"` unless the caller supplied it.
43
+ */
44
+ export interface ClickHouseTlsResolved {
45
+ readonly caSecret: ISecret | undefined;
46
+ readonly serverSecret: ISecret | undefined;
47
+ readonly caCertSha256: string | undefined;
48
+ }
@@ -263,12 +263,14 @@ export interface IClickHouseDatabase extends IDatabase, IConnectable, IMigration
263
263
  /** Native TCP port (typically 9000). */
264
264
  getNativePort(): number;
265
265
  /**
266
- * HTTP URL for clickhouse-client / chproxy (`http://<host>:<httpPort>`).
267
- * No credentials in the URL ClickHouse HTTP uses Basic Auth headers or
268
- * `X-ClickHouse-User`/`X-ClickHouse-Key` headers. Compose creds at runtime
269
- * via `getUser(name).getImport(...)`.
266
+ * Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
267
+ * active (default), `http` only when `tls: { mode: "none", }` is set.
268
+ * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise. No
269
+ * credentials in the URL — ClickHouse HTTP uses Basic Auth headers or
270
+ * `X-ClickHouse-User`/`X-ClickHouse-Key` headers. Compose creds at
271
+ * runtime via `getUser(name).getImport(...)`.
270
272
  */
271
- getHttpUrl(): string;
273
+ getUrl(): string;
272
274
  /**
273
275
  * Native TCP URL (`tcp://<host>:<nativePort>`).
274
276
  * No credentials in the URL — the native protocol handshake carries them.
@@ -53,6 +53,27 @@ export declare const CLICKHOUSE_TASK_MEMORY_MIB = 3072;
53
53
  export declare const CLICKHOUSE_HTTP_PORT = 8123;
54
54
  export declare const CLICKHOUSE_NATIVE_PORT = 9000;
55
55
  export declare const CLICKHOUSE_PROMETHEUS_PORT = 9363;
56
+ /** TLS-mode ClickHouse ports.
57
+ * HTTPS replaces 8123; secure native protocol replaces 9000.
58
+ * See aiDocs/patterns/clickhouse-tls-pattern.md § "The contract". */
59
+ export declare const CLICKHOUSE_HTTPS_PORT = 8443;
60
+ export declare const CLICKHOUSE_TCP_SECURE_PORT = 9440;
61
+ /** Mount path inside the main ClickHouse container for the materialised
62
+ * TLS cert + key. The init container writes to a shared task-scoped volume
63
+ * mounted here read-only on the main container. */
64
+ export declare const CLICKHOUSE_TLS_CERT_MOUNT_PATH = "/etc/clickhouse-server/certs";
65
+ /** Task-scoped Docker volume name shared between the TLS init container
66
+ * (writer) and the main ClickHouse container (reader). */
67
+ export declare const CLICKHOUSE_TLS_CERT_VOLUME_NAME = "tls-certs";
68
+ /** UID:GID the official ClickHouse image runs as. Init container `chown`s
69
+ * the materialised cert files to this UID so the server can read them. */
70
+ export declare const CLICKHOUSE_UID = 101;
71
+ /** Digest-pinned alpine image used by the TLS init container. The init
72
+ * container needs `jq` (extracts `cert`+`key` from the server-cert JSON
73
+ * secret); alpine ships it via `apk add` — but pinning the digest avoids
74
+ * fetching `:latest` on every deploy. Bump in lockstep with renovate
75
+ * alerts; CI verifies the digest resolves. */
76
+ export declare const ALPINE_INIT_CONTAINER_IMAGE = "public.ecr.aws/docker/library/alpine:3.20@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d";
56
77
  /** EBS device name for the data volume (must match user data script). */
57
78
  export declare const CLICKHOUSE_EBS_DEVICE_NAME = "/dev/xvdf";
58
79
  /** EBS mount path on the EC2 host. */
@@ -53,6 +53,27 @@ export const CLICKHOUSE_TASK_MEMORY_MIB = 3072;
53
53
  export const CLICKHOUSE_HTTP_PORT = 8123;
54
54
  export const CLICKHOUSE_NATIVE_PORT = 9000;
55
55
  export const CLICKHOUSE_PROMETHEUS_PORT = 9363;
56
+ /** TLS-mode ClickHouse ports.
57
+ * HTTPS replaces 8123; secure native protocol replaces 9000.
58
+ * See aiDocs/patterns/clickhouse-tls-pattern.md § "The contract". */
59
+ export const CLICKHOUSE_HTTPS_PORT = 8443;
60
+ export const CLICKHOUSE_TCP_SECURE_PORT = 9440;
61
+ /** Mount path inside the main ClickHouse container for the materialised
62
+ * TLS cert + key. The init container writes to a shared task-scoped volume
63
+ * mounted here read-only on the main container. */
64
+ export const CLICKHOUSE_TLS_CERT_MOUNT_PATH = "/etc/clickhouse-server/certs";
65
+ /** Task-scoped Docker volume name shared between the TLS init container
66
+ * (writer) and the main ClickHouse container (reader). */
67
+ export const CLICKHOUSE_TLS_CERT_VOLUME_NAME = "tls-certs";
68
+ /** UID:GID the official ClickHouse image runs as. Init container `chown`s
69
+ * the materialised cert files to this UID so the server can read them. */
70
+ export const CLICKHOUSE_UID = 101;
71
+ /** Digest-pinned alpine image used by the TLS init container. The init
72
+ * container needs `jq` (extracts `cert`+`key` from the server-cert JSON
73
+ * secret); alpine ships it via `apk add` — but pinning the digest avoids
74
+ * fetching `:latest` on every deploy. Bump in lockstep with renovate
75
+ * alerts; CI verifies the digest resolves. */
76
+ export const ALPINE_INIT_CONTAINER_IMAGE = "public.ecr.aws/docker/library/alpine:3.20@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d";
56
77
  /** EBS device name for the data volume (must match user data script). */
57
78
  export const CLICKHOUSE_EBS_DEVICE_NAME = "/dev/xvdf";
58
79
  /** EBS mount path on the EC2 host. */
@@ -10,5 +10,7 @@ import { SecurityGroup } from "../networking/securityGroup.js";
10
10
  *
11
11
  * Self-referencing native-port ingress is kept so the optimise/backup
12
12
  * scheduled tasks (which share this SG) can reach the ClickHouse server.
13
+ * Under TLS modes, the secure native port (9440) is the self-referencing
14
+ * port; under `mode: "none"` the plaintext native port stays.
13
15
  */
14
16
  export declare function createClickHouseSecurityGroup(scope: Construct, vpc: IVpc, nativePort: number): SecurityGroup;
@@ -9,6 +9,8 @@ import { SecurityGroup } from "../networking/securityGroup.js";
9
9
  *
10
10
  * Self-referencing native-port ingress is kept so the optimise/backup
11
11
  * scheduled tasks (which share this SG) can reach the ClickHouse server.
12
+ * Under TLS modes, the secure native port (9440) is the self-referencing
13
+ * port; under `mode: "none"` the plaintext native port stays.
12
14
  */
13
15
  export function createClickHouseSecurityGroup(scope, vpc, nativePort) {
14
16
  const sg = new SecurityGroup(scope, "ClickHouseSecurityGroup", {
@@ -19,6 +19,27 @@ export interface BuildClickHouseUserDataOptions {
19
19
  bucketName: string;
20
20
  region: string;
21
21
  };
22
+ /**
23
+ * When true, the server config emits `<https_port>`, `<tcp_port_secure>`,
24
+ * and an `<openSSL><server>` block referencing the cert files materialised
25
+ * at `CLICKHOUSE_TLS_CERT_MOUNT_PATH` by the user-data bootstrap step.
26
+ * Plaintext `<http_port>` is omitted to avoid mixed-protocol exposure.
27
+ * Default: false. When true, `caSecretArn` and `serverSecretArn` MUST be
28
+ * supplied so the bootstrap step can materialise the cert PEMs.
29
+ */
30
+ tlsActive?: boolean;
31
+ /**
32
+ * Secrets Manager ARN holding the CA cert as raw PEM. Required when
33
+ * `tlsActive` is true. Fetched at instance boot via the EC2 instance role
34
+ * and written to `${dataMount}/server-certs/ca.crt`.
35
+ */
36
+ caSecretArn?: string;
37
+ /**
38
+ * Secrets Manager ARN holding the server cert + key as JSON
39
+ * `{ "cert": "<pem>", "key": "<pem>" }`. Required when `tlsActive` is true.
40
+ * Fetched at instance boot and written to `${dataMount}/server-certs/server.{crt,key}`.
41
+ */
42
+ serverSecretArn?: string;
22
43
  }
23
44
  export declare function generateServerConfigXml(options: BuildClickHouseUserDataOptions): string;
24
45
  export interface GenerateUsersConfigXmlOptions {