@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 +15 -0
- package/dist/cli/index.js +18 -5
- package/dist/cloud-plan.d.ts +8 -1
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +18 -5
- package/dist/index.js +18 -5
- package/docs/aws-deployment-runbook.md +7 -5
- package/infra/aws/README.md +8 -3
- package/infra/aws/main.tf +47 -3
- package/infra/aws/outputs.tf +8 -0
- package/infra/aws/terraform.tfvars.example +4 -1
- package/infra/aws/variables.tf +86 -1
- package/package.json +1 -1
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.
|
|
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:
|
|
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.",
|
package/dist/cloud-plan.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface AwsDeploymentPlanOptions {
|
|
|
24
24
|
}
|
|
25
25
|
export interface AwsDeploymentPlan {
|
|
26
26
|
kind: "open-uptime.aws-deployment-plan";
|
|
27
|
-
version:
|
|
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>;
|
package/dist/cloud-plan.d.ts.map
CHANGED
|
@@ -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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"}
|
package/dist/cloud-plan.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
|
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.
|
|
390
|
-
|
|
391
|
-
|
|
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
|
package/infra/aws/README.md
CHANGED
|
@@ -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.
|
|
46
|
-
|
|
47
|
-
|
|
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" {
|
package/infra/aws/outputs.tf
CHANGED
|
@@ -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.
|
|
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"
|
package/infra/aws/variables.tf
CHANGED
|
@@ -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.
|
|
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