@fjall/components-infrastructure 0.102.0 → 1.1.0

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 (70) 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 +37 -0
  4. package/dist/lib/patterns/aws/clickhouseDatabase.js +120 -19
  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/resources/aws/database/clickhouseConstants.d.ts +21 -0
  9. package/dist/lib/resources/aws/database/clickhouseConstants.js +21 -0
  10. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.d.ts +2 -0
  11. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.js +2 -0
  12. package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +21 -0
  13. package/dist/lib/resources/aws/database/clickhouseUserData.js +48 -3
  14. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.d.ts +1 -1
  15. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +1 -1
  16. package/dist/lib/resources/aws/secrets/index.d.ts +2 -0
  17. package/dist/lib/resources/aws/secrets/index.js +2 -0
  18. package/dist/lib/resources/aws/secrets/tlsCaSecret.d.ts +13 -0
  19. package/dist/lib/resources/aws/secrets/tlsCaSecret.js +15 -0
  20. package/dist/lib/resources/aws/secrets/tlsServerSecret.d.ts +15 -0
  21. package/dist/lib/resources/aws/secrets/tlsServerSecret.js +17 -0
  22. package/dist/lib/resources/aws/utilities/index.d.ts +1 -0
  23. package/dist/lib/resources/aws/utilities/index.js +1 -0
  24. package/dist/lib/resources/aws/utilities/tlsCertGenerator.d.ts +33 -0
  25. package/dist/lib/resources/aws/utilities/tlsCertGenerator.js +67 -0
  26. package/package.json +7 -5
  27. package/dist/lib/config/aws/__t17fixture.js +0 -3
  28. package/dist/lib/config/aws/__t17fixtureType.d.ts +0 -2
  29. package/dist/lib/config/aws/__t17fixtureType.js +0 -1
  30. package/dist/lib/config/aws/eventBus.d.ts +0 -7
  31. package/dist/lib/config/aws/eventBus.js +0 -21
  32. package/dist/lib/config/aws/identityCenterGroupMembership.d.ts +0 -10
  33. package/dist/lib/config/aws/identityCenterGroupMembership.js +0 -102
  34. package/dist/lib/config/aws/securityBaseline.d.ts +0 -15
  35. package/dist/lib/config/aws/securityBaseline.js +0 -27
  36. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.d.ts +0 -1
  37. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.js +0 -4
  38. package/dist/lib/patterns/aws/managedIdentityCenter.d.ts +0 -4
  39. package/dist/lib/patterns/aws/managedIdentityCenter.js +0 -19
  40. package/dist/lib/patterns/aws/subdomainHostedZone.d.ts +0 -9
  41. package/dist/lib/patterns/aws/subdomainHostedZone.js +0 -34
  42. package/dist/lib/resources/aws/analytics/clickhouse.d.ts +0 -15
  43. package/dist/lib/resources/aws/analytics/clickhouse.js +0 -310
  44. package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +0 -49
  45. package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +0 -140
  46. package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +0 -73
  47. package/dist/lib/resources/aws/analytics/clickhouseConstants.js +0 -89
  48. package/dist/lib/resources/aws/analytics/clickhouseSecurityGroup.d.ts +0 -13
  49. package/dist/lib/resources/aws/analytics/clickhouseSecurityGroup.js +0 -28
  50. package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +0 -59
  51. package/dist/lib/resources/aws/analytics/clickhouseTypes.js +0 -1
  52. package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +0 -6
  53. package/dist/lib/resources/aws/analytics/clickhouseUserData.js +0 -299
  54. package/dist/lib/resources/aws/analytics/index.d.ts +0 -4
  55. package/dist/lib/resources/aws/analytics/index.js +0 -2
  56. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.d.ts +0 -2
  57. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.js +0 -11
  58. package/dist/lib/resources/aws/messaging/defaultEventBus.d.ts +0 -7
  59. package/dist/lib/resources/aws/messaging/defaultEventBus.js +0 -21
  60. package/dist/lib/resources/aws/networking/domain.d.ts +0 -13
  61. package/dist/lib/resources/aws/networking/domain.js +0 -100
  62. package/dist/lib/synth_dump.d.ts +0 -1
  63. package/dist/lib/synth_dump.js +0 -42
  64. package/dist/lib/utils/bastionFactory.d.ts +0 -10
  65. package/dist/lib/utils/bastionFactory.js +0 -29
  66. package/dist/lib/utils/constructMap.d.ts +0 -33
  67. package/dist/lib/utils/constructMap.js +0 -154
  68. package/dist/lib/utils/dnsRecords.d.ts +0 -4
  69. package/dist/lib/utils/dnsRecords.js +0 -104
  70. /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
