@hasna/uptime 0.1.24 → 0.1.25

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/dist/index.js CHANGED
@@ -4623,6 +4623,7 @@ var DEFAULT_WORKSPACE_ID = "workspace-id";
4623
4623
  var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
4624
4624
  var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
4625
4625
  var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
4626
+ var DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY = "http-only";
4626
4627
  function buildAwsDeploymentPlan(options = {}) {
4627
4628
  const region = clean(options.region, DEFAULT_REGION);
4628
4629
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -4635,8 +4636,11 @@ function buildAwsDeploymentPlan(options = {}) {
4635
4636
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
4636
4637
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
4637
4638
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
4638
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.24");
4639
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
4640
+ const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
4639
4641
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
4642
+ const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
4643
+ const cloudfrontOriginDomainName = clean(options.cloudfrontOriginDomainName, "<alb-dns-name>");
4640
4644
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
4641
4645
  const cluster = `${prefix}-${stage}`;
4642
4646
  const secrets = {
@@ -4703,6 +4707,13 @@ function buildAwsDeploymentPlan(options = {}) {
4703
4707
  protectedAccessMode,
4704
4708
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
4705
4709
  protectedAccessUrl,
4710
+ cloudfrontOrigin: protectedAccessMode === "cloudfront_default_domain" ? {
4711
+ protocolPolicy: cloudfrontOriginProtocolPolicy,
4712
+ domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
4713
+ requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
4714
+ liveTrafficApproved: false,
4715
+ risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: do not use for token-bearing live traffic without explicit risk acceptance, or switch to https-only with cloudfront_origin_domain_name plus certificate_arn." : "CloudFront HTTPS-origin mode requires the origin hostname to resolve to the ALB and match certificate_arn."
4716
+ } : undefined,
4706
4717
  originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
4707
4718
  mode: "cloudfront_origin_header",
4708
4719
  requiredBeforeScaleUp: true,
@@ -4735,6 +4746,7 @@ function buildAwsDeploymentPlan(options = {}) {
4735
4746
  repository: ecrRepository,
4736
4747
  uri: image,
4737
4748
  dockerfile: "Dockerfile.package",
4749
+ expectedIntegrity: runtimePackageIntegrity,
4738
4750
  buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
4739
4751
  pushCommands: [
4740
4752
  `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
@@ -4761,16 +4773,16 @@ function buildAwsDeploymentPlan(options = {}) {
4761
4773
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
4762
4774
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
4763
4775
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
4764
- 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.`,
4776
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin 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. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit HTTP-origin risk acceptance." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
4765
4777
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
4766
4778
  ],
4767
4779
  deploy: [
4768
4780
  "Build and publish the image only after the Dockerfile/container target is reviewed.",
4769
- `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
4781
+ runtimePackageIntegrity ? `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; it must verify npm dist.integrity ${runtimePackageIntegrity} before extracting the package, then record the pushed image digest.` : `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; set runtime_package_integrity from npm dist.integrity before live use, then record the pushed image digest.`,
4770
4782
  "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.",
4771
4783
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
4772
4784
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
4773
- 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.`
4785
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or record explicit HTTP-origin risk acceptance." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
4774
4786
  ],
4775
4787
  rollback: [
4776
4788
  "Keep previous task definition ARNs before each service update.",
@@ -4787,6 +4799,8 @@ function buildAwsDeploymentPlan(options = {}) {
4787
4799
  blockers: [
4788
4800
  "The infrastructure owner repository was not found in this workspace.",
4789
4801
  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.",
4802
+ ...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; token-bearing live traffic needs https-only origin mode or explicit risk acceptance."] : [],
4803
+ ...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "https-only" && cloudfrontOriginDomainName === "<alb-dns-name>" ? ["CloudFront https-only origin mode needs cloudfront_origin_domain_name that resolves to the ALB and matches certificate_arn."] : [],
4790
4804
  "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.",
4791
4805
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
4792
4806
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -4795,8 +4809,9 @@ function buildAwsDeploymentPlan(options = {}) {
4795
4809
  requiredEvidence: [
4796
4810
  "Infrastructure PR/synth/plan from the approved infra repository.",
4797
4811
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
4812
+ "Published package dist.integrity pinned in the private infra root or an explicit not-live exception.",
4798
4813
  "ECS task definitions using secrets.valueFrom only.",
4799
- "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
4814
+ "CloudFront-default-domain origin-header config, origin transport decision, direct-origin denial evidence, auth-denial smokes, and web alarm checks.",
4800
4815
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
4801
4816
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
4802
4817
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -4809,11 +4824,13 @@ function buildAwsDeploymentPlan(options = {}) {
4809
4824
  notes: [
4810
4825
  "This plan generator does not call AWS.",
4811
4826
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
4812
- "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
4827
+ "Default protected access uses CloudFront's HTTPS default domain so first zero-count deploy is not blocked on custom DNS or ACM.",
4813
4828
  "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.",
4829
+ "CloudFront HTTPS-origin mode requires a dedicated origin DNS hostname and matching ACM certificate; do not assume the ALB DNS name can satisfy TLS verification.",
4814
4830
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
4815
4831
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
4816
4832
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
4833
+ "Set runtime_package_integrity in the approved infra root after publish so the AWS image builder verifies the npm tarball before ECR build.",
4817
4834
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
4818
4835
  ]
4819
4836
  }
@@ -60,9 +60,14 @@ start a private probe until the JSON output says `canStart: true`.
60
60
 
61
61
  4. Confirm the target VPC, private subnets, KMS key, and EFS/Backup plan inputs
62
62
  still match the plan.
63
- 5. Confirm the protected access mode. The first deploy can use the CloudFront
64
- default HTTPS domain without custom DNS or ACM. Custom hostname deploys still
65
- require Route53/edge ownership and an ACM certificate.
63
+ 5. Confirm the protected access mode. The first zero-count deploy can use the
64
+ CloudFront default HTTPS domain without custom DNS or ACM. Before
65
+ token-bearing live traffic, either set
66
+ `cloudfront_origin_protocol_policy = "https-only"` with a dedicated
67
+ `cloudfront_origin_domain_name` that resolves to the ALB and a matching ACM
68
+ `certificate_arn`, or record an explicit risk acceptance for the temporary
69
+ HTTP-origin bridge. Custom hostname deploys still require Route53/edge
70
+ ownership and an ACM certificate.
66
71
  6. Confirm the deployment role uses short-lived credentials or OIDC, not copied
67
72
  access keys.
68
73
  7. Create a private evidence directory outside the public repository. Store
@@ -77,9 +82,12 @@ The plan expects:
77
82
  - ECS/Fargate cluster with separate services for web, scheduler, public probe,
78
83
  reporter, and one-off migrations. In the current EFS SQLite bridge, only web
79
84
  may be enabled and it must run at desired count `0` or `1`.
80
- - CloudFront default-domain HTTPS edge plus ALB HTTP origin restricted to
81
- CloudFront origin-facing ranges, or an ALB HTTPS listener with ACM certificate
82
- when custom DNS is approved.
85
+ - CloudFront default-domain HTTPS edge plus an ALB origin restricted to
86
+ CloudFront origin-facing ranges. The default zero-count bridge uses HTTP to
87
+ the origin; token-bearing live traffic should use the module's HTTPS-origin
88
+ mode with `cloudfront_origin_domain_name` plus `certificate_arn`, or a
89
+ documented risk acceptance. Direct ALB HTTPS mode also requires custom DNS and
90
+ an ACM certificate.
83
91
  - Encrypted EFS file system, access point, mount targets, and AWS Backup plan
84
92
  for `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db`.
85
93
  - S3 bucket for redacted browser evidence and generated report artifacts.
@@ -151,6 +159,12 @@ aws codebuild start-build \
151
159
  --project-name "$IMAGE_BUILDER_PROJECT"
152
160
  ```
153
161
 
162
+ The private infra root should set `runtime_package_integrity` to the published
163
+ npm `dist.integrity` value for the exact `runtime_package_version`. The image
164
+ builder verifies that value before extracting the package. If the value is not
165
+ set, record why the tarball is not integrity-pinned and keep the service
166
+ not-live.
167
+
154
168
  Update the approved infra root so `container_image` is the immutable ECR digest,
155
169
  then re-plan with all services still at `0`.
156
170
 
@@ -179,6 +193,9 @@ Before setting `desired_counts.web = 1`, verify:
179
193
  - the image is an immutable digest, not a mutable tag or placeholder;
180
194
  - required secrets have `AWSCURRENT` versions;
181
195
  - `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
196
+ - CloudFront-to-origin transport is either `https-only` with an origin hostname
197
+ whose certificate matches that hostname, or the HTTP-origin bridge has a
198
+ named risk owner and approval recorded in private evidence;
182
199
  - CloudFront origin access is distribution-bound with the CloudFront-only origin
183
200
  verification header, not just narrowed to CloudFront origin-facing ranges;
184
201
  - web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
@@ -419,6 +436,10 @@ routes are backed by cloud check jobs and cloud audit rows.
419
436
  the public repo and shared logs. Terraform redacts the sensitive input in CLI
420
437
  output, but the value is still stored in encrypted Terraform state, saved plan
421
438
  files, and AWS CloudFront/ALB configuration; restrict access accordingly.
439
+ - Do not treat `cloudfront_origin_protocol_policy = "http-only"` as final for
440
+ token-bearing traffic. The module supports `https-only`, but that mode needs a
441
+ real origin DNS name and matching ACM certificate because CloudFront verifies
442
+ the custom-origin TLS certificate against the origin host.
422
443
  - Do not treat local SQLite, local project DBs, or private-probe local state as cloud
423
444
  authority after cutover.
424
445
  - Do configure owner/project/environment/service/cost-center tags, AWS Budgets
@@ -110,10 +110,12 @@ Target shape inside the approved VPC:
110
110
 
111
111
  Security groups:
112
112
 
113
- - `open-uptime-alb-sg`: in `cloudfront_default_domain` mode, inbound `80` only
114
- from AWS's CloudFront origin-facing managed prefix list; in `alb_https_cert`
115
- mode, inbound `443` only from the approved edge/source CIDR policy. Outbound
116
- is only to the web target group.
113
+ - `open-uptime-alb-sg`: in `cloudfront_default_domain` mode, inbound origin
114
+ traffic is limited to AWS's CloudFront origin-facing managed prefix list on
115
+ `80` for the temporary HTTP bridge or `443` when
116
+ `cloudfront_origin_protocol_policy = "https-only"`; in `alb_https_cert` mode,
117
+ inbound `443` is limited to the approved edge/source CIDR policy. Outbound is
118
+ only to the web target group.
117
119
  - `open-uptime-web-sg`: inbound only from ALB, outbound to RDS, S3 endpoint,
118
120
  Secrets Manager, Logs, and internal service endpoints.
119
121
  - `open-uptime-scheduler-sg`: no inbound, outbound to RDS, Logs, Secrets
@@ -136,10 +138,15 @@ infra PR.
136
138
 
137
139
  Public web exposure requires defense in depth:
138
140
 
139
- - first deployment may terminate viewer TLS at CloudFront's default HTTPS
140
- domain, restrict ALB HTTP origin ingress to CloudFront origin-facing ranges,
141
+ - first zero-count deployment may terminate viewer TLS at CloudFront's default
142
+ HTTPS domain, restrict ALB origin ingress to CloudFront origin-facing ranges,
141
143
  and require the module's CloudFront-only origin verification header at the ALB
142
144
  listener;
145
+ - token-bearing live traffic should use CloudFront HTTPS-origin mode by setting
146
+ `cloudfront_origin_protocol_policy = "https-only"`, a dedicated
147
+ `cloudfront_origin_domain_name` that resolves to the ALB, and a matching ACM
148
+ `certificate_arn`. CloudFront validates the custom-origin certificate against
149
+ the origin hostname, so the ALB DNS name is not enough for this mode;
143
150
  - CloudFront prefix-list ingress is not distribution-bound by itself. In
144
151
  `cloudfront_default_domain` mode, set
145
152
  `enable_cloudfront_origin_verify_header = true` and provide a high-entropy
@@ -154,7 +161,7 @@ Public web exposure requires defense in depth:
154
161
  identity layer;
155
162
  - hosted web tasks must set `HASNA_UPTIME_ALLOWED_ORIGINS` to the public HTTPS
156
163
  edge origin so browser mutation checks do not compare CloudFront HTTPS origins
157
- against the private HTTP ALB origin hop;
164
+ against the ALB origin hostname;
158
165
  - Open Uptime still enforces app-level auth and workspace RBAC on every route
159
166
  except `/health`;
160
167
  - `/health` returns only service liveness/readiness and no monitor data;
@@ -379,7 +386,9 @@ Minimum implementation path:
379
386
 
380
387
  1. review the repo-owned `Dockerfile` and package-image `Dockerfile.package`;
381
388
  2. add the ECR repository and CodeBuild package image builder;
382
- 3. build the published npm package into ECR and record the immutable digest;
389
+ 3. build the published npm package into ECR, verify the expected npm
390
+ `dist.integrity` when `runtime_package_integrity` is set, install production
391
+ dependencies with the published `bun.lock`, and record the immutable digest;
383
392
  4. run typecheck, tests, package checks, and container smoke locally/CI;
384
393
  5. for the EFS bridge, keep the desired count at one web task maximum and zero
385
394
  scheduler/public-probe/reporter/migration tasks;
@@ -425,6 +434,11 @@ PR must include a rough monthly estimate for:
425
434
  Evidence retention and browser trace capture are the primary variable costs.
426
435
  Default retention must be short until usage is measured.
427
436
 
437
+ ECS services must enable AWS-managed tags and `propagate_tags = "SERVICE"` so
438
+ service-launched tasks retain cost allocation tags. One-off smoke tasks run
439
+ outside ECS service propagation and must pass equivalent tags explicitly in the
440
+ operator command/evidence.
441
+
428
442
  The AWS Terraform starter exposes optional AWS Budgets alerts through
429
443
  `monthly_budget_limit_usd` and `budget_alert_email_addresses`; the approved
430
444
  infra root must set real human/on-call notification targets and prove
@@ -438,8 +452,9 @@ required before browser evidence or public probe scale-out.
438
452
  evidence bucket, encrypted logs, Backup, EFS, and service secret containers.
439
453
  It is not live: services remain at desired count `0`, secrets have
440
454
  `AWSCURRENT` values, scoped hosted-token descriptors can be used for operator
441
- smokes, and no ACM cert or Route53 record exists for a later custom-hostname
442
- path. Full production identity/RBAC is still not implemented.
455
+ smokes, and the HTTPS-origin/custom-hostname path still needs an approved ACM
456
+ cert, DNS record, plan/apply, and edge smoke. Full production identity/RBAC is
457
+ still not implemented.
443
458
  - Open Uptime is still SQLite-only for this bridge; only one protected web task
444
459
  may write EFS until Postgres and cloud leases exist.
445
460
  - Hosted API/dashboard auth, workspace RBAC, target policy, and Postgres leases
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "service": "open-uptime",
3
3
  "package": "@hasna/uptime",
4
- "intendedVersion": "0.1.24",
4
+ "intendedVersion": "0.1.25",
5
5
  "accountProfile": "<aws-profile>",
6
6
  "accountId": "<aws-account-id>",
7
7
  "region": "us-east-1",
@@ -18,7 +18,9 @@
18
18
  "mode": "cloudfront_default_domain",
19
19
  "url": "https://<cloudfront-domain>",
20
20
  "allowedOriginsEnv": "HASNA_UPTIME_ALLOWED_ORIGINS=https://<cloudfront-domain>",
21
- "originPolicy": "ALB HTTP ingress restricted to CloudFront origin-facing ranges plus CloudFront-only origin verification header before scale-up",
21
+ "originPolicy": "ALB origin ingress restricted to CloudFront origin-facing ranges plus CloudFront-only origin verification header before scale-up; HTTPS-origin mode requires cloudfront_origin_domain_name plus matching certificate_arn",
22
+ "originProtocolPolicy": "http-only-or-https-only",
23
+ "originDomainName": "<alb-dns-name-or-approved-origin-hostname>",
22
24
  "originVerifyHeaderRequiredBeforeScaleUp": true,
23
25
  "originVerifyHeaderEnabled": "<true-after-private-root-apply>",
24
26
  "originVerifyHeaderName": "<private-header-name>",
@@ -31,31 +31,43 @@ adapter and cloud leases are implemented. Do not set
31
31
 
32
32
  The included CodeBuild project builds `@hasna/uptime` from npm with
33
33
  `Dockerfile.package` and pushes the resulting image to ECR. This avoids
34
- depending on a local Docker daemon for image publication.
34
+ depending on a local Docker daemon for image publication. Set
35
+ `runtime_package_integrity` in the private root after publish to make CodeBuild
36
+ verify the npm tarball `dist.integrity` before extracting it. The package image
37
+ also installs production dependencies from the published `bun.lock` with
38
+ `--frozen-lockfile`.
35
39
 
36
40
  The default protected access mode is `cloudfront_default_domain`: CloudFront
37
- serves HTTPS on its default domain while the ALB origin accepts HTTP only from
38
- AWS's CloudFront origin-facing managed prefix list. Use `alb_https_cert` only
39
- after custom DNS and an ACM certificate are approved.
41
+ serves HTTPS on its default domain while the ALB origin is limited to AWS's
42
+ CloudFront origin-facing managed prefix list. The default origin protocol is the
43
+ temporary `http-only` bridge. Before token-bearing live traffic, prefer setting
44
+ `cloudfront_origin_protocol_policy = "https-only"` with a dedicated
45
+ `cloudfront_origin_domain_name` that resolves to the ALB and a matching ACM
46
+ `certificate_arn`. Use `alb_https_cert` only when bypassing CloudFront after
47
+ custom DNS and an ACM certificate are approved.
40
48
  The web task receives `HASNA_UPTIME_ALLOWED_ORIGINS` for the selected public
41
- HTTPS origin so hosted mutation CSRF checks still work through the private HTTP
42
- origin hop.
49
+ HTTPS origin so hosted mutation CSRF checks still work through the selected
50
+ edge/origin path.
43
51
 
44
52
  CloudFront prefix-list ingress is only a network narrowing control; it is not
45
53
  bound to one distribution. Before enabling the web task, set
46
54
  `enable_cloudfront_origin_verify_header = true` and provide a high-entropy
47
55
  `cloudfront_origin_verify_header_value` from a private operator workflow. The
48
56
  module then configures CloudFront to send that header, makes the ALB default
49
- action return `403`, and forwards only requests with the matching header.
57
+ action return `403`, and forwards only requests with the matching header on the
58
+ selected HTTP or HTTPS origin listener.
50
59
  Terraform marks the value sensitive, but it still lives in encrypted Terraform
51
60
  state and in CloudFront/ALB configuration; restrict state, saved plan,
52
61
  CloudFront distribution-read, and ELB listener-rule-read access accordingly.
53
62
 
54
63
  All module resources carry owner, project, environment, service, account, app
55
- type, and cost-center tags. Set `monthly_budget_limit_usd` plus
56
- `budget_alert_email_addresses` in the approved infra root to create AWS Budgets
57
- forecasted and actual spend alerts. Leaving the email list empty skips budget
58
- creation and is not sufficient for live scale-out approval.
64
+ type, and cost-center tags. ECS services enable AWS-managed tags and propagate
65
+ service tags to launched tasks. Any one-off `run-task` smoke must pass the same
66
+ tag set explicitly because it is outside service propagation. Set
67
+ `monthly_budget_limit_usd` plus `budget_alert_email_addresses` in the approved
68
+ infra root to create AWS Budgets forecasted and actual spend alerts. Leaving the
69
+ email list empty skips budget creation and is not sufficient for live scale-out
70
+ approval.
59
71
 
60
72
  Private AWS API egress can be pinned through opt-in VPC endpoints by setting
61
73
  `enable_private_vpc_endpoints = true` and passing `private_route_table_ids`.
@@ -92,6 +104,9 @@ Store instead of Secrets Manager, add `ssm` to
92
104
  ## Current Blockers
93
105
 
94
106
  - Hosted production auth/RBAC still needs scoped, revocable credentials.
107
+ - The default `http-only` CloudFront origin bridge must be replaced with the
108
+ explicit HTTPS-origin mode or consciously accepted with documented risk before
109
+ token-bearing live traffic.
95
110
  - Public probe runtime has SDK-level hosted HTTP target-policy enforcement, but
96
111
  the public-probe worker and cloud check-job lease path are still disabled until
97
112
  they are wired to that runner and validated in AWS.
package/infra/aws/main.tf CHANGED
@@ -17,16 +17,21 @@ data "aws_caller_identity" "current" {}
17
17
  data "aws_partition" "current" {}
18
18
 
19
19
  locals {
20
- prefix = "${var.service_name}-${var.stage}"
21
- container_port = 3899
22
- evidence_bucket = "hasna-${var.stage}-${var.service_name}-evidence"
23
- efs_uid = 10001
24
- efs_gid = 10001
25
- hosted_sqlite_db_path = "/data/uptime/uptime.db"
26
- efs_enabled_services = toset(["web"])
27
- use_alb_https = var.protected_access_mode == "alb_https_cert"
28
- use_cloudfront = var.protected_access_mode == "cloudfront_default_domain"
29
- use_origin_verify = local.use_cloudfront && var.enable_cloudfront_origin_verify_header
20
+ prefix = "${var.service_name}-${var.stage}"
21
+ container_port = 3899
22
+ evidence_bucket = "hasna-${var.stage}-${var.service_name}-evidence"
23
+ efs_uid = 10001
24
+ efs_gid = 10001
25
+ hosted_sqlite_db_path = "/data/uptime/uptime.db"
26
+ efs_enabled_services = toset(["web"])
27
+ expected_runtime_package_integrity = coalesce(var.runtime_package_integrity, "")
28
+ use_alb_https = var.protected_access_mode == "alb_https_cert"
29
+ use_cloudfront = var.protected_access_mode == "cloudfront_default_domain"
30
+ cloudfront_https_origin = (
31
+ local.use_cloudfront && var.cloudfront_origin_protocol_policy == "https-only"
32
+ )
33
+ alb_https_listener_enabled = local.use_alb_https || local.cloudfront_https_origin
34
+ use_origin_verify = local.use_cloudfront && var.enable_cloudfront_origin_verify_header
30
35
  services = {
31
36
  web = {
32
37
  desired_count = lookup(var.desired_counts, "web", 0)
@@ -236,9 +241,18 @@ resource "aws_codebuild_project" "image_builder" {
236
241
  - aws ecr get-login-password --region ${var.region} | docker login --username AWS --password-stdin ${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.region}.amazonaws.com
237
242
  build:
238
243
  commands:
239
- - npm pack @hasna/uptime@${var.runtime_package_version}
244
+ - EXPECTED_RUNTIME_PACKAGE_INTEGRITY='${local.expected_runtime_package_integrity}'
245
+ - PACKAGE_TARBALL=$(npm pack @hasna/uptime@${var.runtime_package_version} --silent)
246
+ - PACKAGE_INTEGRITY=$(npm view @hasna/uptime@${var.runtime_package_version} dist.integrity --json | tr -d '"')
247
+ - test -n "$PACKAGE_INTEGRITY"
248
+ - |
249
+ if [ -n "$EXPECTED_RUNTIME_PACKAGE_INTEGRITY" ] && [ "$PACKAGE_INTEGRITY" != "$EXPECTED_RUNTIME_PACKAGE_INTEGRITY" ]; then
250
+ echo "runtime package integrity mismatch" >&2
251
+ exit 1
252
+ fi
253
+ - printf 'runtime package integrity %s\n' "$PACKAGE_INTEGRITY"
240
254
  - mkdir package
241
- - tar -xzf hasna-uptime-*.tgz -C package --strip-components=1
255
+ - tar -xzf "$PACKAGE_TARBALL" -C package --strip-components=1
242
256
  - cd package
243
257
  - docker build -f Dockerfile.package -t ${aws_ecr_repository.open_uptime.repository_url}:${var.runtime_package_version} .
244
258
  - docker push ${aws_ecr_repository.open_uptime.repository_url}:${var.runtime_package_version}
@@ -366,8 +380,19 @@ resource "aws_security_group_rule" "alb_https_ingress" {
366
380
  cidr_blocks = var.alb_ingress_cidr_blocks
367
381
  }
368
382
 
383
+ resource "aws_security_group_rule" "alb_https_from_cloudfront" {
384
+ count = local.cloudfront_https_origin ? 1 : 0
385
+ type = "ingress"
386
+ description = "HTTPS from CloudFront origin-facing ranges"
387
+ security_group_id = aws_security_group.alb.id
388
+ from_port = 443
389
+ to_port = 443
390
+ protocol = "tcp"
391
+ prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront_origin_facing[0].id]
392
+ }
393
+
369
394
  resource "aws_security_group_rule" "alb_http_from_cloudfront" {
370
- count = local.use_cloudfront ? 1 : 0
395
+ count = local.use_cloudfront && !local.cloudfront_https_origin ? 1 : 0
371
396
  type = "ingress"
372
397
  description = "HTTP from CloudFront origin-facing ranges"
373
398
  security_group_id = aws_security_group.alb.id
@@ -881,21 +906,37 @@ resource "aws_lb_target_group" "web" {
881
906
  }
882
907
 
883
908
  resource "aws_lb_listener" "https" {
884
- count = local.use_alb_https ? 1 : 0
909
+ count = local.alb_https_listener_enabled ? 1 : 0
885
910
  load_balancer_arn = aws_lb.open_uptime.arn
886
911
  port = 443
887
912
  protocol = "HTTPS"
888
913
  certificate_arn = var.certificate_arn
889
914
  tags = local.tags
890
915
 
891
- default_action {
892
- type = "forward"
893
- target_group_arn = aws_lb_target_group.web.arn
916
+ dynamic "default_action" {
917
+ for_each = local.cloudfront_https_origin && local.use_origin_verify ? [] : [1]
918
+ content {
919
+ type = "forward"
920
+ target_group_arn = aws_lb_target_group.web.arn
921
+ }
922
+ }
923
+
924
+ dynamic "default_action" {
925
+ for_each = local.cloudfront_https_origin && local.use_origin_verify ? [1] : []
926
+ content {
927
+ type = "fixed-response"
928
+
929
+ fixed_response {
930
+ content_type = "text/plain"
931
+ message_body = "forbidden"
932
+ status_code = "403"
933
+ }
934
+ }
894
935
  }
895
936
  }
896
937
 
897
938
  resource "aws_lb_listener" "http_cloudfront" {
898
- count = local.use_cloudfront ? 1 : 0
939
+ count = local.use_cloudfront && !local.cloudfront_https_origin ? 1 : 0
899
940
  load_balancer_arn = aws_lb.open_uptime.arn
900
941
  port = 80
901
942
  protocol = "HTTP"
@@ -924,7 +965,7 @@ resource "aws_lb_listener" "http_cloudfront" {
924
965
  }
925
966
 
926
967
  resource "aws_lb_listener_rule" "http_cloudfront_origin_verify" {
927
- count = local.use_origin_verify ? 1 : 0
968
+ count = local.use_origin_verify && !local.cloudfront_https_origin ? 1 : 0
928
969
  listener_arn = aws_lb_listener.http_cloudfront[0].arn
929
970
  priority = var.cloudfront_origin_verify_listener_rule_priority
930
971
  tags = local.tags
@@ -942,6 +983,25 @@ resource "aws_lb_listener_rule" "http_cloudfront_origin_verify" {
942
983
  }
943
984
  }
944
985
 
986
+ resource "aws_lb_listener_rule" "https_cloudfront_origin_verify" {
987
+ count = local.use_origin_verify && local.cloudfront_https_origin ? 1 : 0
988
+ listener_arn = aws_lb_listener.https[0].arn
989
+ priority = var.cloudfront_origin_verify_listener_rule_priority
990
+ tags = local.tags
991
+
992
+ action {
993
+ type = "forward"
994
+ target_group_arn = aws_lb_target_group.web.arn
995
+ }
996
+
997
+ condition {
998
+ http_header {
999
+ http_header_name = var.cloudfront_origin_verify_header_name
1000
+ values = [var.cloudfront_origin_verify_header_value]
1001
+ }
1002
+ }
1003
+ }
1004
+
945
1005
  resource "aws_cloudfront_distribution" "open_uptime" {
946
1006
  count = local.use_cloudfront ? 1 : 0
947
1007
  enabled = true
@@ -951,7 +1011,7 @@ resource "aws_cloudfront_distribution" "open_uptime" {
951
1011
  tags = local.tags
952
1012
 
953
1013
  origin {
954
- domain_name = aws_lb.open_uptime.dns_name
1014
+ domain_name = local.cloudfront_https_origin ? var.cloudfront_origin_domain_name : aws_lb.open_uptime.dns_name
955
1015
  origin_id = "${local.prefix}-alb"
956
1016
 
957
1017
  dynamic "custom_header" {
@@ -965,7 +1025,7 @@ resource "aws_cloudfront_distribution" "open_uptime" {
965
1025
  custom_origin_config {
966
1026
  http_port = 80
967
1027
  https_port = 443
968
- origin_protocol_policy = "http-only"
1028
+ origin_protocol_policy = var.cloudfront_origin_protocol_policy
969
1029
  origin_ssl_protocols = ["TLSv1.2"]
970
1030
  }
971
1031
  }
@@ -1000,7 +1060,12 @@ resource "aws_cloudfront_distribution" "open_uptime" {
1000
1060
  cloudfront_default_certificate = true
1001
1061
  }
1002
1062
 
1003
- depends_on = [aws_lb_listener.http_cloudfront, aws_lb_listener_rule.http_cloudfront_origin_verify]
1063
+ depends_on = [
1064
+ aws_lb_listener.http_cloudfront,
1065
+ aws_lb_listener.https,
1066
+ aws_lb_listener_rule.http_cloudfront_origin_verify,
1067
+ aws_lb_listener_rule.https_cloudfront_origin_verify,
1068
+ ]
1004
1069
  }
1005
1070
 
1006
1071
  resource "aws_route53_record" "open_uptime" {
@@ -1016,6 +1081,19 @@ resource "aws_route53_record" "open_uptime" {
1016
1081
  }
1017
1082
  }
1018
1083
 
1084
+ resource "aws_route53_record" "cloudfront_origin" {
1085
+ count = local.cloudfront_https_origin && var.hosted_zone_id != null ? 1 : 0
1086
+ zone_id = var.hosted_zone_id
1087
+ name = var.cloudfront_origin_domain_name
1088
+ type = "A"
1089
+
1090
+ alias {
1091
+ name = aws_lb.open_uptime.dns_name
1092
+ zone_id = aws_lb.open_uptime.zone_id
1093
+ evaluate_target_health = true
1094
+ }
1095
+ }
1096
+
1019
1097
  data "aws_iam_policy_document" "ecs_assume_role" {
1020
1098
  statement {
1021
1099
  actions = ["sts:AssumeRole"]
@@ -1194,12 +1272,14 @@ resource "aws_ecs_task_definition" "service" {
1194
1272
  }
1195
1273
 
1196
1274
  resource "aws_ecs_service" "web" {
1197
- name = "${local.prefix}-web"
1198
- cluster = aws_ecs_cluster.open_uptime.id
1199
- task_definition = aws_ecs_task_definition.service["web"].arn
1200
- desired_count = local.services.web.desired_count
1201
- launch_type = "FARGATE"
1202
- tags = local.tags
1275
+ name = "${local.prefix}-web"
1276
+ cluster = aws_ecs_cluster.open_uptime.id
1277
+ task_definition = aws_ecs_task_definition.service["web"].arn
1278
+ desired_count = local.services.web.desired_count
1279
+ launch_type = "FARGATE"
1280
+ enable_ecs_managed_tags = true
1281
+ propagate_tags = "SERVICE"
1282
+ tags = local.tags
1203
1283
 
1204
1284
  deployment_circuit_breaker {
1205
1285
  enable = true
@@ -1226,12 +1306,14 @@ resource "aws_ecs_service" "worker" {
1226
1306
  for key, value in local.services : key => value if key != "web" && key != "migration"
1227
1307
  }
1228
1308
 
1229
- name = "${local.prefix}-${each.key}"
1230
- cluster = aws_ecs_cluster.open_uptime.id
1231
- task_definition = aws_ecs_task_definition.service[each.key].arn
1232
- desired_count = each.value.desired_count
1233
- launch_type = "FARGATE"
1234
- tags = local.tags
1309
+ name = "${local.prefix}-${each.key}"
1310
+ cluster = aws_ecs_cluster.open_uptime.id
1311
+ task_definition = aws_ecs_task_definition.service[each.key].arn
1312
+ desired_count = each.value.desired_count
1313
+ launch_type = "FARGATE"
1314
+ enable_ecs_managed_tags = true
1315
+ propagate_tags = "SERVICE"
1316
+ tags = local.tags
1235
1317
 
1236
1318
  deployment_circuit_breaker {
1237
1319
  enable = true
@@ -18,6 +18,14 @@ output "cloudfront_domain_name" {
18
18
  value = try(aws_cloudfront_distribution.open_uptime[0].domain_name, null)
19
19
  }
20
20
 
21
+ output "cloudfront_origin_protocol_policy" {
22
+ value = local.use_cloudfront ? var.cloudfront_origin_protocol_policy : null
23
+ }
24
+
25
+ output "cloudfront_origin_domain_name" {
26
+ value = local.use_cloudfront ? (local.cloudfront_https_origin ? var.cloudfront_origin_domain_name : aws_lb.open_uptime.dns_name) : null
27
+ }
28
+
21
29
  output "protected_access_url" {
22
30
  value = var.protected_access_mode == "cloudfront_default_domain" ? "https://${aws_cloudfront_distribution.open_uptime[0].domain_name}" : "https://${var.hostname}"
23
31
  }
@@ -11,6 +11,8 @@ 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
+ cloudfront_origin_protocol_policy = "http-only"
15
+ cloudfront_origin_domain_name = null
14
16
  enable_cloudfront_origin_verify_header = false
15
17
  cloudfront_origin_verify_header_name = "X-Open-Uptime-Origin-Verify"
16
18
  cloudfront_origin_verify_header_value = null
@@ -19,7 +21,8 @@ alb_ingress_cidr_blocks = []
19
21
  private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
20
22
  private_route_table_ids = ["rtb-replace-private"]
21
23
  container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
22
- runtime_package_version = "0.1.24"
24
+ runtime_package_version = "0.1.25"
25
+ runtime_package_integrity = null
23
26
  certificate_arn = null
24
27
  hosted_zone_id = null
25
28
  app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/app/env"