rcf-lite 0.24.0 → 0.25.0
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 +11 -0
- package/blueprints/application-account-settings/docs/topics.md +3 -0
- package/blueprints/application-admin-console/docs/topics.md +3 -0
- package/blueprints/application-api-rest/docs/topics.md +3 -0
- package/blueprints/application-charts/docs/topics.md +3 -0
- package/blueprints/application-dashboard/docs/topics.md +3 -0
- package/blueprints/application-datatable/docs/topics.md +3 -0
- package/blueprints/application-empty-error-states/docs/topics.md +3 -0
- package/blueprints/application-error-handling/docs/topics.md +3 -0
- package/blueprints/application-file-upload/docs/topics.md +3 -0
- package/blueprints/application-forms-wizard/docs/topics.md +3 -0
- package/blueprints/application-notifications-in-app/docs/topics.md +3 -0
- package/blueprints/application-onboarding-tour/docs/topics.md +3 -0
- package/blueprints/application-spa/docs/topics.md +3 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +3 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -0
- package/blueprints/deploy-hetzner-server/CHANGELOG.md +34 -0
- package/blueprints/deploy-hetzner-server/README.md +202 -0
- package/blueprints/deploy-hetzner-server/blueprint.json +233 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +20 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +64 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +116 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +180 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +106 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +63 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs +60 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-throwaway-server-provision.mjs +58 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-cloud-init-render-lint.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-hcloud-dry-run-mock.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-manifest-schema-validate.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-cloud-init-hardened.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-snapshot-on-demand.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-throwaway-server-provision.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-001.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-002.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-003.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-004.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-005.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-006.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json +108 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3801-deploy-hetzner-server-provisioner.json +27 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +93 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37101.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37102.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37103.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37104.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37105.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37106.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37107.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37108.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37109.json +24 -0
- package/blueprints/deploy-hetzner-server/docs/topics.md +48 -0
- package/blueprints/deploy-hetzner-server/guide/deploy-hetzner-server.md +180 -0
- package/blueprints/edge-cloudflare-access/docs/topics.md +3 -0
- package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +3 -0
- package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +13 -0
- package/blueprints/edge-cloudflare-tunnel/README.md +257 -0
- package/blueprints/edge-cloudflare-tunnel/blueprint.json +168 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +23 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/aud-presence-check.mjs +114 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/cloudflared-config-lint.mjs +80 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +234 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/probe-utils.mjs +231 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +54 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +73 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-aud-presence-check.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-cloudflared-config-lint.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-manifest-schema-validate.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-connector-healthy.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-tunnel-hostname-routes.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/tunnel-manifest.schema.json +55 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-001.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-002.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-003.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-004.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-005.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +25 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39101.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39102.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39103.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39104.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39105.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39106.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39107.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39108.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/docs/topics.md +72 -0
- package/blueprints/edge-cloudflare-tunnel/guide/edge-cloudflare-tunnel.md +204 -0
- package/blueprints/edge-cloudflare-turnstile/docs/topics.md +3 -0
- package/blueprints/email-smtp-resend/docs/topics.md +3 -0
- package/blueprints/jobs-background/docs/topics.md +3 -0
- package/blueprints/messaging-queue-cloudflare/docs/topics.md +3 -0
- package/blueprints/object-storage-s3/docs/topics.md +3 -0
- package/blueprints/observability-essentials/docs/topics.md +3 -0
- package/blueprints/observability-logging/docs/topics.md +3 -0
- package/blueprints/observability-probe-endpoints/docs/topics.md +3 -0
- package/blueprints/persistence-data-d1/docs/topics.md +3 -0
- package/blueprints/persistence-data-postgres/docs/topics.md +3 -0
- package/blueprints/persistence-data-sqlite/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-kv/docs/topics.md +3 -0
- package/blueprints/platform-docker-compose-host/CHANGELOG.md +12 -0
- package/blueprints/platform-docker-compose-host/README.md +76 -0
- package/blueprints/platform-docker-compose-host/blueprint.json +209 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3901-platform-docker-compose-host-contract.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3904-platform-docker-compose-host-log-driver.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +108 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +231 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +185 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +41 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +36 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-caddyfile-validate.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-compose-config-lint.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-minimal-stack-up.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-reload-burst.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-secrets-as-files-scan.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +116 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-001.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-002.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-003.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-004.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-005.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-006.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38101.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38102.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38103.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38104.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38105.json +25 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38106.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38107.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38108.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38109.json +24 -0
- package/blueprints/platform-docker-compose-host/docs/topics.md +71 -0
- package/blueprints/platform-docker-compose-host/guide/platform-docker-compose-host.md +94 -0
- package/blueprints/security-auth-clerk/docs/topics.md +3 -0
- package/blueprints/security-auth-keycloak/docs/topics.md +3 -0
- package/blueprints/security-auth-magic-link/docs/topics.md +3 -0
- package/blueprints/security-auth-oauth2/docs/topics.md +3 -0
- package/blueprints/security-secrets-management/docs/topics.md +3 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +1 -1
- package/rcf/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +13 -0
- package/rcf/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +13 -0
- package/rcf/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +13 -0
- package/rcf/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +13 -0
- package/rcf/adrs/adr-3901-platform-docker-compose-host-contract.json +13 -0
- package/rcf/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +13 -0
- package/rcf/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +13 -0
- package/rcf/adrs/adr-3904-platform-docker-compose-host-log-driver.json +13 -0
- package/rcf/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +13 -0
- package/rcf/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +13 -0
- package/rcf/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +13 -0
- package/rcf/code-nodes/cn-410.json +20 -0
- package/rcf/code-nodes/cn-440.json +20 -0
- package/rcf/code-nodes/cn-470.json +19 -0
- package/rcf/fbs/fbs-130.json +23 -0
- package/rcf/fbs/fbs-140.json +23 -0
- package/rcf/fbs/fbs-150.json +22 -0
- package/rcf/requirements/req-110.json +58 -0
- package/rcf/requirements/req-111.json +32 -0
- package/rcf/requirements/req-112.json +42 -0
- package/rcf/requirements/req-113.json +33 -0
- package/rcf/requirements/req-114.json +20 -0
- package/rcf/requirements/req-115.json +38 -0
- package/rcf/requirements/req-120.json +20 -0
- package/rcf/requirements/req-121.json +20 -0
- package/rcf/requirements/req-122.json +20 -0
- package/rcf/requirements/req-123.json +42 -0
- package/rcf/requirements/req-124.json +27 -0
- package/rcf/requirements/req-125.json +20 -0
- package/rcf/requirements/req-130.json +32 -0
- package/rcf/requirements/req-131.json +54 -0
- package/rcf/requirements/req-132.json +27 -0
- package/rcf/requirements/req-133.json +20 -0
- package/rcf/requirements/req-134.json +42 -0
- package/rcf/tacs/tac-3801-deploy-hetzner-server-provisioner.json +17 -0
- package/rcf/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +17 -0
- package/rcf/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +17 -0
- package/rcf/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +16 -0
- package/rcf/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +17 -0
- package/rcf/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +17 -0
- package/rcf/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +17 -0
- package/rcf/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +17 -0
- package/rcf/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +17 -0
- package/rcf/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +17 -0
- package/rcf/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +16 -0
- package/rcf/test-suites/ts-140.json +86 -0
- package/rcf/test-suites/ts-150.json +86 -0
- package/rcf/test-suites/ts-160.json +78 -0
- package/rcf/user-stories/us-11001.json +23 -0
- package/rcf/user-stories/us-11101.json +23 -0
- package/rcf/user-stories/us-11102.json +24 -0
- package/rcf/user-stories/us-11201.json +23 -0
- package/rcf/user-stories/us-11202.json +23 -0
- package/rcf/user-stories/us-11301.json +24 -0
- package/rcf/user-stories/us-11401.json +23 -0
- package/rcf/user-stories/us-11402.json +23 -0
- package/rcf/user-stories/us-11501.json +23 -0
- package/rcf/user-stories/us-12001.json +23 -0
- package/rcf/user-stories/us-12101.json +23 -0
- package/rcf/user-stories/us-12102.json +23 -0
- package/rcf/user-stories/us-12201.json +23 -0
- package/rcf/user-stories/us-12202.json +24 -0
- package/rcf/user-stories/us-12301.json +23 -0
- package/rcf/user-stories/us-12401.json +23 -0
- package/rcf/user-stories/us-12402.json +23 -0
- package/rcf/user-stories/us-12501.json +23 -0
- package/rcf/user-stories/us-13001.json +23 -0
- package/rcf/user-stories/us-13002.json +23 -0
- package/rcf/user-stories/us-13101.json +23 -0
- package/rcf/user-stories/us-13102.json +23 -0
- package/rcf/user-stories/us-13201.json +23 -0
- package/rcf/user-stories/us-13301.json +23 -0
- package/rcf/user-stories/us-13302.json +23 -0
- package/rcf/user-stories/us-13401.json +23 -0
- package/releases/releases.yaml +11 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Shared helpers for deploy-hetzner-server probes.
|
|
2
|
+
//
|
|
3
|
+
// Runtime-dependency posture:
|
|
4
|
+
// - cloud-init-render-lint, manifest-schema-validate and
|
|
5
|
+
// hcloud-dry-run-mock run in-process against the shared throwaway-
|
|
6
|
+
// server fixture at packages/rcf-lite/test/fixtures/hetzner-throwaway-
|
|
7
|
+
// server/. No real API call fires; hcloud is mocked via
|
|
8
|
+
// src/hcloud-mock.mjs so the shim never crosses the process boundary.
|
|
9
|
+
// - real-account-* probes call the fixture's provision.mjs / destroy.mjs
|
|
10
|
+
// / snapshot verbs. Without CI_HAS_HETZNER_ACCOUNT they record
|
|
11
|
+
// accountBoundSkipped: true and the aggregate flips to pass per
|
|
12
|
+
// hetzner-round-7-spec-2026-09-07.md section 3.5.
|
|
13
|
+
|
|
14
|
+
import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
|
|
15
|
+
import { dirname, resolve, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
export const PROJECT_ROOT = resolve(HERE, '..', '..', '..', '..');
|
|
20
|
+
export const FIXTURE_DIR = resolve(
|
|
21
|
+
PROJECT_ROOT,
|
|
22
|
+
'packages/rcf-lite/test/fixtures/hetzner-throwaway-server',
|
|
23
|
+
);
|
|
24
|
+
export const MANIFEST_DIR = resolve(FIXTURE_DIR, 'hetzner/servers');
|
|
25
|
+
export const SCHEMA_PATH = resolve(HERE, '..', 'schemas', 'hetzner-server.schema.json');
|
|
26
|
+
export const REPORT_DIR = resolve(
|
|
27
|
+
PROJECT_ROOT,
|
|
28
|
+
'.rcf/reports/blueprints/deploy-hetzner-server',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export function aggregate(results) {
|
|
32
|
+
if (results.some((r) => r.verdict === 'fail')) return 'fail';
|
|
33
|
+
if (results.some((r) => r.verdict === 'warn')) return 'warn';
|
|
34
|
+
return 'pass';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isSkipped(results) {
|
|
38
|
+
return results.length > 0 && results.every((r) => r.accountBoundSkipped === true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function writeReport({ probeName, engine, results, extra }) {
|
|
42
|
+
await mkdir(REPORT_DIR, { recursive: true });
|
|
43
|
+
const rawVerdict = aggregate(results);
|
|
44
|
+
const aggregateVerdict = isSkipped(results) ? 'pass' : rawVerdict;
|
|
45
|
+
const report = {
|
|
46
|
+
slug: 'deploy-hetzner-server',
|
|
47
|
+
probeName,
|
|
48
|
+
runAt: new Date().toISOString(),
|
|
49
|
+
engine,
|
|
50
|
+
results,
|
|
51
|
+
aggregateVerdict,
|
|
52
|
+
...(extra ?? {}),
|
|
53
|
+
};
|
|
54
|
+
const path = resolve(REPORT_DIR, `${probeName}.json`);
|
|
55
|
+
await writeFile(path, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
56
|
+
return { report, path };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runShim(probeName, engine, mainFn) {
|
|
60
|
+
try {
|
|
61
|
+
const outcome = (await mainFn()) ?? { results: [] };
|
|
62
|
+
const { results, extra } = outcome;
|
|
63
|
+
const { report, path } = await writeReport({ probeName, engine, results, extra });
|
|
64
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
65
|
+
process.stdout.write(`report written to ${path}\n`);
|
|
66
|
+
if (report.aggregateVerdict === 'fail') process.exitCode = 1;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const results = [{
|
|
69
|
+
anchorAcId: 'unknown',
|
|
70
|
+
verdict: 'fail',
|
|
71
|
+
detail: `probe threw: ${err && err.message ? err.message : String(err)}`,
|
|
72
|
+
}];
|
|
73
|
+
const { report, path } = await writeReport({ probeName, engine, results });
|
|
74
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
75
|
+
process.stderr.write(`probe error: ${err && err.stack ? err.stack : String(err)}\n`);
|
|
76
|
+
process.stderr.write(`report written to ${path}\n`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function readManifestFiles(dir = MANIFEST_DIR) {
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = (await readdir(dir))
|
|
85
|
+
.filter((f) => f.endsWith('.json'))
|
|
86
|
+
.sort();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { present: false, entries: [], files: [], error: err.message };
|
|
89
|
+
}
|
|
90
|
+
const files = [];
|
|
91
|
+
for (const name of entries) {
|
|
92
|
+
const p = join(dir, name);
|
|
93
|
+
const text = await readFile(p, 'utf8');
|
|
94
|
+
files.push({ name, path: p, text });
|
|
95
|
+
}
|
|
96
|
+
return { present: entries.length > 0, entries, files };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function accountBoundSkippedResult(anchorAcId, note) {
|
|
100
|
+
return {
|
|
101
|
+
anchorAcId,
|
|
102
|
+
verdict: 'skipped',
|
|
103
|
+
accountBoundSkipped: true,
|
|
104
|
+
detail: `accountBound: CI_HAS_HETZNER_ACCOUNT unset; ${note}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Probe: real-account cloud-init hardened.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-37105-1. accountBound: true.
|
|
4
|
+
//
|
|
5
|
+
// Without CI_HAS_HETZNER_ACCOUNT the probe records
|
|
6
|
+
// accountBoundSkipped: true and the aggregate flips to pass.
|
|
7
|
+
//
|
|
8
|
+
// With the env var set the probe:
|
|
9
|
+
// - provisions the throwaway server via the fixture provision.mjs;
|
|
10
|
+
// - shells cloud-init status --wait over ssh to the throwaway server;
|
|
11
|
+
// - runs the six baseline checks over ssh (grep for the marker per
|
|
12
|
+
// baseline block); FAILS naming the first missing block on any
|
|
13
|
+
// defect.
|
|
14
|
+
// - always() calls destroy.mjs regardless of verdict.
|
|
15
|
+
|
|
16
|
+
import { accountBoundSkippedResult, FIXTURE_DIR } from './probe-utils.mjs';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
|
|
19
|
+
export const anchorAcId = 'AC-37105-1';
|
|
20
|
+
export const accountBound = true;
|
|
21
|
+
|
|
22
|
+
export default async function runProbe() {
|
|
23
|
+
if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
|
|
24
|
+
return {
|
|
25
|
+
results: [accountBoundSkippedResult(anchorAcId, 'real-account cloud-init hardened check skipped; run with CI_HAS_HETZNER_ACCOUNT=true to exercise the six ssh baseline checks against a throwaway cx23.')],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const provisionPath = resolve(FIXTURE_DIR, 'provision.mjs');
|
|
29
|
+
const destroyPath = resolve(FIXTURE_DIR, 'destroy.mjs');
|
|
30
|
+
const sshCheckPath = resolve(FIXTURE_DIR, 'src/ssh-baseline-check.mjs');
|
|
31
|
+
const { provisionThrowawayServer } = await import(provisionPath);
|
|
32
|
+
const { destroyThrowawayServer } = await import(destroyPath);
|
|
33
|
+
const { runSshBaselineChecks } = await import(sshCheckPath);
|
|
34
|
+
let provisioned;
|
|
35
|
+
try {
|
|
36
|
+
provisioned = await provisionThrowawayServer({ runId: process.env.GITHUB_RUN_ID ?? 'local' });
|
|
37
|
+
const report = await runSshBaselineChecks(provisioned);
|
|
38
|
+
const failed = report.checks.filter((c) => c.verdict === 'fail');
|
|
39
|
+
if (failed.length > 0) {
|
|
40
|
+
return {
|
|
41
|
+
results: failed.map((c) => ({
|
|
42
|
+
anchorAcId,
|
|
43
|
+
verdict: 'fail',
|
|
44
|
+
detail: `cloud-init baseline block "${c.label}" failed on server ${provisioned.id}: ${c.detail}`,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
results: [{
|
|
50
|
+
anchorAcId,
|
|
51
|
+
verdict: 'pass',
|
|
52
|
+
detail: `cloud-init status --wait completed on server ${provisioned.id}; all six ssh baseline checks passed.`,
|
|
53
|
+
}],
|
|
54
|
+
extra: report,
|
|
55
|
+
};
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { results: [{ anchorAcId, verdict: 'fail', detail: `check failed: ${err.message}` }] };
|
|
58
|
+
} finally {
|
|
59
|
+
if (provisioned && provisioned.id) {
|
|
60
|
+
try { await destroyThrowawayServer(provisioned); } catch (_) { /* swept by orphan cron */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Probe: real-account snapshot on demand.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-37108-1. accountBound: true.
|
|
4
|
+
//
|
|
5
|
+
// Without CI_HAS_HETZNER_ACCOUNT the probe records
|
|
6
|
+
// accountBoundSkipped: true and the aggregate flips to pass.
|
|
7
|
+
//
|
|
8
|
+
// With the env var set the probe:
|
|
9
|
+
// - provisions the throwaway server via provision.mjs;
|
|
10
|
+
// - fires the snapshot verb on the provisioner facade;
|
|
11
|
+
// - asserts hcloud image list --type=snapshot --output json shows a
|
|
12
|
+
// snapshot with the server name and current wall-clock time in the
|
|
13
|
+
// labels;
|
|
14
|
+
// - always() calls destroy.mjs regardless of verdict; the snapshot is
|
|
15
|
+
// deleted as part of destroy.mjs so the run leaves no orphaned
|
|
16
|
+
// image behind.
|
|
17
|
+
|
|
18
|
+
import { accountBoundSkippedResult, FIXTURE_DIR } from './probe-utils.mjs';
|
|
19
|
+
import { resolve } from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const anchorAcId = 'AC-37108-1';
|
|
22
|
+
export const accountBound = true;
|
|
23
|
+
|
|
24
|
+
export default async function runProbe() {
|
|
25
|
+
if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
|
|
26
|
+
return {
|
|
27
|
+
results: [accountBoundSkippedResult(anchorAcId, 'real-account snapshot on-demand skipped; run with CI_HAS_HETZNER_ACCOUNT=true to exercise the hcloud image create-image path.')],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const provisionPath = resolve(FIXTURE_DIR, 'provision.mjs');
|
|
31
|
+
const destroyPath = resolve(FIXTURE_DIR, 'destroy.mjs');
|
|
32
|
+
const snapshotPath = resolve(FIXTURE_DIR, 'src/snapshot-verb.mjs');
|
|
33
|
+
const { provisionThrowawayServer } = await import(provisionPath);
|
|
34
|
+
const { destroyThrowawayServer } = await import(destroyPath);
|
|
35
|
+
const { takeAndVerifySnapshot } = await import(snapshotPath);
|
|
36
|
+
let provisioned;
|
|
37
|
+
try {
|
|
38
|
+
provisioned = await provisionThrowawayServer({ runId: process.env.GITHUB_RUN_ID ?? 'local' });
|
|
39
|
+
const outcome = await takeAndVerifySnapshot(provisioned);
|
|
40
|
+
if (!outcome.snapshotId) {
|
|
41
|
+
return {
|
|
42
|
+
results: [{ anchorAcId, verdict: 'fail', detail: `snapshot verb ran but no matching snapshot appeared in hcloud image list.` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
results: [{
|
|
47
|
+
anchorAcId,
|
|
48
|
+
verdict: 'pass',
|
|
49
|
+
detail: `snapshot ${outcome.snapshotId} tagged with server name ${provisioned.name} and time ${outcome.wallClockTime} on server ${provisioned.id}.`,
|
|
50
|
+
}],
|
|
51
|
+
extra: outcome,
|
|
52
|
+
};
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return { results: [{ anchorAcId, verdict: 'fail', detail: `snapshot verb threw: ${err.message}` }] };
|
|
55
|
+
} finally {
|
|
56
|
+
if (provisioned && provisioned.id) {
|
|
57
|
+
try { await destroyThrowawayServer(provisioned); } catch (_) { /* swept by orphan cron */ }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Probe: real-account throwaway-server provision.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-37103-1. accountBound: true.
|
|
4
|
+
//
|
|
5
|
+
// Without CI_HAS_HETZNER_ACCOUNT the probe records
|
|
6
|
+
// accountBoundSkipped: true and the aggregate flips to pass per
|
|
7
|
+
// hetzner-round-7-spec-2026-09-07.md section 3.5.
|
|
8
|
+
//
|
|
9
|
+
// With the env var set the probe:
|
|
10
|
+
// - runs the fixture's provision.mjs against the ci-throwaway manifest;
|
|
11
|
+
// - asserts a new server appears in `hcloud server list --output json`
|
|
12
|
+
// whose id matches the scratch-file-written id;
|
|
13
|
+
// - asserts the fixture's captured hetznerServerProvisioned event
|
|
14
|
+
// carries id, primaryIpv4, location=fsn1, serverType=cx23;
|
|
15
|
+
// - always() calls destroy.mjs so the throwaway server is torn down
|
|
16
|
+
// regardless of verdict.
|
|
17
|
+
|
|
18
|
+
import { accountBoundSkippedResult, FIXTURE_DIR } from './probe-utils.mjs';
|
|
19
|
+
import { resolve } from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const anchorAcId = 'AC-37103-1';
|
|
22
|
+
export const accountBound = true;
|
|
23
|
+
|
|
24
|
+
export default async function runProbe() {
|
|
25
|
+
if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
|
|
26
|
+
return {
|
|
27
|
+
results: [accountBoundSkippedResult(anchorAcId, 'real-account throwaway-server provision skipped; run with CI_HAS_HETZNER_ACCOUNT=true to exercise.')],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const provisionPath = resolve(FIXTURE_DIR, 'provision.mjs');
|
|
31
|
+
const destroyPath = resolve(FIXTURE_DIR, 'destroy.mjs');
|
|
32
|
+
const { provisionThrowawayServer } = await import(provisionPath);
|
|
33
|
+
const { destroyThrowawayServer } = await import(destroyPath);
|
|
34
|
+
let provisioned;
|
|
35
|
+
try {
|
|
36
|
+
provisioned = await provisionThrowawayServer({ runId: process.env.GITHUB_RUN_ID ?? 'local' });
|
|
37
|
+
return {
|
|
38
|
+
results: [{
|
|
39
|
+
anchorAcId,
|
|
40
|
+
verdict: 'pass',
|
|
41
|
+
detail: `throwaway server ${provisioned.id} provisioned; hetznerServerProvisioned event captured; primaryIpv4=${provisioned.primaryIpv4} location=${provisioned.location} serverType=${provisioned.serverType}.`,
|
|
42
|
+
}],
|
|
43
|
+
extra: { serverId: provisioned.id, primaryIpv4: provisioned.primaryIpv4 },
|
|
44
|
+
};
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
results: [{
|
|
48
|
+
anchorAcId,
|
|
49
|
+
verdict: 'fail',
|
|
50
|
+
detail: `provision failed: ${err.message}`,
|
|
51
|
+
}],
|
|
52
|
+
};
|
|
53
|
+
} finally {
|
|
54
|
+
if (provisioned && provisioned.id) {
|
|
55
|
+
try { await destroyThrowawayServer(provisioned); } catch (_) { /* swept by orphan cron */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import runProbe from './cloud-init-render-lint.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = {
|
|
5
|
+
kind: 'template-render',
|
|
6
|
+
image: 'hetzner-throwaway-server fixture (in-process template render)',
|
|
7
|
+
healthy: true,
|
|
8
|
+
};
|
|
9
|
+
runShim('cloud-init-render-lint', engine, runProbe);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import runProbe from './hcloud-dry-run-mock.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = {
|
|
5
|
+
kind: 'facade-round-trip',
|
|
6
|
+
image: 'hetzner-throwaway-server fixture provisioner facade against a mocked hcloud shim',
|
|
7
|
+
healthy: true,
|
|
8
|
+
};
|
|
9
|
+
runShim('hcloud-dry-run-mock', engine, runProbe);
|
package/blueprints/deploy-hetzner-server/contributions/probes/run-manifest-schema-validate.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import runProbe from './manifest-schema-validate.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = {
|
|
5
|
+
kind: 'schema-validate',
|
|
6
|
+
image: 'hetzner-throwaway-server fixture manifest directory (draft-07 subset in-process)',
|
|
7
|
+
healthy: true,
|
|
8
|
+
};
|
|
9
|
+
runShim('manifest-schema-validate', engine, runProbe);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import runProbe from './real-account-cloud-init-hardened.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = {
|
|
5
|
+
kind: 'real-account-ssh',
|
|
6
|
+
image: 'Hetzner Cloud throwaway cx23 with cloud-init user-data (ssh baseline checks)',
|
|
7
|
+
healthy: true,
|
|
8
|
+
};
|
|
9
|
+
runShim('real-account-cloud-init-hardened', engine, runProbe);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import runProbe from './real-account-snapshot-on-demand.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = {
|
|
5
|
+
kind: 'real-account-snapshot',
|
|
6
|
+
image: 'Hetzner Cloud throwaway cx23 with snapshot verb via hcloud image create-image',
|
|
7
|
+
healthy: true,
|
|
8
|
+
};
|
|
9
|
+
runShim('real-account-snapshot-on-demand', engine, runProbe);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import runProbe from './real-account-throwaway-server-provision.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = {
|
|
5
|
+
kind: 'real-account-throwaway-server',
|
|
6
|
+
image: 'Hetzner Cloud API via hcloud CLI (provision + list + destroy)',
|
|
7
|
+
healthy: true,
|
|
8
|
+
};
|
|
9
|
+
runShim('real-account-throwaway-server-provision', engine, runProbe);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reqId": "deploy-hetzner-server-REQ-001",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"title": "The provisioner facade is the sole reader of the Hetzner API token and opens on boot with provisionerReady",
|
|
5
|
+
"description": "The applying project reaches the Hetzner Cloud API through a single provisioner facade module that is the ONLY module holding a reference to the HETZNER_ACCOUNT_API_KEY secret (or the elicited project-scoped alternative). The facade wraps typed verbs (createServer, applyFirewall, takeSnapshot, listSnapshots, destroyServer) and, when provisioningTool=hcloud (default), shells to the hcloud CLI with `--output json`; when provisioningTool=raw-api it fetches the equivalent Hetzner Cloud REST endpoints per https://docs.hetzner.cloud/. On process boot the facade opens against the token via a bounded ready-check and emits a provisionerReady lifecycle event on the injected event sink with a metadata-only payload {tool, apiHost}. No other module reads the token; the injected event sink never carries the token or user-data bodies.",
|
|
6
|
+
"category": "functional",
|
|
7
|
+
"domain": "platform",
|
|
8
|
+
"priority": "must",
|
|
9
|
+
"rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-001). Ships as v1.0.0 of deploy-hetzner-server.",
|
|
10
|
+
"tags": [
|
|
11
|
+
"blueprint:deploy-hetzner-server",
|
|
12
|
+
"capability:cloudHost"
|
|
13
|
+
],
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"status": "approved",
|
|
16
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
17
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reqId": "deploy-hetzner-server-REQ-002",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"title": "The applying project holds server declarations at hetzner/servers/name.json against a shipped JSON Schema",
|
|
5
|
+
"description": "The applying project holds every server declaration as one JSON document under hetzner/servers/<name>.json, matching a shipped draft-07 JSON Schema at blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json. Required fields: name, serverType (cx23, cx33, and other Hetzner shared-cpu shapes), location (fsn1 default per Baz ruling 2026-09-07 09:50Z; also nbg1, hel1, ash, hil), image (ubuntu-24.04 default), sshKeyIds (nonempty array of Hetzner ssh-key references delegated to security-secrets-management), firewallId (an existing firewall or auto-create), networkId (optional private network), cloudInitPath (relative path to the rendered cloud-init user-data), labels (Hetzner tag map that must include role and blueprint), firewallRules (see REQ-004), snapshotCadence (see REQ-005). The schema refuses on missing required fields and on rule-shape violations (see REQ-004).",
|
|
6
|
+
"category": "functional",
|
|
7
|
+
"domain": "platform",
|
|
8
|
+
"priority": "must",
|
|
9
|
+
"rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-002).",
|
|
10
|
+
"tags": [
|
|
11
|
+
"blueprint:deploy-hetzner-server",
|
|
12
|
+
"capability:cloudHost"
|
|
13
|
+
],
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"status": "approved",
|
|
16
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
17
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reqId": "deploy-hetzner-server-REQ-003",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"title": "Cloud-init template renders YAML that lands the six hardening baseline blocks on every provisioned server",
|
|
5
|
+
"description": "The blueprint ships a cloud-init user-data template at blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl. Rendering the template against a manifest lands the six baseline blocks named by the estate baseline at operator/knowledge/systems/environments/hetzner-cloud/overview.md Security posture baseline and by the Debian unattended-upgrades wiki: (1) SSH key-only (PasswordAuthentication no via write_files on /etc/ssh/sshd_config.d/hardening.conf), (2) root disabled (PermitRootLogin no via the same file), (3) UFW default-deny incoming with 22, 80, 443 allowed per the manifest firewall shape, (4) DOCKER-USER iptables chain wired to drop non-conforming egress, (5) fail2ban SSH jail, (6) unattended-upgrades on for security updates. A render-lint asserts every block appears in the rendered YAML; a mutation SIMULATE_HARDENING_DRIFT=true removes the unattended-upgrades block and the lint FAILS naming the missing line.",
|
|
6
|
+
"category": "functional",
|
|
7
|
+
"domain": "platform",
|
|
8
|
+
"priority": "must",
|
|
9
|
+
"rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-003).",
|
|
10
|
+
"tags": [
|
|
11
|
+
"blueprint:deploy-hetzner-server",
|
|
12
|
+
"capability:cloudHost"
|
|
13
|
+
],
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"status": "approved",
|
|
16
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
17
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reqId": "deploy-hetzner-server-REQ-004",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"title": "Firewall shape: 22 from operator-nominated prefixes, 80/443 open, all other inbound denied, applied at provision time",
|
|
5
|
+
"description": "Every hetzner/servers/name.json manifest carries a firewallRules array with the three-rule minimum shape: (a) an ssh rule permitting inbound TCP on 22 from a nonempty list of source-prefix ranges (the operator-nominated set; the schema refuses on 0.0.0.0/0 in this list), (b) an http rule permitting inbound TCP on 80 from any source, (c) an https rule permitting inbound TCP on 443 from any source, (d) an implicit deny for every other inbound rule. The provisioner applies the rule set to the server at provision time via hcloud firewall create (or reuse) plus hcloud firewall apply-to-resource. A raw-api provisioner posts the equivalent create-firewall and apply-to-resources bodies per https://docs.hetzner.cloud/#firewalls. Manifest schema validation refuses on any rule-set shape that violates (a) through (c).",
|
|
6
|
+
"category": "functional",
|
|
7
|
+
"domain": "platform",
|
|
8
|
+
"priority": "must",
|
|
9
|
+
"rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-004).",
|
|
10
|
+
"tags": [
|
|
11
|
+
"blueprint:deploy-hetzner-server",
|
|
12
|
+
"capability:cloudHost"
|
|
13
|
+
],
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"status": "approved",
|
|
16
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
17
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reqId": "deploy-hetzner-server-REQ-005",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"title": "Snapshot cadence elicited (weekly default, daily and off alternatives), snapshot-on-demand fires a tagged snapshot",
|
|
5
|
+
"description": "The applying project elicits a snapshot cadence per ADR-3802 with an enum of weekly (recommended default), daily and off. The value lives on hetzner/servers/name.json as snapshotCadence. When the cadence is non-off, a scheduled snapshot runner in the applying project fires the snapshot verb on the provisioner facade against every server whose cadence matches, and tags each snapshot with the server name and wall-clock time. A snapshot-on-demand verb on the provisioner fires the same shape without a schedule. Under provisioningTool=hcloud the facade shells to hcloud image create-image and tags via hcloud image update; under raw-api it posts to POST /servers/{id}/actions/create_image and PATCH /images/{id}. The hetznerSnapshotTaken lifecycle event carries snapshot id and wall-clock time. Hetzner-managed backups are a separately elicited paid opt-in (approximately 20 percent of monthly server price per https://docs.hetzner.com/cloud/servers/backups-snapshots/overview) and compose with the scheduled snapshot runner.",
|
|
6
|
+
"category": "functional",
|
|
7
|
+
"domain": "platform",
|
|
8
|
+
"priority": "must",
|
|
9
|
+
"rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-005).",
|
|
10
|
+
"tags": [
|
|
11
|
+
"blueprint:deploy-hetzner-server",
|
|
12
|
+
"capability:cloudHost"
|
|
13
|
+
],
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"status": "approved",
|
|
16
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
17
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reqId": "deploy-hetzner-server-REQ-006",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"title": "Four lifecycle events fire with metadata-only payloads and no secret ever crosses the sink boundary",
|
|
5
|
+
"description": "The provisioner facade emits four lifecycle events on the injected event sink: provisionerReady (tool, apiHost), hetznerServerProvisioned (id, primaryIpv4, location, serverType, labels), hetznerSnapshotTaken (serverId, snapshotId, wallClockTime), hetznerServerDestroyed (id, wallClockTime). Every payload carries only metadata; no event body carries the HETZNER_ACCOUNT_API_KEY value, an ssh private key, or a rendered user-data body. The event-secrecy check inside the hcloud-dry-run-mock probe accumulates every emitted event body across a full lifecycle run and asserts the token, an ssh private-key marker (BEGIN OPENSSH PRIVATE KEY), and the elicited user-data bytes appear in NO event body; a mutation SIMULATE_EVENT_SECRECY_LEAK=true injects the token into the hetznerServerProvisioned payload and the probe FAILS with a defensive-fake finding naming the leaked field.",
|
|
6
|
+
"category": "functional",
|
|
7
|
+
"domain": "platform",
|
|
8
|
+
"priority": "must",
|
|
9
|
+
"rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-006).",
|
|
10
|
+
"tags": [
|
|
11
|
+
"blueprint:deploy-hetzner-server",
|
|
12
|
+
"capability:cloudHost"
|
|
13
|
+
],
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"status": "approved",
|
|
16
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
17
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
18
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://stravica.ai/rcf-lite/schemas/hetzner-server.schema.json",
|
|
4
|
+
"title": "Hetzner Cloud server manifest file",
|
|
5
|
+
"description": "One server per file under the applying project's hetzner/servers/ directory. Fields track the Hetzner Cloud API server resource shape at https://docs.hetzner.cloud/.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"name",
|
|
10
|
+
"serverType",
|
|
11
|
+
"location",
|
|
12
|
+
"image",
|
|
13
|
+
"sshKeyIds",
|
|
14
|
+
"firewallId",
|
|
15
|
+
"cloudInitPath",
|
|
16
|
+
"labels",
|
|
17
|
+
"firewallRules",
|
|
18
|
+
"snapshotCadence"
|
|
19
|
+
],
|
|
20
|
+
"properties": {
|
|
21
|
+
"name": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"description": "Operator-facing stable server name (kebab-case). Used as the Hetzner server name and prefixed onto snapshot names."
|
|
25
|
+
},
|
|
26
|
+
"serverType": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["cx23", "cx33", "cx43", "cpx11", "cpx21", "cpx31", "cpx41", "cax11", "cax21", "cax31"],
|
|
29
|
+
"description": "Hetzner Cloud shared-vCPU server type. cx23 is the shipped default per the round-7 spec section 5.1."
|
|
30
|
+
},
|
|
31
|
+
"location": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"enum": ["fsn1", "nbg1", "hel1", "ash", "hil"],
|
|
34
|
+
"description": "Hetzner data centre location. fsn1 is the shipped default per Baz ruling 2026-09-07 09:50Z on the Q2 ratification."
|
|
35
|
+
},
|
|
36
|
+
"image": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"description": "Hetzner Cloud base image identifier. ubuntu-24.04 is the shipped default."
|
|
40
|
+
},
|
|
41
|
+
"sshKeyIds": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"minItems": 1,
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"minLength": 1
|
|
47
|
+
},
|
|
48
|
+
"description": "Nonempty array of Hetzner ssh-key references. Delegated to security-secrets-management for the actual key material."
|
|
49
|
+
},
|
|
50
|
+
"networkId": {
|
|
51
|
+
"type": ["string", "null"],
|
|
52
|
+
"description": "Optional Hetzner Cloud private network id. null for no attached private network."
|
|
53
|
+
},
|
|
54
|
+
"firewallId": {
|
|
55
|
+
"type": ["string", "null"],
|
|
56
|
+
"description": "Existing Hetzner Cloud firewall id to reuse; when null the provisioner creates one from firewallRules."
|
|
57
|
+
},
|
|
58
|
+
"cloudInitPath": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"minLength": 1,
|
|
61
|
+
"description": "Relative path to the rendered cloud-init user-data file. The renderer at src/cloud-init-renderer.mjs consumes contributions/templates/cloud-init.yaml.tmpl and writes the output here."
|
|
62
|
+
},
|
|
63
|
+
"labels": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"additionalProperties": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
68
|
+
"required": [
|
|
69
|
+
"role",
|
|
70
|
+
"blueprint"
|
|
71
|
+
],
|
|
72
|
+
"description": "Hetzner tag map. Must include role and blueprint. Under the shared throwaway-server fixture role=rcf-lite-ci-throwaway is required per section 3.3 of the round-7 spec."
|
|
73
|
+
},
|
|
74
|
+
"firewallRules": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"minItems": 3,
|
|
77
|
+
"description": "The rule shape from TAC-3804. Must include an ssh rule (protocol tcp, port 22, direction in, sourceIps nonempty and NOT containing 0.0.0.0/0), an http rule (protocol tcp, port 80, direction in, sourceIps [\"0.0.0.0/0\", \"::/0\"]) and an https rule (protocol tcp, port 443, direction in, sourceIps [\"0.0.0.0/0\", \"::/0\"]).",
|
|
78
|
+
"items": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"required": ["name", "protocol", "port", "direction", "sourceIps"],
|
|
82
|
+
"properties": {
|
|
83
|
+
"name": { "type": "string", "enum": ["ssh", "http", "https"] },
|
|
84
|
+
"protocol": { "type": "string", "enum": ["tcp", "udp", "icmp"] },
|
|
85
|
+
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
|
86
|
+
"direction": { "type": "string", "enum": ["in", "out"] },
|
|
87
|
+
"sourceIps": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"minItems": 1,
|
|
90
|
+
"items": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"minLength": 1
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"snapshotCadence": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"enum": ["weekly", "daily", "off"],
|
|
101
|
+
"description": "Snapshot cadence per ADR-3802. weekly is the recommended default."
|
|
102
|
+
},
|
|
103
|
+
"hetznerManagedBackups": {
|
|
104
|
+
"type": "boolean",
|
|
105
|
+
"description": "Optional paid opt-in for Hetzner-managed backups per ADR-3802 (approximately 20 percent of monthly server price)."
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tacId": "TAC-3801-deploy-hetzner-server-provisioner",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "approved",
|
|
7
|
+
"name": "Deploy-hetzner-server provisioner facade",
|
|
8
|
+
"purpose": "The single project-side module that dereferences HETZNER_ACCOUNT_API_KEY, wraps typed Hetzner Cloud verbs (createServer, applyFirewall, takeSnapshot, listSnapshots, destroyServer), and emits four lifecycle events to the injected event sink. Under provisioningTool=hcloud the module shells to hcloud CLI with --output json; under raw-api it fetches the equivalent Hetzner Cloud REST endpoints. Consumers never dereference the token; the facade is the one place vendor credentials live.",
|
|
9
|
+
"responsibilities": [
|
|
10
|
+
"Open the facade on boot via a bounded ready-check; be the ONLY module that reads the HETZNER_ACCOUNT_API_KEY value, and export no symbol that leaks it (AC-37101-1).",
|
|
11
|
+
"Expose typed verbs createServer, applyFirewall, takeSnapshot, listSnapshots and destroyServer as returned methods on the factory result; refuse a raw-token passthrough on the public surface (AC-37101-1, AC-37103-1, AC-37108-1).",
|
|
12
|
+
"Emit provisionerReady, hetznerServerProvisioned, hetznerSnapshotTaken and hetznerServerDestroyed on the injected event sink; every payload is metadata-only per REQ-006 (AC-37101-1, AC-37103-1, AC-37108-1, AC-37109-1).",
|
|
13
|
+
"Shell path (hcloud): call hcloud <verb> --output json, JSON.parse the stdout, project the documented fields (id, primaryIpv4, location, serverType, snapshotId) into the returned record (AC-37103-1, AC-37108-1).",
|
|
14
|
+
"Raw-api path: fetch the documented REST endpoints (POST /servers, POST /firewalls, POST /servers/{id}/actions/create_image, DELETE /servers/{id}) with the token in Authorization: Bearer and project the same records (AC-37103-1)."
|
|
15
|
+
],
|
|
16
|
+
"internalStructure": "One module exporting createProvisionerFacade({ token, tool, apiHost, eventSink, hcloudPath?, fetch? }) returning a ProvisionerFacade whose methods are the typed verbs. The token capture lives at the top of the factory; the shell path passes the token via HCLOUD_TOKEN in the child environment (never on the argv); the raw-api path passes it in the Authorization header. Consumers call the returned methods for the process lifetime; the raw token never crosses the module boundary again.",
|
|
17
|
+
"interfaces": [
|
|
18
|
+
{
|
|
19
|
+
"name": "default export",
|
|
20
|
+
"kind": "factory",
|
|
21
|
+
"summary": "Provisioner facade factory exported from the applied fixture module."
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"dependencies": [],
|
|
25
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
26
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tacId": "TAC-3802-deploy-hetzner-server-manifest-schema",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "approved",
|
|
7
|
+
"name": "Hetzner server manifest schema",
|
|
8
|
+
"purpose": "One draft-07 JSON Schema declaring the shape of hetzner/servers/name.json in the applying project. Names required fields (name, serverType, location, image, sshKeyIds, firewallId, cloudInitPath, labels, firewallRules, snapshotCadence) and enforces enums (location, serverType, snapshotCadence) and rule-shape invariants (see TAC-3804). Manifest-schema-validate.mjs uses the schema to accept or refuse every fixture manifest.",
|
|
9
|
+
"responsibilities": [
|
|
10
|
+
"Ship a draft-07 JSON Schema for the hetzner-server manifest at contributions/schemas/hetzner-server.schema.json (AC-37102-1).",
|
|
11
|
+
"Refuse on missing required fields with an error message that names the offending field (AC-37102-1).",
|
|
12
|
+
"Refuse on firewall-rule-shape violations (22 opened to 0.0.0.0/0, or 80/443 missing) with an error message that names the offending rule (AC-37106-1).",
|
|
13
|
+
"Enforce snapshotCadence enum {weekly, daily, off} and refuse otherwise (AC-37107-1)."
|
|
14
|
+
],
|
|
15
|
+
"internalStructure": "One JSON Schema file at blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json; one shipped example manifest under the shared throwaway-server fixture at packages/rcf-lite/test/fixtures/hetzner-throwaway-server/hetzner/servers/ci-throwaway.json.",
|
|
16
|
+
"interfaces": [
|
|
17
|
+
{
|
|
18
|
+
"name": "hetzner-server.schema.json",
|
|
19
|
+
"kind": "jsonSchema",
|
|
20
|
+
"summary": "draft-07 JSON Schema for the Hetzner server manifest."
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"dependencies": [],
|
|
24
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
25
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
26
|
+
}
|