+ }
@@ -4,6 +4,8 @@ import { type IBucket } from "aws-cdk-lib/aws-s3";
4
4
  import { Construct } from "constructs";
5
5
  import { Secret } 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,6 +168,13 @@ export declare class ClickHouseDatabase extends Construct implements IClickHouse
138
168
  getHttpPort(): number;
139
169
  getNativePort(): number;
140
170
  getHostEndpoint(): 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
+ /** @deprecated Alias for {@link getUrl}. Removed in a follow-up landing. */
141
178
  getHttpUrl(): string;
142
179
  getNativeUrl(): string;
143
180
  getDatabaseName(): string;
@@ -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`
@@ -239,9 +307,10 @@ export class ClickHouseDatabase extends Construct {
239
307
  "--host",
240
308
  clickHouseHost,
241
309
  "--port",
242
- String(CLICKHOUSE_NATIVE_PORT),
310
+ String(nativePort),
243
311
  "--user",
244
312
  schemaAdmin.name,
313
+ ...(tlsActive ? ["--secure", "--accept-invalid-certificate"] : []),
245
314
  "--query",
246
315
  `${optimiseQuery};`
247
316
  ],
@@ -263,7 +332,7 @@ export class ClickHouseDatabase extends Construct {
263
332
  "sh",
264
333
  "-c",
265
334
  // 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/')"`
335
+ `STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${tlsActive ? " --secure --accept-invalid-certificate" : ""} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
267
336
  ],
268
337
  secrets: {
269
338
  CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
@@ -331,12 +400,12 @@ export class ClickHouseDatabase extends Construct {
331
400
  secretsImport: clickHouseContainerSecrets,
332
401
  portMappings: [
333
402
  {
334
- containerPort: CLICKHOUSE_HTTP_PORT,
335
- hostPort: CLICKHOUSE_HTTP_PORT
403
+ containerPort: httpPort,
404
+ hostPort: httpPort
336
405
  },
337
406
  {
338
- containerPort: CLICKHOUSE_NATIVE_PORT,
339
- hostPort: CLICKHOUSE_NATIVE_PORT
407
+ containerPort: nativePort,
408
+ hostPort: nativePort
340
409
  },
341
410
  {
342
411
  containerPort: CLICKHOUSE_PROMETHEUS_PORT,
@@ -360,12 +429,24 @@ export class ClickHouseDatabase extends Construct {
360
429
  hostSourcePath: `${CLICKHOUSE_DATA_MOUNT_PATH}/${CLICKHOUSE_USERS_SUBDIR}`,
361
430
  mountPath: "/etc/clickhouse-server/users.d",
362
431
  readOnly: true
363
- }
432
+ },
433
+ ...(tlsActive
434
+ ? [
435
+ {
436
+ name: "clickhouse-certs",
437
+ hostSourcePath: `${CLICKHOUSE_DATA_MOUNT_PATH}/server-certs`,
438
+ mountPath: CLICKHOUSE_TLS_CERT_MOUNT_PATH,
439
+ readOnly: true
440
+ }
441
+ ]
442
+ : [])
364
443
  ],
365
444
  healthCheck: {
366
445
  command: [
367
446
  "CMD-SHELL",
368
- `wget -q -O /dev/null http://127.0.0.1:${CLICKHOUSE_HTTP_PORT}/ping || exit 1`
447
+ tlsActive
448
+ ? `wget -q --no-check-certificate -O /dev/null https://127.0.0.1:${httpPort}/ping || exit 1`
449
+ : `wget -q -O /dev/null http://127.0.0.1:${httpPort}/ping || exit 1`
369
450
  ],
370
451
  interval: CLICKHOUSE_HEALTH_CHECK.INTERVAL_SECONDS,
371
452
  timeout: CLICKHOUSE_HEALTH_CHECK.TIMEOUT_SECONDS,
@@ -390,6 +471,10 @@ export class ClickHouseDatabase extends Construct {
390
471
  instanceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));
391
472
  backupBucket.grantRead(instanceRole, "config/*");
392
473
  adminSecret.secret.grantRead(instanceRole);
474
+ if (tlsCaSecret !== undefined)
475
+ tlsCaSecret.grantRead(instanceRole);
476
+ if (tlsServerSecret !== undefined)
477
+ tlsServerSecret.grantRead(instanceRole);
393
478
  const adminSecretName = clickHouseUserSecretName(schemaAdmin.name);
