@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/CHANGELOG.md +14 -0
- package/Dockerfile.package +2 -2
- package/README.md +5 -2
- package/bun.lock +221 -0
- package/dist/cli/index.js +29 -7
- package/dist/cloud-plan.d.ts +11 -0
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +23 -6
- package/dist/index.js +23 -6
- package/docs/aws-deployment-runbook.md +27 -6
- package/docs/aws-runtime-security.md +25 -10
- package/docs/deployment-metadata.example.json +4 -2
- package/infra/aws/README.md +26 -11
- package/infra/aws/main.tf +116 -34
- package/infra/aws/outputs.tf +8 -0
- package/infra/aws/terraform.tfvars.example +4 -1
- package/infra/aws/variables.tf +54 -5
- package/package.json +2 -1
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.
|
|
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
|
|
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}
|
|
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;
|
|
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
|
|
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
|
|
64
|
-
default HTTPS domain without custom DNS or ACM.
|
|
65
|
-
|
|
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
|
|
81
|
-
CloudFront origin-facing ranges
|
|
82
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
140
|
-
domain, restrict ALB
|
|
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
|
|
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
|
|
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
|
|
442
|
-
|
|
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.
|
|
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
|
|
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>",
|
package/infra/aws/README.md
CHANGED
|
@@ -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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
origin
|
|
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.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
21
|
-
container_port
|
|
22
|
-
evidence_bucket
|
|
23
|
-
efs_uid
|
|
24
|
-
efs_gid
|
|
25
|
-
hosted_sqlite_db_path
|
|
26
|
-
efs_enabled_services
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
893
|
-
|
|
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 =
|
|
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 = [
|
|
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
|
|
1198
|
-
cluster
|
|
1199
|
-
task_definition
|
|
1200
|
-
desired_count
|
|
1201
|
-
launch_type
|
|
1202
|
-
|
|
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
|
|
1230
|
-
cluster
|
|
1231
|
-
task_definition
|
|
1232
|
-
desired_count
|
|
1233
|
-
launch_type
|
|
1234
|
-
|
|
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
|
package/infra/aws/outputs.tf
CHANGED
|
@@ -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
|
|
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"
|