@hasna/uptime 0.1.7 → 0.1.9
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 +27 -3
- package/README.md +11 -5
- 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 +80 -31
- package/dist/cloud-plan.d.ts +11 -7
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +30 -22
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -26
- package/docs/aws-deployment-runbook.md +29 -11
- package/infra/aws/README.md +22 -1
- package/infra/aws/main.tf +138 -10
- package/infra/aws/outputs.tf +8 -0
- package/infra/aws/terraform.tfvars.example +12 -3
- package/infra/aws/variables.tf +72 -3
- package/package.json +1 -1
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.9");
|
|
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.",
|
|
@@ -146,9 +153,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
146
153
|
"Disable scheduler/reporter services before data rollback.",
|
|
147
154
|
"Restore EFS backup recovery point only after explicit operator approval and audit record."
|
|
148
155
|
],
|
|
149
|
-
|
|
156
|
+
privateProbe: [
|
|
150
157
|
"Create a private probe identity with a caller-managed public key.",
|
|
151
|
-
"Install @hasna/uptime on
|
|
158
|
+
"Install @hasna/uptime on the private probe operator machine and write the generated env file with mode 0600.",
|
|
152
159
|
"Run the private probe against the hosted /api/v1 probe endpoint once it exists."
|
|
153
160
|
]
|
|
154
161
|
},
|
|
@@ -157,17 +164,17 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
157
164
|
"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.",
|
|
158
165
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
159
166
|
"Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
|
|
160
|
-
"
|
|
167
|
+
"Private probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
|
|
161
168
|
],
|
|
162
169
|
requiredEvidence: [
|
|
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.",
|
|
170
|
-
"
|
|
177
|
+
"Private-probe registration, key-file mode, heartbeat, and revocation evidence."
|
|
171
178
|
],
|
|
172
179
|
safety: {
|
|
173
180
|
liveAwsMutation: false,
|
|
@@ -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.",
|
|
@@ -184,16 +192,16 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
184
192
|
}
|
|
185
193
|
};
|
|
186
194
|
}
|
|
187
|
-
function
|
|
195
|
+
function buildPrivateProbeCloudConfig(options = {}) {
|
|
188
196
|
const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
|
|
189
197
|
const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
|
|
190
|
-
const machineId = clean(options.machineId, "
|
|
191
|
-
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/
|
|
198
|
+
const machineId = clean(options.machineId, "private-probe-01");
|
|
199
|
+
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/private-probe-01.key.pem");
|
|
192
200
|
const probeId = options.probeId?.trim();
|
|
193
201
|
const blockers = [
|
|
194
202
|
...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
|
|
195
203
|
"Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
|
|
196
|
-
"
|
|
204
|
+
"Private probe enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
|
|
197
205
|
];
|
|
198
206
|
const env = {
|
|
199
207
|
HASNA_UPTIME_MODE: "hosted",
|
|
@@ -207,7 +215,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
207
215
|
if (probeId)
|
|
208
216
|
env.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
|
|
209
217
|
return {
|
|
210
|
-
kind: "open-uptime.
|
|
218
|
+
kind: "open-uptime.private-probe-cloud-config",
|
|
211
219
|
version: 1,
|
|
212
220
|
generatedAt: new Date().toISOString(),
|
|
213
221
|
status: "blocked",
|
|
@@ -219,7 +227,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
219
227
|
{
|
|
220
228
|
path: privateKeyFile,
|
|
221
229
|
mode: "0600",
|
|
222
|
-
purpose: "Ed25519 private key generated on
|
|
230
|
+
purpose: "Ed25519 private key generated on the private probe machine; never paste into cloud config."
|
|
223
231
|
},
|
|
224
232
|
{
|
|
225
233
|
path: "~/.hasna/uptime/cloud.env",
|
|
@@ -229,7 +237,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
229
237
|
],
|
|
230
238
|
commands: [
|
|
231
239
|
"bun install -g @hasna/uptime@latest",
|
|
232
|
-
"Generate the
|
|
240
|
+
"Generate the private probe key locally and register only its public key with the hosted control plane once registration exists.",
|
|
233
241
|
"Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
|
|
234
242
|
"Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
|
|
235
243
|
],
|
|
@@ -238,18 +246,18 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
238
246
|
privateKeyInline: false,
|
|
239
247
|
tokenInline: false,
|
|
240
248
|
notes: [
|
|
241
|
-
"This config is hosted-targeted preflight:
|
|
249
|
+
"This config is hosted-targeted preflight: the private probe must not start until cloud probe routes are backed by hosted state.",
|
|
242
250
|
"The private key file path is referenced, not embedded.",
|
|
243
251
|
"Hosted token or probe auth material must come from the machine secret store, not this generated config."
|
|
244
252
|
]
|
|
245
253
|
}
|
|
246
254
|
};
|
|
247
255
|
}
|
|
248
|
-
function
|
|
256
|
+
function renderPrivateProbeEnv(config) {
|
|
249
257
|
const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
|
|
250
258
|
const missing = required.filter((key) => !config.env[key]);
|
|
251
259
|
if (missing.length > 0) {
|
|
252
|
-
throw new Error(`
|
|
260
|
+
throw new Error(`private probe env output requires ${missing.join(", ")}`);
|
|
253
261
|
}
|
|
254
262
|
return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
|
|
255
263
|
`);
|
|
@@ -282,7 +290,7 @@ function shellEscape(value) {
|
|
|
282
290
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
283
291
|
}
|
|
284
292
|
export {
|
|
285
|
-
|
|
286
|
-
|
|
293
|
+
renderPrivateProbeEnv,
|
|
294
|
+
buildPrivateProbeCloudConfig,
|
|
287
295
|
buildAwsDeploymentPlan
|
|
288
296
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ export { createApiHandler, serveUptime } from "./api.js";
|
|
|
5
5
|
export { applyImport, previewImport, rollbackImport } from "./imports.js";
|
|
6
6
|
export { buildUptimeReport, sendUptimeReport } from "./report.js";
|
|
7
7
|
export { generateProbeKeyPair, probePublicKeyFingerprint, probeResultSigningPayload, signProbeResult, verifyProbeResultSignature } from "./probes.js";
|
|
8
|
-
export { buildAwsDeploymentPlan,
|
|
8
|
+
export { buildAwsDeploymentPlan, buildPrivateProbeCloudConfig, renderPrivateProbeEnv } from "./cloud-plan.js";
|
|
9
9
|
export { uptimeHome, uptimeDbPath, uptimeHostedFallbackDbPath, ensureUptimeHome } from "./paths.js";
|
|
10
10
|
export type { UptimeBackup, UptimeBackupCheck, UptimeRuntimeMode, UptimeStoreOptions, MonitorProvenance, SaveImportBatchInput, StoredImportBatch, UpsertMonitorProvenanceInput, } from "./store.js";
|
|
11
11
|
export type { BrowserPageRunner, BrowserPageRunnerResult, FetchLike, } from "./checks.js";
|
|
12
12
|
export type { ImportAction, ImportApplyItem, ImportApplyResult, ImportCandidate, ImportPreview, ImportPreviewItem, ImportRequest, ImportRollbackItem, ImportRollbackResult, ImportSource, } from "./imports.js";
|
|
13
13
|
export type { BrowserFailedRequest, BrowserPageEvidence, AuditEvent, CheckAttemptResult, CheckEvidence, CheckResult, CheckStatus, CreateMonitorKind, CreateMonitorInput, CreateReportScheduleInput, ImportedMonitorInput, ImportedUpdateMonitorInput, EvidenceArtifact, Incident, IncidentStatus, ListAuditEventsOptions, ListReportRunsOptions, ListResultsOptions, Monitor, MonitorKind, MonitorStatus, MonitorSummary, ProbeCheckJob, ProbeCheckJobStatus, ProbeIdentity, ProbeResultSubmission, ProbeSubmissionReceipt, RecordAuditEventInput, ReportDeliveryChannel, ReportDeliveryRecord, ReportEmailChannelConfig, ReportLogsChannelConfig, ReportRun, ReportRunStatus, ReportSchedule, ReportScheduleChannels, ReportScheduleStatus, ReportSmsChannelConfig, SchedulerHandle, UpdateMonitorInput, UpdateReportScheduleInput, UptimeSummary, } from "./types.js";
|
|
14
14
|
export type { ProbeKeyPair, ProbeSigningInput } from "./probes.js";
|
|
15
|
-
export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan,
|
|
15
|
+
export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan, PrivateProbeCloudConfig, PrivateProbeCloudConfigOptions, } from "./cloud-plan.js";
|
|
16
16
|
export type { BuildUptimeReportOptions, SendUptimeReportOptions, UptimeEmailReportTarget, UptimeLogsReportTarget, UptimeReport, UptimeReportDelivery, UptimeSmsReportTarget, } from "./report.js";
|
|
17
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,GACV,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,SAAS,EACT,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,8BAA8B,GAC/B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
|
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.9");
|
|
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.",
|
|
@@ -3967,9 +4013,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3967
4013
|
"Disable scheduler/reporter services before data rollback.",
|
|
3968
4014
|
"Restore EFS backup recovery point only after explicit operator approval and audit record."
|
|
3969
4015
|
],
|
|
3970
|
-
|
|
4016
|
+
privateProbe: [
|
|
3971
4017
|
"Create a private probe identity with a caller-managed public key.",
|
|
3972
|
-
"Install @hasna/uptime on
|
|
4018
|
+
"Install @hasna/uptime on the private probe operator machine and write the generated env file with mode 0600.",
|
|
3973
4019
|
"Run the private probe against the hosted /api/v1 probe endpoint once it exists."
|
|
3974
4020
|
]
|
|
3975
4021
|
},
|
|
@@ -3978,17 +4024,17 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3978
4024
|
"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.",
|
|
3979
4025
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
3980
4026
|
"Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
|
|
3981
|
-
"
|
|
4027
|
+
"Private probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
|
|
3982
4028
|
],
|
|
3983
4029
|
requiredEvidence: [
|
|
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.",
|
|
3991
|
-
"
|
|
4037
|
+
"Private-probe registration, key-file mode, heartbeat, and revocation evidence."
|
|
3992
4038
|
],
|
|
3993
4039
|
safety: {
|
|
3994
4040
|
liveAwsMutation: false,
|
|
@@ -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.",
|
|
@@ -4005,16 +4052,16 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4005
4052
|
}
|
|
4006
4053
|
};
|
|
4007
4054
|
}
|
|
4008
|
-
function
|
|
4055
|
+
function buildPrivateProbeCloudConfig(options = {}) {
|
|
4009
4056
|
const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
|
|
4010
4057
|
const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
|
|
4011
|
-
const machineId = clean(options.machineId, "
|
|
4012
|
-
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/
|
|
4058
|
+
const machineId = clean(options.machineId, "private-probe-01");
|
|
4059
|
+
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/private-probe-01.key.pem");
|
|
4013
4060
|
const probeId = options.probeId?.trim();
|
|
4014
4061
|
const blockers = [
|
|
4015
4062
|
...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
|
|
4016
4063
|
"Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
|
|
4017
|
-
"
|
|
4064
|
+
"Private probe enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
|
|
4018
4065
|
];
|
|
4019
4066
|
const env2 = {
|
|
4020
4067
|
HASNA_UPTIME_MODE: "hosted",
|
|
@@ -4028,7 +4075,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4028
4075
|
if (probeId)
|
|
4029
4076
|
env2.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
|
|
4030
4077
|
return {
|
|
4031
|
-
kind: "open-uptime.
|
|
4078
|
+
kind: "open-uptime.private-probe-cloud-config",
|
|
4032
4079
|
version: 1,
|
|
4033
4080
|
generatedAt: new Date().toISOString(),
|
|
4034
4081
|
status: "blocked",
|
|
@@ -4040,7 +4087,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4040
4087
|
{
|
|
4041
4088
|
path: privateKeyFile,
|
|
4042
4089
|
mode: "0600",
|
|
4043
|
-
purpose: "Ed25519 private key generated on
|
|
4090
|
+
purpose: "Ed25519 private key generated on the private probe machine; never paste into cloud config."
|
|
4044
4091
|
},
|
|
4045
4092
|
{
|
|
4046
4093
|
path: "~/.hasna/uptime/cloud.env",
|
|
@@ -4050,7 +4097,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4050
4097
|
],
|
|
4051
4098
|
commands: [
|
|
4052
4099
|
"bun install -g @hasna/uptime@latest",
|
|
4053
|
-
"Generate the
|
|
4100
|
+
"Generate the private probe key locally and register only its public key with the hosted control plane once registration exists.",
|
|
4054
4101
|
"Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
|
|
4055
4102
|
"Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
|
|
4056
4103
|
],
|
|
@@ -4059,18 +4106,18 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4059
4106
|
privateKeyInline: false,
|
|
4060
4107
|
tokenInline: false,
|
|
4061
4108
|
notes: [
|
|
4062
|
-
"This config is hosted-targeted preflight:
|
|
4109
|
+
"This config is hosted-targeted preflight: the private probe must not start until cloud probe routes are backed by hosted state.",
|
|
4063
4110
|
"The private key file path is referenced, not embedded.",
|
|
4064
4111
|
"Hosted token or probe auth material must come from the machine secret store, not this generated config."
|
|
4065
4112
|
]
|
|
4066
4113
|
}
|
|
4067
4114
|
};
|
|
4068
4115
|
}
|
|
4069
|
-
function
|
|
4116
|
+
function renderPrivateProbeEnv(config) {
|
|
4070
4117
|
const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
|
|
4071
4118
|
const missing = required.filter((key) => !config.env[key]);
|
|
4072
4119
|
if (missing.length > 0) {
|
|
4073
|
-
throw new Error(`
|
|
4120
|
+
throw new Error(`private probe env output requires ${missing.join(", ")}`);
|
|
4074
4121
|
}
|
|
4075
4122
|
return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
|
|
4076
4123
|
`);
|
|
@@ -4115,7 +4162,7 @@ export {
|
|
|
4115
4162
|
runHttpCheck,
|
|
4116
4163
|
runBrowserPageCheck,
|
|
4117
4164
|
rollbackImport,
|
|
4118
|
-
|
|
4165
|
+
renderPrivateProbeEnv,
|
|
4119
4166
|
probeResultSigningPayload,
|
|
4120
4167
|
probePublicKeyFingerprint,
|
|
4121
4168
|
previewImport,
|
|
@@ -4124,7 +4171,7 @@ export {
|
|
|
4124
4171
|
createUptimeClient,
|
|
4125
4172
|
createApiHandler,
|
|
4126
4173
|
buildUptimeReport,
|
|
4127
|
-
|
|
4174
|
+
buildPrivateProbeCloudConfig,
|
|
4128
4175
|
buildAwsDeploymentPlan,
|
|
4129
4176
|
applyImport,
|
|
4130
4177
|
UptimeStore,
|
|
@@ -8,7 +8,7 @@ call AWS or mutate infrastructure.
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
uptime cloud plan --json > open-uptime-aws-plan.json
|
|
11
|
-
uptime cloud
|
|
11
|
+
uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --env > private-probe-01-uptime.env
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Public package defaults are placeholders:
|
|
@@ -19,12 +19,13 @@ 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.
|
|
25
26
|
|
|
26
27
|
The generated AWS plan currently returns `status: "blocked"` and
|
|
27
|
-
`canApply: false`. The generated
|
|
28
|
+
`canApply: false`. The generated private-probe config returns `status: "blocked"` and
|
|
28
29
|
`canStart: false`. Treat both as review/preflight artifacts until the blockers
|
|
29
30
|
and required evidence in the JSON output are resolved.
|
|
30
31
|
|
|
@@ -32,7 +33,7 @@ The app repo includes a hosted runtime `Dockerfile` and Terraform/OpenTofu
|
|
|
32
33
|
starter files in `infra/aws`. The plan output points to these files and keeps
|
|
33
34
|
`applyAllowed: false`.
|
|
34
35
|
|
|
35
|
-
`uptime cloud
|
|
36
|
+
`uptime cloud private-probe-config --env` requires a real `--probe-id`; it will not
|
|
36
37
|
write a sourceable env file with a placeholder probe identity.
|
|
37
38
|
|
|
38
39
|
## Preflight
|
|
@@ -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,12 +87,14 @@ terraform -chdir=infra/aws validate
|
|
|
82
87
|
terraform -chdir=infra/aws plan -out open-uptime.tfplan
|
|
83
88
|
```
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
Use Terraform/OpenTofu 1.9 or newer for this starter.
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
## Private Probe Operator
|
|
93
|
+
|
|
94
|
+
The operator machine should be a private probe/operator machine, not the hosted
|
|
95
|
+
source of truth. The generated env file points the machine at hosted `/api/v1`
|
|
96
|
+
state and references a local private-key file path. It does not include private
|
|
97
|
+
key or token contents.
|
|
91
98
|
|
|
92
99
|
The private probe service should not be enabled until hosted probe claim/submit
|
|
93
100
|
routes are backed by cloud check jobs and cloud audit rows.
|
|
@@ -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,8 +115,16 @@ 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.
|
|
108
|
-
- Do not
|
|
118
|
+
- Do not expose the ALB directly in CloudFront mode; ALB ingress must be limited
|
|
119
|
+
to CloudFront origin-facing ranges.
|
|
120
|
+
- Do not treat CloudFront prefix-list ingress as distribution-bound origin
|
|
121
|
+
protection. Before enabling the web task, add CloudFront VPC origin/private
|
|
122
|
+
ALB routing or require a CloudFront-only origin header whose secret value is
|
|
123
|
+
not stored in Terraform state.
|
|
124
|
+
- Do not treat local SQLite, local project DBs, or private-probe local state as cloud
|
|
109
125
|
authority after cutover.
|
|
126
|
+
- Do configure owner/project/environment/service/cost-center tags and AWS
|
|
127
|
+
Budgets alert recipients in the approved infra root before live scale-out.
|
|
110
128
|
|
|
111
129
|
## Rollback
|
|
112
130
|
|
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,12 +33,31 @@ 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
|
+
|
|
44
|
+
CloudFront prefix-list ingress is only a network narrowing control; it is not
|
|
45
|
+
bound to one distribution. Add CloudFront VPC origin/private ALB routing or an
|
|
46
|
+
ALB origin-header rule with the secret value managed outside Terraform state
|
|
47
|
+
before enabling the web task.
|
|
48
|
+
|
|
49
|
+
All module resources carry owner, project, environment, service, account, app
|
|
50
|
+
type, and cost-center tags. Set `monthly_budget_limit_usd` plus
|
|
51
|
+
`budget_alert_email_addresses` in the approved infra root to create AWS Budgets
|
|
52
|
+
forecasted and actual spend alerts. Leaving the email list empty skips budget
|
|
53
|
+
creation and is not sufficient for live scale-out approval.
|
|
54
|
+
|
|
34
55
|
## Current Blockers
|
|
35
56
|
|
|
36
57
|
- Hosted production auth/RBAC still needs scoped, revocable credentials.
|
|
37
58
|
- Public probe runtime still needs execution-time DNS/redirect/rebinding SSRF
|
|
38
59
|
enforcement.
|
|
39
|
-
-
|
|
60
|
+
- Hosted private-probe enrollment/heartbeat/revocation is still
|
|
40
61
|
fail-closed.
|
|
41
62
|
|
|
42
63
|
Keep `desired_count` at `0`, or at `1` for the protected web bridge only after
|