394
479
  // Password via CLICKHOUSE_CLIENT_PASSWORD env, not --password on argv
395
480
  // (argv → /proc/<pid>/cmdline). `jq -r .password` on an empty pipeline
@@ -404,7 +489,7 @@ export class ClickHouseDatabase extends Construct {
404
489
  `ADMIN_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "${adminSecretName}" --query SecretString --output text | jq -r .password)`,
405
490
  `if [ -z "$ADMIN_PASSWORD" ] || [ "$ADMIN_PASSWORD" = "null" ]; then echo "fjall:reload:status=failed reason=password-fetch" >&2; exit 1; fi`,
406
491
  `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"`
492
+ `docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client${tlsActive ? " --secure --accept-invalid-certificate" : ""} --port ${nativePort} --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
408
493
  ].join("\n");
409
494
  const region = Stack.of(this).region;
410
495
  const account = Stack.of(this).account;
@@ -456,15 +541,21 @@ export class ClickHouseDatabase extends Construct {
456
541
  reloadCustomResource.node.addDependency(usersConfigDeployment);
457
542
  this.connections = new Connections({
458
543
  securityGroups: [securityGroup],
459
- defaultPort: Port.tcp(CLICKHOUSE_HTTP_PORT)
544
+ defaultPort: Port.tcp(httpPort)
460
545
  });
461
546
  const declareOutput = (name, value) => {
462
547
  const output = new CfnOutput(this, name, { value });
463
548
  output.overrideLogicalId(name);
464
549
  };
465
550
  declareOutput("Endpoint", clickHouseHost);
466
- declareOutput("HttpPort", String(CLICKHOUSE_HTTP_PORT));
467
- declareOutput("NativePort", String(CLICKHOUSE_NATIVE_PORT));
551
+ declareOutput("HttpPort", String(httpPort));
552
+ declareOutput("NativePort", String(nativePort));
553
+ if (tlsActive) {
554
+ declareOutput("TlsActive", "true");
555
+ }
556
+ if (caCertSha256 !== undefined) {
557
+ declareOutput("CaCertSha256", caCertSha256);
558
+ }
468
559
  declareOutput("DatabaseName", CLICKHOUSE_DATABASE_NAME);
469
560
  for (const [name, secret] of userSecrets) {
470
561
  declareOutput(`${toPascalCase(name)}SecretArn`, secret.secret.secretArn);
@@ -497,19 +588,29 @@ export class ClickHouseDatabase extends Construct {
497
588
  return this.#users.get(name);
498
589
  }
499
590
  getHttpPort() {
500
- return CLICKHOUSE_HTTP_PORT;
591
+ return this.#httpPort;
501
592
  }
502
593
  getNativePort() {
503
- return CLICKHOUSE_NATIVE_PORT;
594
+ return this.#nativePort;
504
595
  }
505
596
  getHostEndpoint() {
506
597
  return `${CLICKHOUSE_CLOUDMAP_SERVICE_NAME}.${App.getInstance().getNamespace().namespaceName}`;
507
598
  }
599
+ /**
600
+ * Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
601
+ * active (default), `http` only when `tls: { mode: "none", … }` is set.
602
+ * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise.
603
+ */
604
+ getUrl() {
605
+ const scheme = this.tlsCaSecret !== undefined ? "https" : "http";
606
+ return `${scheme}://${this.getHostEndpoint()}:${this.#httpPort}`;
607
+ }
608
+ /** @deprecated Alias for {@link getUrl}. Removed in a follow-up landing. */
508
609
  getHttpUrl() {
509
- return `http://${this.getHostEndpoint()}:${this.getHttpPort()}`;
610
+ return this.getUrl();
510
611
  }
511
612
  getNativeUrl() {
512
- return `tcp://${this.getHostEndpoint()}:${this.getNativePort()}`;
613
+ return `tcp://${this.getHostEndpoint()}:${this.#nativePort}`;
513
614
  }
