@hasna/uptime 0.1.7 → 0.1.8
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/README.md +7 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +43 -4
- package/dist/cli/index.js +60 -11
- package/dist/cloud-plan.d.ts +5 -1
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +14 -6
- package/dist/index.js +57 -10
- package/docs/aws-deployment-runbook.md +14 -2
- package/infra/aws/README.md +10 -0
- package/infra/aws/main.tf +94 -5
- package/infra/aws/outputs.tf +8 -0
- package/infra/aws/terraform.tfvars.example +4 -3
- package/infra/aws/variables.tf +25 -3
- 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.8] - 2026-06-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- CloudFront default-domain protected web access mode for first AWS deployment,
|
|
14
|
+
with ALB HTTP restricted to CloudFront origin-facing ranges.
|
|
15
|
+
- Hosted public-origin allow-list support through
|
|
16
|
+
`HASNA_UPTIME_ALLOWED_ORIGINS`, wired by the AWS template for CloudFront and
|
|
17
|
+
custom HTTPS access modes.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- AWS Terraform and cloud-plan defaults no longer require custom Route53/ACM
|
|
22
|
+
inputs for the first protected web deployment path.
|
|
23
|
+
|
|
9
24
|
## [0.1.7] - 2026-06-28
|
|
10
25
|
|
|
11
26
|
### Added
|
package/README.md
CHANGED
|
@@ -48,7 +48,11 @@ in `docs/aws-deployment-runbook.md` is satisfied.
|
|
|
48
48
|
|
|
49
49
|
Deployment review artifacts live in `Dockerfile` and `infra/aws`. The Terraform
|
|
50
50
|
desired counts default to zero, and `uptime cloud plan --json` exposes the
|
|
51
|
-
format/init/validate/plan commands with `applyAllowed: false`.
|
|
51
|
+
format/init/validate/plan commands with `applyAllowed: false`. The first
|
|
52
|
+
protected access path uses the CloudFront default HTTPS domain with ALB origin
|
|
53
|
+
ingress restricted to CloudFront. The hosted web task must set
|
|
54
|
+
`HASNA_UPTIME_ALLOWED_ORIGINS` to the public HTTPS edge origin so same-origin
|
|
55
|
+
browser mutations still pass when the private origin hop is HTTP. Hosted AWS
|
|
52
56
|
runtime state currently uses explicit EFS-backed SQLite via
|
|
53
57
|
`HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db` for one protected web
|
|
54
58
|
task maximum; do not set `HASNA_UPTIME_DATABASE_URL` until the async Postgres
|
|
@@ -87,6 +91,8 @@ State-changing API requests reject cross-origin browser requests and
|
|
|
87
91
|
non-loopback mutation hosts by default. For a trusted remote bind, set
|
|
88
92
|
`HASNA_UPTIME_API_TOKEN` or pass `uptime serve --api-token <token>` and send
|
|
89
93
|
`Authorization: Bearer <token>` or `X-Uptime-Token: <token>`.
|
|
94
|
+
Hosted mode additionally accepts comma-separated public origins from
|
|
95
|
+
`HASNA_UPTIME_ALLOWED_ORIGINS` for deployments behind a TLS-terminating edge.
|
|
90
96
|
Endpoints that accept request bodies require `content-type: application/json`.
|
|
91
97
|
|
|
92
98
|
## Uptime Semantics
|
package/dist/api.d.ts
CHANGED
|
@@ -9,12 +9,14 @@ export interface ServeOptions extends UptimeServiceOptions {
|
|
|
9
9
|
apiToken?: string;
|
|
10
10
|
hostedToken?: string;
|
|
11
11
|
hostedTokens?: HostedToken[];
|
|
12
|
+
hostedAllowedOrigins?: string[];
|
|
12
13
|
allowUnsafeRemoteMutations?: boolean;
|
|
13
14
|
}
|
|
14
15
|
export interface CreateApiHandlerOptions {
|
|
15
16
|
apiToken?: string;
|
|
16
17
|
hostedToken?: string;
|
|
17
18
|
hostedTokens?: HostedToken[];
|
|
19
|
+
hostedAllowedOrigins?: string[];
|
|
18
20
|
allowUnsafeRemoteMutations?: boolean;
|
|
19
21
|
fetchImpl?: typeof fetch;
|
|
20
22
|
trustedLoopback?: boolean;
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,YAAa,SAAQ,oBAAoB;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,cAAc,GAAG,cAAc,GAAG,eAAe,GAAG,cAAc,CAAC;AAE7G,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,uBAA4B,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2BvI;AAED,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,SAAS,CAAC,EAAE,eAAe,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,YAAa,SAAQ,oBAAoB;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,cAAc,GAAG,cAAc,GAAG,eAAe,GAAG,cAAc,CAAC;AAE7G,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,uBAA4B,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2BvI;AAED,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,SAAS,CAAC,EAAE,eAAe,CAAA;CAAE,CA4BrJ"}
|
package/dist/api.js
CHANGED
|
@@ -3510,6 +3510,7 @@ function serveUptime(options = {}) {
|
|
|
3510
3510
|
apiToken: options.apiToken,
|
|
3511
3511
|
hostedToken: options.hostedToken,
|
|
3512
3512
|
hostedTokens: options.hostedTokens,
|
|
3513
|
+
hostedAllowedOrigins: options.hostedAllowedOrigins,
|
|
3513
3514
|
allowUnsafeRemoteMutations: options.allowUnsafeRemoteMutations,
|
|
3514
3515
|
trustedLoopback: isLoopbackHost(options.host ?? "127.0.0.1"),
|
|
3515
3516
|
mode
|
|
@@ -3569,13 +3570,23 @@ async function handleHostedRequest(service, request, url, options) {
|
|
|
3569
3570
|
const scope = hostedScopeFor(request.method, apiPath);
|
|
3570
3571
|
requireHostedActor(request, url, options, scope);
|
|
3571
3572
|
if (["POST", "PATCH", "DELETE"].includes(request.method)) {
|
|
3572
|
-
|
|
3573
|
-
if (origin && origin !== `${url.protocol}//${url.host}`) {
|
|
3574
|
-
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3575
|
-
}
|
|
3573
|
+
validateHostedMutationOrigin(request, url, options);
|
|
3576
3574
|
}
|
|
3577
3575
|
return handleApiRoute(service, request, url, apiPath, options, true);
|
|
3578
3576
|
}
|
|
3577
|
+
function validateHostedMutationOrigin(request, url, options) {
|
|
3578
|
+
const rawOrigin = request.headers.get("origin");
|
|
3579
|
+
const origin = normalizeOrigin(rawOrigin);
|
|
3580
|
+
if (rawOrigin && !origin) {
|
|
3581
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3582
|
+
}
|
|
3583
|
+
if (!origin)
|
|
3584
|
+
return;
|
|
3585
|
+
const allowedOrigins = new Set([`${url.protocol}//${url.host}`, ...resolveHostedAllowedOrigins(options)]);
|
|
3586
|
+
if (!allowedOrigins.has(origin)) {
|
|
3587
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3579
3590
|
async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
3580
3591
|
if (request.method === "GET" && apiPath === "/api/summary") {
|
|
3581
3592
|
return json(service.summary());
|
|
@@ -3794,6 +3805,34 @@ function resolveHostedTokens(options) {
|
|
|
3794
3805
|
workspaceId: process.env.HASNA_UPTIME_WORKSPACE_ID ?? "default"
|
|
3795
3806
|
}];
|
|
3796
3807
|
}
|
|
3808
|
+
function resolveHostedAllowedOrigins(options) {
|
|
3809
|
+
const configured = options.hostedAllowedOrigins ?? splitCsv(process.env.HASNA_UPTIME_ALLOWED_ORIGINS);
|
|
3810
|
+
return configured.map((origin) => normalizeAllowedOrigin(origin)).filter((origin) => Boolean(origin));
|
|
3811
|
+
}
|
|
3812
|
+
function splitCsv(value) {
|
|
3813
|
+
if (!value)
|
|
3814
|
+
return [];
|
|
3815
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
3816
|
+
}
|
|
3817
|
+
function normalizeAllowedOrigin(value) {
|
|
3818
|
+
const origin = normalizeOrigin(value);
|
|
3819
|
+
if (!origin) {
|
|
3820
|
+
throw new ApiError(`invalid hosted allowed origin: ${value}`, 500);
|
|
3821
|
+
}
|
|
3822
|
+
return origin;
|
|
3823
|
+
}
|
|
3824
|
+
function normalizeOrigin(value) {
|
|
3825
|
+
if (!value?.trim())
|
|
3826
|
+
return;
|
|
3827
|
+
try {
|
|
3828
|
+
const parsed = new URL(value.trim());
|
|
3829
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
3830
|
+
return;
|
|
3831
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
3832
|
+
} catch {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3797
3836
|
function safeTokenEqual(candidate, expected) {
|
|
3798
3837
|
if (!candidate)
|
|
3799
3838
|
return false;
|
package/dist/cli/index.js
CHANGED
|
@@ -6107,6 +6107,7 @@ function serveUptime(options = {}) {
|
|
|
6107
6107
|
apiToken: options.apiToken,
|
|
6108
6108
|
hostedToken: options.hostedToken,
|
|
6109
6109
|
hostedTokens: options.hostedTokens,
|
|
6110
|
+
hostedAllowedOrigins: options.hostedAllowedOrigins,
|
|
6110
6111
|
allowUnsafeRemoteMutations: options.allowUnsafeRemoteMutations,
|
|
6111
6112
|
trustedLoopback: isLoopbackHost(options.host ?? "127.0.0.1"),
|
|
6112
6113
|
mode
|
|
@@ -6166,13 +6167,23 @@ async function handleHostedRequest(service, request, url, options) {
|
|
|
6166
6167
|
const scope = hostedScopeFor(request.method, apiPath);
|
|
6167
6168
|
requireHostedActor(request, url, options, scope);
|
|
6168
6169
|
if (["POST", "PATCH", "DELETE"].includes(request.method)) {
|
|
6169
|
-
|
|
6170
|
-
if (origin && origin !== `${url.protocol}//${url.host}`) {
|
|
6171
|
-
throw new ApiError("cross-origin mutation rejected", 403);
|
|
6172
|
-
}
|
|
6170
|
+
validateHostedMutationOrigin(request, url, options);
|
|
6173
6171
|
}
|
|
6174
6172
|
return handleApiRoute(service, request, url, apiPath, options, true);
|
|
6175
6173
|
}
|
|
6174
|
+
function validateHostedMutationOrigin(request, url, options) {
|
|
6175
|
+
const rawOrigin = request.headers.get("origin");
|
|
6176
|
+
const origin = normalizeOrigin(rawOrigin);
|
|
6177
|
+
if (rawOrigin && !origin) {
|
|
6178
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
6179
|
+
}
|
|
6180
|
+
if (!origin)
|
|
6181
|
+
return;
|
|
6182
|
+
const allowedOrigins = new Set([`${url.protocol}//${url.host}`, ...resolveHostedAllowedOrigins(options)]);
|
|
6183
|
+
if (!allowedOrigins.has(origin)) {
|
|
6184
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6176
6187
|
async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
6177
6188
|
if (request.method === "GET" && apiPath === "/api/summary") {
|
|
6178
6189
|
return json(service.summary());
|
|
@@ -6391,6 +6402,34 @@ function resolveHostedTokens(options) {
|
|
|
6391
6402
|
workspaceId: process.env.HASNA_UPTIME_WORKSPACE_ID ?? "default"
|
|
6392
6403
|
}];
|
|
6393
6404
|
}
|
|
6405
|
+
function resolveHostedAllowedOrigins(options) {
|
|
6406
|
+
const configured = options.hostedAllowedOrigins ?? splitCsv(process.env.HASNA_UPTIME_ALLOWED_ORIGINS);
|
|
6407
|
+
return configured.map((origin) => normalizeAllowedOrigin(origin)).filter((origin) => Boolean(origin));
|
|
6408
|
+
}
|
|
6409
|
+
function splitCsv(value) {
|
|
6410
|
+
if (!value)
|
|
6411
|
+
return [];
|
|
6412
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
6413
|
+
}
|
|
6414
|
+
function normalizeAllowedOrigin(value) {
|
|
6415
|
+
const origin = normalizeOrigin(value);
|
|
6416
|
+
if (!origin) {
|
|
6417
|
+
throw new ApiError(`invalid hosted allowed origin: ${value}`, 500);
|
|
6418
|
+
}
|
|
6419
|
+
return origin;
|
|
6420
|
+
}
|
|
6421
|
+
function normalizeOrigin(value) {
|
|
6422
|
+
if (!value?.trim())
|
|
6423
|
+
return;
|
|
6424
|
+
try {
|
|
6425
|
+
const parsed = new URL(value.trim());
|
|
6426
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
6427
|
+
return;
|
|
6428
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
6429
|
+
} catch {
|
|
6430
|
+
return;
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6394
6433
|
function safeTokenEqual(candidate, expected) {
|
|
6395
6434
|
if (!candidate)
|
|
6396
6435
|
return false;
|
|
@@ -6426,6 +6465,7 @@ var DEFAULT_HOSTNAME = "uptime.example.com";
|
|
|
6426
6465
|
var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
6427
6466
|
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
6428
6467
|
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
6468
|
+
var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
|
|
6429
6469
|
function buildAwsDeploymentPlan(options = {}) {
|
|
6430
6470
|
const region = clean(options.region, DEFAULT_REGION);
|
|
6431
6471
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -6438,7 +6478,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6438
6478
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
6439
6479
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
6440
6480
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
6441
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
6481
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.8");
|
|
6482
|
+
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
6483
|
+
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
6442
6484
|
const cluster = `${prefix}-${stage}`;
|
|
6443
6485
|
const secrets = {
|
|
6444
6486
|
appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
|
|
@@ -6452,7 +6494,8 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6452
6494
|
HASNA_UPTIME_MODE: "hosted",
|
|
6453
6495
|
HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
|
|
6454
6496
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
6455
|
-
HASNA_UPTIME_HOSTNAME: hostname
|
|
6497
|
+
HASNA_UPTIME_HOSTNAME: hostname,
|
|
6498
|
+
HASNA_UPTIME_ALLOWED_ORIGINS: protectedAccessUrl
|
|
6456
6499
|
}),
|
|
6457
6500
|
servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
|
|
6458
6501
|
HASNA_UPTIME_MODE: "hosted",
|
|
@@ -6478,7 +6521,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6478
6521
|
];
|
|
6479
6522
|
return {
|
|
6480
6523
|
kind: "open-uptime.aws-deployment-plan",
|
|
6481
|
-
version:
|
|
6524
|
+
version: 3,
|
|
6482
6525
|
generatedAt: new Date().toISOString(),
|
|
6483
6526
|
status: "blocked",
|
|
6484
6527
|
canApply: false,
|
|
@@ -6500,6 +6543,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6500
6543
|
hostedSqliteDbPath,
|
|
6501
6544
|
evidenceBucket,
|
|
6502
6545
|
loadBalancer: `${prefix}-${stage}-alb`,
|
|
6546
|
+
protectedAccessMode,
|
|
6547
|
+
edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
|
|
6548
|
+
protectedAccessUrl,
|
|
6503
6549
|
targetGroups: [`${prefix}-${stage}-web-tg`],
|
|
6504
6550
|
securityGroups: [
|
|
6505
6551
|
`${prefix}-${stage}-alb-sg`,
|
|
@@ -6547,7 +6593,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6547
6593
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
6548
6594
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
6549
6595
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
6550
|
-
`Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
6596
|
+
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.`,
|
|
6551
6597
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
6552
6598
|
],
|
|
6553
6599
|
deploy: [
|
|
@@ -6556,7 +6602,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6556
6602
|
"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.",
|
|
6557
6603
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
6558
6604
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
6559
|
-
`Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
6605
|
+
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.`
|
|
6560
6606
|
],
|
|
6561
6607
|
rollback: [
|
|
6562
6608
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -6581,7 +6627,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6581
6627
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
6582
6628
|
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
6583
6629
|
"ECS task definitions using secrets.valueFrom only.",
|
|
6584
|
-
"ALB
|
|
6630
|
+
"CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
|
|
6585
6631
|
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
6586
6632
|
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
6587
6633
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
@@ -6594,6 +6640,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
6594
6640
|
notes: [
|
|
6595
6641
|
"This plan generator does not call AWS.",
|
|
6596
6642
|
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
6643
|
+
"Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
|
|
6597
6644
|
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
6598
6645
|
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
6599
6646
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
|
@@ -7008,7 +7055,7 @@ program2.command("audit").description("List local audit events").option("--resou
|
|
|
7008
7055
|
}
|
|
7009
7056
|
});
|
|
7010
7057
|
var cloud = program2.command("cloud").description("Generate dry-run cloud deployment and Spark01 configuration artifacts");
|
|
7011
|
-
cloud.command("plan").description("Generate a dry-run AWS deployment plan").option("--account <name>", "AWS account/profile label", "aws-profile").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.example.com").option("--workspace-id <id>", "workspace id", "workspace-id").option("--vpc-id <id>", "target VPC id").option("--hosted-sqlite-db <path>", "hosted SQLite path on the EFS mount").option("--rds-instance-id <id>", "deprecated; ignored until the hosted Postgres adapter exists").option("--database-secret-name <name>", "deprecated; ignored until the hosted Postgres adapter exists").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--runtime-package-version <version>", "published @hasna/uptime version for the AWS image builder").option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
|
|
7058
|
+
cloud.command("plan").description("Generate a dry-run AWS deployment plan").option("--account <name>", "AWS account/profile label", "aws-profile").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.example.com").option("--workspace-id <id>", "workspace id", "workspace-id").option("--vpc-id <id>", "target VPC id").option("--hosted-sqlite-db <path>", "hosted SQLite path on the EFS mount").option("--rds-instance-id <id>", "deprecated; ignored until the hosted Postgres adapter exists").option("--database-secret-name <name>", "deprecated; ignored until the hosted Postgres adapter exists").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--runtime-package-version <version>", "published @hasna/uptime version for the AWS image builder").addOption(new Option("--protected-access-mode <mode>", "protected web access mode").choices(["cloudfront_default_domain", "alb_https_cert"]).default("cloudfront_default_domain")).option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
|
|
7012
7059
|
try {
|
|
7013
7060
|
const plan = buildAwsDeploymentPlan({
|
|
7014
7061
|
accountName: opts.account,
|
|
@@ -7023,6 +7070,7 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan").opti
|
|
|
7023
7070
|
ecrRepository: opts.ecrRepository,
|
|
7024
7071
|
image: opts.image,
|
|
7025
7072
|
runtimePackageVersion: opts.runtimePackageVersion,
|
|
7073
|
+
protectedAccessMode: opts.protectedAccessMode,
|
|
7026
7074
|
evidenceBucket: opts.evidenceBucket
|
|
7027
7075
|
});
|
|
7028
7076
|
print(plan, renderCloudPlan(plan), opts);
|
|
@@ -7416,6 +7464,7 @@ function renderCloudPlan(plan) {
|
|
|
7416
7464
|
`vpc: ${plan.resources.vpcId}`,
|
|
7417
7465
|
`efs: ${plan.resources.efsFileSystem}`,
|
|
7418
7466
|
`hosted sqlite: ${plan.resources.hostedSqliteDbPath}`,
|
|
7467
|
+
`protected access: ${plan.resources.protectedAccessMode} ${plan.resources.protectedAccessUrl}`,
|
|
7419
7468
|
`services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}/${service2.targetDesiredCount}`).join(", ")}`,
|
|
7420
7469
|
`evidence bucket: ${plan.resources.evidenceBucket}`,
|
|
7421
7470
|
`blockers: ${plan.blockers.length}`,
|
package/dist/cloud-plan.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface AwsDeploymentPlanOptions {
|
|
|
11
11
|
evidenceBucket?: string;
|
|
12
12
|
hostedSqliteDbPath?: string;
|
|
13
13
|
runtimePackageVersion?: string;
|
|
14
|
+
protectedAccessMode?: "cloudfront_default_domain" | "alb_https_cert";
|
|
14
15
|
/** @deprecated Postgres is target-state only until the async adapter is implemented. */
|
|
15
16
|
rdsInstanceId?: string;
|
|
16
17
|
/** @deprecated Postgres is target-state only until the async adapter is implemented. */
|
|
@@ -23,7 +24,7 @@ export interface AwsDeploymentPlanOptions {
|
|
|
23
24
|
}
|
|
24
25
|
export interface AwsDeploymentPlan {
|
|
25
26
|
kind: "open-uptime.aws-deployment-plan";
|
|
26
|
-
version:
|
|
27
|
+
version: 3;
|
|
27
28
|
generatedAt: string;
|
|
28
29
|
status: "blocked";
|
|
29
30
|
canApply: false;
|
|
@@ -45,6 +46,9 @@ export interface AwsDeploymentPlan {
|
|
|
45
46
|
hostedSqliteDbPath: string;
|
|
46
47
|
evidenceBucket: string;
|
|
47
48
|
loadBalancer: string;
|
|
49
|
+
protectedAccessMode: "cloudfront_default_domain" | "alb_https_cert";
|
|
50
|
+
edgeDistribution?: string;
|
|
51
|
+
protectedAccessUrl: string;
|
|
48
52
|
targetGroups: string[];
|
|
49
53
|
securityGroups: string[];
|
|
50
54
|
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":"AAAA,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,2BAA2B,GAAG,gBAAgB,CAAC;IACrE,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE;QACT,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,cAAc,EAAE,CAAC;QAC3B,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;QACrB,mBAAmB,EAAE,2BAA2B,GAAG,gBAAgB,CAAC;QACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;IACF,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,KAAK,CAAC;KACrB,CAAC;IACF,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE;QACN,eAAe,EAAE,KAAK,CAAC;QACvB,gBAAgB,EAAE,KAAK,CAAC;QACxB,wBAAwB,EAAE,KAAK,CAAC;QAChC,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,kCAAkC,CAAC;IACzC,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE;QACN,gBAAgB,EAAE,KAAK,CAAC;QACxB,WAAW,EAAE,KAAK,CAAC;QACnB,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAYD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CA2LhG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,yBAA8B,GAAG,kBAAkB,CA2DnG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CASnE"}
|
package/dist/cloud-plan.js
CHANGED
|
@@ -8,6 +8,7 @@ var DEFAULT_HOSTNAME = "uptime.example.com";
|
|
|
8
8
|
var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
9
9
|
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
10
10
|
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
11
|
+
var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
|
|
11
12
|
function buildAwsDeploymentPlan(options = {}) {
|
|
12
13
|
const region = clean(options.region, DEFAULT_REGION);
|
|
13
14
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -20,7 +21,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
20
21
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
21
22
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
22
23
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
23
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
24
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.8");
|
|
25
|
+
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
26
|
+
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
24
27
|
const cluster = `${prefix}-${stage}`;
|
|
25
28
|
const secrets = {
|
|
26
29
|
appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
|
|
@@ -34,7 +37,8 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
34
37
|
HASNA_UPTIME_MODE: "hosted",
|
|
35
38
|
HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
|
|
36
39
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
37
|
-
HASNA_UPTIME_HOSTNAME: hostname
|
|
40
|
+
HASNA_UPTIME_HOSTNAME: hostname,
|
|
41
|
+
HASNA_UPTIME_ALLOWED_ORIGINS: protectedAccessUrl
|
|
38
42
|
}),
|
|
39
43
|
servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
|
|
40
44
|
HASNA_UPTIME_MODE: "hosted",
|
|
@@ -60,7 +64,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
60
64
|
];
|
|
61
65
|
return {
|
|
62
66
|
kind: "open-uptime.aws-deployment-plan",
|
|
63
|
-
version:
|
|
67
|
+
version: 3,
|
|
64
68
|
generatedAt: new Date().toISOString(),
|
|
65
69
|
status: "blocked",
|
|
66
70
|
canApply: false,
|
|
@@ -82,6 +86,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
82
86
|
hostedSqliteDbPath,
|
|
83
87
|
evidenceBucket,
|
|
84
88
|
loadBalancer: `${prefix}-${stage}-alb`,
|
|
89
|
+
protectedAccessMode,
|
|
90
|
+
edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
|
|
91
|
+
protectedAccessUrl,
|
|
85
92
|
targetGroups: [`${prefix}-${stage}-web-tg`],
|
|
86
93
|
securityGroups: [
|
|
87
94
|
`${prefix}-${stage}-alb-sg`,
|
|
@@ -129,7 +136,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
129
136
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
130
137
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
131
138
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
132
|
-
`Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
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.`,
|
|
133
140
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
134
141
|
],
|
|
135
142
|
deploy: [
|
|
@@ -138,7 +145,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
138
145
|
"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.",
|
|
139
146
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
140
147
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
141
|
-
`Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
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.`
|
|
142
149
|
],
|
|
143
150
|
rollback: [
|
|
144
151
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -163,7 +170,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
163
170
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
164
171
|
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
165
172
|
"ECS task definitions using secrets.valueFrom only.",
|
|
166
|
-
"ALB
|
|
173
|
+
"CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
|
|
167
174
|
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
168
175
|
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
169
176
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
@@ -176,6 +183,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
176
183
|
notes: [
|
|
177
184
|
"This plan generator does not call AWS.",
|
|
178
185
|
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
186
|
+
"Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
|
|
179
187
|
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
180
188
|
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
181
189
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
package/dist/index.js
CHANGED
|
@@ -3510,6 +3510,7 @@ function serveUptime(options = {}) {
|
|
|
3510
3510
|
apiToken: options.apiToken,
|
|
3511
3511
|
hostedToken: options.hostedToken,
|
|
3512
3512
|
hostedTokens: options.hostedTokens,
|
|
3513
|
+
hostedAllowedOrigins: options.hostedAllowedOrigins,
|
|
3513
3514
|
allowUnsafeRemoteMutations: options.allowUnsafeRemoteMutations,
|
|
3514
3515
|
trustedLoopback: isLoopbackHost(options.host ?? "127.0.0.1"),
|
|
3515
3516
|
mode
|
|
@@ -3569,13 +3570,23 @@ async function handleHostedRequest(service, request, url, options) {
|
|
|
3569
3570
|
const scope = hostedScopeFor(request.method, apiPath);
|
|
3570
3571
|
requireHostedActor(request, url, options, scope);
|
|
3571
3572
|
if (["POST", "PATCH", "DELETE"].includes(request.method)) {
|
|
3572
|
-
|
|
3573
|
-
if (origin && origin !== `${url.protocol}//${url.host}`) {
|
|
3574
|
-
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3575
|
-
}
|
|
3573
|
+
validateHostedMutationOrigin(request, url, options);
|
|
3576
3574
|
}
|
|
3577
3575
|
return handleApiRoute(service, request, url, apiPath, options, true);
|
|
3578
3576
|
}
|
|
3577
|
+
function validateHostedMutationOrigin(request, url, options) {
|
|
3578
|
+
const rawOrigin = request.headers.get("origin");
|
|
3579
|
+
const origin = normalizeOrigin(rawOrigin);
|
|
3580
|
+
if (rawOrigin && !origin) {
|
|
3581
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3582
|
+
}
|
|
3583
|
+
if (!origin)
|
|
3584
|
+
return;
|
|
3585
|
+
const allowedOrigins = new Set([`${url.protocol}//${url.host}`, ...resolveHostedAllowedOrigins(options)]);
|
|
3586
|
+
if (!allowedOrigins.has(origin)) {
|
|
3587
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3579
3590
|
async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
3580
3591
|
if (request.method === "GET" && apiPath === "/api/summary") {
|
|
3581
3592
|
return json(service.summary());
|
|
@@ -3794,6 +3805,34 @@ function resolveHostedTokens(options) {
|
|
|
3794
3805
|
workspaceId: process.env.HASNA_UPTIME_WORKSPACE_ID ?? "default"
|
|
3795
3806
|
}];
|
|
3796
3807
|
}
|
|
3808
|
+
function resolveHostedAllowedOrigins(options) {
|
|
3809
|
+
const configured = options.hostedAllowedOrigins ?? splitCsv(process.env.HASNA_UPTIME_ALLOWED_ORIGINS);
|
|
3810
|
+
return configured.map((origin) => normalizeAllowedOrigin(origin)).filter((origin) => Boolean(origin));
|
|
3811
|
+
}
|
|
3812
|
+
function splitCsv(value) {
|
|
3813
|
+
if (!value)
|
|
3814
|
+
return [];
|
|
3815
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
3816
|
+
}
|
|
3817
|
+
function normalizeAllowedOrigin(value) {
|
|
3818
|
+
const origin = normalizeOrigin(value);
|
|
3819
|
+
if (!origin) {
|
|
3820
|
+
throw new ApiError(`invalid hosted allowed origin: ${value}`, 500);
|
|
3821
|
+
}
|
|
3822
|
+
return origin;
|
|
3823
|
+
}
|
|
3824
|
+
function normalizeOrigin(value) {
|
|
3825
|
+
if (!value?.trim())
|
|
3826
|
+
return;
|
|
3827
|
+
try {
|
|
3828
|
+
const parsed = new URL(value.trim());
|
|
3829
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
3830
|
+
return;
|
|
3831
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
3832
|
+
} catch {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3797
3836
|
function safeTokenEqual(candidate, expected) {
|
|
3798
3837
|
if (!candidate)
|
|
3799
3838
|
return false;
|
|
@@ -3829,6 +3868,7 @@ var DEFAULT_HOSTNAME = "uptime.example.com";
|
|
|
3829
3868
|
var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
3830
3869
|
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
3831
3870
|
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
3871
|
+
var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
|
|
3832
3872
|
function buildAwsDeploymentPlan(options = {}) {
|
|
3833
3873
|
const region = clean(options.region, DEFAULT_REGION);
|
|
3834
3874
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -3841,7 +3881,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3841
3881
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
3842
3882
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
3843
3883
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
3844
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
3884
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.8");
|
|
3885
|
+
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
3886
|
+
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
3845
3887
|
const cluster = `${prefix}-${stage}`;
|
|
3846
3888
|
const secrets = {
|
|
3847
3889
|
appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
|
|
@@ -3855,7 +3897,8 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3855
3897
|
HASNA_UPTIME_MODE: "hosted",
|
|
3856
3898
|
HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
|
|
3857
3899
|
HASNA_UPTIME_WORKSPACE_ID: workspaceId,
|
|
3858
|
-
HASNA_UPTIME_HOSTNAME: hostname
|
|
3900
|
+
HASNA_UPTIME_HOSTNAME: hostname,
|
|
3901
|
+
HASNA_UPTIME_ALLOWED_ORIGINS: protectedAccessUrl
|
|
3859
3902
|
}),
|
|
3860
3903
|
servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
|
|
3861
3904
|
HASNA_UPTIME_MODE: "hosted",
|
|
@@ -3881,7 +3924,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3881
3924
|
];
|
|
3882
3925
|
return {
|
|
3883
3926
|
kind: "open-uptime.aws-deployment-plan",
|
|
3884
|
-
version:
|
|
3927
|
+
version: 3,
|
|
3885
3928
|
generatedAt: new Date().toISOString(),
|
|
3886
3929
|
status: "blocked",
|
|
3887
3930
|
canApply: false,
|
|
@@ -3903,6 +3946,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3903
3946
|
hostedSqliteDbPath,
|
|
3904
3947
|
evidenceBucket,
|
|
3905
3948
|
loadBalancer: `${prefix}-${stage}-alb`,
|
|
3949
|
+
protectedAccessMode,
|
|
3950
|
+
edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
|
|
3951
|
+
protectedAccessUrl,
|
|
3906
3952
|
targetGroups: [`${prefix}-${stage}-web-tg`],
|
|
3907
3953
|
securityGroups: [
|
|
3908
3954
|
`${prefix}-${stage}-alb-sg`,
|
|
@@ -3950,7 +3996,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3950
3996
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
3951
3997
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
3952
3998
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
3953
|
-
`Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
3999
|
+
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.`,
|
|
3954
4000
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
3955
4001
|
],
|
|
3956
4002
|
deploy: [
|
|
@@ -3959,7 +4005,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3959
4005
|
"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.",
|
|
3960
4006
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
3961
4007
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
3962
|
-
`Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
4008
|
+
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.`
|
|
3963
4009
|
],
|
|
3964
4010
|
rollback: [
|
|
3965
4011
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -3984,7 +4030,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3984
4030
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
3985
4031
|
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
3986
4032
|
"ECS task definitions using secrets.valueFrom only.",
|
|
3987
|
-
"ALB
|
|
4033
|
+
"CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
|
|
3988
4034
|
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
3989
4035
|
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
3990
4036
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
@@ -3997,6 +4043,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3997
4043
|
notes: [
|
|
3998
4044
|
"This plan generator does not call AWS.",
|
|
3999
4045
|
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
4046
|
+
"Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
|
|
4000
4047
|
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
4001
4048
|
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
4002
4049
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
|
@@ -19,6 +19,7 @@ Public package defaults are placeholders:
|
|
|
19
19
|
- hosted data path: EFS-mounted SQLite at `/data/uptime/uptime.db`
|
|
20
20
|
- hostname: `uptime.example.com`
|
|
21
21
|
- workspace id: `workspace-id`
|
|
22
|
+
- protected access mode: `cloudfront_default_domain`
|
|
22
23
|
|
|
23
24
|
Override these with CLI flags or private deployment evidence for the real
|
|
24
25
|
account, hostname, workspace id, VPC id, secret refs, and repository names.
|
|
@@ -47,7 +48,9 @@ write a sourceable env file with a placeholder probe identity.
|
|
|
47
48
|
|
|
48
49
|
3. Confirm the target VPC, private subnets, KMS key, and EFS/Backup plan inputs
|
|
49
50
|
still match the plan.
|
|
50
|
-
4. Confirm
|
|
51
|
+
4. Confirm the protected access mode. The first deploy can use the CloudFront
|
|
52
|
+
default HTTPS domain without custom DNS or ACM. Custom hostname deploys still
|
|
53
|
+
require Route53/edge ownership and an ACM certificate.
|
|
51
54
|
5. Confirm the deployment role uses short-lived credentials or OIDC, not copied
|
|
52
55
|
access keys.
|
|
53
56
|
|
|
@@ -59,7 +62,9 @@ The plan expects:
|
|
|
59
62
|
- ECS/Fargate cluster with separate services for web, scheduler, public probe,
|
|
60
63
|
reporter, and one-off migrations. In the current EFS SQLite bridge, only web
|
|
61
64
|
may be enabled and it must run at desired count `0` or `1`.
|
|
62
|
-
-
|
|
65
|
+
- CloudFront default-domain HTTPS edge plus ALB HTTP origin restricted to
|
|
66
|
+
CloudFront origin-facing ranges, or an ALB HTTPS listener with ACM certificate
|
|
67
|
+
when custom DNS is approved.
|
|
63
68
|
- Encrypted EFS file system, access point, mount targets, and AWS Backup plan
|
|
64
69
|
for `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db`.
|
|
65
70
|
- S3 bucket for redacted browser evidence and generated report artifacts.
|
|
@@ -82,6 +87,8 @@ terraform -chdir=infra/aws validate
|
|
|
82
87
|
terraform -chdir=infra/aws plan -out open-uptime.tfplan
|
|
83
88
|
```
|
|
84
89
|
|
|
90
|
+
Use Terraform/OpenTofu 1.9 or newer for this starter.
|
|
91
|
+
|
|
85
92
|
## Spark01
|
|
86
93
|
|
|
87
94
|
Spark01 should be a private probe/operator machine, not the hosted source of
|
|
@@ -98,6 +105,9 @@ routes are backed by cloud check jobs and cloud audit rows.
|
|
|
98
105
|
- Do deploy hosted mode with `HASNA_UPTIME_HOSTED_SQLITE_DB` pointing at the EFS
|
|
99
106
|
mount path `/data/uptime/uptime.db`. Do not set `HASNA_UPTIME_DATABASE_URL`
|
|
100
107
|
until the async Postgres adapter exists.
|
|
108
|
+
- Do set `HASNA_UPTIME_ALLOWED_ORIGINS` on the hosted web task to the public
|
|
109
|
+
HTTPS edge origin, such as the CloudFront default domain or approved custom
|
|
110
|
+
hostname.
|
|
101
111
|
- Do not inline AWS keys, hosted tokens, Mailery keys, Open Logs tokens, database
|
|
102
112
|
URLs, or probe private keys in task definitions. Use ECS `secrets.valueFrom`
|
|
103
113
|
refs such as `HASNA_UPTIME_HOSTED_TOKEN`.
|
|
@@ -105,6 +115,8 @@ routes are backed by cloud check jobs and cloud audit rows.
|
|
|
105
115
|
- Do not enable scheduler, public-probe, reporter, or migration workers against
|
|
106
116
|
the EFS SQLite bridge; those services need Postgres/cloud leases first.
|
|
107
117
|
- Do not expose dashboard/API routes without hosted auth and workspace checks.
|
|
118
|
+
- Do not expose the ALB directly in CloudFront mode; ALB ingress must be limited
|
|
119
|
+
to CloudFront origin-facing ranges.
|
|
108
120
|
- Do not treat local SQLite, local project DBs, or Spark01 local state as cloud
|
|
109
121
|
authority after cutover.
|
|
110
122
|
|
package/infra/aws/README.md
CHANGED
|
@@ -15,6 +15,8 @@ terraform -chdir=infra/aws validate
|
|
|
15
15
|
terraform -chdir=infra/aws plan -out open-uptime.tfplan
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Terraform 1.9 or newer is required by the variable validation in this starter.
|
|
19
|
+
|
|
18
20
|
Required inputs are declared in `variables.tf` and illustrated in
|
|
19
21
|
`terraform.tfvars.example`. Secrets are passed as Secrets Manager/SSM ARNs only;
|
|
20
22
|
never place plaintext tokens, database URLs, private keys, or channel
|
|
@@ -31,6 +33,14 @@ The included CodeBuild project builds `@hasna/uptime` from npm with
|
|
|
31
33
|
`Dockerfile.package` and pushes the resulting image to ECR. This avoids
|
|
32
34
|
depending on a local Docker daemon for image publication.
|
|
33
35
|
|
|
36
|
+
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.
|
|
40
|
+
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.
|
|
43
|
+
|
|
34
44
|
## Current Blockers
|
|
35
45
|
|
|
36
46
|
- Hosted production auth/RBAC still needs scoped, revocable credentials.
|
package/infra/aws/main.tf
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
terraform {
|
|
2
|
-
required_version = ">= 1.
|
|
2
|
+
required_version = ">= 1.9.0"
|
|
3
3
|
|
|
4
4
|
required_providers {
|
|
5
5
|
aws = {
|
|
@@ -23,6 +23,8 @@ locals {
|
|
|
23
23
|
efs_gid = 10001
|
|
24
24
|
hosted_sqlite_db_path = "/data/uptime/uptime.db"
|
|
25
25
|
efs_enabled_services = toset(["web"])
|
|
26
|
+
use_alb_https = var.protected_access_mode == "alb_https_cert"
|
|
27
|
+
use_cloudfront = var.protected_access_mode == "cloudfront_default_domain"
|
|
26
28
|
services = {
|
|
27
29
|
web = {
|
|
28
30
|
desired_count = lookup(var.desired_counts, "web", 0)
|
|
@@ -62,6 +64,11 @@ data "aws_vpc" "target" {
|
|
|
62
64
|
id = var.vpc_id
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
data "aws_ec2_managed_prefix_list" "cloudfront_origin_facing" {
|
|
68
|
+
count = local.use_cloudfront ? 1 : 0
|
|
69
|
+
name = "com.amazonaws.global.cloudfront.origin-facing"
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
resource "aws_ecr_repository" "open_uptime" {
|
|
66
73
|
name = var.ecr_repository_name
|
|
67
74
|
image_tag_mutability = "IMMUTABLE"
|
|
@@ -290,7 +297,7 @@ resource "aws_security_group" "alb" {
|
|
|
290
297
|
}
|
|
291
298
|
|
|
292
299
|
resource "aws_security_group_rule" "alb_https_ingress" {
|
|
293
|
-
count = length(var.alb_ingress_cidr_blocks) > 0 ? 1 : 0
|
|
300
|
+
count = local.use_alb_https && length(var.alb_ingress_cidr_blocks) > 0 ? 1 : 0
|
|
294
301
|
type = "ingress"
|
|
295
302
|
description = "HTTPS"
|
|
296
303
|
security_group_id = aws_security_group.alb.id
|
|
@@ -300,6 +307,17 @@ resource "aws_security_group_rule" "alb_https_ingress" {
|
|
|
300
307
|
cidr_blocks = var.alb_ingress_cidr_blocks
|
|
301
308
|
}
|
|
302
309
|
|
|
310
|
+
resource "aws_security_group_rule" "alb_http_from_cloudfront" {
|
|
311
|
+
count = local.use_cloudfront ? 1 : 0
|
|
312
|
+
type = "ingress"
|
|
313
|
+
description = "HTTP from CloudFront origin-facing ranges"
|
|
314
|
+
security_group_id = aws_security_group.alb.id
|
|
315
|
+
from_port = 80
|
|
316
|
+
to_port = 80
|
|
317
|
+
protocol = "tcp"
|
|
318
|
+
prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront_origin_facing[0].id]
|
|
319
|
+
}
|
|
320
|
+
|
|
303
321
|
resource "aws_security_group_rule" "alb_to_web" {
|
|
304
322
|
type = "egress"
|
|
305
323
|
description = "To Open Uptime web"
|
|
@@ -506,6 +524,7 @@ resource "aws_lb_target_group" "web" {
|
|
|
506
524
|
}
|
|
507
525
|
|
|
508
526
|
resource "aws_lb_listener" "https" {
|
|
527
|
+
count = local.use_alb_https ? 1 : 0
|
|
509
528
|
load_balancer_arn = aws_lb.open_uptime.arn
|
|
510
529
|
port = 443
|
|
511
530
|
protocol = "HTTPS"
|
|
@@ -517,8 +536,73 @@ resource "aws_lb_listener" "https" {
|
|
|
517
536
|
}
|
|
518
537
|
}
|
|
519
538
|
|
|
539
|
+
resource "aws_lb_listener" "http_cloudfront" {
|
|
540
|
+
count = local.use_cloudfront ? 1 : 0
|
|
541
|
+
load_balancer_arn = aws_lb.open_uptime.arn
|
|
542
|
+
port = 80
|
|
543
|
+
protocol = "HTTP"
|
|
544
|
+
|
|
545
|
+
default_action {
|
|
546
|
+
type = "forward"
|
|
547
|
+
target_group_arn = aws_lb_target_group.web.arn
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
resource "aws_cloudfront_distribution" "open_uptime" {
|
|
552
|
+
count = local.use_cloudfront ? 1 : 0
|
|
553
|
+
enabled = true
|
|
554
|
+
is_ipv6_enabled = true
|
|
555
|
+
comment = "Open Uptime ${local.prefix} protected HTTPS edge"
|
|
556
|
+
price_class = "PriceClass_100"
|
|
557
|
+
tags = local.tags
|
|
558
|
+
|
|
559
|
+
origin {
|
|
560
|
+
domain_name = aws_lb.open_uptime.dns_name
|
|
561
|
+
origin_id = "${local.prefix}-alb"
|
|
562
|
+
|
|
563
|
+
custom_origin_config {
|
|
564
|
+
http_port = 80
|
|
565
|
+
https_port = 443
|
|
566
|
+
origin_protocol_policy = "http-only"
|
|
567
|
+
origin_ssl_protocols = ["TLSv1.2"]
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
default_cache_behavior {
|
|
572
|
+
target_origin_id = "${local.prefix}-alb"
|
|
573
|
+
viewer_protocol_policy = "redirect-to-https"
|
|
574
|
+
compress = true
|
|
575
|
+
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
|
|
576
|
+
cached_methods = ["GET", "HEAD"]
|
|
577
|
+
default_ttl = 0
|
|
578
|
+
max_ttl = 0
|
|
579
|
+
min_ttl = 0
|
|
580
|
+
|
|
581
|
+
forwarded_values {
|
|
582
|
+
query_string = true
|
|
583
|
+
headers = ["Authorization", "Content-Type", "Origin", "X-Uptime-Hosted-Token"]
|
|
584
|
+
|
|
585
|
+
cookies {
|
|
586
|
+
forward = "all"
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
restrictions {
|
|
592
|
+
geo_restriction {
|
|
593
|
+
restriction_type = "none"
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
viewer_certificate {
|
|
598
|
+
cloudfront_default_certificate = true
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
depends_on = [aws_lb_listener.http_cloudfront]
|
|
602
|
+
}
|
|
603
|
+
|
|
520
604
|
resource "aws_route53_record" "open_uptime" {
|
|
521
|
-
count = var.hosted_zone_id == null ? 0 : 1
|
|
605
|
+
count = var.hosted_zone_id == null || !local.use_alb_https ? 0 : 1
|
|
522
606
|
zone_id = var.hosted_zone_id
|
|
523
607
|
name = var.hostname
|
|
524
608
|
type = "A"
|
|
@@ -670,7 +754,12 @@ resource "aws_ecs_task_definition" "service" {
|
|
|
670
754
|
{ name = "HASNA_UPTIME_WORKSPACE_ID", value = var.workspace_id },
|
|
671
755
|
{ name = "HASNA_UPTIME_COMPONENT", value = each.key },
|
|
672
756
|
{ name = "HASNA_UPTIME_HOSTNAME", value = var.hostname },
|
|
673
|
-
],
|
|
757
|
+
], each.key == "web" ? [
|
|
758
|
+
{
|
|
759
|
+
name = "HASNA_UPTIME_ALLOWED_ORIGINS"
|
|
760
|
+
value = local.use_cloudfront ? "https://${aws_cloudfront_distribution.open_uptime[0].domain_name}" : "https://${var.hostname}"
|
|
761
|
+
},
|
|
762
|
+
] : [], contains(local.efs_enabled_services, each.key) ? [
|
|
674
763
|
{ name = "HASNA_UPTIME_HOSTED_SQLITE_DB", value = local.hosted_sqlite_db_path },
|
|
675
764
|
] : [])
|
|
676
765
|
mountPoints = contains(local.efs_enabled_services, each.key) ? [
|
|
@@ -725,7 +814,7 @@ resource "aws_ecs_service" "web" {
|
|
|
725
814
|
container_port = local.container_port
|
|
726
815
|
}
|
|
727
816
|
|
|
728
|
-
depends_on = [aws_lb_listener.https, aws_efs_mount_target.data]
|
|
817
|
+
depends_on = [aws_lb_listener.https, aws_lb_listener.http_cloudfront, aws_efs_mount_target.data]
|
|
729
818
|
}
|
|
730
819
|
|
|
731
820
|
resource "aws_ecs_service" "worker" {
|
package/infra/aws/outputs.tf
CHANGED
|
@@ -14,6 +14,14 @@ output "alb_dns_name" {
|
|
|
14
14
|
value = aws_lb.open_uptime.dns_name
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
output "cloudfront_domain_name" {
|
|
18
|
+
value = try(aws_cloudfront_distribution.open_uptime[0].domain_name, null)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
output "protected_access_url" {
|
|
22
|
+
value = var.protected_access_mode == "cloudfront_default_domain" ? "https://${aws_cloudfront_distribution.open_uptime[0].domain_name}" : "https://${var.hostname}"
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
output "evidence_bucket" {
|
|
18
26
|
value = aws_s3_bucket.evidence.bucket
|
|
19
27
|
}
|
|
@@ -5,13 +5,14 @@ hostname = "uptime.example.com"
|
|
|
5
5
|
workspace_id = "workspace-id"
|
|
6
6
|
vpc_id = "vpc-xxxxxxxx"
|
|
7
7
|
ecr_repository_name = "open-uptime"
|
|
8
|
+
protected_access_mode = "cloudfront_default_domain"
|
|
8
9
|
public_subnet_ids = ["subnet-replace-public-a", "subnet-replace-public-b"]
|
|
9
10
|
alb_ingress_cidr_blocks = []
|
|
10
11
|
private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
|
|
11
12
|
container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
12
|
-
runtime_package_version = "0.1.
|
|
13
|
-
certificate_arn =
|
|
14
|
-
hosted_zone_id =
|
|
13
|
+
runtime_package_version = "0.1.8"
|
|
14
|
+
certificate_arn = null
|
|
15
|
+
hosted_zone_id = null
|
|
15
16
|
app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/app/env"
|
|
16
17
|
hosted_token_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/hosted-token"
|
|
17
18
|
public_probe_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/probe/public"
|
package/infra/aws/variables.tf
CHANGED
|
@@ -46,13 +46,24 @@ variable "ecr_repository_name" {
|
|
|
46
46
|
default = "open-uptime"
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
variable "protected_access_mode" {
|
|
50
|
+
description = "Protected web access mode. cloudfront_default_domain uses the CloudFront HTTPS default domain and restricts ALB HTTP to CloudFront origin-facing ranges. alb_https_cert uses an ALB HTTPS listener with certificate_arn."
|
|
51
|
+
type = string
|
|
52
|
+
default = "cloudfront_default_domain"
|
|
53
|
+
|
|
54
|
+
validation {
|
|
55
|
+
condition = contains(["cloudfront_default_domain", "alb_https_cert"], var.protected_access_mode)
|
|
56
|
+
error_message = "protected_access_mode must be cloudfront_default_domain or alb_https_cert."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
variable "public_subnet_ids" {
|
|
50
61
|
description = "Public subnets for the ALB."
|
|
51
62
|
type = list(string)
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
variable "alb_ingress_cidr_blocks" {
|
|
55
|
-
description = "Approved HTTPS source CIDR blocks for
|
|
66
|
+
description = "Approved HTTPS source CIDR blocks for ALB HTTPS mode. Keep empty until edge/source policy is approved."
|
|
56
67
|
type = list(string)
|
|
57
68
|
default = []
|
|
58
69
|
}
|
|
@@ -75,7 +86,7 @@ variable "container_image" {
|
|
|
75
86
|
variable "runtime_package_version" {
|
|
76
87
|
description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
|
|
77
88
|
type = string
|
|
78
|
-
default = "0.1.
|
|
89
|
+
default = "0.1.8"
|
|
79
90
|
|
|
80
91
|
validation {
|
|
81
92
|
condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
|
|
@@ -84,8 +95,19 @@ variable "runtime_package_version" {
|
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
variable "certificate_arn" {
|
|
87
|
-
description = "ACM certificate ARN for HTTPS
|
|
98
|
+
description = "ACM certificate ARN for ALB HTTPS mode. Leave null when protected_access_mode is cloudfront_default_domain."
|
|
88
99
|
type = string
|
|
100
|
+
default = null
|
|
101
|
+
|
|
102
|
+
validation {
|
|
103
|
+
condition = var.certificate_arn == null || can(regex("^arn:aws:acm:", var.certificate_arn))
|
|
104
|
+
error_message = "certificate_arn must be null or an ACM certificate ARN."
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
validation {
|
|
108
|
+
condition = var.protected_access_mode != "alb_https_cert" || var.certificate_arn != null
|
|
109
|
+
error_message = "certificate_arn is required when protected_access_mode is alb_https_cert."
|
|
110
|
+
}
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
variable "hosted_zone_id" {
|
package/package.json
CHANGED