@hasna/uptime 0.1.18 → 0.1.19

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/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.19] - 2026-06-28
10
+
11
+ ### Added
12
+
13
+ - Added optional CloudFront origin verification header binding to the AWS
14
+ Terraform module. When enabled, CloudFront sends a private origin header and
15
+ the ALB listener returns `403` for direct origin requests that do not present
16
+ the matching value.
17
+
18
+ ### Changed
19
+
20
+ - Updated AWS runbooks, deployment metadata, and cloud source-of-truth docs to
21
+ distinguish CloudFront prefix-list narrowing from distribution-bound origin
22
+ access.
23
+
9
24
  ## [0.1.18] - 2026-06-28
10
25
 
11
26
  ### Changed
package/dist/cli/index.js CHANGED
@@ -6943,7 +6943,7 @@ function buildAwsDeploymentPlan(options = {}) {
6943
6943
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
6944
6944
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
6945
6945
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
6946
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.18");
6946
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.19");
6947
6947
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
6948
6948
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
6949
6949
  const cluster = `${prefix}-${stage}`;
@@ -6986,7 +6986,7 @@ function buildAwsDeploymentPlan(options = {}) {
6986
6986
  ];
6987
6987
  return {
6988
6988
  kind: "open-uptime.aws-deployment-plan",
6989
- version: 3,
6989
+ version: 4,
6990
6990
  generatedAt: new Date().toISOString(),
6991
6991
  status: "blocked",
6992
6992
  canApply: false,
@@ -7011,6 +7011,17 @@ function buildAwsDeploymentPlan(options = {}) {
7011
7011
  protectedAccessMode,
7012
7012
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
7013
7013
  protectedAccessUrl,
7014
+ originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
7015
+ mode: "cloudfront_origin_header",
7016
+ requiredBeforeScaleUp: true,
7017
+ headerName: "X-Open-Uptime-Origin-Verify",
7018
+ valueStoredInTerraformState: true,
7019
+ stateAccessWarning: "The origin verification header value is sensitive but is stored in encrypted Terraform state and CloudFront/ALB configuration; restrict state, plan, CloudFront distribution-read, and ELB rule-read access."
7020
+ } : {
7021
+ mode: "alb_tls",
7022
+ requiredBeforeScaleUp: false,
7023
+ valueStoredInTerraformState: false
7024
+ },
7014
7025
  targetGroups: [`${prefix}-${stage}-web-tg`],
7015
7026
  securityGroups: [
7016
7027
  `${prefix}-${stage}-alb-sg`,
@@ -7058,7 +7069,7 @@ function buildAwsDeploymentPlan(options = {}) {
7058
7069
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
7059
7070
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
7060
7071
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
7061
- protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
7072
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
7062
7073
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
7063
7074
  ],
7064
7075
  deploy: [
@@ -7067,7 +7078,7 @@ function buildAwsDeploymentPlan(options = {}) {
7067
7078
  "For the EFS SQLite bridge, do not run migration, scheduler, public-probe, or reporter tasks; keep them at desired count 0 until Postgres and cloud leases exist.",
7068
7079
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
7069
7080
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
7070
- protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
7081
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
7071
7082
  ],
7072
7083
  rollback: [
7073
7084
  "Keep previous task definition ARNs before each service update.",
@@ -7083,6 +7094,7 @@ function buildAwsDeploymentPlan(options = {}) {
7083
7094
  },
7084
7095
  blockers: [
7085
7096
  "The infrastructure owner repository was not found in this workspace.",
7097
+ protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
7086
7098
  "The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
7087
7099
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
7088
7100
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -7092,7 +7104,7 @@ function buildAwsDeploymentPlan(options = {}) {
7092
7104
  "Infrastructure PR/synth/plan from the approved infra repository.",
7093
7105
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
7094
7106
  "ECS task definitions using secrets.valueFrom only.",
7095
- "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
7107
+ "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
7096
7108
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
7097
7109
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
7098
7110
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -7106,6 +7118,7 @@ function buildAwsDeploymentPlan(options = {}) {
7106
7118
  "This plan generator does not call AWS.",
7107
7119
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
7108
7120
  "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
7121
+ "CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
7109
7122
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
7110
7123
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
7111
7124
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
@@ -24,7 +24,7 @@ export interface AwsDeploymentPlanOptions {
24
24
  }
25
25
  export interface AwsDeploymentPlan {
26
26
  kind: "open-uptime.aws-deployment-plan";
27
- version: 3;
27
+ version: 4;
28
28
  generatedAt: string;
29
29
  status: "blocked";
30
30
  canApply: false;
@@ -49,6 +49,13 @@ export interface AwsDeploymentPlan {
49
49
  protectedAccessMode: "cloudfront_default_domain" | "alb_https_cert";
50
50
  edgeDistribution?: string;
51
51
  protectedAccessUrl: string;
52
+ originVerification: {
53
+ mode: "cloudfront_origin_header" | "alb_tls";
54
+ requiredBeforeScaleUp: boolean;
55
+ headerName?: string;
56
+ valueStoredInTerraformState: boolean;
57
+ stateAccessWarning?: string;
58
+ };
52
59
  targetGroups: string[];
53
60
  securityGroups: string[];
54
61
  secrets: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
@@ -21,7 +21,7 @@ function buildAwsDeploymentPlan(options = {}) {
21
21
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
22
22
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
23
23
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
24
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.18");
24
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.19");
25
25
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
26
26
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
27
27
  const cluster = `${prefix}-${stage}`;
@@ -64,7 +64,7 @@ function buildAwsDeploymentPlan(options = {}) {
64
64
  ];
65
65
  return {
66
66
  kind: "open-uptime.aws-deployment-plan",
67
- version: 3,
67
+ version: 4,
68
68
  generatedAt: new Date().toISOString(),
69
69
  status: "blocked",
70
70
  canApply: false,
@@ -89,6 +89,17 @@ function buildAwsDeploymentPlan(options = {}) {
89
89
  protectedAccessMode,
90
90
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
91
91
  protectedAccessUrl,
92
+ originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
93
+ mode: "cloudfront_origin_header",
94
+ requiredBeforeScaleUp: true,
95
+ headerName: "X-Open-Uptime-Origin-Verify",
96
+ valueStoredInTerraformState: true,
97
+ stateAccessWarning: "The origin verification header value is sensitive but is stored in encrypted Terraform state and CloudFront/ALB configuration; restrict state, plan, CloudFront distribution-read, and ELB rule-read access."
98
+ } : {
99
+ mode: "alb_tls",
100
+ requiredBeforeScaleUp: false,
101
+ valueStoredInTerraformState: false
102
+ },
92
103
  targetGroups: [`${prefix}-${stage}-web-tg`],
93
104
  securityGroups: [
94
105
  `${prefix}-${stage}-alb-sg`,
@@ -136,7 +147,7 @@ function buildAwsDeploymentPlan(options = {}) {
136
147
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
137
148
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
138
149
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
139
- protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
150
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
140
151
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
141
152
  ],
142
153
  deploy: [
@@ -145,7 +156,7 @@ function buildAwsDeploymentPlan(options = {}) {
145
156
  "For the EFS SQLite bridge, do not run migration, scheduler, public-probe, or reporter tasks; keep them at desired count 0 until Postgres and cloud leases exist.",
146
157
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
147
158
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
148
- protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
159
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
149
160
  ],
150
161
  rollback: [
151
162
  "Keep previous task definition ARNs before each service update.",
@@ -161,6 +172,7 @@ function buildAwsDeploymentPlan(options = {}) {
161
172
  },
162
173
  blockers: [
163
174
  "The infrastructure owner repository was not found in this workspace.",
175
+ protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
164
176
  "The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
165
177
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
166
178
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -170,7 +182,7 @@ function buildAwsDeploymentPlan(options = {}) {
170
182
  "Infrastructure PR/synth/plan from the approved infra repository.",
171
183
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
172
184
  "ECS task definitions using secrets.valueFrom only.",
173
- "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
185
+ "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
174
186
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
175
187
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
176
188
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -184,6 +196,7 @@ function buildAwsDeploymentPlan(options = {}) {
184
196
  "This plan generator does not call AWS.",
185
197
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
186
198
  "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
199
+ "CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
187
200
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
188
201
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
189
202
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
package/dist/index.js CHANGED
@@ -4349,7 +4349,7 @@ function buildAwsDeploymentPlan(options = {}) {
4349
4349
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
4350
4350
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
4351
4351
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
4352
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.18");
4352
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.19");
4353
4353
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
4354
4354
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
4355
4355
  const cluster = `${prefix}-${stage}`;
@@ -4392,7 +4392,7 @@ function buildAwsDeploymentPlan(options = {}) {
4392
4392
  ];
4393
4393
  return {
4394
4394
  kind: "open-uptime.aws-deployment-plan",
4395
- version: 3,
4395
+ version: 4,
4396
4396
  generatedAt: new Date().toISOString(),
4397
4397
  status: "blocked",
4398
4398
  canApply: false,
@@ -4417,6 +4417,17 @@ function buildAwsDeploymentPlan(options = {}) {
4417
4417
  protectedAccessMode,
4418
4418
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
4419
4419
  protectedAccessUrl,
4420
+ originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
4421
+ mode: "cloudfront_origin_header",
4422
+ requiredBeforeScaleUp: true,
4423
+ headerName: "X-Open-Uptime-Origin-Verify",
4424
+ valueStoredInTerraformState: true,
4425
+ stateAccessWarning: "The origin verification header value is sensitive but is stored in encrypted Terraform state and CloudFront/ALB configuration; restrict state, plan, CloudFront distribution-read, and ELB rule-read access."
4426
+ } : {
4427
+ mode: "alb_tls",
4428
+ requiredBeforeScaleUp: false,
4429
+ valueStoredInTerraformState: false
4430
+ },
4420
4431
  targetGroups: [`${prefix}-${stage}-web-tg`],
4421
4432
  securityGroups: [
4422
4433
  `${prefix}-${stage}-alb-sg`,
@@ -4464,7 +4475,7 @@ function buildAwsDeploymentPlan(options = {}) {
4464
4475
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
4465
4476
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
4466
4477
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
4467
- protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
4478
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
4468
4479
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
4469
4480
  ],
4470
4481
  deploy: [
@@ -4473,7 +4484,7 @@ function buildAwsDeploymentPlan(options = {}) {
4473
4484
  "For the EFS SQLite bridge, do not run migration, scheduler, public-probe, or reporter tasks; keep them at desired count 0 until Postgres and cloud leases exist.",
4474
4485
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
4475
4486
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
4476
- protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
4487
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
4477
4488
  ],
4478
4489
  rollback: [
4479
4490
  "Keep previous task definition ARNs before each service update.",
@@ -4489,6 +4500,7 @@ function buildAwsDeploymentPlan(options = {}) {
4489
4500
  },
4490
4501
  blockers: [
4491
4502
  "The infrastructure owner repository was not found in this workspace.",
4503
+ protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
4492
4504
  "The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
4493
4505
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
4494
4506
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -4498,7 +4510,7 @@ function buildAwsDeploymentPlan(options = {}) {
4498
4510
  "Infrastructure PR/synth/plan from the approved infra repository.",
4499
4511
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
4500
4512
  "ECS task definitions using secrets.valueFrom only.",
4501
- "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
4513
+ "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
4502
4514
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
4503
4515
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
4504
4516
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -4512,6 +4524,7 @@ function buildAwsDeploymentPlan(options = {}) {
4512
4524
  "This plan generator does not call AWS.",
4513
4525
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
4514
4526
  "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
4527
+ "CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
4515
4528
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
4516
4529
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
4517
4530
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
@@ -176,8 +176,8 @@ Before setting `desired_counts.web = 1`, verify:
176
176
  - the image is an immutable digest, not a mutable tag or placeholder;
177
177
  - required secrets have `AWSCURRENT` versions;
178
178
  - `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
179
- - CloudFront origin access is distribution-bound, not just narrowed to
180
- CloudFront origin-facing ranges;
179
+ - CloudFront origin access is distribution-bound with the CloudFront-only origin
180
+ verification header, not just narrowed to CloudFront origin-facing ranges;
181
181
  - web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
182
182
  required endpoints has been proven from a real ECS task. Terraform endpoint
183
183
  ids, route tables, and security-group rules are creation evidence only; the
@@ -386,9 +386,11 @@ routes are backed by cloud check jobs and cloud audit rows.
386
386
  - Do not expose the ALB directly in CloudFront mode; ALB ingress must be limited
387
387
  to CloudFront origin-facing ranges.
388
388
  - Do not treat CloudFront prefix-list ingress as distribution-bound origin
389
- protection. Before enabling the web task, add CloudFront VPC origin/private
390
- ALB routing or require a CloudFront-only origin header whose secret value is
391
- not stored in Terraform state.
389
+ protection. In `cloudfront_default_domain` mode, enable the module's
390
+ CloudFront-only origin verification header and keep its generated value out of
391
+ the public repo and shared logs. Terraform redacts the sensitive input in CLI
392
+ output, but the value is still stored in encrypted Terraform state, saved plan
393
+ files, and AWS CloudFront/ALB configuration; restrict access accordingly.
392
394
  - Do not treat local SQLite, local project DBs, or private-probe local state as cloud
393
395
  authority after cutover.
394
396
  - Do configure owner/project/environment/service/cost-center tags and AWS
@@ -42,9 +42,14 @@ HTTPS origin so hosted mutation CSRF checks still work through the private HTTP
42
42
  origin hop.
43
43
 
44
44
  CloudFront prefix-list ingress is only a network narrowing control; it is not
45
- bound to one distribution. Add CloudFront VPC origin/private ALB routing or an
46
- ALB origin-header rule with the secret value managed outside Terraform state
47
- before enabling the web task.
45
+ bound to one distribution. Before enabling the web task, set
46
+ `enable_cloudfront_origin_verify_header = true` and provide a high-entropy
47
+ `cloudfront_origin_verify_header_value` from a private operator workflow. The
48
+ module then configures CloudFront to send that header, makes the ALB default
49
+ action return `403`, and forwards only requests with the matching header.
50
+ Terraform marks the value sensitive, but it still lives in encrypted Terraform
51
+ state and in CloudFront/ALB configuration; restrict state, saved plan,
52
+ CloudFront distribution-read, and ELB listener-rule-read access accordingly.
48
53
 
49
54
  All module resources carry owner, project, environment, service, account, app
50
55
  type, and cost-center tags. Set `monthly_budget_limit_usd` plus
package/infra/aws/main.tf CHANGED
@@ -26,6 +26,7 @@ locals {
26
26
  efs_enabled_services = toset(["web"])
27
27
  use_alb_https = var.protected_access_mode == "alb_https_cert"
28
28
  use_cloudfront = var.protected_access_mode == "cloudfront_default_domain"
29
+ use_origin_verify = local.use_cloudfront && var.enable_cloudfront_origin_verify_header
29
30
  services = {
30
31
  web = {
31
32
  desired_count = lookup(var.desired_counts, "web", 0)
@@ -900,10 +901,45 @@ resource "aws_lb_listener" "http_cloudfront" {
900
901
  protocol = "HTTP"
901
902
  tags = local.tags
902
903
 
903
- default_action {
904
+ dynamic "default_action" {
905
+ for_each = local.use_origin_verify ? [] : [1]
906
+ content {
907
+ type = "forward"
908
+ target_group_arn = aws_lb_target_group.web.arn
909
+ }
910
+ }
911
+
912
+ dynamic "default_action" {
913
+ for_each = local.use_origin_verify ? [1] : []
914
+ content {
915
+ type = "fixed-response"
916
+
917
+ fixed_response {
918
+ content_type = "text/plain"
919
+ message_body = "forbidden"
920
+ status_code = "403"
921
+ }
922
+ }
923
+ }
924
+ }
925
+
926
+ resource "aws_lb_listener_rule" "http_cloudfront_origin_verify" {
927
+ count = local.use_origin_verify ? 1 : 0
928
+ listener_arn = aws_lb_listener.http_cloudfront[0].arn
929
+ priority = var.cloudfront_origin_verify_listener_rule_priority
930
+ tags = local.tags
931
+
932
+ action {
904
933
  type = "forward"
905
934
  target_group_arn = aws_lb_target_group.web.arn
906
935
  }
936
+
937
+ condition {
938
+ http_header {
939
+ http_header_name = var.cloudfront_origin_verify_header_name
940
+ values = [var.cloudfront_origin_verify_header_value]
941
+ }
942
+ }
907
943
  }
908
944
 
909
945
  resource "aws_cloudfront_distribution" "open_uptime" {
@@ -918,6 +954,14 @@ resource "aws_cloudfront_distribution" "open_uptime" {
918
954
  domain_name = aws_lb.open_uptime.dns_name
919
955
  origin_id = "${local.prefix}-alb"
920
956
 
957
+ dynamic "custom_header" {
958
+ for_each = local.use_origin_verify ? [1] : []
959
+ content {
960
+ name = var.cloudfront_origin_verify_header_name
961
+ value = var.cloudfront_origin_verify_header_value
962
+ }
963
+ }
964
+
921
965
  custom_origin_config {
922
966
  http_port = 80
923
967
  https_port = 443
@@ -956,7 +1000,7 @@ resource "aws_cloudfront_distribution" "open_uptime" {
956
1000
  cloudfront_default_certificate = true
957
1001
  }
958
1002
 
959
- depends_on = [aws_lb_listener.http_cloudfront]
1003
+ depends_on = [aws_lb_listener.http_cloudfront, aws_lb_listener_rule.http_cloudfront_origin_verify]
960
1004
  }
961
1005
 
962
1006
  resource "aws_route53_record" "open_uptime" {
@@ -1173,7 +1217,7 @@ resource "aws_ecs_service" "web" {
1173
1217
  container_port = local.container_port
1174
1218
  }
1175
1219
 
1176
- depends_on = [aws_lb_listener.https, aws_lb_listener.http_cloudfront, aws_efs_mount_target.data]
1220
+ depends_on = [aws_lb_listener.https, aws_lb_listener.http_cloudfront, aws_lb_listener_rule.http_cloudfront_origin_verify, aws_efs_mount_target.data]
1177
1221
  }
1178
1222
 
1179
1223
  resource "aws_ecs_service" "worker" {
@@ -22,6 +22,14 @@ output "protected_access_url" {
22
22
  value = var.protected_access_mode == "cloudfront_default_domain" ? "https://${aws_cloudfront_distribution.open_uptime[0].domain_name}" : "https://${var.hostname}"
23
23
  }
24
24
 
25
+ output "cloudfront_origin_verify_header_enabled" {
26
+ value = local.use_origin_verify
27
+ }
28
+
29
+ output "cloudfront_origin_verify_header_name" {
30
+ value = local.use_origin_verify ? var.cloudfront_origin_verify_header_name : null
31
+ }
32
+
25
33
  output "evidence_bucket" {
26
34
  value = aws_s3_bucket.evidence.bucket
27
35
  }
@@ -11,12 +11,15 @@ workspace_id = "workspace-id"
11
11
  vpc_id = "vpc-xxxxxxxx"
12
12
  ecr_repository_name = "open-uptime"
13
13
  protected_access_mode = "cloudfront_default_domain"
14
+ enable_cloudfront_origin_verify_header = false
15
+ cloudfront_origin_verify_header_name = "X-Open-Uptime-Origin-Verify"
16
+ cloudfront_origin_verify_header_value = null
14
17
  public_subnet_ids = ["subnet-replace-public-a", "subnet-replace-public-b"]
15
18
  alb_ingress_cidr_blocks = []
16
19
  private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
17
20
  private_route_table_ids = ["rtb-replace-private"]
18
21
  container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
19
- runtime_package_version = "0.1.18"
22
+ runtime_package_version = "0.1.19"
20
23
  certificate_arn = null
21
24
  hosted_zone_id = null
22
25
  app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/app/env"
@@ -87,6 +87,91 @@ variable "protected_access_mode" {
87
87
  }
88
88
  }
89
89
 
90
+ variable "enable_cloudfront_origin_verify_header" {
91
+ description = "When true in cloudfront_default_domain mode, CloudFront sends a private origin header and the ALB listener rejects requests missing the matching value."
92
+ type = bool
93
+ default = false
94
+
95
+ validation {
96
+ condition = !var.enable_cloudfront_origin_verify_header || var.protected_access_mode == "cloudfront_default_domain"
97
+ error_message = "enable_cloudfront_origin_verify_header can only be true when protected_access_mode is cloudfront_default_domain."
98
+ }
99
+ }
100
+
101
+ variable "cloudfront_origin_verify_header_name" {
102
+ description = "CloudFront-only origin verification header name used when enable_cloudfront_origin_verify_header is true."
103
+ type = string
104
+ default = "X-Open-Uptime-Origin-Verify"
105
+
106
+ validation {
107
+ condition = (
108
+ can(regex("^[A-Za-z0-9-]+$", var.cloudfront_origin_verify_header_name))
109
+ && !startswith(lower(var.cloudfront_origin_verify_header_name), "x-amz-")
110
+ && !startswith(lower(var.cloudfront_origin_verify_header_name), "x-edge-")
111
+ && !contains([
112
+ "authorization",
113
+ "cache-control",
114
+ "connection",
115
+ "content-length",
116
+ "content-type",
117
+ "cookie",
118
+ "host",
119
+ "if-match",
120
+ "if-modified-since",
121
+ "if-none-match",
122
+ "if-range",
123
+ "if-unmodified-since",
124
+ "max-forwards",
125
+ "origin",
126
+ "pragma",
127
+ "proxy-authenticate",
128
+ "proxy-authorization",
129
+ "proxy-connection",
130
+ "range",
131
+ "request-range",
132
+ "te",
133
+ "trailer",
134
+ "transfer-encoding",
135
+ "upgrade",
136
+ "via",
137
+ "x-real-ip",
138
+ "x-uptime-hosted-token",
139
+ ], lower(var.cloudfront_origin_verify_header_name))
140
+ )
141
+ error_message = "cloudfront_origin_verify_header_name must be a safe CloudFront custom origin header name and must not use reserved, app-forwarded, or viewer-controlled header names."
142
+ }
143
+ }
144
+
145
+ variable "cloudfront_origin_verify_header_value" {
146
+ description = "Sensitive CloudFront-only origin verification header value. Required when enable_cloudfront_origin_verify_header is true."
147
+ type = string
148
+ default = null
149
+ nullable = true
150
+ sensitive = true
151
+
152
+ validation {
153
+ condition = (
154
+ !(var.enable_cloudfront_origin_verify_header && var.protected_access_mode == "cloudfront_default_domain")
155
+ || (
156
+ var.cloudfront_origin_verify_header_value != null
157
+ && can(regex("^[A-Za-z0-9_-]{32,256}$", var.cloudfront_origin_verify_header_value))
158
+ )
159
+ )
160
+ error_message = "cloudfront_origin_verify_header_value is required when origin verification is enabled and must be 32-256 URL-safe characters."
161
+ }
162
+ }
163
+
164
+ variable "cloudfront_origin_verify_listener_rule_priority" {
165
+ description = "ALB listener rule priority for the CloudFront origin verification header rule."
166
+ type = number
167
+ default = 100
168
+
169
+ validation {
170
+ condition = var.cloudfront_origin_verify_listener_rule_priority >= 1 && var.cloudfront_origin_verify_listener_rule_priority <= 50000
171
+ error_message = "cloudfront_origin_verify_listener_rule_priority must be between 1 and 50000."
172
+ }
173
+ }
174
+
90
175
  variable "public_subnet_ids" {
91
176
  description = "Public subnets for the ALB."
92
177
  type = list(string)
@@ -116,7 +201,7 @@ variable "container_image" {
116
201
  variable "runtime_package_version" {
117
202
  description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
118
203
  type = string
119
- default = "0.1.18"
204
+ default = "0.1.19"
120
205
 
121
206
  validation {
122
207
  condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/uptime",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Local-first uptime and downtime monitoring service with CLI, MCP, SDK, SQLite persistence, and a dashboard.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",