514
615
  getDatabaseName() {
515
616
  return CLICKHOUSE_DATABASE_NAME;
@@ -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
+ }
@@ -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 {
@@ -1,7 +1,8 @@
1
- import { CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_PROMETHEUS_PORT, clickHousePasswordSha256Snippet } from "./clickhouseConstants.js";
1
+ import { CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, clickHousePasswordSha256Snippet } from "./clickhouseConstants.js";
2
2
  import { renderUsersXml } from "./clickhouseXmlRenderer.js";
3
3
  export function generateServerConfigXml(options) {
4
4
  const { backupBucketName, backupBucketRegion, coldTier } = options;
5
+ const tlsActive = options.tlsActive ?? false;
5
6
  const storageBlock = coldTier !== undefined
6
7
  ? ` <storage_configuration>
7
8
  <!-- Same CH 26 rule as the no-cold-tier branch: a <local_ssd> disk
@@ -145,7 +146,19 @@ export function generateServerConfigXml(options) {
145
146
  <number_of_free_entries_in_pool_to_lower_max_size_of_merge>1</number_of_free_entries_in_pool_to_lower_max_size_of_merge>
146
147
  <number_of_free_entries_in_pool_to_execute_optimize_entire_partition>1</number_of_free_entries_in_pool_to_execute_optimize_entire_partition>
147
148
  </merge_tree>
148
- <http_port>${CLICKHOUSE_HTTP_PORT}</http_port>
149
+ ${tlsActive
150
+ ? ` <https_port>${CLICKHOUSE_HTTPS_PORT}</https_port>
151
+ <tcp_port_secure>${CLICKHOUSE_TCP_SECURE_PORT}</tcp_port_secure>
152
+ <openSSL>
153
+ <server>
154
+ <certificateFile>${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/server.crt</certificateFile>
155
+ <privateKeyFile>${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/server.key</privateKeyFile>
156
+ <verificationMode>relaxed</verificationMode>
157
+ <disableProtocols>sslv2,sslv3,tlsv1,tlsv1_1</disableProtocols>
158
+ <preferServerCiphers>true</preferServerCiphers>
159
+ </server>
160
+ </openSSL>`
161
+ : ` <http_port>${CLICKHOUSE_HTTP_PORT}</http_port>`}
149
162
  <custom_settings_prefixes>current_</custom_settings_prefixes>
150
163
  <!-- HTTP keep-alive window. Must exceed @clickhouse/client idle_socket_ttl (15 s)
151
164
  so the client always closes the socket first. Prevents ECONNRESET on reuse. -->
@@ -247,6 +260,38 @@ function compactUserDataScript(script) {
247
260
  */
248
261
  export function buildClickHouseUserData(options) {
249
262
  const serverConfigXml = generateServerConfigXml(options);
263
+ const tlsActive = options.tlsActive ?? false;
264
+ if (tlsActive &&
265
+ (options.caSecretArn === undefined || options.serverSecretArn === undefined)) {
266
+ throw new Error("buildClickHouseUserData: tlsActive=true requires both caSecretArn and serverSecretArn");
267
+ }
268
+ const tlsBootstrap = tlsActive
269
+ ? `
270
+ # Materialise TLS certs from Secrets Manager. The EC2 instance role carries
271
+ # the secretsmanager:GetSecretValue grant; certs land on the EBS-backed mount
272
+ # at \`$MOUNT_POINT/server-certs/\` and are bind-mounted read-only into the
273
+ # CH container at /etc/clickhouse-server/certs. Cert rotation triggers task
274
+ # replacement via the CA_CERT_SHA256 env on dependent services (the cert
275
+ # generator emits the digest as a CFN Data attribute).
276
+ mkdir -p "$MOUNT_POINT/server-certs"
277
+
278
+ # jq is needed to extract cert + key fields from the server bundle JSON.
279
+ # AL2023 ECS-optimised AMIs ship without jq; install it if missing.
280
+ command -v jq >/dev/null 2>&1 || dnf install -y jq || yum install -y jq
281
+
282
+ # Fetch CA (raw PEM SecretString). Quoting prevents word-splitting on multi-line PEM.
283
+ CA_PEM=$(aws secretsmanager get-secret-value --secret-id "${options.caSecretArn ?? ""}" --query SecretString --output text)
284
+ printf '%s\\n' "$CA_PEM" > "$MOUNT_POINT/server-certs/ca.crt"
285
+
286
+ # Fetch server bundle (JSON { "cert": "<pem>", "key": "<pem>" }).
287
+ SERVER_JSON=$(aws secretsmanager get-secret-value --secret-id "${options.serverSecretArn ?? ""}" --query SecretString --output text)
288
+ printf '%s' "$SERVER_JSON" | jq -r .cert > "$MOUNT_POINT/server-certs/server.crt"
289
+ printf '%s' "$SERVER_JSON" | jq -r .key > "$MOUNT_POINT/server-certs/server.key"
290
+
291
+ chown -R 101:101 "$MOUNT_POINT/server-certs"
292
+ chmod 600 "$MOUNT_POINT/server-certs/server.key"
293
+ `
294
+ : "";
250
295
  const script = `#!/bin/bash
251
296
  set -euo pipefail
252
297
 
@@ -333,7 +378,7 @@ if [ "$USERS_CONFIG_RETRIEVED" != "1" ]; then
333
378
  fi
334
379
 
335
380
  chown -R 101:101 "$MOUNT_POINT"
336
- `;
381
+ ${tlsBootstrap}`;
337
382
  return compactUserDataScript(script);
338
383
  }
339
384
  /**
@@ -45,7 +45,7 @@ export interface RenderUsersXmlOptions {
45
45
  *
46
46
  * **Schema-admin-only emission.** Only the schema admin lands in the XML.
47
47
  * Managed-password workload users are created at runtime by the migration
48
- * helper (`@fjall/clickhouse-migrations § provisionUsersFromEnv`) — they
48
+ * helper (`@fjall/clickhouse § provisionUsersFromEnv`) — they
49
49
  * authenticate via plaintext from `USER_<NAME>_PASSWORD` env vars at runtime
50
50
  * and bind their profiles via customer SQL.
51
51
  *
@@ -73,7 +73,7 @@ ${body}
73
73
  *
74
74
  * **Schema-admin-only emission.** Only the schema admin lands in the XML.
75
75
  * Managed-password workload users are created at runtime by the migration
76
- * helper (`@fjall/clickhouse-migrations § provisionUsersFromEnv`) — they
76
+ * helper (`@fjall/clickhouse § provisionUsersFromEnv`) — they
77
77
  * authenticate via plaintext from `USER_<NAME>_PASSWORD` env vars at runtime
78
78
  * and bind their profiles via customer SQL.
79
79
  *
@@ -2,3 +2,5 @@ export * from "./alias.js";
2
2
  export * from "./kms.js";
3
3
  export * from "./parameter.js";
4
4
  export * from "./secret.js";
5
+ export * from "./tlsCaSecret.js";
6
+ export * from "./tlsServerSecret.js";
@@ -2,3 +2,5 @@ export * from "./alias.js";
2
2
  export * from "./kms.js";
3
3
  export * from "./parameter.js";
4
4
  export * from "./secret.js";
5
+ export * from "./tlsCaSecret.js";
6
+ export * from "./tlsServerSecret.js";
@@ -0,0 +1,13 @@
1
+ import type { Construct } from "constructs";
2
+ import { Secret } from "./secret.js";
3
+ export interface TlsCaSecretProps {
4
+ /** Drives the deterministic secret name `fjall-<appName>-clickhouse-tls-ca`. */
5
+ readonly appName: string;
6
+ }
7
+ /**
8
+ * ClickHouse TLS CA cert (public — clients pin against this). Retained on
9
+ * delete so client-side trust anchors survive accidental stack teardown.
10
+ */
11
+ export declare class TlsCaSecret extends Secret {
12
+ constructor(scope: Construct, id: string, props: TlsCaSecretProps);
13
+ }
@@ -0,0 +1,15 @@
1
+ import { RemovalPolicy } from "aws-cdk-lib";
2
+ import { Secret } from "./secret.js";
3
+ /**
4
+ * ClickHouse TLS CA cert (public — clients pin against this). Retained on
5
+ * delete so client-side trust anchors survive accidental stack teardown.
6
+ */
7
+ export class TlsCaSecret extends Secret {
8
+ constructor(scope, id, props) {
9
+ super(scope, id, {
10
+ secretName: `fjall-${props.appName}-clickhouse-tls-ca`,
11
+ description: "ClickHouse TLS CA cert (public — clients pin against this)"
12
+ });
13
+ this.secret.applyRemovalPolicy(RemovalPolicy.RETAIN);
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ import type { Construct } from "constructs";
2
+ import { Secret } from "./secret.js";
3
+ export interface TlsServerSecretProps {
4
+ /** Drives the deterministic secret name `fjall-<appName>-clickhouse-tls-server`. */
5
+ readonly appName: string;
6
+ }
7
+ /**
8
+ * ClickHouse TLS server cert + key (JSON: `{ cert, key }`). Retained on
9
+ * delete so cert material survives accidental stack teardown; the private
10
+ * key only leaves Secrets Manager via ECS executionRole + the TLS init
11
+ * container that materialises it onto the shared volume.
12
+ */
13
+ export declare class TlsServerSecret extends Secret {
14
+ constructor(scope: Construct, id: string, props: TlsServerSecretProps);
15
+ }